# Data Custody Contract — Pointer and Consent Schema (D14)

Status: DRAFT — awaiting ratification (webui-audit S2, contract 7 of 9).
Authority: PRD D14/D6 (Part I §7) — sensitive profile categories live in
the **user's own brain ONLY**; PostgreSQL holds structural data, consent
records, and pointers — never the content; "user data does not leak" is
enforced by architecture, not policy. The Standalone split is a MAY with
a recommended-default (kept for conversion forward-compatibility, D3).
PRD D4 (Part I §6) — profile answers feed `USER.md` and/or the user's
data store subject to the custody rule; connectors carry granular
agentic-access consent. PRD D6 — estate brains hold operational records;
only product-relevant material migrates into repository docs.

Revision 2 (sol r1 findings F1–F6): the registry is now a full profile
classifier that can represent non-sensitive rows, with an exact row
shape, versioning, refusal semantics for unknown keys, and transition
rules (F1); the consent model gains a concrete grantee reference model,
an active-row uniqueness constraint, append-only re-grant semantics,
and explicit mutation authority (F2); the pointer schema names its
exact column set, binds the one-pointer rule to a database constraint,
defines the `brain_ref` grammar and owner-bound resolution, replaces
the unkeyed hash with a keyed construction, and bounds orphan repair
(F3); the witnesses import the hierarchy contract's §6.2 column
allowlist and §6.3-style closed route inventories, add negative
controls and a column-type allowlist, and cover every binding rule
(F4); the Standalone physical split, both valid layouts, the election
record, and v1 phase timing are defined consistently with the wizard
and mode-conversion contracts (F5); drafting additions are disclosed in
§8 and the ruling request is one sentence with one decision (F6).

Revision 3 (sol r2 findings F1–F9): reclassification gains a
custody-config write fence (shared/exclusive row lock) so no
concurrent writer can commit a stale-version route, a declared
supported-version refusal, and witnesses for both transition
directions (F1). The grantee reference becomes three per-type FK
columns with a generated discriminant, real referent tables for
`connector` and `feature` (contract-owned, empty at v1), the
enrolled-agent binding named to the rank-4 family's table, and
revoked-row immutability enforced by a database trigger (F2). The
content hash is domain-separated per (user, category, locator) with
canonical bytes, an embedded key id, and a rotation rule; the audit FK
referent and the non-sensitive profile table (`profile_answers`) are
named; tree-object resolution prohibits symlink escape (F3). The §7
route witnesses import hierarchy §6.3's full prong set, scan
`plugins/`, extend the no-content probe to every platform table, and
add the missing negatives (F4). `custody_config` is singleton by
constraint (F5). §8 discloses the previously omitted policies (F6).
Mutation authority is split into two disjoint actor predicates —
subject mutations and the precondition-checked system auto-revocation
(F7). Hash mismatch gets a representable pointer state with a defined
terminal outcome (F8). Managed deletion is an ordered git-then-database
protocol with fault-injection witnesses at both interruption points,
replacing the impossible cross-store transaction (F9).

Revision 4 (sol r3 findings F1/F2/F4/F6/F7/F9 residual, F10/F11 new):
the write fence pins `READ COMMITTED` isolation and lock-first
statement order for the reclassification migration, and the
supported-version declaration becomes a named source symbol checked
under the same lock as each decision (F1). The user foreign keys
adopt the identity store's actual key type — `users.id` is `text`
(BetterAuth identifiers), so every `user_id` column is `text` (F10).
The agent referent binds to the live platform `agents` table by
symbol, since `identity-lifecycle.md` defines no enrollment surface;
an amendment re-binds if one arrives (F2). The system auto-revocation
predicate is withdrawn entirely: grantee cessation is handled
structurally by `ON DELETE CASCADE` from the grantee referent (as
subject cessation already is from `users`), default-deny ends access,
and the audit event table keeps the durable history — which removes
the trigger/actor contradiction rather than patching it (F2, F7).
Sensitive-write routes gain a static write-set closure witness so a
derived representation cannot land in any generic platform table
(F4). Managed deletion step 2 becomes a compare-and-delete on the
hash removed in step 1, witnessed against a concurrent rewrite (F9).
Ordinary sensitive writes get an ordered content-first protocol with
deterministic locators, ack-after-pointer-commit, idempotent retry,
and fault-injection witnesses (F11). §8 discloses every rule above
plus the previously undisclosed §4.3 committed-tree/symlink rule
(F6).

This contract binds the profile-category registry (§2), the custody
placement rule (§3), the pointer schema (§4), the consent schema and its
evaluation (§5), mode application (§6), witnesses (§7), and disclosed
drafting additions (§8). It defines schemas and placement; wizard step
flow stays with contract 3 (`onboarding-wizard.md`), mode and conversion
with contract 6 (`mode-conversion.md`), identity with
`identity-lifecycle.md`, tool mapping with `tool-gateway-mapping.md`.

## 1. Definitions

1. **User brain**: the git-tracked per-user data store. Its two valid
   Standalone layouts are defined in §6.2; in Enterprise it is the
   user's own brain repository.
2. **Sensitive content**: any profile answer or derived text in a §2
   category classified `sensitive`.
3. **Pointer**: a database record referencing sensitive content that
   lives in a user brain, carrying no content (§4).
4. **Grantee**: a non-subject principal that may be granted access to a
   user's sensitive content: a platform agent (a row of the `agents`
   table, §5.2), a connector, or a platform feature (§5.2).
5. **Consent record**: a database record granting one grantee access to
   one category of one user's data (§5).
