docs: custody-schema contract revision 3 (sol re-review residuals F1-F9)
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
+324
-103
@@ -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 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).
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user