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:
@@ -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
|
and mode-conversion contracts (F5); drafting additions are disclosed in
|
||||||
§8 and the ruling request is one sentence with one decision (F6).
|
§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
|
This contract binds the profile-category registry (§2), the custody
|
||||||
placement rule (§3), the pointer schema (§4), the consent schema and its
|
placement rule (§3), the pointer schema (§4), the consent schema and its
|
||||||
evaluation (§5), mode application (§6), witnesses (§7), and disclosed
|
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
|
per-user brain repository separate from the estate mosaic-brain, as
|
||||||
opposed to the unsplit layout where it lives in a dedicated
|
opposed to the unsplit layout where it lives in a dedicated
|
||||||
user-files subtree of the single mosaic-brain (§6.2).
|
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
|
## 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
|
cannot be read at decision time, every routing and consent decision
|
||||||
that depends on it is refused. There is no cached-default or
|
that depends on it is refused. There is no cached-default or
|
||||||
assume-sensitive fallback that performs a write.
|
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
|
## 3. Custody placement rule
|
||||||
|
|
||||||
@@ -125,21 +176,32 @@ with contract 6 (`mode-conversion.md`), identity with
|
|||||||
of §4, the consent records of §5, the registry of §2, and the
|
of §4, the consent records of §5, the registry of §2, and the
|
||||||
custody configuration of §6.3. Nothing else.
|
custody configuration of §6.3. Nothing else.
|
||||||
3. Every write path for profile answers routes by the registry:
|
3. Every write path for profile answers routes by the registry:
|
||||||
`sensitive` → brain write + pointer upsert; `non-sensitive` → its
|
`sensitive` → brain write + pointer upsert; `non-sensitive` → the
|
||||||
declared platform table; unknown → refusal (§2.3). The routing
|
`profile_answers` table (§3.5); unknown → refusal (§2.3). The
|
||||||
decision is made server-side from the registry at its current
|
routing decision is made server-side from the registry at its
|
||||||
version; a client-supplied classification or routing override is
|
current version, inside the §2.6 write fence; a client-supplied
|
||||||
ignored.
|
classification or routing override is ignored.
|
||||||
4. D6 boundary: operational records stay in estate brains and are
|
4. D6 boundary: operational records stay in estate brains and are
|
||||||
linked, not migrated. This contract governs user-profile custody
|
linked, not migrated. This contract governs user-profile custody
|
||||||
only and creates no new obligation on estate brains.
|
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
|
## 4. Pointer schema
|
||||||
|
|
||||||
1. **Exact columns.** Table `profile_pointers`, columns exactly:
|
1. **Exact columns.** Table `profile_pointers`, columns exactly:
|
||||||
|
|
||||||
| Column | Type | Constraints |
|
| Column | Type | Constraints |
|
||||||
| ---------------- | ----------- | ----------------------------------------------------------- |
|
| ---------------- | ----------- | ------------------------------------------------------ |
|
||||||
| `id` | uuid | primary key |
|
| `id` | uuid | primary key |
|
||||||
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE |
|
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE |
|
||||||
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
|
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
|
||||||
@@ -147,7 +209,8 @@ with contract 6 (`mode-conversion.md`), identity with
|
|||||||
| `content_hash` | text | NOT NULL (§4.4 construction) |
|
| `content_hash` | text | NOT NULL (§4.4 construction) |
|
||||||
| `created_at` | timestamptz | NOT NULL |
|
| `created_at` | timestamptz | NOT NULL |
|
||||||
| `updated_at` | timestamptz | NOT NULL |
|
| `updated_at` | timestamptz | NOT NULL |
|
||||||
| `audit_event_id` | uuid | NOT NULL, FK → the platform audit log table (audit linkage) |
|
| `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`,
|
plus the database constraint UNIQUE (`user_id`, `category_key`,
|
||||||
`brain_ref`) — the one-pointer rule is a constraint, not a
|
`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
|
(the resolver takes the owner from the row, never from the
|
||||||
locator); the locator carries no repository, host, or user
|
locator); the locator carries no repository, host, or user
|
||||||
component, so a cross-user or traversal reference is
|
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
|
4. **`content_hash` construction.** `content_hash` is
|
||||||
`hmac-sha256:<hex>` — HMAC-SHA-256 over the canonical content
|
`hmac-sha256:<key id>:<hex>` — HMAC-SHA-256, keyed with a platform
|
||||||
bytes, keyed with a platform integrity key held in the secrets
|
integrity key held in the secrets backend and never stored in the
|
||||||
backend and never stored in the database or any repository. Because
|
database or any repository, over the length-prefixed concatenation
|
||||||
the key is external, the stored value is not an offline dictionary
|
of: a fixed domain-separation string naming this contract and the
|
||||||
oracle for low-entropy answers. On read, a hash mismatch refuses
|
construction version, the row's `user_id`, the row's
|
||||||
the read and flags the pointer for §4.5 reconciliation.
|
`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
|
5. **Bounded orphan repair.** Pointers are deleted when their content
|
||||||
is deleted; dangling pointers are repaired toward deletion, never
|
is deleted; dangling pointers are repaired toward deletion, never
|
||||||
toward re-creating content in the database. Reconciliation for a
|
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
|
declares (at most daily). A pointer whose content is absent is
|
||||||
deleted by the next triggered reconciliation — an orphan survives
|
deleted by the next triggered reconciliation — an orphan survives
|
||||||
at most one cycle, and repair performs no database content write.
|
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
|
## 5. Consent schema and evaluation
|
||||||
|
|
||||||
1. **Exact columns.** Table `profile_consents`, columns exactly:
|
1. **Exact columns.** Table `profile_consents`, columns exactly:
|
||||||
|
|
||||||
| Column | Type | Constraints |
|
| Column | Type | Constraints |
|
||||||
| ---------------- | ----------- | --------------------------------------------------------------- |
|
| ---------------- | ----------- | --------------------------------------------------------------------- |
|
||||||
| `id` | uuid | primary key |
|
| `id` | uuid | primary key |
|
||||||
| `user_id` | uuid | NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject) |
|
| `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_type` | text | NOT NULL, CHECK in (`agent`, `connector`, `feature`) |
|
||||||
| `grantee_id` | text | NOT NULL (§5.2 per-type referential integrity) |
|
| `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) |
|
| `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) |
|
||||||
| `state` | text | NOT NULL, CHECK in (`granted`, `revoked`) |
|
| `state` | text | NOT NULL, CHECK in (`granted`, `revoked`) |
|
||||||
| `granted_at` | timestamptz | NOT NULL |
|
| `granted_at` | timestamptz | NOT NULL |
|
||||||
| `revoked_at` | timestamptz | CHECK ((state = 'granted') = (revoked_at IS NULL)) |
|
| `revoked_at` | timestamptz | CHECK ((state = 'granted') = (revoked_at IS NULL)) |
|
||||||
| `actor` | text | NOT NULL (the authenticated principal that recorded the change) |
|
| `actor` | text | NOT NULL (the authenticated principal that recorded the change) |
|
||||||
| `audit_event_id` | uuid | NOT NULL, FK → the platform audit log table |
|
| `audit_event_id` | uuid | NOT NULL, FK → the §1.8 audit event table |
|
||||||
|
|
||||||
plus the partial unique index UNIQUE (`user_id`, `grantee_type`,
|
plus: the CHECK that exactly one referent column is non-NULL and
|
||||||
`grantee_id`, `category_key`) WHERE `state = 'granted'` — at most
|
matches the discriminant — `(grantee_type = 'agent') = (agent_id
|
||||||
one active grant per (user, concrete grantee, category), as a
|
IS NOT NULL)` AND `(grantee_type = 'connector') = (connector_id IS
|
||||||
database constraint.
|
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
|
2. **Grantee reference model.** A grantee is identified by
|
||||||
(`grantee_type`, `grantee_id`); its canonical display form is
|
(`grantee_type`, the matching referent column); its canonical
|
||||||
`<grantee_type>:<grantee_id>` (e.g. `agent:<agent id>`). The
|
display form is `<grantee_type>:<grantee_ref>`. The discriminant
|
||||||
discriminant set is closed at the three CHECK values. Referential
|
set is closed at the three CHECK values, and every referent is a
|
||||||
integrity is per type: `agent` ids reference the enrolled-agent
|
real foreign key:
|
||||||
registry of `identity-lifecycle.md`; `connector` ids reference the
|
- `agent` → `agent_id` references the **enrolled-agent table**: the
|
||||||
platform connector registry; `feature` ids reference a closed
|
single platform table in which agent enrollment under
|
||||||
feature-key list owned by amendments to this contract, and that
|
`identity-lifecycle.md` (the contract 5 §3.1 rank-4 command
|
||||||
list is EMPTY at version 1 (no feature grantee exists until an
|
family) records enrolled agents. No sibling contract names that
|
||||||
amendment names one). A grant to one agent confers nothing on
|
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
|
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,
|
3. **Default deny.** Absence of an active `granted` row for (user,
|
||||||
grantee, category) means no access. There are no implicit grants,
|
grantee, category) means no access. There are no implicit grants,
|
||||||
no platform-admin bypass, and no mode in which default-deny is
|
no platform-admin bypass, and no mode in which default-deny is
|
||||||
suspended.
|
suspended.
|
||||||
4. **Mutation authority.** Only the authenticated data subject may
|
4. **Mutation authority — two disjoint predicates.** Every consent
|
||||||
create, grant, or revoke consent rows for their own data: the
|
mutation must satisfy exactly one of:
|
||||||
server enforces that `actor` is the subject's principal and equals
|
- **Subject mutation**: the authenticated actor IS the row's data
|
||||||
the row's `user_id` before any consent mutation commits. A platform
|
subject — the server asserts `actor` equals the principal of the
|
||||||
admin has no consent-mutation capability over another user's rows —
|
row's `user_id` before the mutation commits. This is the only
|
||||||
an admin self-grant is refused at write time, closing the
|
predicate under which rows are created or granted. A platform
|
||||||
write-side route around §5.3. One system exception (disclosed,
|
admin has no consent-mutation capability over another user's
|
||||||
§8): when a grantee ceases to exist (e.g. agent retirement under
|
rows — an admin self-grant is refused at write time, closing the
|
||||||
`identity-lifecycle.md`), the platform auto-revokes its active
|
write-side route around §5.3.
|
||||||
rows, recording a system principal as `actor`.
|
- **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
|
5. **Revocation and re-grant.** Revocation flips exactly one active
|
||||||
row to `revoked` and stamps `revoked_at`; it is effective for every
|
row to `revoked` and stamps `revoked_at`; it is effective for every
|
||||||
access evaluated after the revoking write commits. Revoked rows are
|
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
|
revocation inserts a NEW row (append-only history) — repeated
|
||||||
grant/revoke cycles are represented as successive rows, and the
|
grant/revoke cycles are represented as successive rows, and the
|
||||||
§5.1 partial unique index guarantees the old revoked rows cannot
|
§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.
|
never whether routing applies.
|
||||||
3. **Election record.** The election lives in this contract's own
|
3. **Election record.** The election lives in this contract's own
|
||||||
one-row table `custody_config`, columns exactly: `id` (uuid,
|
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`
|
(`split`, `unsplit`), default `split`), `registry_version`
|
||||||
(integer, NOT NULL, §2.1), `elected_at` (timestamptz, NOT NULL),
|
(integer, NOT NULL, §2.1), `elected_at` (timestamptz, NOT NULL),
|
||||||
`actor` (text, NOT NULL), `audit_event_id` (uuid, NOT NULL, FK →
|
`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,
|
amended only by an explicit operator action; contract 6's exact,
|
||||||
immutable mode record is not touched or extended by this contract.
|
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
|
4. **Phase timing.** v1 ships the §2–§6 schemas and the D14 database
|
||||||
boundary, and the wizard collects no sensitive category in v1
|
boundary, and the wizard collects no sensitive category in v1
|
||||||
(contract 3 §3), so v1 contains no sensitive write surface. §3
|
(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
|
Binding on the implementing PRs. Every witness below MUST name, in its
|
||||||
implementation, the exact tables, columns, commands, and source roots
|
implementation, the exact tables, columns, commands, and source roots
|
||||||
it scans; "the custody tables" means `profile_category_registry`,
|
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
|
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,
|
custody tables' live column sets are exactly
|
||||||
and no platform table outside the declared non-sensitive profile
|
§2.1/§4.1/§5.1/§5.2/§6.3 and `profile_answers` is exactly §3.5;
|
||||||
tables carries profile answer content.
|
no platform table outside `profile_answers` carries profile
|
||||||
2. **Column-type allowlist witness:** the custody tables use only the
|
answer content; and every `audit_event_id` FK (§3.5, §4.1, §5.1,
|
||||||
column types named in §2.1/§4.1/§5.1/§6.3 (uuid, text, integer,
|
§6.3) references the same physical table that the contract-1
|
||||||
timestamptz) — no bytea, json/jsonb, array, vector, or tsvector
|
(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
|
column exists in them, closing the encoded/derived-representation
|
||||||
routes by type rather than by probe alone.
|
routes by type rather than by probe alone.
|
||||||
3. **Closed write-route witness** (hierarchy contract §6.3 style): a
|
3. **Closed write-route witness** (hierarchy contract §6.3, full
|
||||||
static, re-export-aware inventory over `apps/` and `packages/`
|
prong set): a static, re-export-aware inventory over `apps/`,
|
||||||
(production code, tests excluded) enumerates every module that
|
`packages/`, and `plugins/` (production code, tests excluded)
|
||||||
writes the custody tables or writes profile answers, and every
|
enumerates every module that writes the custody tables,
|
||||||
enumerated route implements §3.3 registry routing; a route outside
|
`profile_answers`, or profile answers generally, detecting access
|
||||||
the enumeration fails the assertion.
|
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
|
4. **Closed brain-read witness** (same style): the inventory enumerates
|
||||||
every production route that reads user-brain content on behalf of a
|
every production route that reads user-brain content on behalf of a
|
||||||
grantee, and every enumerated route calls the §5.6 consent
|
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
|
witness first plants the fixture text in a scratch column of a
|
||||||
throwaway table to prove the probe detects it, then asserts its
|
throwaway table to prove the probe detects it, then asserts its
|
||||||
absence — as plaintext, base64, hex, and JSON-string encodings —
|
absence — as plaintext, base64, hex, and JSON-string encodings —
|
||||||
across every column of the custody tables and declared profile
|
across every column of every table in the platform database: §3.1
|
||||||
tables.
|
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
|
6. **Registry witnesses:** (a) the version-1 registry state is exactly
|
||||||
the seven §2.2 rows with their classifications; (b) a
|
the seven §2.2 rows with their classifications; (b) a
|
||||||
`professional-background` answer routes relationally (or per the
|
`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);
|
`profile_category_registry` outside a migration is refused (§2.1);
|
||||||
(f) a reclassification migration (non-sensitive → sensitive) on
|
(f) a reclassification migration (non-sensitive → sensitive) on
|
||||||
seeded data moves the values to brains, creates pointers, and
|
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
|
7. **Server-side classification witness:** a client-supplied
|
||||||
classification or routing override on a profile write is ignored;
|
classification or routing override on a profile write is ignored;
|
||||||
the registry decision is applied (§3.3).
|
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 `/`,
|
constraint; a `brain_ref` failing the §4.3 grammar (leading `/`,
|
||||||
`..` segment, empty segment) is rejected by the CHECK; resolution
|
`..` segment, empty segment) is rejected by the CHECK; resolution
|
||||||
of a valid `brain_ref` under user A's row never reads user B's
|
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
|
9. **Hash witnesses:** `content_hash` verifies via the keyed §4.4
|
||||||
construction; a mismatch refuses the read and flags the pointer;
|
construction; the database value alone, without the external key,
|
||||||
the database value alone, without the external key, does not equal
|
does not equal any unkeyed digest of the fixture content (oracle
|
||||||
any unkeyed digest of the fixture content (oracle control).
|
control); the same fixture content stored for two users, and for
|
||||||
10. **Orphan witnesses:** starting from a PRE-EXISTING orphan (content
|
two categories of one user, yields different stored hashes
|
||||||
already absent, pointer present), the next triggered
|
(domain-separation control — no cross-row equality oracle); after
|
||||||
reconciliation deletes the pointer and writes no content anywhere
|
a key rotation, a pointer written under the old key id still
|
||||||
in the database (§4.5); a managed deletion removes its pointer in
|
verifies and a new write stores the new key id; a mismatch refuses
|
||||||
the same transaction.
|
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
|
11. **Default-deny and granularity witnesses:** an agent grantee with
|
||||||
no active row is refused; with a `granted` row for category A
|
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
|
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
|
same type is refused for the same (user, category); `connector`
|
||||||
three grantee types is exercised — `feature` via its refusal path,
|
and `feature` are exercised via their FK refusal paths, since
|
||||||
since the version-1 feature list is empty (§5.2).
|
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
|
12. **Mutation-authority witnesses:** a platform admin attempting to
|
||||||
create a grant on another user's data is refused at write time; a
|
create a grant on another user's data is refused at write time; a
|
||||||
hierarchy owner or manager likewise; the data subject succeeds;
|
hierarchy owner or manager likewise; the data subject succeeds;
|
||||||
`actor` equals the subject's principal on every committed row
|
`actor` equals the subject's principal on every subject-mutation
|
||||||
(§5.4).
|
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
|
13. **Revocation/re-grant witnesses:** after revocation commits, the
|
||||||
next evaluation refuses and the revoked row persists unmutated; a
|
next evaluation refuses and the revoked row persists unmutated; a
|
||||||
full grant → revoke → re-grant cycle yields two rows (one
|
full grant → revoke → re-grant cycle yields two rows (one
|
||||||
revoked, one active) and access follows only the active row; a
|
revoked, one active) and access follows only the active row; a
|
||||||
second concurrent grant attempt for the same key violates the
|
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
|
14. **Self-access witness:** the data subject reads their own content
|
||||||
without consent rows (§5.6).
|
without consent rows (§5.6).
|
||||||
15. **Mode witnesses:** the custody-table schemas are byte-identical
|
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
|
the mode flip until partitioning has produced the per-user region
|
||||||
(§6.5, with contract 6 §4.2); in an Enterprise fixture with two
|
(§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
|
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)
|
## 8. Drafting additions (PRD §12.1 disclosure)
|
||||||
|
|
||||||
The following are proposed drafting additions, visible here for
|
The following are proposed drafting additions, visible here for
|
||||||
ratification; none is claimed as a PRD mandate, and each is severable:
|
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
|
1. The `feature` grantee type, with a contract-owned registry table
|
||||||
empty at version 1 (§5.2).
|
that is empty at version 1 (§5.2).
|
||||||
2. Append-only consent history: re-grants insert new rows; revoked
|
2. Append-only consent history: re-grants insert new rows; revoked
|
||||||
rows are retained unmutated (§5.5).
|
rows are retained unmutated, enforced by trigger (§5.5).
|
||||||
3. System-actor auto-revocation when a grantee ceases to exist (§5.4).
|
3. System-actor auto-revocation when a grantee ceases to exist, as a
|
||||||
4. The `custody_config` election record for the Standalone layout
|
precondition-checked predicate disjoint from subject mutation
|
||||||
(§6.3).
|
(§5.4).
|
||||||
5. The keyed `content_hash` construction and its mismatch handling
|
4. The `custody_config` election record for the Standalone layout,
|
||||||
(§4.4).
|
singleton by constraint (§6.3).
|
||||||
6. The bounded dangling-pointer reconciliation policy (§4.5).
|
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).
|
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
|
The revision-1 "reporting" rationale for relational
|
||||||
background/education storage is withdrawn; the traced rationale is
|
background/education storage is withdrawn; the traced rationale is
|
||||||
|
|||||||
Reference in New Issue
Block a user