6. **Registry version**: the monotonically increasing integer
   identifying the active state of the §2 registry.
7. **Physical split** (Standalone): sensitive user content living in a
   per-user brain repository separate from the estate mosaic-brain, as
   opposed to the unsplit layout where it lives in a dedicated
   user-files subtree of the single mosaic-brain (§6.2).
8. **Audit event table**: the single platform table into which the
   hierarchy contract (contract 1, `hierarchy-schema.md` §5.2) commits
   its semantic audit events. No sibling contract names the physical
   table, so this contract binds it by identity, not by name: every
   `audit_event_id` column in §3.5, §4.1, §5.1, and §6.3 is a foreign
   key to that table, the implementing PR that creates the audit store
   binds these FKs to it, and the §7.1 witness asserts that each such
   FK references the same physical table the contract-1 audit write
   path inserts into.

## 2. Profile-category registry

1. **Model.** The registry is the single classifier for every profile
   category, closed and versioned, in the platform database. Table
   `profile_category_registry`, columns exactly:

   | Column           | Type        | Constraints                                          |
   | ---------------- | ----------- | ---------------------------------------------------- |
   | `category_key`   | text        | primary key                                          |
   | `classification` | text        | NOT NULL, CHECK in (`sensitive`, `non-sensitive`)    |
   | `since_version`  | integer     | NOT NULL (registry version that introduced this row) |
   | `created_at`     | timestamptz | NOT NULL                                             |

   The current registry version is a single integer held in
   `custody_config` (§6.3). Rows are added or reclassified ONLY by
   amendment to this contract shipped as a migration that bumps the
   registry version; no runtime write path may insert, update, or
   delete registry rows.

2. **Initial registry (version 1).** Drawn from the D4 profile step and
   D14's "disabilities, family, communication style, and similar":

   | Category key              | Classification             | Covers                                                              |
   | ------------------------- | -------------------------- | ------------------------------------------------------------------- |
   | `disabilities`            | sensitive                  | disabilities including ADHD/autism/PDA/vision                       |
   | `family-social`           | sensitive                  | family, pets, friends                                               |
   | `communication-style`     | sensitive                  | desired agent communication style, voice-matching interview product |
   | `personal-interests`      | sensitive                  | hobbies, likes/dislikes                                             |
   | `connector-content`       | sensitive                  | email and drive content reached through user connectors             |
   | `professional-background` | non-sensitive (per ruling) | professional background summary used for agent configuration        |
   | `education`               | non-sensitive (per ruling) | education summary used for agent configuration                      |

   The `Covers` column is contract documentation, not a database
   column. Account identity fields (email, name, credentials) are
   identity-contract data, not profile custody data, and have no
   registry row.

3. **Unknown category — refusal.** A profile write naming a
   `category_key` with no registry row is REFUSED with an explicit
   error. Nothing is stored anywhere, no registry row is auto-added
   (§2.1), and no pointer is created (so the §4.1 foreign key is never
   asked to reference a missing row). Fail-closed means refusal, not
   silent routing.
4. **Reclassification transitions.** A reclassification ships as a
   contract amendment plus migration that bumps the registry version.
   Non-sensitive → sensitive: the same migration moves every existing
   relational value for that category into its owning user's brain,
   creates the pointers, and deletes the relational values, all before
   the new version activates. Sensitive → non-sensitive: existing
   brain content and pointers remain valid and are never automatically
   materialized into the database; only writes evaluated after the new
   version activates route relationally.
5. **Unreadable registry — refusal.** If the registry or its version
   cannot be read at decision time, every routing and consent decision
   that depends on it is refused. There is no cached-default or
   assume-sensitive fallback that performs a write.
6. **Write fence.** Every profile-write transaction reads the current
   registry version by taking a **shared lock** on the `custody_config`
   row (`SELECT ... FOR SHARE`) inside the same database transaction
   that performs the write, and routes by the version so read. A
   reclassification migration takes an **exclusive lock** on that row
   (`SELECT ... FOR UPDATE`) before its scan and holds it through the
   value moves and the version bump. Isolation and statement order
   are part of the fence, not implementation freedom: both the
   profile-write transaction and the migration transaction run at
   `READ COMMITTED`, and the migration's `SELECT ... FOR UPDATE` is
   the FIRST statement of its transaction — no read precedes it, so
   the migration establishes no snapshot before it holds the lock.
   Under `READ COMMITTED` each subsequent statement takes a fresh
   snapshot, so the migration's scan — which runs only after the lock
   is granted — sees every row committed by every `FOR SHARE` holder
   that finished before the lock was granted. Consequently a
   concurrent writer either commits before the lock is granted — its
   value is then visible to and swept by the scan — or blocks until
   the migration commits and then routes by the new version. No
   interleaving exists in which a value routed under the old version
   escapes the scan, including the writer-commits-while-migration-
   waits interleaving (witness §7.6g).
7. **Supported version.** Each release declares the exact registry
   version its code implements as an exported compile-time constant
   in the custody module of the shared schema package; the
   implementing PR names the symbol and §7.6(h) pins it by symbol
   identity, so the declaration is source, not configuration. Every
   profile routing decision and every consent decision reads
   `custody_config.registry_version` under a `SELECT ... FOR SHARE`
   on the `custody_config` row inside the same database transaction
   as the decision (write decisions share the §2.6 fence transaction;
   read-only consent evaluations take the shared lock in their own
   transaction), so the supported-version comparison and the decision
   it gates use one atomically read version. If the declared version
   does not equal the version so read, the decision is refused — a
   readable but unsupported registry is a refusal, never best-effort
   routing under either version (witness §7.6h).

