960 lines
59 KiB
Markdown
960 lines
59 KiB
Markdown
# Data Custody Contract — Pointer and Consent Schema (D14)
|
||
|
||
Status: DRAFT — awaiting ratification (webui-audit S2, contract 7 of 9).
|
||
Authority: PRD D14/D6 (Part I §7) — sensitive profile categories live in
|
||
the **user's own brain ONLY**; PostgreSQL holds structural data, consent
|
||
records, and pointers — never the content; "user data does not leak" is
|
||
enforced by architecture, not policy. The Standalone split is a MAY with
|
||
a recommended-default (kept for conversion forward-compatibility, D3).
|
||
PRD D4 (Part I §6) — profile answers feed `USER.md` and/or the user's
|
||
data store subject to the custody rule; connectors carry granular
|
||
agentic-access consent. PRD D6 — estate brains hold operational records;
|
||
only product-relevant material migrates into repository docs.
|
||
|
||
Revision 2 (sol r1 findings F1–F6): the registry is now a full profile
|
||
classifier that can represent non-sensitive rows, with an exact row
|
||
shape, versioning, refusal semantics for unknown keys, and transition
|
||
rules (F1); the consent model gains a concrete grantee reference model,
|
||
an active-row uniqueness constraint, append-only re-grant semantics,
|
||
and explicit mutation authority (F2); the pointer schema names its
|
||
exact column set, binds the one-pointer rule to a database constraint,
|
||
defines the `brain_ref` grammar and owner-bound resolution, replaces
|
||
the unkeyed hash with a keyed construction, and bounds orphan repair
|
||
(F3); the witnesses import the hierarchy contract's §6.2 column
|
||
allowlist and §6.3-style closed route inventories, add negative
|
||
controls and a column-type allowlist, and cover every binding rule
|
||
(F4); the Standalone physical split, both valid layouts, the election
|
||
record, and v1 phase timing are defined consistently with the wizard
|
||
and mode-conversion contracts (F5); drafting additions are disclosed in
|
||
§8 and the ruling request is one sentence with one decision (F6).
|
||
|
||
Revision 3 (sol r2 findings F1–F9): reclassification gains a
|
||
custody-config write fence (shared/exclusive row lock) so no
|
||
concurrent writer can commit a stale-version route, a declared
|
||
supported-version refusal, and witnesses for both transition
|
||
directions (F1). The grantee reference becomes three per-type FK
|
||
columns with a generated discriminant, real referent tables for
|
||
`connector` and `feature` (contract-owned, empty at v1), the
|
||
enrolled-agent binding named to the rank-4 family's table, and
|
||
revoked-row immutability enforced by a database trigger (F2). The
|
||
content hash is domain-separated per (user, category, locator) with
|
||
canonical bytes, an embedded key id, and a rotation rule; the audit FK
|
||
referent and the non-sensitive profile table (`profile_answers`) are
|
||
named; tree-object resolution prohibits symlink escape (F3). The §7
|
||
route witnesses import hierarchy §6.3's full prong set, scan
|
||
`plugins/`, extend the no-content probe to every platform table, and
|
||
add the missing negatives (F4). `custody_config` is singleton by
|
||
constraint (F5). §8 discloses the previously omitted policies (F6).
|
||
Mutation authority is split into two disjoint actor predicates —
|
||
subject mutations and the precondition-checked system auto-revocation
|
||
(F7). Hash mismatch gets a representable pointer state with a defined
|
||
terminal outcome (F8). Managed deletion is an ordered git-then-database
|
||
protocol with fault-injection witnesses at both interruption points,
|
||
replacing the impossible cross-store transaction (F9).
|
||
|
||
Revision 4 (sol r3 findings F1/F2/F4/F6/F7/F9 residual, F10/F11 new):
|
||
the write fence pins `READ COMMITTED` isolation and lock-first
|
||
statement order for the reclassification migration, and the
|
||
supported-version declaration becomes a named source symbol checked
|
||
under the same lock as each decision (F1). The user foreign keys
|
||
adopt the identity store's actual key type — `users.id` is `text`
|
||
(BetterAuth identifiers), so every `user_id` column is `text` (F10).
|
||
The agent referent binds to the live platform `agents` table by
|
||
symbol, since `identity-lifecycle.md` defines no enrollment surface;
|
||
an amendment re-binds if one arrives (F2). The system auto-revocation
|
||
predicate is withdrawn entirely: grantee cessation is handled
|
||
structurally by `ON DELETE CASCADE` from the grantee referent (as
|
||
subject cessation already is from `users`), default-deny ends access,
|
||
and the audit event table keeps the durable history — which removes
|
||
the trigger/actor contradiction rather than patching it (F2, F7).
|
||
Sensitive-write routes gain a static write-set closure witness so a
|
||
derived representation cannot land in any generic platform table
|
||
(F4). Managed deletion step 2 becomes a compare-and-delete on the
|
||
hash removed in step 1, witnessed against a concurrent rewrite (F9).
|
||
Ordinary sensitive writes get an ordered content-first protocol with
|
||
deterministic locators, ack-after-pointer-commit, idempotent retry,
|
||
and fault-injection witnesses (F11). §8 discloses every rule above
|
||
plus the previously undisclosed §4.3 committed-tree/symlink rule
|
||
(F6).
|
||
|
||
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).
|
||
|
||
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). 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.
|
||
The repair DELETE is generation-conditioned like §4.7 step 2: it
|
||
records the row's `generation` when it observes the content
|
||
absent and conditions the DELETE on it, so a concurrent §3.7
|
||
write that recommits the locator between the observation and the
|
||
delete (bumping `generation`) makes the repair match zero rows —
|
||
reconciliation never unpoints content it did not observe absent.
|
||
6. **Mismatch state.** A failed §4.4 verification stamps the pointer's
|
||
`mismatch_at` and the read is refused; every subsequent read of a
|
||
pointer with `mismatch_at` set is refused without re-serving
|
||
content. Reconciliation (§4.5 triggers) re-verifies each flagged
|
||
pointer: verification success clears `mismatch_at`; content absent
|
||
→ the pointer is deleted (§4.5); persistent mismatch → the pointer
|
||
is retained flagged with reads refused — the terminal outcome. The
|
||
subject's next successful profile write for that category replaces
|
||
the content, recomputes the hash, and clears the flag. Mismatch
|
||
handling never deletes brain content and never copies content into
|
||
the database.
|
||
7. **Managed deletion protocol.** A managed deletion of sensitive
|
||
content spans two stores and is an ordered protocol, not a single
|
||
transaction: step 1 commits the content deletion to the user's
|
||
brain repository, recording the `generation` of the row it
|
||
intends to delete as read before step 1; step 2, only after step 1
|
||
has committed, deletes the pointer row in its own database
|
||
transaction as a **compare-and-delete** — the DELETE is
|
||
conditioned on (`user_id`, `category_key`, `brain_ref`) AND
|
||
`generation` equal to the recorded value. The token is the
|
||
generation, not the content hash, deliberately: every §3.7 upsert
|
||
increments `generation` even when it recommits byte-identical
|
||
content (whose §4.4 hash is unchanged), so a concurrent §3.7
|
||
write that recreates the same locator — with new content OR with
|
||
identical content — moves the row past the recorded generation,
|
||
step 2's condition matches zero rows, deletes nothing, and the
|
||
fresh content stays pointed. Deletion never removes a pointer for
|
||
a write it did not observe; the hash-token ABA (identical bytes
|
||
reproducing the recorded value) is unrepresentable because the
|
||
counter never repeats. 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).
|
||
|
||
## 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) — a snapshot of the ended grant
|
||
(grantee type and ref, `category_key`, `state`, `granted_at`,
|
||
`revoked_at`), and the event time. 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; before step 2
|
||
runs, a concurrent §3.7 write recreates the same `brain_ref` and
|
||
completes its pointer upsert; step 2 then executes its
|
||
compare-and-delete. 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. A
|
||
static control asserts the deletion implementation conditions on
|
||
`generation`, not on `content_hash` (§4.7).
|
||
11. **Default-deny and granularity witnesses:** an agent grantee with
|
||
no active row is refused; with a `granted` row for category A
|
||
only, category B is refused; with agent X granted, agent Y of the
|
||
same type is refused for the same (user, category); `connector`
|
||
and `feature` are exercised via their FK refusal paths, since
|
||
both registries are empty at version 1 (§5.2); a consent insert
|
||
naming a nonexistent agent id violates the `agent_id` FK, and the
|
||
witness pins that FK's referenced table by symbol identity to the
|
||
`agents` table exported by the shared schema package (§5.2
|
||
binding); a platform admin requesting
|
||
another user's sensitive content through any production read
|
||
surface is refused — admins are not a grantee type and
|
||
default-deny applies (the access-time complement of §7.12's
|
||
write-time refusal); a hierarchy owner or manager grant over the
|
||
subject confers no consent access (§5.7); with the consent state
|
||
unreadable, evaluation refuses (§5.6).
|
||
12. **Mutation-authority witnesses:** a platform admin attempting to
|
||
create a grant on another user's data is refused at write time; a
|
||
hierarchy owner or manager likewise; the data subject succeeds;
|
||
`actor` equals the subject's principal on every mutation row —
|
||
there is no non-subject mutation path (§5.4). **Cessation
|
||
cascade:** deleting an agent's `agents` row removes that agent's
|
||
consent rows in the same transaction via the FK cascade, the next
|
||
consent evaluation for that grantee refuses (no row), the linked
|
||
audit events survive, and no other grantee's rows are touched; a
|
||
connector-registry amendment migration removing a key cascades
|
||
identically (§5.4). **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, and time); 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, 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 that serializes
|
||
same-locator writers (§3.7, §7.16).
|
||
17. The compare-and-delete managed-deletion step: deletion step 2 is
|
||
conditioned on the monotonic `generation` counter recorded in
|
||
step 1 (revision 5; the revision-4 `content_hash` token is
|
||
withdrawn as ABA-prone under identical-content rewrites), so a
|
||
concurrent recreate of the same locator — changed or
|
||
byte-identical — is never unpointed by a stale deletion (§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?
|