docs: custody-schema contract revision 3 (sol re-review residuals F1-F9)
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
fred
2026-08-26 20:33:21 -05:00
parent 8079d8dc91
commit 671b872ec9
+324 -103
View File
@@ -28,6 +28,30 @@ record, and v1 phase timing are defined consistently with the wizard
and mode-conversion contracts (F5); drafting additions are disclosed in
§8 and the ruling request is one sentence with one decision (F6).
Revision 3 (sol r2 findings F1F9): reclassification gains a
custody-config write fence (shared/exclusive row lock) so no
concurrent writer can commit a stale-version route, a declared
supported-version refusal, and witnesses for both transition
directions (F1). The grantee reference becomes three per-type FK
columns with a generated discriminant, real referent tables for
`connector` and `feature` (contract-owned, empty at v1), the
enrolled-agent binding named to the rank-4 family's table, and
revoked-row immutability enforced by a database trigger (F2). The
content hash is domain-separated per (user, category, locator) with
canonical bytes, an embedded key id, and a rotation rule; the audit FK
referent and the non-sensitive profile table (`profile_answers`) are
named; tree-object resolution prohibits symlink escape (F3). The §7
route witnesses import hierarchy §6.3's full prong set, scan
`plugins/`, extend the no-content probe to every platform table, and
add the missing negatives (F4). `custody_config` is singleton by
constraint (F5). §8 discloses the previously omitted policies (F6).
Mutation authority is split into two disjoint actor predicates —
subject mutations and the precondition-checked system auto-revocation
(F7). Hash mismatch gets a representable pointer state with a defined
terminal outcome (F8). Managed deletion is an ordered git-then-database
protocol with fault-injection witnesses at both interruption points,
replacing the impossible cross-store transaction (F9).
This contract binds the profile-category registry (§2), the custody
placement rule (§3), the pointer schema (§4), the consent schema and its
evaluation (§5), mode application (§6), witnesses (§7), and disclosed
@@ -56,6 +80,15 @@ with contract 6 (`mode-conversion.md`), identity with
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
@@ -113,6 +146,24 @@ with contract 6 (`mode-conversion.md`), identity with
cannot be read at decision time, every routing and consent decision
that depends on it is refused. There is no cached-default or
assume-sensitive fallback that performs a write.
6. **Write fence.** Every profile-write transaction reads the current
registry version by taking a **shared lock** on the `custody_config`
row (`SELECT ... FOR SHARE`) inside the same database transaction
that performs the write, and routes by the version so read. A
reclassification migration takes an **exclusive lock** on that row
(`SELECT ... FOR UPDATE`) before its scan and holds it through the
value moves and the version bump. Consequently a concurrent writer
either commits before the migration's scan begins — its value is
then swept by the scan — or blocks until the migration commits and
then routes by the new version. No interleaving exists in which a
value routed under the old version commits after the migration's
scan (witness §7.6g).
7. **Supported version.** Each release declares the exact registry
version its code implements. If the declared version does not equal
`custody_config.registry_version`, every profile routing and
consent decision is refused — a readable but unsupported registry
is a refusal, never best-effort routing under either version
(witness §7.6h).
## 3. Custody placement rule
@@ -125,29 +176,41 @@ with contract 6 (`mode-conversion.md`), identity with
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`its
declared platform table; unknown → refusal (§2.3). The routing
decision is made server-side from the registry at its current
version; a client-supplied classification or routing override is
ignored.
`sensitive` → brain write + pointer upsert; `non-sensitive`the
`profile_answers` table (§3.5); unknown → refusal (§2.3). The
routing decision is made server-side from the registry at its
current version, inside the §2.6 write fence; a client-supplied
classification or routing override is ignored.
4. D6 boundary: operational records stay in estate brains and are
linked, not migrated. This contract governs user-profile custody
only and creates no new obligation on estate brains.
5. **Non-sensitive profile table.** Non-sensitive profile answers land
in exactly one declared table, `profile_answers`, columns exactly:
`id` (uuid, primary key), `user_id` (uuid, NOT NULL, FK → users(id)
ON DELETE CASCADE), `category_key` (text, NOT NULL, FK →
profile_category_registry(category_key)), `value` (text, NOT NULL),
`created_at` and `updated_at` (timestamptz, NOT NULL), and
`audit_event_id` (uuid, NOT NULL, FK → the §1.8 audit event table);
plus UNIQUE (`user_id`, `category_key`). A value may sit in this
table ONLY while its category's registry classification is
`non-sensitive` (§2.4 migrates the rows on reclassification). No
other platform table stores profile answer content.
## 4. Pointer schema
1. **Exact columns.** Table `profile_pointers`, columns exactly:
| Column | Type | Constraints |
| ---------------- | ----------- | ----------------------------------------------------------- |
| `id` | uuid | primary key |
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE |
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
| `brain_ref` | text | NOT NULL, CHECK against the §4.3 grammar |
| `content_hash` | text | NOT NULL (§4.4 construction) |
| `created_at` | timestamptz | NOT NULL |
| `updated_at` | timestamptz | NOT NULL |
| `audit_event_id` | uuid | NOT NULL, FK → the platform audit log table (audit linkage) |
| Column | Type | Constraints |
| ---------------- | ----------- | ------------------------------------------------------ |
| `id` | uuid | primary key |
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE |
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
| `brain_ref` | text | NOT NULL, CHECK against the §4.3 grammar |
| `content_hash` | text | NOT NULL (§4.4 construction) |
| `created_at` | timestamptz | NOT NULL |
| `updated_at` | timestamptz | NOT NULL |
| `mismatch_at` | timestamptz | NULL unless the pointer is in the §4.6 mismatch state |
| `audit_event_id` | uuid | NOT NULL, FK → the §1.8 audit event table |
plus the database constraint UNIQUE (`user_id`, `category_key`,
`brain_ref`) — the one-pointer rule is a constraint, not a
@@ -166,14 +229,31 @@ with contract 6 (`mode-conversion.md`), identity with
(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.
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:<hex>` — HMAC-SHA-256 over the canonical content
bytes, keyed with a platform integrity key held in the secrets
backend and never stored in the database or any repository. Because
the key is external, the stored value is not an offline dictionary
oracle for low-entropy answers. On read, a hash mismatch refuses
the read and flags the pointer for §4.5 reconciliation.
`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
@@ -182,59 +262,122 @@ with contract 6 (`mode-conversion.md`), identity with
declares (at most daily). A pointer whose content is absent is
deleted by the next triggered reconciliation — an orphan survives
at most one cycle, and repair performs no database content write.
6. **Mismatch state.** A failed §4.4 verification stamps the pointer's
`mismatch_at` and the read is refused; every subsequent read of a
pointer with `mismatch_at` set is refused without re-serving
content. Reconciliation (§4.5 triggers) re-verifies each flagged
pointer: verification success clears `mismatch_at`; content absent
→ the pointer is deleted (§4.5); persistent mismatch → the pointer
is retained flagged with reads refused — the terminal outcome. The
subject's next successful profile write for that category replaces
the content, recomputes the hash, and clears the flag. Mismatch
handling never deletes brain content and never copies content into
the database.
7. **Managed deletion protocol.** A managed deletion of sensitive
content spans two stores and is an ordered protocol, not a single
transaction: step 1 commits the content deletion to the user's
brain repository; step 2, only after step 1 has committed, deletes
the pointer row in its own database transaction. Interruption
before step 1 commits leaves both stores unchanged. Interruption
between the steps leaves a dangling pointer, which §4.5 repairs
toward deletion within one reconciliation cycle. At no point does
any compensation write content into the database (witness §7.10).
## 5. Consent schema and evaluation
1. **Exact columns.** Table `profile_consents`, columns exactly:
| Column | Type | Constraints |
| ---------------- | ----------- | --------------------------------------------------------------- |
| `id` | uuid | primary key |
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject) |
| `grantee_type` | text | NOT NULL, CHECK in (`agent`, `connector`, `feature`) |
| `grantee_id` | text | NOT NULL (§5.2 per-type referential integrity) |
| `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 platform audit log table |
| Column | Type | Constraints |
| ---------------- | ----------- | --------------------------------------------------------------------- |
| `id` | uuid | primary key |
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject) |
| `grantee_type` | text | NOT NULL, CHECK in (`agent`, `connector`, `feature`) |
| `agent_id` | uuid | NULL, FK → the enrolled-agent table (§5.2) |
| `connector_id` | text | NULL, FK → custody_connector_registry(connector_key) (§5.2) |
| `feature_key` | text | NULL, FK → custody_feature_registry(feature_key) (§5.2) |
| `grantee_ref` | text | generated stored: COALESCE(agent_id::text, connector_id, feature_key) |
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
| `state` | text | NOT NULL, CHECK in (`granted`, `revoked`) |
| `granted_at` | timestamptz | NOT NULL |
| `revoked_at` | timestamptz | CHECK ((state = 'granted') = (revoked_at IS NULL)) |
| `actor` | text | NOT NULL (the authenticated principal that recorded the change) |
| `audit_event_id` | uuid | NOT NULL, FK → the §1.8 audit event table |
plus the partial unique index UNIQUE (`user_id`, `grantee_type`,
`grantee_id`, `category_key`) WHERE `state = 'granted'` — at most
one active grant per (user, concrete grantee, category), as a
database constraint.
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 the pair
(`grantee_type`, `grantee_id`); its canonical display form is
`<grantee_type>:<grantee_id>` (e.g. `agent:<agent id>`). The
discriminant set is closed at the three CHECK values. Referential
integrity is per type: `agent` ids reference the enrolled-agent
registry of `identity-lifecycle.md`; `connector` ids reference the
platform connector registry; `feature` ids reference a closed
feature-key list owned by amendments to this contract, and that
list is EMPTY at version 1 (no feature grantee exists until an
amendment names one). A grant to one agent confers nothing on
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 **enrolled-agent table**: the
single platform table in which agent enrollment under
`identity-lifecycle.md` (the contract 5 §3.1 rank-4 command
family) records enrolled agents. No sibling contract names that
physical table, so the binding is by identity: the custody
migration orders after that table's migration, the implementing
PR binds this FK to it, and the §7.11 witness pins the physical
name by asserting the FK's referenced table is the one the
enrollment surface writes.
- `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_id`, so two same-type grantees are distinct rows.
`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.** Only the authenticated data subject may
create, grant, or revoke consent rows for their own data: the
server enforces that `actor` is the subject's principal and equals
the row's `user_id` before any consent mutation commits. A platform
admin has no consent-mutation capability over another user's rows —
an admin self-grant is refused at write time, closing the
write-side route around §5.3. One system exception (disclosed,
§8): when a grantee ceases to exist (e.g. agent retirement under
`identity-lifecycle.md`), the platform auto-revokes its active
rows, recording a system principal as `actor`.
4. **Mutation authority — two disjoint predicates.** Every consent
mutation must satisfy exactly one of:
- **Subject mutation**: the authenticated actor IS the row's data
subject — the server asserts `actor` equals the principal of the
row's `user_id` before the mutation commits. This is the only
predicate under which rows are created or granted. A platform
admin has no consent-mutation capability over another user's
rows — an admin self-grant is refused at write time, closing the
write-side route around §5.3.
- **System auto-revocation** (disclosed, §8): the actor is the
single named platform system principal, the mutation is a
granted → revoked flip and nothing else, and the server has
verified the precondition that the row's grantee has ceased to
exist (e.g. agent retirement under `identity-lifecycle.md`). A
system-actor mutation whose precondition does not hold, or that
is anything other than a revocation, is refused.
The predicates are disjoint (the system principal is not a data
subject); §7.12 witnesses both and their refusal complements, and
every §7.3-enumerated consent-mutation route asserts one of them.
5. **Revocation and re-grant.** Revocation flips exactly one active
row to `revoked` and stamps `revoked_at`; it is effective for every
access evaluated after the revoking write commits. Revoked rows are
retained as history and never mutated again. A re-grant after
retained as history and never mutated again — enforced by a
database trigger on `profile_consents` that permits UPDATE only
when it is the granted → revoked transition (the state flip plus
the `revoked_at` stamp, every other column unchanged) and rejects
every other UPDATE and every DELETE, so a revoked row cannot be
flipped back to `granted` by anyone, including through direct SQL
(witness §7.13). A re-grant after
revocation inserts a NEW row (append-only history) — repeated
grant/revoke cycles are represented as successive rows, and the
§5.1 partial unique index guarantees the old revoked rows cannot
@@ -263,13 +406,17 @@ with contract 6 (`mode-conversion.md`), identity with
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), `standalone_layout` (text, NOT NULL, CHECK in
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 platform audit log table). It is written at bootstrap and
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
@@ -291,23 +438,36 @@ with contract 6 (`mode-conversion.md`), identity with
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`, and `custody_config`.
`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/§6.3,
and no platform table outside the declared non-sensitive profile
tables carries profile answer content.
2. **Column-type allowlist witness:** the custody tables use only the
column types named in §2.1/§4.1/§5.1/§6.3 (uuid, text, integer,
timestamptz) — no bytea, json/jsonb, array, vector, or tsvector
custody tables' live column sets are exactly
§2.1/§4.1/§5.1/§5.2/§6.3 and `profile_answers` is exactly §3.5;
no platform table outside `profile_answers` carries profile
answer content; and every `audit_event_id` FK (§3.5, §4.1, §5.1,
§6.3) references the same physical table that the contract-1
(hierarchy §5.2) audit write path inserts into (§1.8 binding).
2. **Column-type allowlist witness:** the custody tables and
`profile_answers` use only the column types named in
§2.1/§3.5/§4.1/§5.1/§5.2/§6.3 (uuid, text, integer, timestamptz,
boolean) — no bytea, json/jsonb, array, vector, or tsvector
column exists in them, closing the encoded/derived-representation
routes by type rather than by probe alone.
3. **Closed write-route witness** (hierarchy contract §6.3 style): a
static, re-export-aware inventory over `apps/` and `packages/`
(production code, tests excluded) enumerates every module that
writes the custody tables or writes profile answers, and every
enumerated route implements §3.3 registry routing; a route outside
the enumeration fails the assertion.
3. **Closed write-route witness** (hierarchy contract §6.3, full
prong set): a static, re-export-aware inventory over `apps/`,
`packages/`, and `plugins/` (production code, tests excluded)
enumerates every module that writes the custody tables,
`profile_answers`, or profile answers generally, detecting access
through each of hierarchy §6.3's prongs — schema-symbol imports,
SQL string literals naming the tables, raw-execution primitives,
and runtime code construction — with the database client reachable
only through a closed importer allowlist. Every enumerated write
route implements §3.3 registry routing inside the §2.6 fence, and
every enumerated consent-mutation route asserts a §5.4 predicate;
a route outside the enumeration, or a client import outside the
allowlist, fails the assertion.
4. **Closed brain-read witness** (same style): the inventory enumerates
every production route that reads user-brain content on behalf of a
grantee, and every enumerated route calls the §5.6 consent
@@ -319,8 +479,9 @@ it scans; "the custody tables" means `profile_category_registry`,
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 the custody tables and declared profile
tables.
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
@@ -330,7 +491,18 @@ it scans; "the custody tables" means `profile_category_registry`,
`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).
leaves zero relational values (§2.4); (g) fence race: a profile
write transaction opened before a reclassification migration takes
its exclusive lock either commits before the scan (its value is
swept) or blocks and, on commit, routes by the new version — the
witness drives both interleavings and asserts no relational value
for the reclassified category exists after either completes
(§2.6); (h) with `custody_config.registry_version` set to a value
the release does not declare, profile writes and consent decisions
are refused (§2.7); (i) sensitive → non-sensitive: after the
version bump, existing brain content and pointers remain intact
and readable, nothing is materialized into the database, and the
next write for that category lands in `profile_answers` (§2.4).
7. **Server-side classification witness:** a client-supplied
classification or routing override on a profile write is ignored;
the registry decision is applied (§3.3).
@@ -339,33 +511,64 @@ it scans; "the custody tables" means `profile_category_registry`,
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).
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; a mismatch refuses the read and flags the pointer;
the database value alone, without the external key, does not equal
any unkeyed digest of the fixture content (oracle control).
10. **Orphan 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); a managed deletion removes its pointer in
the same transaction.
construction; the database value alone, without the external key,
does not equal any unkeyed digest of the fixture content (oracle
control); the same fixture content stored for two users, and for
two categories of one user, yields different stored hashes
(domain-separation control — no cross-row equality oracle); after
a key rotation, a pointer written under the old key id still
verifies and a new write stores the new key id; a mismatch refuses
the read and stamps `mismatch_at`, subsequent reads stay refused,
reconciliation of a persistently mismatched pointer retains it
flagged without deleting brain content or writing content to the
database, and the subject's re-write clears the flag (§4.4, §4.6).
10. **Orphan and deletion-protocol witnesses:** starting from a
PRE-EXISTING orphan (content already absent, pointer present),
the next triggered reconciliation deletes the pointer and writes
no content anywhere in the database (§4.5). The §4.7 protocol is
fault-injected at both interruption points: killed before the
brain commit, both stores are unchanged; killed between the brain
commit and the pointer delete, the dangling pointer is deleted by
the next reconciliation — and at no point is content written to
the database or restored to the brain.
11. **Default-deny and granularity witnesses:** an agent grantee with
no active row is refused; with a `granted` row for category A
only, category B is refused; with agent X granted, agent Y of the
same type is refused for the same (user, category); each of the
three grantee types is exercised — `feature` via its refusal path,
since the version-1 feature list is empty (§5.2).
same type is refused for the same (user, category); `connector`
and `feature` are exercised via their FK refusal paths, since
both registries are empty at version 1 (§5.2); a consent insert
naming a nonexistent agent id violates the `agent_id` FK, and the
witness pins that FK's referenced table as the one the enrollment
surface writes (§5.2 binding); a platform admin requesting
another user's sensitive content through any production read
surface is refused — admins are not a grantee type and
default-deny applies (the access-time complement of §7.12's
write-time refusal); a hierarchy owner or manager grant over the
subject confers no consent access (§5.7); with the consent state
unreadable, evaluation refuses (§5.6).
12. **Mutation-authority witnesses:** a platform admin attempting to
create a grant on another user's data is refused at write time; a
hierarchy owner or manager likewise; the data subject succeeds;
`actor` equals the subject's principal on every committed row
(§5.4).
`actor` equals the subject's principal on every subject-mutation
row; retiring an enrolled agent auto-revokes its active rows with
the system principal as `actor` and granted → revoked as the only
change; a system-principal mutation whose grantee still exists,
or that attempts anything other than a revocation, is refused —
the two §5.4 predicates are witnessed as disjoint (§5.4).
13. **Revocation/re-grant witnesses:** after revocation commits, the
next evaluation refuses and the revoked row persists unmutated; a
full grant → revoke → re-grant cycle yields two rows (one
revoked, one active) and access follows only the active row; a
second concurrent grant attempt for the same key violates the
§5.1 partial unique index (§5.5).
§5.1 partial unique index; a direct SQL UPDATE flipping a revoked
row back to `granted`, an UPDATE altering any other column of a
revoked row, and a DELETE of any row are each rejected by the
§5.5 trigger (§5.5).
14. **Self-access witness:** the data subject reads their own content
without consent rows (§5.6).
15. **Mode witnesses:** the custody-table schemas are byte-identical
@@ -376,24 +579,42 @@ it scans; "the custody tables" means `profile_category_registry`,
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).
user B's content (§6.6); inserting a second `custody_config` row
violates the §6.3 singleton constraint.
## 8. Drafting additions (PRD §12.1 disclosure)
The following are proposed drafting additions, visible here for
ratification; none is claimed as a PRD mandate, and each is severable:
1. The `feature` grantee type, with a closed feature-key list that is
empty at version 1 (§5.2).
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 (§5.5).
3. System-actor auto-revocation when a grantee ceases to exist (§5.4).
4. The `custody_config` election record for the Standalone layout
6.3).
5. The keyed `content_hash` construction and its mismatch handling
(§4.4).
6. The bounded dangling-pointer reconciliation policy (§4.5).
rows are retained unmutated, enforced by trigger (§5.5).
3. System-actor auto-revocation when a grantee ceases to exist, as a
precondition-checked predicate disjoint from subject mutation
5.4).
4. The `custody_config` election record for the Standalone layout,
singleton by constraint (§6.3).
5. The domain-separated keyed `content_hash` construction, its key
rotation rule, and the `mismatch_at` pointer state (§4.4, §4.6).
6. The bounded dangling-pointer reconciliation policy and the ordered
managed-deletion protocol (§4.5, §4.7).
7. The column-type allowlist verification requirement (§7.2).
8. The amendment-only, versioned registry classifier itself, its
unknown-key refusal, and both reclassification transition policies
(§2.1, §2.3, §2.4).
9. The custody-config write fence and the declared supported-version
refusal (§2.6, §2.7).
10. Subject-only consent mutation authority (§5.4, first predicate).
11. The declared non-sensitive profile table `profile_answers` and
its closed schema (§3.5).
12. The typed grantee referent columns, the
`custody_connector_registry` and `custody_feature_registry`
tables, and the audit-table binding by identity (§5.1, §5.2,
§1.8).
13. Shipping all §2–§6 schemas in v1 ahead of any sensitive write
surface (§6.4).
The revision-1 "reporting" rationale for relational
background/education storage is withdrawn; the traced rationale is