## 3. Custody placement rule

1. Sensitive content is written to the owning user's brain ONLY.
   PostgreSQL tables MUST NOT store sensitive content in any column —
   not as text, not as excerpts or previews, not as encodings, and not
   as embeddings or other derived representations that reconstruct
   content.
2. The database MAY hold, about sensitive content: the pointer records
   of §4, the consent records of §5, the registry of §2, and the
   custody configuration of §6.3. Nothing else.
3. Every write path for profile answers routes by the registry:
   `sensitive` → brain write + pointer upsert; `non-sensitive` → the
   `profile_answers` table (§3.5); unknown → refusal (§2.3). The
   routing decision is made server-side from the registry at its
   current version, inside the §2.6 write fence; a client-supplied
   classification or routing override is ignored.
4. D6 boundary: operational records stay in estate brains and are
   linked, not migrated. This contract governs user-profile custody
   only and creates no new obligation on estate brains.
5. **Non-sensitive profile table.** Non-sensitive profile answers land
   in exactly one declared table, `profile_answers`, columns exactly:
   `id` (uuid, primary key), `user_id` (text, NOT NULL, FK → users(id)
   ON DELETE CASCADE), `category_key` (text, NOT NULL, FK →
   profile_category_registry(category_key)), `value` (text, NOT NULL),
   `created_at` and `updated_at` (timestamptz, NOT NULL), and
   `audit_event_id` (uuid, NOT NULL, FK → the §1.8 audit event table);
   plus UNIQUE (`user_id`, `category_key`). A value may sit in this
   table ONLY while its category's registry classification is
   `non-sensitive` (§2.4 migrates the rows on reclassification). No
   other platform table stores profile answer content.
6. **User-key type binding.** The identity store declares `users.id`
   as `text` (BetterAuth identifiers, `packages/db` schema); every
   `user_id` column in this contract (§3.5, §4.1, §5.1) is therefore
   `text`, matching the referenced key's declared type exactly. The
   identifiers are opaque strings, not guaranteed UUIDs; no custody
   column re-types, parses, or reformats them. If a sibling contract
   ever migrates the user key type, an amendment to this contract
   follows it (§7.1 asserts the FK types match the live referenced
   columns).
7. **Ordered sensitive-write protocol.** An ordinary sensitive write
   spans two stores and is ordered, content first: step 1 commits the
   content to the owning user's brain at a deterministic locator —
   the implementing PR declares a locator scheme that maps (category,
   answer slot) to one `brain_ref`, so a retry of the same write
   resolves to the same path; step 2, only after step 1 has
   committed, upserts the pointer row (insert, or update of the
   existing row for that (user, category, brain_ref)) in its own
   database transaction inside the §2.6 fence. The write is
   acknowledged to the caller only after step 2 commits. Interruption
   before step 1 leaves both stores unchanged. Interruption between
   the steps leaves committed brain content with no pointer:
   unpointed content is inert — it lives in the correct custody
   store, no read path serves it (reads resolve through pointers
   only), and nothing dangles in the database. The client's retry —
   the write was never acknowledged — recommits the same locator and
   completes the pointer upsert, converging to the pointed state.
   The reverse order (pointer before content) is forbidden: an
   ordinary write never creates a pointer whose content has not
   committed. Witness §7.16 fault-injects both interruption points
   and drives the retry convergence.

## 4. Pointer schema

1. **Exact columns.** Table `profile_pointers`, columns exactly:

   | Column           | Type        | Constraints                                            |
   | ---------------- | ----------- | ------------------------------------------------------ |
   | `id`             | uuid        | primary key                                            |
   | `user_id`        | text        | NOT NULL, FK → users(id) ON DELETE CASCADE (§3.6)      |
   | `category_key`   | text        | NOT NULL, FK → profile_category_registry(category_key) |
   | `brain_ref`      | text        | NOT NULL, CHECK against the §4.3 grammar               |
   | `content_hash`   | text        | NOT NULL (§4.4 construction)                           |
   | `created_at`     | timestamptz | NOT NULL                                               |
   | `updated_at`     | timestamptz | NOT NULL                                               |
   | `mismatch_at`    | timestamptz | NULL unless the pointer is in the §4.6 mismatch state  |
   | `audit_event_id` | uuid        | NOT NULL, FK → the §1.8 audit event table              |

   plus the database constraint UNIQUE (`user_id`, `category_key`,
   `brain_ref`) — the one-pointer rule is a constraint, not a
   convention.

2. **Opacity.** `brain_ref` and every other pointer column MUST NOT
   embed content or content-derived text (no titles, snippets, or
   free-text descriptions). A locator is structural, not descriptive.
3. **`brain_ref` grammar and owner binding.** `brain_ref` is a
   normalized repository-relative POSIX path: one or more segments
   matching `[A-Za-z0-9][A-Za-z0-9._-]*`, joined by `/`, with no
   leading `/`, no empty segment, and no `.` or `..` segment; the
   stored form matches
   `^[A-Za-z0-9][A-Za-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$`.
   Resolution ALWAYS roots at the brain owned by the row's `user_id`
   (the resolver takes the owner from the row, never from the
   locator); the locator carries no repository, host, or user
   component, so a cross-user or traversal reference is
   unrepresentable, not merely forbidden. Resolution operates on the
   brain repository's committed git tree (tree and blob objects),
   never through filesystem path lookup: if any segment of the path
   resolves to a symbolic link — or to anything other than a tree
   (intermediate segments) or a blob (final segment) — resolution is
   refused. A symlink therefore cannot redirect a locator outside the
   owner's brain (witness §7.8).
