Files
stack/docs/requirements/custody-schema.md
T

1190 lines
73 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).
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).
Revision 5 (sol r4 findings F9/F11 residual, F12/F13 new): the
pointer row gains a monotonic `generation` counter bumped by every
upsert, and managed-deletion step 2 conditions on the generation
recorded in step 1 instead of the content hash, closing the
identical-content ABA in which a byte-identical rewrite reproduced
the old hash and a stale deletion unpointed fresh content (F9). The
ordered-write protocol gains a contract-defined locator scheme —
injective over (category, slot), with the slot set fixed to the
single value `answer` at version 1 — and a per-locator advisory lock
spanning both steps, so same-locator writers are serialized and a
completed write always leaves the pointer hash describing the
current blob; §7.16 adds the concurrent-writer,
crash-before-acknowledgment, stale-retry, and locator-injectivity
witnesses (F11). Cascaded consent deletion now emits a semantic
cessation event in the same transaction as the referent deletion,
written by a trigger on the consent table, so the audit trail
records when and why each active grant ended, not only that it began
(F12). The stale §3.6/§5.2 cross-references and the "first
predicate" remnant are corrected (F13).
Revision 6 (sol r5 residuals F9/F12 + F14): the deletion and repair
token becomes the PAIR (row `id`, `generation`) — the uuid primary
key is minted fresh on every insert and never reused, so it is the
row-incarnation identity the bare counter lacked; a stale
compare-and-delete can no longer match a row reincarnated at
generation 1 after an orphan-repair delete, and §7.10 adds the
delete/reinsert ABA control (F9). The §3.7 locator lock now covers
EVERY blob/pointer mutator for a locator, not only ordinary writers:
managed deletion (§4.7) and reconciliation repair (§4.5) acquire the
same advisory lock across their observe-and-mutate spans, so the
reverse interleaving — a deleter destroying a blob a locked writer
has committed but not yet pointed — is unschedulable while the
tokens keep the crash-window (lock released by session death)
harmless; §7.10 adds the reverse interleaving and its lock-removed
control (F14). The cessation-event snapshot gains the deleted
consent row's `id` and its `user_id`, so events from two subjects'
grants to one ceasing grantee are attributable to their exact rows
and subjects, witnessed with a two-subject case (F12).
Revision 7 (sol r6 F15): the reconciliation try-acquire no longer
discharges a locator for the cycle — a failed try-acquire QUEUES the
locator within the same cycle, and before the cycle completes the
sweep revisits every queued locator with a blocking acquire, running
the same locked observation and token-conditioned repair. A
triggered reconciliation cycle is complete only when every in-scope
pointer has been processed under its locator lock, so the §4.5
one-cycle orphan bound holds unconditionally on lock contention;
the retry terminates because every §3.7 hold is transaction- or
session-scoped and session end releases the lock automatically.
§7.10 adds the lock-held contention control (sweep pass skips
without blocking; cycle completion waits for the queued repair;
the orphan is deleted before the cycle reports completion), its
repeated-contention variant, and a third static control failing an
implementation that defers a skipped locator to a later cycle
(F15).
Revision 8 (sol r7 residual F15): the one-cycle bound gains its
missing termination proof. §3.7 adds two liveness rules for the
locator lock: a declared finite HOLD DEADLINE, enforced with
session-level timeout mechanisms on every lock-holding path — a
live holder that stalls past the deadline is terminated, session
end releases the lock, and the stalled-holder schedule collapses
into the already-harmless crash window — and an arrival-order
GRANT rule, so a blocked waiter's wait is bounded by the current
holder and the waiters already ahead, never by later arrivals.
§4.5's blocking retry therefore terminates within a wall-clock
bound fixed when it starts waiting, and the implementing PR
derives and declares a maximum cycle duration whose exceedance is
surfaced as an error, never a silent hang. §7.10 adds the
hold-deadline control (a non-cooperating stalled holder is
terminated at the deadline and the orphan is still deleted before
cycle completion), the grant-order control (a stream of later
contenders cannot starve the queued repair, and the declared
cycle-duration limit is exercised as a checked bound), and a
fourth static control failing any lock-holding path without the
declared deadline (F15).
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.
**Locator scheme.** The locator is a pure function
`L(category_key, slot) → brain_ref` declared by the implementing
PR, with three contract-bound properties: it is deterministic (a
retry of the same write resolves to the same path), it is
INJECTIVE — distinct (category_key, slot) pairs map to distinct
`brain_ref` values, witnessed with a collision control (§7.16) —
and it is stable across releases (a scheme change is a contract
amendment with a migration). An **answer slot** is the stable
identifier of one answer's position within a category; at
version 1 every registry category is single-valued (mirroring
§3.5's UNIQUE (`user_id`, `category_key`)), so the slot set is
the single constant `answer`. An amendment introducing a
multi-valued category must extend the slot set while preserving
injectivity.
**Serialization.** The entire write — both steps — runs under a
per-locator mutual exclusion: before step 1 the writer acquires
an exclusive advisory lock keyed on (`user_id`, `category_key`,
slot) (e.g. a session-scoped `pg_advisory_lock` on a stable hash
of the triple), held until step 2 commits or the writer's session
ends (session end releases it automatically, so a crashed holder
never wedges the locator). Two liveness rules bound this
exclusion. **Hold deadline:** every session that acquires a
locator lock — ordinary writer, managed deleter (§4.7),
reconciliation repair (§4.5) — runs under a declared finite hold
deadline: the implementing PR declares the value and enforces it
with the database's session-level timeout mechanisms (statement
timeout and idle-in-transaction timeout, plus an application
deadline covering step 1's brain commit), so a holder that
stalls past the deadline has its session TERMINATED — session
end then releases the lock automatically per the rule above, and
the terminated holder's partial protocol is exactly the crash
window the tokens already make harmless (§4.5, §4.7). No holder
— live, stalled, or crashed — holds a locator lock longer than
the declared deadline. **Grant order:** conflicting waiters on
one locator lock are granted in arrival order — a requester that
starts waiting after another never acquires before it (the
queue-order grant of the pinned advisory-lock mechanism,
witnessed §7.10) — so a blocked waiter's wait is bounded by the
current holder's remaining deadline plus the deadline-bounded
holds of the waiters already ahead of it, never by later
arrivals. Writers to one locator are therefore
totally ordered; writes to distinct locators do not contend.
Because no two same-locator writes interleave, a completed write
always leaves the pointer's `content_hash` describing the blob
currently committed at the locator — the divergent interleaving
(writer A's pointer over writer B's content) is unschedulable,
not merely unlikely.
**Steps.** Step 1 commits the content to the owning user's brain
at `L(category_key, slot)`; 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), incrementing
`generation` per §4.1) 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.
Interruption after step 2 commits but before the acknowledgment
reaches the caller leaves the converged state; the retry below
observes it and returns success without corrupting it. The
client's retry — the write was never acknowledged — re-acquires
the locator lock, recommits the same locator, and completes the
pointer upsert, converging to the pointed state; the retry is
idempotent whichever interruption point preceded it. A stale
retry (an earlier write retried after a later same-locator write
completed) also executes under the lock and leaves a consistent
pointed state — same-locator outcomes are last-completed-write
wins, and no completion order can leave the hash describing a
non-current blob. 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 the
interruption points, drives retry convergence, the same-locator
concurrent interleaving, the stale retry, and the injectivity
control.
## 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) |
| `generation` | bigint | NOT NULL (1 on insert; every §3.7 upsert increments) |
| `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.
Per-pointer repair runs under the §3.7 locator lock: before
observing content absence for a pointer, reconciliation acquires
the pointer's locator lock, holds it across the observation and
the DELETE, and releases it after. The sweep pass MAY use a
try-acquire so it never blocks mid-pass behind a writer, but a
failed try-acquire defers the locator, never discharges it: the
skipped locator is queued within the SAME cycle, and before the
cycle completes the sweep revisits every queued locator with a
blocking acquire and runs the same locked observation and
token-conditioned repair, retrying until the lock is acquired. A
triggered reconciliation cycle is COMPLETE only when every
in-scope pointer has been processed under its locator lock;
deferral moves work to the end of the cycle, never into a later
one, so the one-cycle bound above holds unconditionally on lock
contention. The blocking retry terminates within a WALL-CLOCK
BOUND, not merely eventually: every holder is subject to the
§3.7 hold deadline (a stalled live session is terminated at the
deadline and session end releases the lock — voluntary release
is not assumed), and grants follow §3.7 arrival order (no later
requester overtakes the queued retry), so once the retry blocks
it acquires within the current holder's remaining deadline plus
the deadline-bounded holds of the waiters already queued ahead —
a finite quantity fixed when the retry starts waiting,
independent of how many contenders arrive later. Cycle
completion is therefore itself bounded: the implementing PR
derives and declares a maximum cycle duration from the declared
hold deadline, the sweep scope, and the retry queue, and a cycle
exceeding its declared maximum is surfaced as an error — never a
silent hang. A cycle whose retry is still blocked has not
completed, and the orphan is deleted before the cycle reports
completion, within the declared bound.
The repair DELETE is additionally token-conditioned like §4.7
step 2: it records the row's `id` and `generation` when it
observes the content absent and conditions the DELETE on BOTH —
the uuid `id` is minted per insert and never reused, so it
identifies the row incarnation; `generation` orders upserts
within the incarnation. A concurrent §3.7 write that recommits
the locator between the observation and the delete (bumping
`generation`), and a delete-then-reinsert that reincarnates the
locator's row (fresh `id`, `generation` back at 1), each make
the repair match zero rows — reconciliation never unpoints
content it did not observe absent, under either token
dimension.
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. **Lock coverage**: the deleter FIRST acquires the
§3.7 advisory lock on the target locator (`user_id`,
`category_key`, slot) — the same lock ordinary writers hold —
before reading the pointer row or touching either store, and
holds it until step 2 commits or the deleter's session ends.
Every blob/pointer mutator for one locator — ordinary writes
(§3.7), managed deletion, and reconciliation repair (§4.5) —
therefore serializes on one lock: the reverse interleaving, in
which a deleter reads the old pointer and destroys a blob that a
lock-holding writer has committed in its step 1 but not yet
pointed, is unschedulable, because the deleter cannot enter its
protocol while the writer holds the lock. **Steps**: step 1
commits the content deletion to the user's brain repository,
recording the target row's `id` AND `generation` as read after
lock acquisition and 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 the recorded `id` AND `generation`. The token is
this PAIR, not the content hash and not the generation alone,
deliberately: every §3.7 upsert increments `generation` even
when it recommits byte-identical content (whose §4.4 hash is
unchanged), so a same-row rewrite moves the row past the
recorded generation; and the uuid `id` is minted fresh on every
insert and never reused, so a row REINCARNATION — the orphan row
deleted by §4.5 repair, then an ordinary write recreating the
locator with a fresh row whose `generation` is back at 1 —
cannot reproduce the recorded pair even though the counter value
repeats. In every such case step 2 matches zero rows, deletes
nothing, and the fresh content stays pointed: deletion never
removes a pointer row it did not observe. The lock excludes the
live interleavings; the pair token makes the crash windows
harmless — a deleter whose session dies after step 1 releases
the lock (§3.7 rule), and whatever runs afterward, its never-run
step 2 has no effect and a later deletion request is a NEW
protocol run reading current state. 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 both concurrent-rewrite interleavings — changed
content and identical content — the delete/reinsert
reincarnation, and the reverse writer/deleter interleaving with
its lock-removed control).
## 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.
**Cessation event.** The cascade is not silent: an AFTER DELETE
trigger on `profile_consents` inserts, for every deleted row whose
deletion the §5.5 trigger permitted (i.e. a cessation cascade),
one semantic **consent-cessation event** into the §1.8 audit
event table, in the same database transaction as the referent
deletion — if the deleting transaction rolls back, no cessation
event survives, and if it commits, the events commit with it.
Each event carries: the cause (`subject-cessation` or
`grantee-cessation`, plus the parent table and deleted key), the
initiating actor — a platform surface that deletes a referent
sets a transaction-local actor variable the trigger reads; absent
one, the event records actor `system:cascade` (a migration or
direct referential action) — an immutable snapshot of the ended
grant identifying its exact row and subject: the deleted consent
row's `id`, its `user_id` (the data subject), grantee type and
ref, `category_key`, `state`, `granted_at`, and `revoked_at`
and the event time. The row `id` and `user_id` are REQUIRED
precisely because the cause alone identifies only the ceasing
referent: when two users hold grants to one ceasing grantee, the
cascade removes both rows in one transaction, and only the
per-row snapshot distinguishes which subject's grant each event
records. The snapshot is consent
metadata, never profile content, so §3.1 is not implicated. The
audit table therefore records both ends of every grant's life:
the mutation events written at grant/revoke time survive the
cascade (audit events are never cascade-deleted), and the
cessation event records when, why, and by whom the active grant
ended. The same rule already governs subject cessation via the
`user_id` FK. §7.12 witnesses the subject predicate, its refusal
complement, the cascade, and the cessation event's atomicity,
fields, and survival; 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` (§3.6 binding) and `agent_id` is
`uuid` matching `agents.id` (§5.2 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, bigint,
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
interleavings (two):** deletion step 1 commits and the deleting
session is fault-injected dead before step 2 — the §4.7 lock
releases with the session, opening the crash window; a
concurrent §3.7 write then recreates the same `brain_ref` and
completes its pointer upsert; the stale step-2 compare-and-delete
is then replayed with the recorded token. The witness drives
this once with CHANGED
content (new `content_hash`, incremented `generation`) and once
with BYTE-IDENTICAL content (unchanged `content_hash`,
incremented `generation` — the ABA case a hash token would
miss), and in both 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.
**Delete/reinsert reincarnation (third interleaving):** in the
same crash window, §4.5 reconciliation first removes the
now-orphan pointer row; an ordinary §3.7 write then recreates
the locator as a FRESH row — new `id`, `generation` back at 1
and therefore EQUAL to the recorded generation value; the
replayed stale step 2 is asserted to match ZERO rows (the
recorded `id` names the dead incarnation), and the fresh pointer
survives with its content resolving and verifying — the case a
bare generation token fails. **Reverse writer/deleter
interleaving:** a §3.7 writer holds the locator lock and has
committed its step-1 brain content but not its pointer upsert
when a managed deletion for the same locator starts. The witness
asserts the deletion BLOCKS at §4.7 lock acquisition until the
writer's pointer upsert and release, and that afterward no
acknowledged write's content is absent and no pointer names
absent content; a lock-removed control schedules the deleter's
observation and content removal inside the writer's
step-1-to-step-2 gap and asserts the witness DETECTS the
resulting acknowledged-write loss, establishing that the lock is
what excludes it. **Lock-held contention control (one-cycle
bound):** starting from a PRE-EXISTING orphan, a §3.7 writer
session holds the orphan's locator lock while the triggered
reconciliation's sweep pass runs. The witness asserts the pass
completes without blocking and without repairing the held
locator (the try-acquire skip), that the cycle does NOT report
completion while the lock is held, and that after the holder
releases, the queued blocking retry deletes the orphan under
the recorded (`id`, `generation`) token BEFORE the cycle
reports completion — the orphan is gone by cycle completion,
proving the §4.5 bound under contention. A repeated-contention
variant hands the lock to a second holder the moment the first
releases and asserts the queued locator is retried until
acquired — the orphan is still deleted by cycle completion once
the last holder releases, never dropped after one failed
attempt. A **hold-deadline control** starts from a pre-existing
orphan, has a live holder acquire the orphan's locator lock and
stall past the declared §3.7 hold deadline WITHOUT ending its
session (idle transaction kept open), and asserts the holder's
session is terminated at the deadline, the lock releases
without any voluntary action by the holder, and the queued
blocking retry then deletes the orphan under the recorded
token before the cycle reports completion — proving termination
does not depend on a cooperative holder. A **grant-order
control** blocks the end-of-cycle retry behind a holder, keeps
a stream of later contenders requesting the same locator lock
while the retry waits, and asserts the retry acquires before
every later contender and the cycle completes within its
declared maximum duration — proving later arrivals cannot
starve the queued repair, and exercising the declared
cycle-duration limit as a checked bound rather than prose. A
static control asserts the deletion implementation conditions on
BOTH the recorded row `id` and `generation` — not on
`content_hash` and not on `generation` alone — a second
static control asserts the managed-deletion and
reconciliation-repair implementations each acquire the §3.7
locator lock before their observation reads (§4.5, §4.7), and a
third static control asserts the reconciliation implementation
queues a failed try-acquire for a within-cycle blocking retry —
an implementation that defers a skipped locator to a later
cycle, or that reports cycle completion with a queued locator
unprocessed, FAILS. A fourth static control asserts every
lock-holding path (ordinary write §3.7, managed deletion §4.7,
reconciliation repair §4.5) is configured with the declared
§3.7 hold deadline — an implementation with any unbounded
lock-holding path FAILS.
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). **Cessation events:** the same deletions
each produce one consent-cessation event per removed row in the
§1.8 audit event table, carrying the §5.4 fields (cause with
parent table and key, actor — the surface-set actor when one
initiated the deletion, `system:cascade` otherwise — the grant
snapshot including the deleted consent row's `id` and `user_id`,
and time); a **two-subject discrimination witness** has two
distinct users each hold a `granted` row to ONE agent grantee
for the SAME category, deletes that grantee's `agents` row, and
asserts exactly two cessation events exist and that each event's
snapshot carries a distinct consent-row `id` and the correct
`user_id` — every event names exactly which subject's grant of
which row ended; an atomicity control rolls the deleting
transaction back after the parent DELETE and asserts no
cessation event persists (no orphan event without a deletion,
no deletion without its events); the events survive subsequent
operation; and a subject-cessation (`users` row) deletion
produces its events 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 its 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); killed after the pointer commit
but before the acknowledgment reaches the caller, the retry
executes under the locator lock, observes the converged state,
returns success, and the witness asserts exactly one pointer with
a verifying hash and an unchanged answer (idempotent completion,
no duplicate content commit that changes the served bytes).
**Same-locator interleaving:** two concurrent writers to one
(user, category, slot) run to completion; the witness asserts
both are serialized by the §3.7 advisory lock (their step-1/step-2
spans do not interleave), the final pointer's hash verifies
against the blob committed at the locator, and no `mismatch_at`
is stamped by a subsequent read; a control with the lock removed
schedules the divergent order (A's pointer upsert after B's
content commit) and asserts the witness DETECTS the divergence —
proving the lock is what excludes it. **Stale retry:** writer A
is interrupted before acknowledgment; writer B then completes a
full write to the same locator; A's retry executes under the
lock and the witness asserts the end state is consistent (the
pointer's hash verifies against the current blob — last
completed write wins, no permanent mismatch). **Locator
injectivity:** the declared scheme maps every distinct
(category_key, slot) pair in the registry to a distinct
`brain_ref` (exhaustive at version 1: seven categories x the
`answer` slot), and a deliberately colliding scheme control
fails the witness. 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.7).
## 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). Each cascaded
deletion additionally emits a consent-cessation audit event —
written by an AFTER DELETE trigger in the same transaction as the
referent deletion, carrying cause, actor (surface-set or
`system:cascade`), the grant snapshot — which, per revision 6,
includes the deleted consent row's `id` and its `user_id`, so
events from different subjects' grants to one ceasing grantee
remain distinguishable — and time — so the audit
trail records when and why each active grant ended (§5.4,
§7.12). 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, the single
subject 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 — with the revision-5
locator scheme (injective over (category, slot), the slot set
fixed to `answer` at version 1, stable across releases) and the
per-locator advisory lock spanning both steps — held, per
revision 6, by EVERY blob/pointer mutator for the locator:
ordinary writers, managed deletion, and reconciliation repair
(try-acquire with the revision-7 within-cycle blocking retry,
so a failed try-acquire defers repair to the end of the cycle,
never past it; per revision 8 every hold runs under a declared
finite hold deadline with arrival-order grants, so the deferred
repair — and with it cycle completion — is wall-clock bounded,
not merely eventual), so live interleavings between any two
mutators are excluded by one serialization protocol (§3.7,
§4.5, §4.7, §7.16).
17. The compare-and-delete managed-deletion step: deletion step 2 is
conditioned on the PAIR of the target row's uuid `id` (the
immutable row-incarnation identity) and its monotonic
`generation` counter, both recorded in step 1 under the locator
lock (revision 6; the revision-4 `content_hash` token was
withdrawn as ABA-prone under identical-content rewrites, and the
revision-5 bare `generation` token is withdrawn as ABA-prone
under delete/reinsert reincarnation, where the counter restarts
at 1), so a stale deletion surviving a crash window is never
able to unpoint a fresh row — changed, byte-identical, or
reincarnated (§4.1, §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?