docs: custody-schema contract revision 4 (sol r3 residuals F1/F2/F4/F6/F7/F9 + F10/F11)
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
This commit is contained in:
@@ -52,6 +52,31 @@ 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).
|
||||
|
||||
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
|
||||
@@ -70,8 +95,8 @@ with contract 6 (`mode-conversion.md`), identity with
|
||||
3. **Pointer**: a database record referencing sensitive content that
|
||||
lives in a user brain, carrying no content (§4).
|
||||
4. **Grantee**: a non-subject principal that may be granted access to a
|
||||
user's sensitive content: an enrolled agent, a connector, or a
|
||||
platform feature (§5.2).
|
||||
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
|
||||
@@ -152,18 +177,37 @@ with contract 6 (`mode-conversion.md`), identity with
|
||||
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).
|
||||
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. 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).
|
||||
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
|
||||
|
||||
@@ -186,7 +230,7 @@ with contract 6 (`mode-conversion.md`), identity with
|
||||
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)
|
||||
`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
|
||||
@@ -195,6 +239,36 @@ with contract 6 (`mode-conversion.md`), identity with
|
||||
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: step 1 commits the
|
||||
content to the owning user's brain at a deterministic locator —
|
||||
the implementing PR declares a locator scheme that maps (category,
|
||||
answer slot) to one `brain_ref`, so a retry of the same write
|
||||
resolves to the same path; 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)) 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. The client's retry —
|
||||
the write was never acknowledged — recommits the same locator and
|
||||
completes the pointer upsert, converging to the pointed state.
|
||||
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 both interruption points
|
||||
and drives the retry convergence.
|
||||
|
||||
## 4. Pointer schema
|
||||
|
||||
@@ -203,7 +277,7 @@ with contract 6 (`mode-conversion.md`), identity with
|
||||
| Column | Type | Constraints |
|
||||
| ---------------- | ----------- | ------------------------------------------------------ |
|
||||
| `id` | uuid | primary key |
|
||||
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE |
|
||||
| `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) |
|
||||
@@ -276,32 +350,41 @@ with contract 6 (`mode-conversion.md`), identity with
|
||||
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
|
||||
brain repository, recording the `content_hash` 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
|
||||
`content_hash` equal to the recorded value. A concurrent §3.6
|
||||
write that recreates the same locator with new content commits a
|
||||
new hash on the pointer row, so step 2's condition fails, deletes
|
||||
nothing, and the fresh content stays pointed — deletion never
|
||||
removes a pointer for content it did not delete. 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).
|
||||
any compensation write content into the database (witness §7.10,
|
||||
including the concurrent-rewrite interleaving).
|
||||
|
||||
## 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`) |
|
||||
| `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 |
|
||||
| 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
|
||||
@@ -317,15 +400,18 @@ NULL)` — and the partial unique index UNIQUE (`user_id`,
|
||||
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.
|
||||
- `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,
|
||||
@@ -347,26 +433,31 @@ NULL)` — and the partial unique index UNIQUE (`user_id`,
|
||||
grantee, category) means no access. There are no implicit grants,
|
||||
no platform-admin bypass, and no mode in which default-deny is
|
||||
suspended.
|
||||
4. **Mutation authority — two disjoint predicates.** Every consent
|
||||
mutation must satisfy exactly one of:
|
||||
- **Subject mutation**: the authenticated actor IS the row's data
|
||||
subject — the server asserts `actor` equals the principal of the
|
||||
row's `user_id` before the mutation commits. This is the only
|
||||
predicate under which rows are created or granted. A platform
|
||||
admin has no consent-mutation capability over another user's
|
||||
rows — an admin self-grant is refused at write time, closing the
|
||||
write-side route around §5.3.
|
||||
- **System auto-revocation** (disclosed, §8): the actor is the
|
||||
single named platform system principal, the mutation is a
|
||||
granted → revoked flip and nothing else, and the server has
|
||||
verified the precondition that the row's grantee has ceased to
|
||||
exist (e.g. agent retirement under `identity-lifecycle.md`). A
|
||||
system-actor mutation whose precondition does not hold, or that
|
||||
is anything other than a revocation, is refused.
|
||||
|
||||
The predicates are disjoint (the system principal is not a data
|
||||
subject); §7.12 witnesses both and their refusal complements, and
|
||||
every §7.3-enumerated consent-mutation route asserts one of them.
|
||||
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. The
|
||||
durable record of the grants and their lifecycle is the audit
|
||||
event table (§1.8): every mutation carried an `audit_event_id`,
|
||||
and audit events are not deleted by the cascade. The same rule
|
||||
already governs subject cessation via the `user_id` FK. §7.12
|
||||
witnesses the subject predicate, its refusal complement, and the
|
||||
cascade; 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
|
||||
@@ -374,10 +465,17 @@ NULL)` — and the partial unique index UNIQUE (`user_id`,
|
||||
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
|
||||
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
|
||||
@@ -446,9 +544,13 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
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,
|
||||
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).
|
||||
(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` and `agent_id` is `uuid` matching
|
||||
`agents.id` (§3.6 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,
|
||||
@@ -465,9 +567,23 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
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.
|
||||
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
|
||||
@@ -493,13 +609,25 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
seeded data moves the values to brains, creates pointers, and
|
||||
leaves zero relational values (§2.4); (g) fence race: a profile
|
||||
write transaction opened before a reclassification migration takes
|
||||
its exclusive lock either commits before the scan (its value is
|
||||
swept) or blocks and, on commit, routes by the new version — the
|
||||
witness drives both interleavings and asserts no relational value
|
||||
for the reclassified category exists after either completes
|
||||
(§2.6); (h) with `custody_config.registry_version` set to a value
|
||||
the release does not declare, profile writes and consent decisions
|
||||
are refused (§2.7); (i) sensitive → non-sensitive: after the
|
||||
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).
|
||||
@@ -535,7 +663,14 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
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.
|
||||
the database or restored to the brain. **Concurrent-rewrite
|
||||
interleaving:** deletion step 1 commits; before step 2 runs, a
|
||||
concurrent §3.7 write recreates the same `brain_ref` with new
|
||||
content and completes its pointer upsert (new `content_hash`);
|
||||
step 2 then executes its compare-and-delete — the witness 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 (§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
|
||||
@@ -543,8 +678,9 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
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
|
||||
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
|
||||
@@ -554,12 +690,14 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
12. **Mutation-authority witnesses:** a platform admin attempting to
|
||||
create a grant on another user's data is refused at write time; a
|
||||
hierarchy owner or manager likewise; the data subject succeeds;
|
||||
`actor` equals the subject's principal on every subject-mutation
|
||||
row; retiring an enrolled agent auto-revokes its active rows with
|
||||
the system principal as `actor` and granted → revoked as the only
|
||||
change; a system-principal mutation whose grantee still exists,
|
||||
or that attempts anything other than a revocation, is refused —
|
||||
the two §5.4 predicates are witnessed as disjoint (§5.4).
|
||||
`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).
|
||||
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
|
||||
@@ -567,8 +705,12 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
second concurrent grant attempt for the same key violates the
|
||||
§5.1 partial unique index; a direct SQL UPDATE flipping a revoked
|
||||
row back to `granted`, an UPDATE altering any other column of a
|
||||
revoked row, and a DELETE of any row are each rejected by the
|
||||
§5.5 trigger (§5.5).
|
||||
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
|
||||
@@ -581,6 +723,19 @@ declared profile table" means `profile_answers` (§3.5).
|
||||
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 both 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). 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.6).
|
||||
|
||||
## 8. Drafting additions (PRD §12.1 disclosure)
|
||||
|
||||
@@ -591,9 +746,14 @@ ratification; none is claimed as a PRD mandate, and each is severable:
|
||||
that is empty at version 1 (§5.2).
|
||||
2. Append-only consent history: re-grants insert new rows; revoked
|
||||
rows are retained unmutated, enforced by trigger (§5.5).
|
||||
3. System-actor auto-revocation when a grantee ceases to exist, as a
|
||||
precondition-checked predicate disjoint from subject mutation
|
||||
(§5.4).
|
||||
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). 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
|
||||
@@ -605,16 +765,42 @@ ratification; none is claimed as a PRD mandate, and each is severable:
|
||||
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).
|
||||
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, 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).
|
||||
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 (§3.7, §7.16).
|
||||
17. The compare-and-delete managed-deletion step: deletion step 2 is
|
||||
conditioned on the `content_hash` recorded in step 1, so a
|
||||
concurrent recreate of the same locator is never unpointed by a
|
||||
stale deletion (§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
|
||||
|
||||
Reference in New Issue
Block a user