4. **`content_hash` construction.** `content_hash` is
   `hmac-sha256:<key id>:<hex>` — HMAC-SHA-256, keyed with a platform
   integrity key held in the secrets backend and never stored in the
   database or any repository, over the length-prefixed concatenation
   of: a fixed domain-separation string naming this contract and the
   construction version, the row's `user_id`, the row's
   `category_key`, the `brain_ref`, and the canonical content bytes.
   The canonical content bytes are the exact bytes of the committed
   blob the locator resolves to (§4.3) — no normalization. Because
   the input is domain-separated per (user, category, locator), equal
   answers from different users or categories store different hashes:
   the column is neither an offline dictionary oracle (external key)
   nor an equality/correlation oracle across rows (domain
   separation). `<key id>` names the key used; rotation introduces a
   new key id for new writes, re-verification accepts any
   still-registered key id, and a key is retired only when no stored
   hash references it. On read, a hash mismatch refuses the read and
   puts the pointer into the §4.6 mismatch state.
5. **Bounded orphan repair.** Pointers are deleted when their content
   is deleted; dangling pointers are repaired toward deletion, never
   toward re-creating content in the database. Reconciliation for a
   user's pointers runs on two triggers: every profile write for that
   user, and a periodic sweep whose interval the implementing PR
   declares (at most daily). A pointer whose content is absent is
   deleted by the next triggered reconciliation — an orphan survives
   at most one cycle, and repair performs no database content write.
6. **Mismatch state.** A failed §4.4 verification stamps the pointer's
   `mismatch_at` and the read is refused; every subsequent read of a
   pointer with `mismatch_at` set is refused without re-serving
   content. Reconciliation (§4.5 triggers) re-verifies each flagged
   pointer: verification success clears `mismatch_at`; content absent
   → the pointer is deleted (§4.5); persistent mismatch → the pointer
   is retained flagged with reads refused — the terminal outcome. The
   subject's next successful profile write for that category replaces
   the content, recomputes the hash, and clears the flag. Mismatch
   handling never deletes brain content and never copies content into
   the database.
7. **Managed deletion protocol.** A managed deletion of sensitive
   content spans two stores and is an ordered protocol, not a single
   transaction: step 1 commits the content deletion to the user's
   brain repository, recording the `content_hash` of the row it
   intends to delete as read before step 1; step 2, only after step 1
   has committed, deletes the pointer row in its own database
   transaction as a **compare-and-delete** — the DELETE is
   conditioned on (`user_id`, `category_key`, `brain_ref`) AND
   `content_hash` equal to the recorded value. A concurrent §3.6
   write that recreates the same locator with new content commits a
   new hash on the pointer row, so step 2's condition fails, deletes
   nothing, and the fresh content stays pointed — deletion never
   removes a pointer for content it did not delete. Interruption
   before step 1 commits leaves both stores unchanged. Interruption
   between the steps leaves a dangling pointer, which §4.5 repairs
   toward deletion within one reconciliation cycle. At no point does
   any compensation write content into the database (witness §7.10,
   including the concurrent-rewrite interleaving).

## 5. Consent schema and evaluation

1. **Exact columns.** Table `profile_consents`, columns exactly:

   | Column           | Type        | Constraints                                                                   |
   | ---------------- | ----------- | ----------------------------------------------------------------------------- |
   | `id`             | uuid        | primary key                                                                   |
   | `user_id`        | text        | NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject, §3.6)           |
   | `grantee_type`   | text        | NOT NULL, CHECK in (`agent`, `connector`, `feature`)                          |
   | `agent_id`       | uuid        | NULL, FK → agents(id) ON DELETE CASCADE (§5.2)                                |
   | `connector_id`   | text        | NULL, FK → custody_connector_registry(connector_key) ON DELETE CASCADE (§5.2) |
   | `feature_key`    | text        | NULL, FK → custody_feature_registry(feature_key) ON DELETE CASCADE (§5.2)     |
   | `grantee_ref`    | text        | generated stored: COALESCE(agent_id::text, connector_id, feature_key)         |
   | `category_key`   | text        | NOT NULL, FK → profile_category_registry(category_key)                        |
   | `state`          | text        | NOT NULL, CHECK in (`granted`, `revoked`)                                     |
   | `granted_at`     | timestamptz | NOT NULL                                                                      |
   | `revoked_at`     | timestamptz | CHECK ((state = 'granted') = (revoked_at IS NULL))                            |
   | `actor`          | text        | NOT NULL (the authenticated principal that recorded the change)               |
   | `audit_event_id` | uuid        | NOT NULL, FK → the §1.8 audit event table                                     |

   plus: the CHECK that exactly one referent column is non-NULL and
   matches the discriminant — `(grantee_type = 'agent') = (agent_id
IS NOT NULL)` AND `(grantee_type = 'connector') = (connector_id IS
NOT NULL)` AND `(grantee_type = 'feature') = (feature_key IS NOT
NULL)` — and the partial unique index UNIQUE (`user_id`,
   `grantee_type`, `grantee_ref`, `category_key`) WHERE `state =
'granted'` — at most one active grant per (user, concrete grantee,
   category), as a database constraint.

