Files
stack/docs/requirements/custody-schema.md
T
2026-08-26 20:33:21 -05:00

38 KiB
Raw Blame History

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 F1F6): 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 F1F9): 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).

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: an enrolled agent, 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. Consequently a concurrent writer either commits before the migration's scan begins — its value is then 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 commits after the migration's scan (witness §7.6g).

  7. Supported version. Each release declares the exact registry version its code implements. If the declared version does not equal custody_config.registry_version, every profile routing and consent 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 (uuid, 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.

4. Pointer schema

  1. Exact columns. Table profile_pointers, columns exactly:

    Column Type Constraints
    id uuid primary key
    user_id uuid NOT NULL, FK → users(id) ON DELETE CASCADE
    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; step 2, only after step 1 has committed, deletes the pointer row in its own database transaction. 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).

  1. Exact columns. Table profile_consents, columns exactly:

    Column Type Constraints
    id uuid primary key
    user_id uuid NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject)
    grantee_type text NOT NULL, CHECK in (agent, connector, feature)
    agent_id uuid NULL, FK → the enrolled-agent table (§5.2)
    connector_id text NULL, FK → custody_connector_registry(connector_key) (§5.2)
    feature_key text NULL, FK → custody_feature_registry(feature_key) (§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:

    • agentagent_id references the enrolled-agent table: the single platform table in which agent enrollment under identity-lifecycle.md (the contract 5 §3.1 rank-4 command family) records enrolled agents. No sibling contract names that physical table, so the binding is by identity: the custody migration orders after that table's migration, the implementing PR binds this FK to it, and the §7.11 witness pins the physical name by asserting the FK's referenced table is the one the enrollment surface writes.
    • connectorconnector_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.
    • featurefeature_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 — two disjoint predicates. Every consent mutation must satisfy exactly one of:

    • Subject mutation: 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. This is the only predicate under which rows are created or granted. A platform admin has no consent-mutation capability over another user's rows — an admin self-grant is refused at write time, closing the write-side route around §5.3.
    • System auto-revocation (disclosed, §8): the actor is the single named platform system principal, the mutation is a granted → revoked flip and nothing else, and the server has verified the precondition that the row's grantee has ceased to exist (e.g. agent retirement under identity-lifecycle.md). A system-actor mutation whose precondition does not hold, or that is anything other than a revocation, is refused.

    The predicates are disjoint (the system principal is not a data subject); §7.12 witnesses both and their refusal complements, and every §7.3-enumerated consent-mutation route asserts one of them.

  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 rejects every other UPDATE and every DELETE, so a revoked row cannot be flipped back to granted 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; and 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).
  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 a §5.4 predicate; a route outside the enumeration, or a client import outside the allowlist, fails the assertion.
  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 scan (its value is swept) or blocks and, on commit, routes by the new version — the witness drives both interleavings and asserts no relational value for the reclassified category exists after either completes (§2.6); (h) with custody_config.registry_version set to a value the release does not declare, profile writes and consent decisions are refused (§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.
  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 as the one the enrollment surface writes (§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 subject-mutation row; retiring an enrolled agent auto-revokes its active rows with the system principal as actor and granted → revoked as the only change; a system-principal mutation whose grantee still exists, or that attempts anything other than a revocation, is refused — the two §5.4 predicates are witnessed as disjoint (§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 DELETE of any row are each rejected by the §5.5 trigger (§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.

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. System-actor auto-revocation when a grantee ceases to exist, as a precondition-checked predicate disjoint from subject mutation (§5.4).
  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 (§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, and the audit-table binding by identity (§5.1, §5.2, §1.8).
  13. Shipping all §2–§6 schemas in v1 ahead of any sensitive write surface (§6.4).

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?