2. **Grantee reference model.** A grantee is identified by
   (`grantee_type`, the matching referent column); its canonical
   display form is `<grantee_type>:<grantee_ref>`. The discriminant
   set is closed at the three CHECK values, and every referent is a
   real foreign key:
   - `agent` → `agent_id` references the platform **`agents` table**
     (`agents.id`, uuid) as declared in the shared schema package
     (`packages/db`). This is a binding to the live schema, named
     honestly: the ratified `identity-lifecycle.md` defines no agent
     enrollment surface or enrolled-agent table, so there is no
     sibling-defined referent to bind to — the `agents` table is
     today's only platform representation of agents, and the §7.11
     witness pins the FK's referenced table to that schema symbol.
     If a sibling amendment later introduces an agent-enrollment
     surface with its own table, an amendment to this contract
     re-binds the FK; until then, rows in `agents` are the closed
     set of possible agent grantees.
   - `connector` → `connector_id` references this contract's own
     table `custody_connector_registry`, columns exactly:
     `connector_key` (text, primary key), `since_version` (integer,
     NOT NULL), `created_at` (timestamptz, NOT NULL). Rows are added
     ONLY by amendment to this contract (§2.1 pattern); the table is
     EMPTY at version 1.
   - `feature` → `feature_key` references this contract's own table
     `custody_feature_registry`, with the same three-column shape
     (`feature_key` primary key) and the same amendment-only rule;
     EMPTY at version 1 (no feature grantee exists until an amendment
     names one).

   A consent row naming a nonexistent referent violates its FK and is
   refused by the database. A grant to one agent confers nothing on
   another agent of the same type; the constraint key includes
   `grantee_ref`, so two same-type grantees are distinct rows.

3. **Default deny.** Absence of an active `granted` row for (user,
   grantee, category) means no access. There are no implicit grants,
   no platform-admin bypass, and no mode in which default-deny is
   suspended.
4. **Mutation authority — subject only.** Every consent mutation
   (create, grant, revoke) must satisfy the single predicate: the
   authenticated actor IS the row's data subject — the server asserts
   `actor` equals the principal of the row's `user_id` before the
   mutation commits. A platform admin has no consent-mutation
   capability over another user's rows — an admin grant or revocation
   on another user's data is refused at write time, closing the
   write-side route around §5.3. There is no system-actor mutation
   predicate: revision 3's system auto-revocation is withdrawn
   (disclosed, §8), because grantee cessation is handled structurally
   instead —
   **Cessation by cascade**: when a grantee ceases to exist — the
   `agents` row is deleted, or a `custody_connector_registry` /
   `custody_feature_registry` row is removed by an amendment
   migration — the referent FK's `ON DELETE CASCADE` removes that
   grantee's consent rows in the same transaction as the referent
   deletion. Access ends with the rows (§5.3 default deny: no row, no
   access); no UPDATE is performed, so no actor question arises. The
   durable record of the grants and their lifecycle is the audit
   event table (§1.8): every mutation carried an `audit_event_id`,
   and audit events are not deleted by the cascade. The same rule
   already governs subject cessation via the `user_id` FK. §7.12
   witnesses the subject predicate, its refusal complement, and the
   cascade; every §7.3-enumerated consent-mutation route asserts the
   subject predicate.

5. **Revocation and re-grant.** Revocation flips exactly one active
   row to `revoked` and stamps `revoked_at`; it is effective for every
   access evaluated after the revoking write commits. Revoked rows are
   retained as history and never mutated again — enforced by a
   database trigger on `profile_consents` that permits UPDATE only
   when it is the granted → revoked transition (the state flip plus
   the `revoked_at` stamp, every other column unchanged), and permits
   DELETE only when the row's referenced subject or grantee no longer
   exists — the trigger checks that the `users` row named by
   `OLD.user_id`, or the referent row named by the grantee column, is
   absent, which is true exactly during a §5.4 cessation cascade
   (referential cascades delete the parent row before the dependent
   rows, so the check distinguishes a cascade from a standalone
   DELETE). Every other UPDATE and every DELETE with both referents
   alive is rejected, so a revoked row cannot be flipped back to
   `granted` and history cannot be pruned by anyone, including
   through direct SQL (witness §7.13). A re-grant after
   revocation inserts a NEW row (append-only history) — repeated
   grant/revoke cycles are represented as successive rows, and the
   §5.1 partial unique index guarantees the old revoked rows cannot
   keep access live.
6. **Evaluation placement.** Access to sensitive content is mediated by
   the platform (Gateway/tooling) evaluating consent before any brain
   read on behalf of a grantee; the evaluation fails closed
   (`rbac-grant-model.md` §3.5 pattern), including when the consent
   state cannot be read (§2.5 pattern). The user reading their own
   data is not a grantee and needs no consent row.
7. Consent records govern agentic/feature access to user data. They are
   distinct from hierarchy grants (contract 2) and confer no platform
   authorization.

## 6. Mode application

1. The §2–§5 schemas are mode-independent: Standalone and Enterprise
   use the same tables and the same routing rule.
2. **Standalone layouts.** The D14 physical split remains a MAY. Its
   two valid layouts are: **split** — sensitive user content in a
   per-user brain repository separate from the estate mosaic-brain
   (the recommended default); **unsplit** — sensitive user content in
   the dedicated user-files subtree `users/<user id>/` of the single
   mosaic-brain. §3 binds the logical user-brain region identically in
   both layouts; the layout election changes where the region lives,
   never whether routing applies.
3. **Election record.** The election lives in this contract's own
   one-row table `custody_config`, columns exactly: `id` (uuid,
   primary key), `singleton` (boolean, NOT NULL, DEFAULT true, CHECK
   (`singleton`), UNIQUE — the one-row rule as a database constraint:
   a second row cannot satisfy the CHECK and the UNIQUE index
   simultaneously), `standalone_layout` (text, NOT NULL, CHECK in
   (`split`, `unsplit`), default `split`), `registry_version`
   (integer, NOT NULL, §2.1), `elected_at` (timestamptz, NOT NULL),
   `actor` (text, NOT NULL), `audit_event_id` (uuid, NOT NULL, FK →
   the §1.8 audit event table). It is written at bootstrap and
   amended only by an explicit operator action; contract 6's exact,
   immutable mode record is not touched or extended by this contract.
   The row is also the §2.6 fence anchor.
4. **Phase timing.** v1 ships the §2–§6 schemas and the D14 database
   boundary, and the wizard collects no sensitive category in v1
   (contract 3 §3), so v1 contains no sensitive write surface. §3
   binds every sensitive write path from the moment one exists — the
   first profile surface that accepts a sensitive category (P2/P3)
   activates routing, consistent with contract 6 §5's v1 slice (D14
   database boundary only, custody mechanics outside v1).
5. **Conversion precondition.** An operator electing `unsplit` accepts
   conversion-time partitioning: `mode-conversion.md` §4.2 requires
   the per-user partition to exist before the Enterprise flip, so
   conversion from an unsplit install performs the partitioning first.
6. In Enterprise, the split is mandatory (D3 table); no-leakage between
   users is enforced by §3 placement plus §5 default-deny — there is
   no cross-user read path to sensitive content through the database,
   because the database has no content to serve.

## 7. Verification requirements

Binding on the implementing PRs. Every witness below MUST name, in its
implementation, the exact tables, columns, commands, and source roots
it scans; "the custody tables" means `profile_category_registry`,
`profile_pointers`, `profile_consents`, `custody_config`,
`custody_connector_registry`, and `custody_feature_registry`; "the
declared profile table" means `profile_answers` (§3.5).

1. **Column-allowlist witness** (hierarchy contract §6.2 style): the
   custody tables' live column sets are exactly
   §2.1/§4.1/§5.1/§5.2/§6.3 and `profile_answers` is exactly §3.5;
   no platform table outside `profile_answers` carries profile
   answer content; every `audit_event_id` FK (§3.5, §4.1, §5.1,
   §6.3) references the same physical table that the contract-1
   (hierarchy §5.2) audit write path inserts into (§1.8 binding);
   and every FK column's declared type equals the referenced
   column's live declared type — in particular each `user_id` is
   `text` matching `users.id` and `agent_id` is `uuid` matching
   `agents.id` (§3.6 binding).
2. **Column-type allowlist witness:** the custody tables and
   `profile_answers` use only the column types named in
   §2.1/§3.5/§4.1/§5.1/§5.2/§6.3 (uuid, text, integer, timestamptz,
   boolean) — no bytea, json/jsonb, array, vector, or tsvector
   column exists in them, closing the encoded/derived-representation
   routes by type rather than by probe alone.
3. **Closed write-route witness** (hierarchy contract §6.3, full
   prong set): a static, re-export-aware inventory over `apps/`,
   `packages/`, and `plugins/` (production code, tests excluded)
   enumerates every module that writes the custody tables,
   `profile_answers`, or profile answers generally, detecting access
   through each of hierarchy §6.3's prongs — schema-symbol imports,
   SQL string literals naming the tables, raw-execution primitives,
   and runtime code construction — with the database client reachable
   only through a closed importer allowlist. Every enumerated write
   route implements §3.3 registry routing inside the §2.6 fence, and
   every enumerated consent-mutation route asserts the §5.4 subject
   predicate; a route outside the enumeration, or a client import
   outside the allowlist, fails the assertion. **Write-set closure
   for sensitive paths:** for every enumerated route that performs a
   sensitive write, the same static inventory (same prong set)
   enumerates every database table that route's code path can write;
   the set MUST equal exactly {`profile_pointers`, the §1.8 audit
   event table} — the brain write goes through the declared
   brain-write surface, never the database. Any additional
   platform-table write statically reachable from a sensitive-write
   path — an embedding insert, a cache row, any derived
   representation in any generic table — fails the witness. Control:
   a mutated route that adds one insert into an arbitrary existing
   platform table is detected. This closes the §3.1 escape in which
   a conforming-looking route also writes derived sensitive data
   elsewhere: §7.5 probes for planted content; this closure bounds
   what a sensitive-write route can write at all.
4. **Closed brain-read witness** (same style): the inventory enumerates
   every production route that reads user-brain content on behalf of a
   grantee, and every enumerated route calls the §5.6 consent
   evaluation; a brain-read route outside the enumeration fails.
5. **Routing witness:** a sensitive-category answer submitted through
   the profile surface results in a brain write plus a pointer row and
   zero content bytes in the database; a non-sensitive answer lands in
   its declared table. The no-content probe is a negative control: the
   witness first plants the fixture text in a scratch column of a
   throwaway table to prove the probe detects it, then asserts its
   absence — as plaintext, base64, hex, and JSON-string encodings —
   across every column of every table in the platform database: §3.1
   bans sensitive content in every column of every table, so the
   probe scope is the entire schema, not only the custody tables.
6. **Registry witnesses:** (a) the version-1 registry state is exactly
   the seven §2.2 rows with their classifications; (b) a
   `professional-background` answer routes relationally (or per the
   ruling's alternative); (c) an unknown `category_key` is refused
   with nothing stored (§2.3); (d) with the registry unreadable, the
   write is refused (§2.5); (e) a runtime insert/update/delete against
   `profile_category_registry` outside a migration is refused (§2.1);
   (f) a reclassification migration (non-sensitive → sensitive) on
   seeded data moves the values to brains, creates pointers, and
   leaves zero relational values (§2.4); (g) fence race: a profile
   write transaction opened before a reclassification migration takes
   its exclusive lock either commits before the lock is granted (its
   value is swept) or blocks and, on commit, routes by the new
   version — the witness drives both interleavings, INCLUDING the
   interleaving in which the writer holds `FOR SHARE` when the
   migration requests `FOR UPDATE`, the migration waits, the writer
   commits its relational row, and the migration's scan (running
   after lock grant, at `READ COMMITTED`, with the lock as its first
   statement per §2.6) is asserted to sweep that row — and asserts no
   relational value for the reclassified category exists after any
   interleaving completes (§2.6); the witness also asserts the
   migration implementation takes its exclusive lock as the first
   statement of its transaction (a migration variant that reads
   before locking fails review of this witness's static check);
   (h) with `custody_config.registry_version` set to a value the
   release's declared compile-time constant (§2.7, pinned by symbol)
   does not equal, profile writes and consent decisions are refused,
   and the witness asserts the version comparison reads the row under
   the §2.7 shared lock in the same transaction as the refused
   decision (§2.7); (i) sensitive → non-sensitive: after the
   version bump, existing brain content and pointers remain intact
   and readable, nothing is materialized into the database, and the
   next write for that category lands in `profile_answers` (§2.4).
7. **Server-side classification witness:** a client-supplied
   classification or routing override on a profile write is ignored;
   the registry decision is applied (§3.3).
8. **Pointer-constraint witnesses:** inserting a second pointer for the
   same (user, category, brain_ref) violates the §4.1 unique
   constraint; a `brain_ref` failing the §4.3 grammar (leading `/`,
   `..` segment, empty segment) is rejected by the CHECK; resolution
   of a valid `brain_ref` under user A's row never reads user B's
   brain (owner binding, §4.3); with a symlink committed at a
   locator's path or as an intermediate segment, resolution is
   refused and no content outside the owner's brain is read (§4.3
   tree-object rule).
9. **Hash witnesses:** `content_hash` verifies via the keyed §4.4
   construction; the database value alone, without the external key,
   does not equal any unkeyed digest of the fixture content (oracle
   control); the same fixture content stored for two users, and for
   two categories of one user, yields different stored hashes
   (domain-separation control — no cross-row equality oracle); after
   a key rotation, a pointer written under the old key id still
   verifies and a new write stores the new key id; a mismatch refuses
   the read and stamps `mismatch_at`, subsequent reads stay refused,
   reconciliation of a persistently mismatched pointer retains it
   flagged without deleting brain content or writing content to the
   database, and the subject's re-write clears the flag (§4.4, §4.6).
10. **Orphan and deletion-protocol witnesses:** starting from a
    PRE-EXISTING orphan (content already absent, pointer present),
    the next triggered reconciliation deletes the pointer and writes
    no content anywhere in the database (§4.5). The §4.7 protocol is
    fault-injected at both interruption points: killed before the
    brain commit, both stores are unchanged; killed between the brain
    commit and the pointer delete, the dangling pointer is deleted by
    the next reconciliation — and at no point is content written to
    the database or restored to the brain. **Concurrent-rewrite
    interleaving:** deletion step 1 commits; before step 2 runs, a
    concurrent §3.7 write recreates the same `brain_ref` with new
    content and completes its pointer upsert (new `content_hash`);
    step 2 then executes its compare-and-delete — the witness asserts
    the DELETE matches zero rows, the fresh pointer survives, its
    content resolves and verifies, and no unpointed content and no
    dangling pointer exist afterward (§4.7).
11. **Default-deny and granularity witnesses:** an agent grantee with
    no active row is refused; with a `granted` row for category A
    only, category B is refused; with agent X granted, agent Y of the
    same type is refused for the same (user, category); `connector`
    and `feature` are exercised via their FK refusal paths, since
    both registries are empty at version 1 (§5.2); a consent insert
    naming a nonexistent agent id violates the `agent_id` FK, and the
    witness pins that FK's referenced table by symbol identity to the
    `agents` table exported by the shared schema package (§5.2
    binding); a platform admin requesting
    another user's sensitive content through any production read
    surface is refused — admins are not a grantee type and
    default-deny applies (the access-time complement of §7.12's
    write-time refusal); a hierarchy owner or manager grant over the
    subject confers no consent access (§5.7); with the consent state
    unreadable, evaluation refuses (§5.6).
12. **Mutation-authority witnesses:** a platform admin attempting to
    create a grant on another user's data is refused at write time; a
    hierarchy owner or manager likewise; the data subject succeeds;
    `actor` equals the subject's principal on every mutation row —
    there is no non-subject mutation path (§5.4). **Cessation
    cascade:** deleting an agent's `agents` row removes that agent's
    consent rows in the same transaction via the FK cascade, the next
    consent evaluation for that grantee refuses (no row), the linked
    audit events survive, and no other grantee's rows are touched; a
    connector-registry amendment migration removing a key cascades
    identically (§5.4).
13. **Revocation/re-grant witnesses:** after revocation commits, the
    next evaluation refuses and the revoked row persists unmutated; a
    full grant → revoke → re-grant cycle yields two rows (one
    revoked, one active) and access follows only the active row; a
    second concurrent grant attempt for the same key violates the
    §5.1 partial unique index; a direct SQL UPDATE flipping a revoked
    row back to `granted`, an UPDATE altering any other column of a
    revoked row, and a standalone DELETE of a row whose subject and
    grantee both still exist are each rejected by the §5.5 trigger;
    the complement controls pass: deleting the subject's `users` row
    cascades the subject's consent rows, and deleting the grantee's
    referent row cascades that grantee's rows — both permitted by the
    trigger's absent-referent prong (§5.5).
14. **Self-access witness:** the data subject reads their own content
    without consent rows (§5.6).
15. **Mode witnesses:** the custody-table schemas are byte-identical
    under Standalone and Enterprise migrations (§6.1); a fresh
    Standalone bootstrap records `standalone_layout = 'split'` by
    default, and an explicit opt-out records `unsplit` with actor and
    audit linkage (§6.3); conversion from an `unsplit` install refuses
    the mode flip until partitioning has produced the per-user region
    (§6.5, with contract 6 §4.2); in an Enterprise fixture with two
    users, user A's grantee with a grant on user A cannot reach any of
    user B's content (§6.6); inserting a second `custody_config` row
    violates the §6.3 singleton constraint.
16. **Ordered-write protocol witnesses:** the §3.7 protocol is
    fault-injected at both interruption points: killed before the
    brain commit, both stores are unchanged and the caller receives
    no acknowledgment; killed between the brain commit and the
    pointer upsert, the brain holds unpointed content, no pointer row
    exists, no read surface serves the content, and the caller
    received no acknowledgment — then the retried write recommits the
    same deterministic locator, completes the pointer upsert, and the
    witness asserts the converged state (one pointer, verifying hash,
    content served to the subject). A static control asserts the
    implementation orders content before pointer: no enumerated
    sensitive-write route creates a pointer row before its brain
    commit has been confirmed (§3.6).

## 8. Drafting additions (PRD §12.1 disclosure)

The following are proposed drafting additions, visible here for
ratification; none is claimed as a PRD mandate, and each is severable:

1. The `feature` grantee type, with a contract-owned registry table
   that is empty at version 1 (§5.2).
2. Append-only consent history: re-grants insert new rows; revoked
   rows are retained unmutated, enforced by trigger (§5.5).
3. Cessation by cascade: when a grantee or subject referent row is
   deleted, consent rows are removed by `ON DELETE CASCADE` in the
   same transaction; access ends through default deny, and durable
   history is carried by the §1.8 audit event table. The trigger's
   DELETE prong permits a child-row DELETE only when the referenced
   subject or grantee row is absent (§5.4, §5.5). This replaces the
   revision-2 system-actor auto-revocation predicate, which is
   withdrawn.
4. The `custody_config` election record for the Standalone layout,
   singleton by constraint (§6.3).
5. The domain-separated keyed `content_hash` construction, its key
   rotation rule, and the `mismatch_at` pointer state (§4.4, §4.6).
6. The bounded dangling-pointer reconciliation policy and the ordered
   managed-deletion protocol (§4.5, §4.7).
7. The column-type allowlist verification requirement (§7.2).
8. The amendment-only, versioned registry classifier itself, its
   unknown-key refusal, and both reclassification transition policies
   (§2.1, §2.3, §2.4).
9. The custody-config write fence and the declared supported-version
   refusal, including the revision-4 pinning: both fence transactions
   run at `READ COMMITTED` with the migration's `SELECT ... FOR
UPDATE` as the first statement of its transaction, and the
   supported version is an exported compile-time constant in the
   custody module of the shared schema package, read under the same
   lock as the decision it gates (§2.6, §2.7).
10. Subject-only consent mutation authority (§5.4, first predicate).
11. The declared non-sensitive profile table `profile_answers` and
    its closed schema (§3.5).
12. The typed grantee referent columns, the
    `custody_connector_registry` and `custody_feature_registry`
    tables, the audit-table binding by identity (§5.1, §5.2, §1.8),
    and the agent-grantee binding by symbol identity to the `agents`
    table exported by the shared schema package (§5.2, §7.11).
13. Shipping all §2–§6 schemas in v1 ahead of any sensitive write
    surface (§6.4).
14. The committed-tree-only resolution rule: content reads resolve
    only through the committed tree of the brain repository, and
    symlink or non-tree/non-blob resolution is refused (§4.3, §7.8).
15. The text user-key binding: every `user_id` column is `text`
    matching the live `users.id` declaration, values are opaque
    strings not guaranteed to be UUIDs, and any sibling user-key
    migration triggers amendment of this contract (§3.6, §7.1).
16. The ordered sensitive-write protocol for ordinary writes:
    content-first at a deterministic locator, pointer upsert second
    in its own transaction, acknowledgement only after the pointer
    commit, idempotent retry convergence, and the
    pointer-before-content prohibition (§3.7, §7.16).
17. The compare-and-delete managed-deletion step: deletion step 2 is
    conditioned on the `content_hash` recorded in step 1, so a
    concurrent recreate of the same locator is never unpointed by a
    stale deletion (§4.7, §7.10).
18. The sensitive-path write-set closure: a static inventory of every
    table a sensitive-write route can write, required to equal
    exactly the pointer table plus the §1.8 audit event table
    (§7.3).

The revision-1 "reporting" rationale for relational
background/education storage is withdrawn; the traced rationale is
agent configuration (PRD D4).

## Ruling request

Ruling requested (one decision): classify `professional-background`
and `education` as **non-sensitive** in the version-1 registry (stored
relationally, used for agent configuration) — or, as the alternative,
classify both **sensitive** (user-brain custody with pointers),
accepting that agent-configuration reads then go through pointer
indirection and consent evaluation?
