From caa5b04096bfb04e1f60e096e6e0fcbe9c18fcda Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 19:10:12 -0500 Subject: [PATCH 01/13] docs: custody pointer and consent schema contract (S2 contract 7) --- docs/requirements/custody-schema.md | 171 ++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 docs/requirements/custody-schema.md diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md new file mode 100644 index 00000000..a36d446b --- /dev/null +++ b/docs/requirements/custody-schema.md @@ -0,0 +1,171 @@ +# Data Custody Contract — Pointer and Consent Schema (D14) + +Status: DRAFT — awaiting ratification (webui-audit S2, contract 7 of 9). +Authority: PRD D14/D6 (Part I §7) — sensitive profile categories live in +the **user's own brain ONLY**; PostgreSQL holds structural data, consent +records, and pointers — never the content; "user data does not leak" is +enforced by architecture, not policy. The Standalone split is a MAY with +a recommended-default (kept for conversion forward-compatibility, D3). +PRD D4 (Part I §6) — profile answers feed `USER.md` and/or the user's +data store subject to the custody rule; connectors carry granular +agentic-access consent. PRD D6 — estate brains hold operational records; +only product-relevant material migrates into repository docs. + +This contract binds the sensitive-category registry (§2), the custody +placement rule (§3), the pointer schema (§4), the consent schema and its +evaluation (§5), mode application (§6), and witnesses (§7). It defines +schemas and placement; wizard step flow stays with contract 3, mode and +conversion with contract 6 (`mode-conversion.md`), identity with +`identity-lifecycle.md`, tool mapping with `tool-gateway-mapping.md`. + +## 1. Definitions + +1. **User brain**: the git-tracked per-user data store (in Standalone, + the user-files region of the single mosaic-brain; in Enterprise, the + user's own brain repository). +2. **Sensitive content**: any profile answer or derived text in a §2 + sensitive category. +3. **Pointer**: a database record referencing sensitive content that + lives in a user brain, carrying no content (§4). +4. **Consent record**: a database record granting a named grantee scope + access to a category of a user's data (§5). + +## 2. Sensitive-category registry + +1. The registry is a closed, versioned list in the platform database; + entries are added or reclassified by amendment to this contract, + never ad hoc. +2. Initial registry, drawn from the D4 profile step and D14's + "disabilities, family, communication style, and similar": + + | Category key | Covers | + | --------------------- | ------------------------------------------------------------------- | + | `disabilities` | disabilities including ADHD/autism/PDA/vision | + | `family-social` | family, pets, friends | + | `communication-style` | desired agent communication style, voice-matching interview product | + | `personal-interests` | hobbies, likes/dislikes | + | `connector-content` | email and drive content reached through user connectors | + +3. **Fail-closed classification.** A profile category not in the + registry is treated as sensitive until an amendment classifies it. + Non-sensitive by classification (not by default) are structural + fields the platform needs relationally: e.g. professional + background/education summaries used for agent configuration MAY be + classified non-sensitive by the ruling below; account identity + fields (email, name, credentials) are identity-contract data, not + profile custody data. + +## 3. Custody placement rule + +1. Sensitive content is written to the owning user's brain ONLY. + PostgreSQL tables MUST NOT store sensitive content in any column — + not as text, not as excerpts or previews, not as embeddings or + other derived representations that reconstruct content. +2. The database MAY hold, about sensitive content: the pointer records + of §4, the consent records of §5, and the category registry of §2. + Nothing else. +3. Every write path for profile answers routes by category: sensitive + → brain write + pointer upsert; non-sensitive → its declared + platform table. The routing decision is made server-side from the + registry, never by the client. +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. + +## 4. Pointer schema + +A pointer row carries exactly: + +1. `id`, `user_id` (the owning user), `category_key` (§2 registry FK), + `brain_ref` (an opaque locator — repository-relative path or key in + the owning user's brain), `content_hash` (integrity check of the + referenced content), `created_at`/`updated_at`, and audit linkage. +2. **Opacity.** `brain_ref` and every other pointer column MUST NOT + embed content or content-derived text (no titles, snippets, or + free-text descriptions). A locator is structural (category + path + discipline), not descriptive. +3. One pointer per (user, category, brain_ref); pointers are deleted + when their content is deleted (dangling pointers are repaired + toward deletion, never toward re-creating content in the DB). + +## 5. Consent schema and evaluation + +1. A consent row carries exactly: `id`, `user_id` (the data subject), + `grantee_scope` (a typed reference: an enrolled agent, a connector, + or a platform feature — closed enum of grantee types), `category_key` + (§2 FK), `state` (`granted` | `revoked`), `granted_at`/`revoked_at`, + `actor` (who recorded the choice), and audit linkage. +2. **Default deny.** Absence of a `granted` consent row for (user, + grantee scope, category) means no access. There are no implicit + grants, no platform-admin bypass, and no mode in which default-deny + is suspended. +3. **Granularity.** Consent is per grantee scope × category (the D4 + "granular agentic-access consent"). A grant to one agent or + connector confers nothing on another. +4. **Revocation.** Revocation is effective for every access evaluated + after the revoking write commits; revoked rows are retained as + history (state flip, not row deletion). +5. **Evaluation placement.** Access to sensitive content is mediated by + the platform (Gateway/tooling) evaluating consent before any brain + read on behalf of a grantee; the evaluation fails closed + (`rbac-grant-model.md` §3.5 pattern). The user reading their own + data is not a grantee and needs no consent row. +6. Consent records govern agentic/feature access to user data. They are + distinct from hierarchy grants (contract 2) and confer no + platform authorization. + +## 6. Mode application + +1. The §3–§5 schemas are mode-independent: Standalone and Enterprise + use the same tables and the same routing rule. +2. In Standalone, D14 makes the physical split a MAY. This contract + keeps it a MAY and binds the recommended default: a fresh v1 + Standalone install routes sensitive content per §3 from the start, + so the Enterprise conversion precondition (`mode-conversion.md` + §4.2, brains) is already satisfied. An operator electing not to + keep the split accepts the resulting conversion-time partitioning + work; the election is recorded. +3. In Enterprise, the split is mandatory (D3 table); no-leakage between + users is enforced by §3 placement plus §5 default-deny — there is + no cross-user read path to sensitive content through the database, + because the database has no content to serve. + +## 7. Verification requirements + +Binding on the implementing PRs: + +1. **No-content witness:** a column-allowlist assertion (contract 1 + §6.3 style) that the pointer and consent tables' column sets are + exactly §4.1/§5.1, and that no platform table outside the declared + non-sensitive profile tables carries profile answer content. +2. **Routing witness:** a sensitive-category answer submitted through + the profile surface results in a brain write plus a pointer row and + zero content bytes in the database (asserted by content-hash + presence in the brain and absence of the plaintext in any DB + column); a non-sensitive answer lands in its declared table. +3. **Fail-closed classification witness:** an answer in an unregistered + category routes as sensitive. +4. **Opacity witness:** pointer rows for seeded sensitive fixtures + contain no fixture text in any column. +5. **Default-deny witness:** an agent grantee with no consent row is + refused; with a `granted` row for category A only, access to + category B is refused. +6. **Revocation witness:** after revocation commits, the next access + evaluation refuses; the revoked row persists as history. +7. **Self-access witness:** the data subject reads their own content + without consent rows. +8. **Deletion witness:** deleting sensitive content removes its + pointer; no path re-materializes content into the database. + +## Ruling request + +Ratify sections 1–7 as written, with one decision embedded: + +- Decision (§2): the initial sensitive-category registry is the five + rows of §2.2, with fail-closed classification for anything + unregistered, and with professional background/education classified + **non-sensitive** (they exist to configure agents and reporting and + are stored relationally). Alternative if rejected: classify + background/education sensitive too — safe, but it moves data the + platform legitimately queries into pointer-indirected storage and + that cost should be chosen deliberately, not defaulted into. -- 2.54.0 From 8079d8dc9166fd5faf7212995e30cb196ecbece8 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 19:39:04 -0500 Subject: [PATCH 02/13] =?UTF-8?q?docs(custody):=20revision=202=20=E2=80=94?= =?UTF-8?q?=20classifier=20registry,=20grantee=20model,=20exact=20schemas,?= =?UTF-8?q?=20witness=20closure=20(sol=20r1=20F1-F6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/requirements/custody-schema.md | 458 +++++++++++++++++++++------- 1 file changed, 348 insertions(+), 110 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index a36d446b..d4c818d8 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -11,161 +11,399 @@ data store subject to the custody rule; connectors carry granular agentic-access consent. PRD D6 — estate brains hold operational records; only product-relevant material migrates into repository docs. -This contract binds the sensitive-category registry (§2), the custody +Revision 2 (sol r1 findings F1–F6): the registry is now a full profile +classifier that can represent non-sensitive rows, with an exact row +shape, versioning, refusal semantics for unknown keys, and transition +rules (F1); the consent model gains a concrete grantee reference model, +an active-row uniqueness constraint, append-only re-grant semantics, +and explicit mutation authority (F2); the pointer schema names its +exact column set, binds the one-pointer rule to a database constraint, +defines the `brain_ref` grammar and owner-bound resolution, replaces +the unkeyed hash with a keyed construction, and bounds orphan repair +(F3); the witnesses import the hierarchy contract's §6.2 column +allowlist and §6.3-style closed route inventories, add negative +controls and a column-type allowlist, and cover every binding rule +(F4); the Standalone physical split, both valid layouts, the election +record, and v1 phase timing are defined consistently with the wizard +and mode-conversion contracts (F5); drafting additions are disclosed in +§8 and the ruling request is one sentence with one decision (F6). + +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), and witnesses (§7). It defines -schemas and placement; wizard step flow stays with contract 3, mode and -conversion with contract 6 (`mode-conversion.md`), identity with +evaluation (§5), mode application (§6), witnesses (§7), and disclosed +drafting additions (§8). It defines schemas and placement; wizard step +flow stays with contract 3 (`onboarding-wizard.md`), mode and conversion +with contract 6 (`mode-conversion.md`), identity with `identity-lifecycle.md`, tool mapping with `tool-gateway-mapping.md`. ## 1. Definitions -1. **User brain**: the git-tracked per-user data store (in Standalone, - the user-files region of the single mosaic-brain; in Enterprise, the - user's own brain repository). +1. **User brain**: the git-tracked per-user data store. Its two valid + Standalone layouts are defined in §6.2; in Enterprise it is the + user's own brain repository. 2. **Sensitive content**: any profile answer or derived text in a §2 - sensitive category. + category classified `sensitive`. 3. **Pointer**: a database record referencing sensitive content that lives in a user brain, carrying no content (§4). -4. **Consent record**: a database record granting a named grantee scope - access to a category of a user's data (§5). +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). +5. **Consent record**: a database record granting one grantee access to + one category of one user's data (§5). +6. **Registry version**: the monotonically increasing integer + identifying the active state of the §2 registry. +7. **Physical split** (Standalone): sensitive user content living in a + per-user brain repository separate from the estate mosaic-brain, as + opposed to the unsplit layout where it lives in a dedicated + user-files subtree of the single mosaic-brain (§6.2). -## 2. Sensitive-category registry +## 2. Profile-category registry -1. The registry is a closed, versioned list in the platform database; - entries are added or reclassified by amendment to this contract, - never ad hoc. -2. Initial registry, drawn from the D4 profile step and D14's - "disabilities, family, communication style, and similar": +1. **Model.** The registry is the single classifier for every profile + category, closed and versioned, in the platform database. Table + `profile_category_registry`, columns exactly: - | Category key | Covers | - | --------------------- | ------------------------------------------------------------------- | - | `disabilities` | disabilities including ADHD/autism/PDA/vision | - | `family-social` | family, pets, friends | - | `communication-style` | desired agent communication style, voice-matching interview product | - | `personal-interests` | hobbies, likes/dislikes | - | `connector-content` | email and drive content reached through user connectors | + | Column | Type | Constraints | + | ---------------- | ----------- | ---------------------------------------------------- | + | `category_key` | text | primary key | + | `classification` | text | NOT NULL, CHECK in (`sensitive`, `non-sensitive`) | + | `since_version` | integer | NOT NULL (registry version that introduced this row) | + | `created_at` | timestamptz | NOT NULL | -3. **Fail-closed classification.** A profile category not in the - registry is treated as sensitive until an amendment classifies it. - Non-sensitive by classification (not by default) are structural - fields the platform needs relationally: e.g. professional - background/education summaries used for agent configuration MAY be - classified non-sensitive by the ruling below; account identity - fields (email, name, credentials) are identity-contract data, not - profile custody data. + The current registry version is a single integer held in + `custody_config` (§6.3). Rows are added or reclassified ONLY by + amendment to this contract shipped as a migration that bumps the + registry version; no runtime write path may insert, update, or + delete registry rows. + +2. **Initial registry (version 1).** Drawn from the D4 profile step and + D14's "disabilities, family, communication style, and similar": + + | Category key | Classification | Covers | + | ------------------------- | -------------------------- | ------------------------------------------------------------------- | + | `disabilities` | sensitive | disabilities including ADHD/autism/PDA/vision | + | `family-social` | sensitive | family, pets, friends | + | `communication-style` | sensitive | desired agent communication style, voice-matching interview product | + | `personal-interests` | sensitive | hobbies, likes/dislikes | + | `connector-content` | sensitive | email and drive content reached through user connectors | + | `professional-background` | non-sensitive (per ruling) | professional background summary used for agent configuration | + | `education` | non-sensitive (per ruling) | education summary used for agent configuration | + + The `Covers` column is contract documentation, not a database + column. Account identity fields (email, name, credentials) are + identity-contract data, not profile custody data, and have no + registry row. + +3. **Unknown category — refusal.** A profile write naming a + `category_key` with no registry row is REFUSED with an explicit + error. Nothing is stored anywhere, no registry row is auto-added + (§2.1), and no pointer is created (so the §4.1 foreign key is never + asked to reference a missing row). Fail-closed means refusal, not + silent routing. +4. **Reclassification transitions.** A reclassification ships as a + contract amendment plus migration that bumps the registry version. + Non-sensitive → sensitive: the same migration moves every existing + relational value for that category into its owning user's brain, + creates the pointers, and deletes the relational values, all before + the new version activates. Sensitive → non-sensitive: existing + brain content and pointers remain valid and are never automatically + materialized into the database; only writes evaluated after the new + version activates route relationally. +5. **Unreadable registry — refusal.** If the registry or its version + cannot be read at decision time, every routing and consent decision + that depends on it is refused. There is no cached-default or + assume-sensitive fallback that performs a write. ## 3. Custody placement rule 1. Sensitive content is written to the owning user's brain ONLY. PostgreSQL tables MUST NOT store sensitive content in any column — - not as text, not as excerpts or previews, not as embeddings or - other derived representations that reconstruct content. + not as text, not as excerpts or previews, not as encodings, and not + as embeddings or other derived representations that reconstruct + content. 2. The database MAY hold, about sensitive content: the pointer records - of §4, the consent records of §5, and the category registry of §2. - Nothing else. -3. Every write path for profile answers routes by category: sensitive - → brain write + pointer upsert; non-sensitive → its declared - platform table. The routing decision is made server-side from the - registry, never by the client. + 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. 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. ## 4. Pointer schema -A pointer row carries exactly: +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) | + + plus the database constraint UNIQUE (`user_id`, `category_key`, + `brain_ref`) — the one-pointer rule is a constraint, not a + convention. -1. `id`, `user_id` (the owning user), `category_key` (§2 registry FK), - `brain_ref` (an opaque locator — repository-relative path or key in - the owning user's brain), `content_hash` (integrity check of the - referenced content), `created_at`/`updated_at`, and audit linkage. 2. **Opacity.** `brain_ref` and every other pointer column MUST NOT embed content or content-derived text (no titles, snippets, or - free-text descriptions). A locator is structural (category + path - discipline), not descriptive. -3. One pointer per (user, category, brain_ref); pointers are deleted - when their content is deleted (dangling pointers are repaired - toward deletion, never toward re-creating content in the DB). + free-text descriptions). A locator is structural, not descriptive. +3. **`brain_ref` grammar and owner binding.** `brain_ref` is a + normalized repository-relative POSIX path: one or more segments + matching `[A-Za-z0-9][A-Za-z0-9._-]*`, joined by `/`, with no + leading `/`, no empty segment, and no `.` or `..` segment; the + stored form matches + `^[A-Za-z0-9][A-Za-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$`. + Resolution ALWAYS roots at the brain owned by the row's `user_id` + (the resolver takes the owner from the row, never from the + locator); the locator carries no repository, host, or user + component, so a cross-user or traversal reference is + unrepresentable, not merely forbidden. +4. **`content_hash` construction.** `content_hash` is + `hmac-sha256:` — 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. +5. **Bounded orphan repair.** Pointers are deleted when their content + is deleted; dangling pointers are repaired toward deletion, never + toward re-creating content in the database. Reconciliation for a + user's pointers runs on two triggers: every profile write for that + user, and a periodic sweep whose interval the implementing PR + declares (at most daily). A pointer whose content is absent is + deleted by the next triggered reconciliation — an orphan survives + at most one cycle, and repair performs no database content write. ## 5. Consent schema and evaluation -1. A consent row carries exactly: `id`, `user_id` (the data subject), - `grantee_scope` (a typed reference: an enrolled agent, a connector, - or a platform feature — closed enum of grantee types), `category_key` - (§2 FK), `state` (`granted` | `revoked`), `granted_at`/`revoked_at`, - `actor` (who recorded the choice), and audit linkage. -2. **Default deny.** Absence of a `granted` consent row for (user, - grantee scope, category) means no access. There are no implicit - grants, no platform-admin bypass, and no mode in which default-deny - is suspended. -3. **Granularity.** Consent is per grantee scope × category (the D4 - "granular agentic-access consent"). A grant to one agent or - connector confers nothing on another. -4. **Revocation.** Revocation is effective for every access evaluated - after the revoking write commits; revoked rows are retained as - history (state flip, not row deletion). -5. **Evaluation placement.** Access to sensitive content is mediated by +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 | + + 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. + +2. **Grantee reference model.** A grantee is identified by the pair + (`grantee_type`, `grantee_id`); its canonical display form is + `:` (e.g. `agent:`). 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 + another agent of the same type; the constraint key includes + `grantee_id`, 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`. +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 + revocation inserts a NEW row (append-only history) — repeated + grant/revoke cycles are represented as successive rows, and the + §5.1 partial unique index guarantees the old revoked rows cannot + keep access live. +6. **Evaluation placement.** Access to sensitive content is mediated by the platform (Gateway/tooling) evaluating consent before any brain read on behalf of a grantee; the evaluation fails closed - (`rbac-grant-model.md` §3.5 pattern). The user reading their own + (`rbac-grant-model.md` §3.5 pattern), including when the consent + state cannot be read (§2.5 pattern). The user reading their own data is not a grantee and needs no consent row. -6. Consent records govern agentic/feature access to user data. They are - distinct from hierarchy grants (contract 2) and confer no - platform authorization. +7. Consent records govern agentic/feature access to user data. They are + distinct from hierarchy grants (contract 2) and confer no platform + authorization. ## 6. Mode application -1. The §3–§5 schemas are mode-independent: Standalone and Enterprise +1. The §2–§5 schemas are mode-independent: Standalone and Enterprise use the same tables and the same routing rule. -2. In Standalone, D14 makes the physical split a MAY. This contract - keeps it a MAY and binds the recommended default: a fresh v1 - Standalone install routes sensitive content per §3 from the start, - so the Enterprise conversion precondition (`mode-conversion.md` - §4.2, brains) is already satisfied. An operator electing not to - keep the split accepts the resulting conversion-time partitioning - work; the election is recorded. -3. In Enterprise, the split is mandatory (D3 table); no-leakage between +2. **Standalone layouts.** The D14 physical split remains a MAY. Its + two valid layouts are: **split** — sensitive user content in a + per-user brain repository separate from the estate mosaic-brain + (the recommended default); **unsplit** — sensitive user content in + the dedicated user-files subtree `users//` of the single + mosaic-brain. §3 binds the logical user-brain region identically in + both layouts; the layout election changes where the region lives, + never whether routing applies. +3. **Election record.** The election lives in this contract's own + one-row table `custody_config`, columns exactly: `id` (uuid, + primary key), `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 + amended only by an explicit operator action; contract 6's exact, + immutable mode record is not touched or extended by this contract. +4. **Phase timing.** v1 ships the §2–§6 schemas and the D14 database + boundary, and the wizard collects no sensitive category in v1 + (contract 3 §3), so v1 contains no sensitive write surface. §3 + binds every sensitive write path from the moment one exists — the + first profile surface that accepts a sensitive category (P2/P3) + activates routing, consistent with contract 6 §5's v1 slice (D14 + database boundary only, custody mechanics outside v1). +5. **Conversion precondition.** An operator electing `unsplit` accepts + conversion-time partitioning: `mode-conversion.md` §4.2 requires + the per-user partition to exist before the Enterprise flip, so + conversion from an unsplit install performs the partitioning first. +6. In Enterprise, the split is mandatory (D3 table); no-leakage between users is enforced by §3 placement plus §5 default-deny — there is no cross-user read path to sensitive content through the database, because the database has no content to serve. ## 7. Verification requirements -Binding on the implementing PRs: +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`. -1. **No-content witness:** a column-allowlist assertion (contract 1 - §6.3 style) that the pointer and consent tables' column sets are - exactly §4.1/§5.1, and that no platform table outside the declared - non-sensitive profile tables carries profile answer content. -2. **Routing witness:** a sensitive-category answer submitted through +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 + 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. +4. **Closed brain-read witness** (same style): the inventory enumerates + every production route that reads user-brain content on behalf of a + grantee, and every enumerated route calls the §5.6 consent + evaluation; a brain-read route outside the enumeration fails. +5. **Routing witness:** a sensitive-category answer submitted through the profile surface results in a brain write plus a pointer row and - zero content bytes in the database (asserted by content-hash - presence in the brain and absence of the plaintext in any DB - column); a non-sensitive answer lands in its declared table. -3. **Fail-closed classification witness:** an answer in an unregistered - category routes as sensitive. -4. **Opacity witness:** pointer rows for seeded sensitive fixtures - contain no fixture text in any column. -5. **Default-deny witness:** an agent grantee with no consent row is - refused; with a `granted` row for category A only, access to - category B is refused. -6. **Revocation witness:** after revocation commits, the next access - evaluation refuses; the revoked row persists as history. -7. **Self-access witness:** the data subject reads their own content - without consent rows. -8. **Deletion witness:** deleting sensitive content removes its - pointer; no path re-materializes content into the database. + zero content bytes in the database; a non-sensitive answer lands in + its declared table. The no-content probe is a negative control: the + witness first plants the fixture text in a scratch column of a + throwaway table to prove the probe detects it, then asserts its + absence — as plaintext, base64, hex, and JSON-string encodings — + across every column of the custody tables and declared profile + tables. +6. **Registry witnesses:** (a) the version-1 registry state is exactly + the seven §2.2 rows with their classifications; (b) a + `professional-background` answer routes relationally (or per the + ruling's alternative); (c) an unknown `category_key` is refused + with nothing stored (§2.3); (d) with the registry unreadable, the + write is refused (§2.5); (e) a runtime insert/update/delete against + `profile_category_registry` outside a migration is refused (§2.1); + (f) a reclassification migration (non-sensitive → sensitive) on + seeded data moves the values to brains, creates pointers, and + leaves zero relational values (§2.4). +7. **Server-side classification witness:** a client-supplied + classification or routing override on a profile write is ignored; + the registry decision is applied (§3.3). +8. **Pointer-constraint witnesses:** inserting a second pointer for the + same (user, category, brain_ref) violates the §4.1 unique + constraint; a `brain_ref` failing the §4.3 grammar (leading `/`, + `..` segment, empty segment) is rejected by the CHECK; resolution + of a valid `brain_ref` under user A's row never reads user B's + brain (owner binding, §4.3). +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. +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). +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). +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). +14. **Self-access witness:** the data subject reads their own content + without consent rows (§5.6). +15. **Mode witnesses:** the custody-table schemas are byte-identical + under Standalone and Enterprise migrations (§6.1); a fresh + Standalone bootstrap records `standalone_layout = 'split'` by + default, and an explicit opt-out records `unsplit` with actor and + audit linkage (§6.3); conversion from an `unsplit` install refuses + the mode flip until partitioning has produced the per-user region + (§6.5, with contract 6 §4.2); in an Enterprise fixture with two + users, user A's grantee with a grant on user A cannot reach any of + user B's content (§6.6). + +## 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). +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). +7. The column-type allowlist verification requirement (§7.2). + +The revision-1 "reporting" rationale for relational +background/education storage is withdrawn; the traced rationale is +agent configuration (PRD D4). ## Ruling request -Ratify sections 1–7 as written, with one decision embedded: - -- Decision (§2): the initial sensitive-category registry is the five - rows of §2.2, with fail-closed classification for anything - unregistered, and with professional background/education classified - **non-sensitive** (they exist to configure agents and reporting and - are stored relationally). Alternative if rejected: classify - background/education sensitive too — safe, but it moves data the - platform legitimately queries into pointer-indirected storage and - that cost should be chosen deliberately, not defaulted into. +Ruling requested (one decision): classify `professional-background` +and `education` as **non-sensitive** in the version-1 registry (stored +relationally, used for agent configuration) — or, as the alternative, +classify both **sensitive** (user-brain custody with pointers), +accepting that agent-configuration reads then go through pointer +indirection and consent evaluation? -- 2.54.0 From 671b872ec989a347a6680d83b6bb9fff953afc31 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 20:33:21 -0500 Subject: [PATCH 03/13] docs: custody-schema contract revision 3 (sol re-review residuals F1-F9) --- docs/requirements/custody-schema.md | 427 +++++++++++++++++++++------- 1 file changed, 324 insertions(+), 103 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index d4c818d8..a61634d4 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -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:` — 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::` — 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). `` 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 - `:` (e.g. `agent:`). 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 `:`. 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 -- 2.54.0 From 269aeda193ac2c89c465822b949b1d628209b9b6 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 21:02:12 -0500 Subject: [PATCH 04/13] docs: custody-schema contract revision 4 (sol r3 residuals F1/F2/F4/F6/F7/F9 + F10/F11) --- docs/requirements/custody-schema.md | 376 +++++++++++++++++++++------- 1 file changed, 281 insertions(+), 95 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index a61634d4..90d009b4 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -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 `:`. 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 -- 2.54.0 From a8111998c7ad54d0c498aa2be3eb6aaf14de3aa9 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 21:25:53 -0500 Subject: [PATCH 05/13] docs: custody-schema contract revision 5 (sol r4 residuals F9/F11 + F12/F13) --- docs/requirements/custody-schema.md | 269 +++++++++++++++++++++------- 1 file changed, 206 insertions(+), 63 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index 90d009b4..3c7992b7 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -77,6 +77,26 @@ and fault-injection witnesses (F11). §8 discloses every rule above plus the previously undisclosed §4.3 committed-tree/symlink rule (F6). +Revision 5 (sol r4 findings F9/F11 residual, F12/F13 new): the +pointer row gains a monotonic `generation` counter bumped by every +upsert, and managed-deletion step 2 conditions on the generation +recorded in step 1 instead of the content hash, closing the +identical-content ABA in which a byte-identical rewrite reproduced +the old hash and a stale deletion unpointed fresh content (F9). The +ordered-write protocol gains a contract-defined locator scheme — +injective over (category, slot), with the slot set fixed to the +single value `answer` at version 1 — and a per-locator advisory lock +spanning both steps, so same-locator writers are serialized and a +completed write always leaves the pointer hash describing the +current blob; §7.16 adds the concurrent-writer, +crash-before-acknowledgment, stale-retry, and locator-injectivity +witnesses (F11). Cascaded consent deletion now emits a semantic +cessation event in the same transaction as the referent deletion, +written by a trigger on the consent table, so the audit trail +records when and why each active grant ended, not only that it began +(F12). The stale §3.6/§5.2 cross-references and the "first +predicate" remnant are corrected (F13). + This contract binds the profile-category registry (§2), the custody placement rule (§3), the pointer schema (§4), the consent schema and its evaluation (§5), mode application (§6), witnesses (§7), and disclosed @@ -249,26 +269,65 @@ with contract 6 (`mode-conversion.md`), identity with 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 + spans two stores and is ordered, content first. + + **Locator scheme.** The locator is a pure function + `L(category_key, slot) → brain_ref` declared by the implementing + PR, with three contract-bound properties: it is deterministic (a + retry of the same write resolves to the same path), it is + INJECTIVE — distinct (category_key, slot) pairs map to distinct + `brain_ref` values, witnessed with a collision control (§7.16) — + and it is stable across releases (a scheme change is a contract + amendment with a migration). An **answer slot** is the stable + identifier of one answer's position within a category; at + version 1 every registry category is single-valued (mirroring + §3.5's UNIQUE (`user_id`, `category_key`)), so the slot set is + the single constant `answer`. An amendment introducing a + multi-valued category must extend the slot set while preserving + injectivity. + + **Serialization.** The entire write — both steps — runs under a + per-locator mutual exclusion: before step 1 the writer acquires + an exclusive advisory lock keyed on (`user_id`, `category_key`, + slot) (e.g. a session-scoped `pg_advisory_lock` on a stable hash + of the triple), held until step 2 commits or the writer's session + ends (session end releases it automatically, so a crashed holder + never wedges the locator). Writers to one locator are therefore + totally ordered; writes to distinct locators do not contend. + Because no two same-locator writes interleave, a completed write + always leaves the pointer's `content_hash` describing the blob + currently committed at the locator — the divergent interleaving + (writer A's pointer over writer B's content) is unschedulable, + not merely unlikely. + + **Steps.** Step 1 commits the content to the owning user's brain + at `L(category_key, slot)`; step 2, only after step 1 has committed, upserts the pointer row (insert, or update of the - existing row for that (user, category, brain_ref)) 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. + existing row for that (user, category, brain_ref), incrementing + `generation` per §4.1) in its own database transaction inside the + §2.6 fence. The write is acknowledged to the caller only after + step 2 commits. Interruption before step 1 leaves both stores + unchanged. Interruption between the steps leaves committed brain + content with no pointer: unpointed content is inert — it lives in + the correct custody store, no read path serves it (reads resolve + through pointers only), and nothing dangles in the database. + Interruption after step 2 commits but before the acknowledgment + reaches the caller leaves the converged state; the retry below + observes it and returns success without corrupting it. The + client's retry — the write was never acknowledged — re-acquires + the locator lock, recommits the same locator, and completes the + pointer upsert, converging to the pointed state; the retry is + idempotent whichever interruption point preceded it. A stale + retry (an earlier write retried after a later same-locator write + completed) also executes under the lock and leaves a consistent + pointed state — same-locator outcomes are last-completed-write + wins, and no completion order can leave the hash describing a + non-current blob. The reverse order (pointer before content) is + forbidden: an ordinary write never creates a pointer whose + content has not committed. Witness §7.16 fault-injects the + interruption points, drives retry convergence, the same-locator + concurrent interleaving, the stale retry, and the injectivity + control. ## 4. Pointer schema @@ -281,6 +340,7 @@ with contract 6 (`mode-conversion.md`), identity with | `category_key` | text | NOT NULL, FK → profile_category_registry(category_key) | | `brain_ref` | text | NOT NULL, CHECK against the §4.3 grammar | | `content_hash` | text | NOT NULL (§4.4 construction) | + | `generation` | bigint | NOT NULL (1 on insert; every §3.7 upsert increments) | | `created_at` | timestamptz | NOT NULL | | `updated_at` | timestamptz | NOT NULL | | `mismatch_at` | timestamptz | NULL unless the pointer is in the §4.6 mismatch state | @@ -336,6 +396,12 @@ 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. + The repair DELETE is generation-conditioned like §4.7 step 2: it + records the row's `generation` when it observes the content + absent and conditions the DELETE on it, so a concurrent §3.7 + write that recommits the locator between the observation and the + delete (bumping `generation`) makes the repair match zero rows — + reconciliation never unpoints content it did not observe absent. 6. **Mismatch state.** A failed §4.4 verification stamps the pointer's `mismatch_at` and the read is refused; every subsequent read of a pointer with `mismatch_at` set is refused without re-serving @@ -350,21 +416,28 @@ 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, recording the `content_hash` of the row it + brain repository, recording the `generation` of the row it intends to delete as read before step 1; step 2, only after step 1 has committed, deletes the pointer row in its own database transaction as a **compare-and-delete** — the DELETE is conditioned on (`user_id`, `category_key`, `brain_ref`) AND - `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, - including the concurrent-rewrite interleaving). + `generation` equal to the recorded value. The token is the + generation, not the content hash, deliberately: every §3.7 upsert + increments `generation` even when it recommits byte-identical + content (whose §4.4 hash is unchanged), so a concurrent §3.7 + write that recreates the same locator — with new content OR with + identical content — moves the row past the recorded generation, + step 2's condition matches zero rows, deletes nothing, and the + fresh content stays pointed. Deletion never removes a pointer for + a write it did not observe; the hash-token ABA (identical bytes + reproducing the recorded value) is unrepresentable because the + counter never repeats. Interruption before step 1 commits leaves + both stores unchanged. Interruption between the steps leaves a + dangling pointer, which §4.5 repairs toward deletion within one + reconciliation cycle. At no point does any compensation write + content into the database (witness §7.10, including both + concurrent-rewrite interleavings — changed content and + identical content). ## 5. Consent schema and evaluation @@ -450,14 +523,32 @@ NULL)` — and the partial unique index UNIQUE (`user_id`, 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. + access); no UPDATE is performed, so no actor question arises. + **Cessation event.** The cascade is not silent: an AFTER DELETE + trigger on `profile_consents` inserts, for every deleted row whose + deletion the §5.5 trigger permitted (i.e. a cessation cascade), + one semantic **consent-cessation event** into the §1.8 audit + event table, in the same database transaction as the referent + deletion — if the deleting transaction rolls back, no cessation + event survives, and if it commits, the events commit with it. + Each event carries: the cause (`subject-cessation` or + `grantee-cessation`, plus the parent table and deleted key), the + initiating actor — a platform surface that deletes a referent + sets a transaction-local actor variable the trigger reads; absent + one, the event records actor `system:cascade` (a migration or + direct referential action) — a snapshot of the ended grant + (grantee type and ref, `category_key`, `state`, `granted_at`, + `revoked_at`), and the event time. The snapshot is consent + metadata, never profile content, so §3.1 is not implicated. The + audit table therefore records both ends of every grant's life: + the mutation events written at grant/revoke time survive the + cascade (audit events are never cascade-deleted), and the + cessation event records when, why, and by whom the active grant + ended. The same rule already governs subject cessation via the + `user_id` FK. §7.12 witnesses the subject predicate, its refusal + complement, the cascade, and the cessation event's atomicity, + fields, and survival; every §7.3-enumerated consent-mutation + route asserts the subject predicate. 5. **Revocation and re-grant.** Revocation flips exactly one active row to `revoked` and stamps `revoked_at`; it is effective for every @@ -549,12 +640,12 @@ declared profile table" means `profile_answers` (§3.5). (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). + `text` matching `users.id` (§3.6 binding) and `agent_id` is + `uuid` matching `agents.id` (§5.2 binding). 2. **Column-type allowlist witness:** the custody tables and `profile_answers` use only the column types named in - §2.1/§3.5/§4.1/§5.1/§5.2/§6.3 (uuid, text, integer, timestamptz, - boolean) — no bytea, json/jsonb, array, vector, or tsvector + §2.1/§3.5/§4.1/§5.1/§5.2/§6.3 (uuid, text, integer, bigint, + timestamptz, boolean) — no bytea, json/jsonb, array, vector, or tsvector column exists in them, closing the encoded/derived-representation routes by type rather than by probe alone. 3. **Closed write-route witness** (hierarchy contract §6.3, full @@ -664,13 +755,18 @@ declared profile table" means `profile_answers` (§3.5). commit and the pointer delete, the dangling pointer is deleted by the next reconciliation — and at no point is content written to the database or restored to the brain. **Concurrent-rewrite - 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). + interleavings (two):** deletion step 1 commits; before step 2 + runs, a concurrent §3.7 write recreates the same `brain_ref` and + completes its pointer upsert; step 2 then executes its + compare-and-delete. The witness drives this once with CHANGED + content (new `content_hash`, incremented `generation`) and once + with BYTE-IDENTICAL content (unchanged `content_hash`, + incremented `generation` — the ABA case a hash token would + miss), and in both asserts the DELETE matches zero rows, the + fresh pointer survives, its content resolves and verifies, and + no unpointed content and no dangling pointer exist afterward. A + static control asserts the deletion implementation conditions on + `generation`, not on `content_hash` (§4.7). 11. **Default-deny and granularity witnesses:** an agent grantee with no active row is refused; with a `granted` row for category A only, category B is refused; with agent X granted, agent Y of the @@ -697,7 +793,17 @@ declared profile table" means `profile_answers` (§3.5). 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). + identically (§5.4). **Cessation events:** the same deletions + each produce one consent-cessation event per removed row in the + §1.8 audit event table, carrying the §5.4 fields (cause with + parent table and key, actor — the surface-set actor when one + initiated the deletion, `system:cascade` otherwise — the grant + snapshot, and time); an atomicity control rolls the deleting + transaction back after the parent DELETE and asserts no + cessation event persists (no orphan event without a deletion, + no deletion without its events); the events survive subsequent + operation; and a subject-cessation (`users` row) deletion + produces its events identically (§5.4). 13. **Revocation/re-grant witnesses:** after revocation commits, the next evaluation refuses and the revoked row persists unmutated; a full grant → revoke → re-grant cycle yields two rows (one @@ -724,7 +830,7 @@ declared profile table" means `profile_answers` (§3.5). 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 + fault-injected at its interruption points: killed before the brain commit, both stores are unchanged and the caller receives no acknowledgment; killed between the brain commit and the pointer upsert, the brain holds unpointed content, no pointer row @@ -732,10 +838,34 @@ declared profile table" means `profile_answers` (§3.5). 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). + content served to the subject); killed after the pointer commit + but before the acknowledgment reaches the caller, the retry + executes under the locator lock, observes the converged state, + returns success, and the witness asserts exactly one pointer with + a verifying hash and an unchanged answer (idempotent completion, + no duplicate content commit that changes the served bytes). + **Same-locator interleaving:** two concurrent writers to one + (user, category, slot) run to completion; the witness asserts + both are serialized by the §3.7 advisory lock (their step-1/step-2 + spans do not interleave), the final pointer's hash verifies + against the blob committed at the locator, and no `mismatch_at` + is stamped by a subsequent read; a control with the lock removed + schedules the divergent order (A's pointer upsert after B's + content commit) and asserts the witness DETECTS the divergence — + proving the lock is what excludes it. **Stale retry:** writer A + is interrupted before acknowledgment; writer B then completes a + full write to the same locator; A's retry executes under the + lock and the witness asserts the end state is consistent (the + pointer's hash verifies against the current blob — last + completed write wins, no permanent mismatch). **Locator + injectivity:** the declared scheme maps every distinct + (category_key, slot) pair in the registry to a distinct + `brain_ref` (exhaustive at version 1: seven categories x the + `answer` slot), and a deliberately colliding scheme control + fails the witness. A static control asserts the implementation + orders content before pointer: no enumerated sensitive-write + route creates a pointer row before its brain commit has been + confirmed (§3.7). ## 8. Drafting additions (PRD §12.1 disclosure) @@ -751,9 +881,14 @@ ratification; none is claimed as a PRD mandate, and each is severable: 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. + subject or grantee row is absent (§5.4, §5.5). Each cascaded + deletion additionally emits a consent-cessation audit event — + written by an AFTER DELETE trigger in the same transaction as the + referent deletion, carrying cause, actor (surface-set or + `system:cascade`), the grant snapshot, and time — so the audit + trail records when and why each active grant ended (§5.4, + §7.12). This replaces the revision-2 system-actor auto-revocation + predicate, which is withdrawn. 4. The `custody_config` election record for the Standalone layout, singleton by constraint (§6.3). 5. The domain-separated keyed `content_hash` construction, its key @@ -771,7 +906,8 @@ 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). +10. Subject-only consent mutation authority (§5.4, the single + subject predicate). 11. The declared non-sensitive profile table `profile_answers` and its closed schema (§3.5). 12. The typed grantee referent columns, the @@ -792,11 +928,18 @@ UPDATE` as the first statement of its transaction, and the 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). + pointer-before-content prohibition — with the revision-5 + locator scheme (injective over (category, slot), the slot set + fixed to `answer` at version 1, stable across releases) and the + per-locator advisory lock spanning both steps that serializes + same-locator writers (§3.7, §7.16). 17. The compare-and-delete managed-deletion step: deletion step 2 is - conditioned on the `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). + conditioned on the monotonic `generation` counter recorded in + step 1 (revision 5; the revision-4 `content_hash` token is + withdrawn as ABA-prone under identical-content rewrites), so a + concurrent recreate of the same locator — changed or + byte-identical — is never unpointed by a stale deletion (§4.1, + §4.7, §7.10). 18. The sensitive-path write-set closure: a static inventory of every table a sensitive-write route can write, required to equal exactly the pointer table plus the §1.8 audit event table -- 2.54.0 From 8ca4564b5ed4254a5dcdf2be61d714543ce05eb0 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 22:06:00 -0500 Subject: [PATCH 06/13] docs: custody-schema contract revision 6 (sol r5 residuals F9/F12 + F14) --- docs/requirements/custody-schema.md | 195 +++++++++++++++++++++------- 1 file changed, 149 insertions(+), 46 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index 3c7992b7..ca9c8b36 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -97,6 +97,25 @@ records when and why each active grant ended, not only that it began (F12). The stale §3.6/§5.2 cross-references and the "first predicate" remnant are corrected (F13). +Revision 6 (sol r5 residuals F9/F12 + F14): the deletion and repair +token becomes the PAIR (row `id`, `generation`) — the uuid primary +key is minted fresh on every insert and never reused, so it is the +row-incarnation identity the bare counter lacked; a stale +compare-and-delete can no longer match a row reincarnated at +generation 1 after an orphan-repair delete, and §7.10 adds the +delete/reinsert ABA control (F9). The §3.7 locator lock now covers +EVERY blob/pointer mutator for a locator, not only ordinary writers: +managed deletion (§4.7) and reconciliation repair (§4.5) acquire the +same advisory lock across their observe-and-mutate spans, so the +reverse interleaving — a deleter destroying a blob a locked writer +has committed but not yet pointed — is unschedulable while the +tokens keep the crash-window (lock released by session death) +harmless; §7.10 adds the reverse interleaving and its lock-removed +control (F14). The cessation-event snapshot gains the deleted +consent row's `id` and its `user_id`, so events from two subjects' +grants to one ceasing grantee are attributable to their exact rows +and subjects, witnessed with a two-subject case (F12). + 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 @@ -396,12 +415,24 @@ 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. - The repair DELETE is generation-conditioned like §4.7 step 2: it - records the row's `generation` when it observes the content - absent and conditions the DELETE on it, so a concurrent §3.7 - write that recommits the locator between the observation and the - delete (bumping `generation`) makes the repair match zero rows — - reconciliation never unpoints content it did not observe absent. + Per-pointer repair runs under the §3.7 locator lock: before + observing content absence for a pointer, reconciliation acquires + the pointer's locator lock (a try-acquire is permitted — a + locator whose lock is held is skipped and repaired on the next + cycle, so the sweep never blocks behind a writer), holds it + across the observation and the DELETE, and releases it after. + The repair DELETE is additionally token-conditioned like §4.7 + step 2: it records the row's `id` and `generation` when it + observes the content absent and conditions the DELETE on BOTH — + the uuid `id` is minted per insert and never reused, so it + identifies the row incarnation; `generation` orders upserts + within the incarnation. A concurrent §3.7 write that recommits + the locator between the observation and the delete (bumping + `generation`), and a delete-then-reinsert that reincarnates the + locator's row (fresh `id`, `generation` back at 1), each make + the repair match zero rows — reconciliation never unpoints + content it did not observe absent, under either token + dimension. 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 @@ -415,29 +446,49 @@ with contract 6 (`mode-conversion.md`), identity with the database. 7. **Managed deletion protocol.** A managed deletion of sensitive content spans two stores and is an ordered protocol, not a single - transaction: step 1 commits the content deletion to the user's - brain repository, recording the `generation` of the row it - intends to delete as read before step 1; step 2, only after step 1 + transaction. **Lock coverage**: the deleter FIRST acquires the + §3.7 advisory lock on the target locator (`user_id`, + `category_key`, slot) — the same lock ordinary writers hold — + before reading the pointer row or touching either store, and + holds it until step 2 commits or the deleter's session ends. + Every blob/pointer mutator for one locator — ordinary writes + (§3.7), managed deletion, and reconciliation repair (§4.5) — + therefore serializes on one lock: the reverse interleaving, in + which a deleter reads the old pointer and destroys a blob that a + lock-holding writer has committed in its step 1 but not yet + pointed, is unschedulable, because the deleter cannot enter its + protocol while the writer holds the lock. **Steps**: step 1 + commits the content deletion to the user's brain repository, + recording the target row's `id` AND `generation` as read after + lock acquisition and before step 1; step 2, only after step 1 has committed, deletes the pointer row in its own database transaction as a **compare-and-delete** — the DELETE is - conditioned on (`user_id`, `category_key`, `brain_ref`) AND - `generation` equal to the recorded value. The token is the - generation, not the content hash, deliberately: every §3.7 upsert - increments `generation` even when it recommits byte-identical - content (whose §4.4 hash is unchanged), so a concurrent §3.7 - write that recreates the same locator — with new content OR with - identical content — moves the row past the recorded generation, - step 2's condition matches zero rows, deletes nothing, and the - fresh content stays pointed. Deletion never removes a pointer for - a write it did not observe; the hash-token ABA (identical bytes - reproducing the recorded value) is unrepresentable because the - counter never repeats. Interruption before step 1 commits leaves - both stores unchanged. Interruption between the steps leaves a - dangling pointer, which §4.5 repairs toward deletion within one - reconciliation cycle. At no point does any compensation write - content into the database (witness §7.10, including both - concurrent-rewrite interleavings — changed content and - identical content). + conditioned on the recorded `id` AND `generation`. The token is + this PAIR, not the content hash and not the generation alone, + deliberately: every §3.7 upsert increments `generation` even + when it recommits byte-identical content (whose §4.4 hash is + unchanged), so a same-row rewrite moves the row past the + recorded generation; and the uuid `id` is minted fresh on every + insert and never reused, so a row REINCARNATION — the orphan row + deleted by §4.5 repair, then an ordinary write recreating the + locator with a fresh row whose `generation` is back at 1 — + cannot reproduce the recorded pair even though the counter value + repeats. In every such case step 2 matches zero rows, deletes + nothing, and the fresh content stays pointed: deletion never + removes a pointer row it did not observe. The lock excludes the + live interleavings; the pair token makes the crash windows + harmless — a deleter whose session dies after step 1 releases + the lock (§3.7 rule), and whatever runs afterward, its never-run + step 2 has no effect and a later deletion request is a NEW + protocol run reading current state. Interruption before step 1 + commits leaves both stores unchanged. Interruption between the + steps leaves a dangling pointer, which §4.5 repairs toward + deletion within one reconciliation cycle. At no point does any + compensation write content into the database (witness §7.10, + including both concurrent-rewrite interleavings — changed + content and identical content — the delete/reinsert + reincarnation, and the reverse writer/deleter interleaving with + its lock-removed control). ## 5. Consent schema and evaluation @@ -536,9 +587,16 @@ NULL)` — and the partial unique index UNIQUE (`user_id`, initiating actor — a platform surface that deletes a referent sets a transaction-local actor variable the trigger reads; absent one, the event records actor `system:cascade` (a migration or - direct referential action) — a snapshot of the ended grant - (grantee type and ref, `category_key`, `state`, `granted_at`, - `revoked_at`), and the event time. The snapshot is consent + direct referential action) — an immutable snapshot of the ended + grant identifying its exact row and subject: the deleted consent + row's `id`, its `user_id` (the data subject), grantee type and + ref, `category_key`, `state`, `granted_at`, and `revoked_at` — + and the event time. The row `id` and `user_id` are REQUIRED + precisely because the cause alone identifies only the ceasing + referent: when two users hold grants to one ceasing grantee, the + cascade removes both rows in one transaction, and only the + per-row snapshot distinguishes which subject's grant each event + records. The snapshot is consent metadata, never profile content, so §3.1 is not implicated. The audit table therefore records both ends of every grant's life: the mutation events written at grant/revoke time survive the @@ -755,18 +813,45 @@ declared profile table" means `profile_answers` (§3.5). commit and the pointer delete, the dangling pointer is deleted by the next reconciliation — and at no point is content written to the database or restored to the brain. **Concurrent-rewrite - interleavings (two):** deletion step 1 commits; before step 2 - runs, a concurrent §3.7 write recreates the same `brain_ref` and - completes its pointer upsert; step 2 then executes its - compare-and-delete. The witness drives this once with CHANGED + interleavings (two):** deletion step 1 commits and the deleting + session is fault-injected dead before step 2 — the §4.7 lock + releases with the session, opening the crash window; a + concurrent §3.7 write then recreates the same `brain_ref` and + completes its pointer upsert; the stale step-2 compare-and-delete + is then replayed with the recorded token. The witness drives + this once with CHANGED content (new `content_hash`, incremented `generation`) and once with BYTE-IDENTICAL content (unchanged `content_hash`, incremented `generation` — the ABA case a hash token would miss), and in both asserts the DELETE matches zero rows, the fresh pointer survives, its content resolves and verifies, and - no unpointed content and no dangling pointer exist afterward. A + no unpointed content and no dangling pointer exist afterward. + **Delete/reinsert reincarnation (third interleaving):** in the + same crash window, §4.5 reconciliation first removes the + now-orphan pointer row; an ordinary §3.7 write then recreates + the locator as a FRESH row — new `id`, `generation` back at 1 + and therefore EQUAL to the recorded generation value; the + replayed stale step 2 is asserted to match ZERO rows (the + recorded `id` names the dead incarnation), and the fresh pointer + survives with its content resolving and verifying — the case a + bare generation token fails. **Reverse writer/deleter + interleaving:** a §3.7 writer holds the locator lock and has + committed its step-1 brain content but not its pointer upsert + when a managed deletion for the same locator starts. The witness + asserts the deletion BLOCKS at §4.7 lock acquisition until the + writer's pointer upsert and release, and that afterward no + acknowledged write's content is absent and no pointer names + absent content; a lock-removed control schedules the deleter's + observation and content removal inside the writer's + step-1-to-step-2 gap and asserts the witness DETECTS the + resulting acknowledged-write loss, establishing that the lock is + what excludes it. A static control asserts the deletion implementation conditions on - `generation`, not on `content_hash` (§4.7). + BOTH the recorded row `id` and `generation` — not on + `content_hash` and not on `generation` alone — and a second + static control asserts the managed-deletion and + reconciliation-repair implementations each acquire the §3.7 + locator lock before their observation reads (§4.5, §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 @@ -798,7 +883,14 @@ declared profile table" means `profile_answers` (§3.5). §1.8 audit event table, carrying the §5.4 fields (cause with parent table and key, actor — the surface-set actor when one initiated the deletion, `system:cascade` otherwise — the grant - snapshot, and time); an atomicity control rolls the deleting + snapshot including the deleted consent row's `id` and `user_id`, + and time); a **two-subject discrimination witness** has two + distinct users each hold a `granted` row to ONE agent grantee + for the SAME category, deletes that grantee's `agents` row, and + asserts exactly two cessation events exist and that each event's + snapshot carries a distinct consent-row `id` and the correct + `user_id` — every event names exactly which subject's grant of + which row ended; an atomicity control rolls the deleting transaction back after the parent DELETE and asserts no cessation event persists (no orphan event without a deletion, no deletion without its events); the events survive subsequent @@ -885,7 +977,10 @@ ratification; none is claimed as a PRD mandate, and each is severable: deletion additionally emits a consent-cessation audit event — written by an AFTER DELETE trigger in the same transaction as the referent deletion, carrying cause, actor (surface-set or - `system:cascade`), the grant snapshot, and time — so the audit + `system:cascade`), the grant snapshot — which, per revision 6, + includes the deleted consent row's `id` and its `user_id`, so + events from different subjects' grants to one ceasing grantee + remain distinguishable — and time — so the audit trail records when and why each active grant ended (§5.4, §7.12). This replaces the revision-2 system-actor auto-revocation predicate, which is withdrawn. @@ -931,15 +1026,23 @@ UPDATE` as the first statement of its transaction, and the pointer-before-content prohibition — with the revision-5 locator scheme (injective over (category, slot), the slot set fixed to `answer` at version 1, stable across releases) and the - per-locator advisory lock spanning both steps that serializes - same-locator writers (§3.7, §7.16). + per-locator advisory lock spanning both steps — held, per + revision 6, by EVERY blob/pointer mutator for the locator: + ordinary writers, managed deletion, and reconciliation repair + (try-acquire), so live interleavings between any two mutators + are excluded by one serialization protocol (§3.7, §4.5, §4.7, + §7.16). 17. The compare-and-delete managed-deletion step: deletion step 2 is - conditioned on the monotonic `generation` counter recorded in - step 1 (revision 5; the revision-4 `content_hash` token is - withdrawn as ABA-prone under identical-content rewrites), so a - concurrent recreate of the same locator — changed or - byte-identical — is never unpointed by a stale deletion (§4.1, - §4.7, §7.10). + conditioned on the PAIR of the target row's uuid `id` (the + immutable row-incarnation identity) and its monotonic + `generation` counter, both recorded in step 1 under the locator + lock (revision 6; the revision-4 `content_hash` token was + withdrawn as ABA-prone under identical-content rewrites, and the + revision-5 bare `generation` token is withdrawn as ABA-prone + under delete/reinsert reincarnation, where the counter restarts + at 1), so a stale deletion surviving a crash window is never + able to unpoint a fresh row — changed, byte-identical, or + reincarnated (§4.1, §4.7, §7.10). 18. The sensitive-path write-set closure: a static inventory of every table a sensitive-write route can write, required to equal exactly the pointer table plus the §1.8 audit event table -- 2.54.0 From 482400d93c53e38c32816c7edd9e2b1f88c78672 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 22:38:20 -0500 Subject: [PATCH 07/13] docs: custody-schema contract revision 7 (sol r6 F15: within-cycle blocking retry preserves the one-cycle orphan bound under lock contention) --- docs/requirements/custody-schema.md | 69 +++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index ca9c8b36..f0516fd9 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -116,6 +116,23 @@ consent row's `id` and its `user_id`, so events from two subjects' grants to one ceasing grantee are attributable to their exact rows and subjects, witnessed with a two-subject case (F12). +Revision 7 (sol r6 F15): the reconciliation try-acquire no longer +discharges a locator for the cycle — a failed try-acquire QUEUES the +locator within the same cycle, and before the cycle completes the +sweep revisits every queued locator with a blocking acquire, running +the same locked observation and token-conditioned repair. A +triggered reconciliation cycle is complete only when every in-scope +pointer has been processed under its locator lock, so the §4.5 +one-cycle orphan bound holds unconditionally on lock contention; +the retry terminates because every §3.7 hold is transaction- or +session-scoped and session end releases the lock automatically. +§7.10 adds the lock-held contention control (sweep pass skips +without blocking; cycle completion waits for the queued repair; +the orphan is deleted before the cycle reports completion), its +repeated-contention variant, and a third static control failing an +implementation that defers a skipped locator to a later cycle +(F15). + 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 @@ -417,10 +434,24 @@ with contract 6 (`mode-conversion.md`), identity with at most one cycle, and repair performs no database content write. Per-pointer repair runs under the §3.7 locator lock: before observing content absence for a pointer, reconciliation acquires - the pointer's locator lock (a try-acquire is permitted — a - locator whose lock is held is skipped and repaired on the next - cycle, so the sweep never blocks behind a writer), holds it - across the observation and the DELETE, and releases it after. + the pointer's locator lock, holds it across the observation and + the DELETE, and releases it after. The sweep pass MAY use a + try-acquire so it never blocks mid-pass behind a writer, but a + failed try-acquire defers the locator, never discharges it: the + skipped locator is queued within the SAME cycle, and before the + cycle completes the sweep revisits every queued locator with a + blocking acquire and runs the same locked observation and + token-conditioned repair, retrying until the lock is acquired. A + triggered reconciliation cycle is COMPLETE only when every + in-scope pointer has been processed under its locator lock; + deferral moves work to the end of the cycle, never into a later + one, so the one-cycle bound above holds unconditionally on lock + contention. The blocking retry terminates: every §3.7 hold is + transaction- or session-scoped and session end releases the lock + automatically (§3.7), so no holder — live or crashed — can hold + a locator past its session; a cycle whose retry is still blocked + has not completed, and the orphan is deleted the moment the lock + becomes available, before the cycle reports completion. The repair DELETE is additionally token-conditioned like §4.7 step 2: it records the row's `id` and `generation` when it observes the content absent and conditions the DELETE on BOTH — @@ -845,13 +876,33 @@ declared profile table" means `profile_answers` (§3.5). observation and content removal inside the writer's step-1-to-step-2 gap and asserts the witness DETECTS the resulting acknowledged-write loss, establishing that the lock is - what excludes it. A + what excludes it. **Lock-held contention control (one-cycle + bound):** starting from a PRE-EXISTING orphan, a §3.7 writer + session holds the orphan's locator lock while the triggered + reconciliation's sweep pass runs. The witness asserts the pass + completes without blocking and without repairing the held + locator (the try-acquire skip), that the cycle does NOT report + completion while the lock is held, and that after the holder + releases, the queued blocking retry deletes the orphan under + the recorded (`id`, `generation`) token BEFORE the cycle + reports completion — the orphan is gone by cycle completion, + proving the §4.5 bound under contention. A repeated-contention + variant hands the lock to a second holder the moment the first + releases and asserts the queued locator is retried until + acquired — the orphan is still deleted by cycle completion once + the last holder releases, never dropped after one failed + attempt. A static control asserts the deletion implementation conditions on BOTH the recorded row `id` and `generation` — not on - `content_hash` and not on `generation` alone — and a second + `content_hash` and not on `generation` alone — a second static control asserts the managed-deletion and reconciliation-repair implementations each acquire the §3.7 - locator lock before their observation reads (§4.5, §4.7). + locator lock before their observation reads (§4.5, §4.7), and a + third static control asserts the reconciliation implementation + queues a failed try-acquire for a within-cycle blocking retry — + an implementation that defers a skipped locator to a later + cycle, or that reports cycle completion with a queued locator + unprocessed, FAILS. 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 @@ -1029,7 +1080,9 @@ UPDATE` as the first statement of its transaction, and the per-locator advisory lock spanning both steps — held, per revision 6, by EVERY blob/pointer mutator for the locator: ordinary writers, managed deletion, and reconciliation repair - (try-acquire), so live interleavings between any two mutators + (try-acquire with the revision-7 within-cycle blocking retry, + so a failed try-acquire defers repair to the end of the cycle, + never past it), so live interleavings between any two mutators are excluded by one serialization protocol (§3.7, §4.5, §4.7, §7.16). 17. The compare-and-delete managed-deletion step: deletion step 2 is -- 2.54.0 From f8ac64f58a88445a02106889ed10e7af08895e97 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 26 Aug 2026 23:08:12 -0500 Subject: [PATCH 08/13] docs: custody-schema contract revision 8 (sol r7 residual F15: declared hold deadline + arrival-order grants make the one-cycle bound wall-clock provable) --- docs/requirements/custody-schema.md | 98 +++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index f0516fd9..164ce4f8 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -133,6 +133,27 @@ repeated-contention variant, and a third static control failing an implementation that defers a skipped locator to a later cycle (F15). +Revision 8 (sol r7 residual F15): the one-cycle bound gains its +missing termination proof. §3.7 adds two liveness rules for the +locator lock: a declared finite HOLD DEADLINE, enforced with +session-level timeout mechanisms on every lock-holding path — a +live holder that stalls past the deadline is terminated, session +end releases the lock, and the stalled-holder schedule collapses +into the already-harmless crash window — and an arrival-order +GRANT rule, so a blocked waiter's wait is bounded by the current +holder and the waiters already ahead, never by later arrivals. +§4.5's blocking retry therefore terminates within a wall-clock +bound fixed when it starts waiting, and the implementing PR +derives and declares a maximum cycle duration whose exceedance is +surfaced as an error, never a silent hang. §7.10 adds the +hold-deadline control (a non-cooperating stalled holder is +terminated at the deadline and the orphan is still deleted before +cycle completion), the grant-order control (a stream of later +contenders cannot starve the queued repair, and the declared +cycle-duration limit is exercised as a checked bound), and a +fourth static control failing any lock-holding path without the +declared deadline (F15). + 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 @@ -328,7 +349,27 @@ with contract 6 (`mode-conversion.md`), identity with slot) (e.g. a session-scoped `pg_advisory_lock` on a stable hash of the triple), held until step 2 commits or the writer's session ends (session end releases it automatically, so a crashed holder - never wedges the locator). Writers to one locator are therefore + never wedges the locator). Two liveness rules bound this + exclusion. **Hold deadline:** every session that acquires a + locator lock — ordinary writer, managed deleter (§4.7), + reconciliation repair (§4.5) — runs under a declared finite hold + deadline: the implementing PR declares the value and enforces it + with the database's session-level timeout mechanisms (statement + timeout and idle-in-transaction timeout, plus an application + deadline covering step 1's brain commit), so a holder that + stalls past the deadline has its session TERMINATED — session + end then releases the lock automatically per the rule above, and + the terminated holder's partial protocol is exactly the crash + window the tokens already make harmless (§4.5, §4.7). No holder + — live, stalled, or crashed — holds a locator lock longer than + the declared deadline. **Grant order:** conflicting waiters on + one locator lock are granted in arrival order — a requester that + starts waiting after another never acquires before it (the + queue-order grant of the pinned advisory-lock mechanism, + witnessed §7.10) — so a blocked waiter's wait is bounded by the + current holder's remaining deadline plus the deadline-bounded + holds of the waiters already ahead of it, never by later + arrivals. Writers to one locator are therefore totally ordered; writes to distinct locators do not contend. Because no two same-locator writes interleave, a completed write always leaves the pointer's `content_hash` describing the blob @@ -446,12 +487,23 @@ with contract 6 (`mode-conversion.md`), identity with in-scope pointer has been processed under its locator lock; deferral moves work to the end of the cycle, never into a later one, so the one-cycle bound above holds unconditionally on lock - contention. The blocking retry terminates: every §3.7 hold is - transaction- or session-scoped and session end releases the lock - automatically (§3.7), so no holder — live or crashed — can hold - a locator past its session; a cycle whose retry is still blocked - has not completed, and the orphan is deleted the moment the lock - becomes available, before the cycle reports completion. + contention. The blocking retry terminates within a WALL-CLOCK + BOUND, not merely eventually: every holder is subject to the + §3.7 hold deadline (a stalled live session is terminated at the + deadline and session end releases the lock — voluntary release + is not assumed), and grants follow §3.7 arrival order (no later + requester overtakes the queued retry), so once the retry blocks + it acquires within the current holder's remaining deadline plus + the deadline-bounded holds of the waiters already queued ahead — + a finite quantity fixed when the retry starts waiting, + independent of how many contenders arrive later. Cycle + completion is therefore itself bounded: the implementing PR + derives and declares a maximum cycle duration from the declared + hold deadline, the sweep scope, and the retry queue, and a cycle + exceeding its declared maximum is surfaced as an error — never a + silent hang. A cycle whose retry is still blocked has not + completed, and the orphan is deleted before the cycle reports + completion, within the declared bound. The repair DELETE is additionally token-conditioned like §4.7 step 2: it records the row's `id` and `generation` when it observes the content absent and conditions the DELETE on BOTH — @@ -891,7 +943,22 @@ declared profile table" means `profile_answers` (§3.5). releases and asserts the queued locator is retried until acquired — the orphan is still deleted by cycle completion once the last holder releases, never dropped after one failed - attempt. A + attempt. A **hold-deadline control** starts from a pre-existing + orphan, has a live holder acquire the orphan's locator lock and + stall past the declared §3.7 hold deadline WITHOUT ending its + session (idle transaction kept open), and asserts the holder's + session is terminated at the deadline, the lock releases + without any voluntary action by the holder, and the queued + blocking retry then deletes the orphan under the recorded + token before the cycle reports completion — proving termination + does not depend on a cooperative holder. A **grant-order + control** blocks the end-of-cycle retry behind a holder, keeps + a stream of later contenders requesting the same locator lock + while the retry waits, and asserts the retry acquires before + every later contender and the cycle completes within its + declared maximum duration — proving later arrivals cannot + starve the queued repair, and exercising the declared + cycle-duration limit as a checked bound rather than prose. A static control asserts the deletion implementation conditions on BOTH the recorded row `id` and `generation` — not on `content_hash` and not on `generation` alone — a second @@ -902,7 +969,11 @@ declared profile table" means `profile_answers` (§3.5). queues a failed try-acquire for a within-cycle blocking retry — an implementation that defers a skipped locator to a later cycle, or that reports cycle completion with a queued locator - unprocessed, FAILS. + unprocessed, FAILS. A fourth static control asserts every + lock-holding path (ordinary write §3.7, managed deletion §4.7, + reconciliation repair §4.5) is configured with the declared + §3.7 hold deadline — an implementation with any unbounded + lock-holding path FAILS. 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 @@ -1082,9 +1153,12 @@ UPDATE` as the first statement of its transaction, and the ordinary writers, managed deletion, and reconciliation repair (try-acquire with the revision-7 within-cycle blocking retry, so a failed try-acquire defers repair to the end of the cycle, - never past it), so live interleavings between any two mutators - are excluded by one serialization protocol (§3.7, §4.5, §4.7, - §7.16). + never past it; per revision 8 every hold runs under a declared + finite hold deadline with arrival-order grants, so the deferred + repair — and with it cycle completion — is wall-clock bounded, + not merely eventual), so live interleavings between any two + mutators are excluded by one serialization protocol (§3.7, + §4.5, §4.7, §7.16). 17. The compare-and-delete managed-deletion step: deletion step 2 is conditioned on the PAIR of the target row's uuid `id` (the immutable row-incarnation identity) and its monotonic -- 2.54.0 From c6a2237dded52f7225f6a0b9431e43b97b5c8b08 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 02:22:17 -0500 Subject: [PATCH 09/13] docs: custody-schema contract revision 9 (sol r8 F15 residual + F16: independent deadline supervisor, brain fence via conditional application, reservation-at-skip, computed fixed cycle maximum with explicit exceedance carry-over) --- docs/requirements/custody-schema.md | 282 +++++++++++++++++++++++----- 1 file changed, 231 insertions(+), 51 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index 164ce4f8..56ffa283 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -154,6 +154,39 @@ cycle-duration limit is exercised as a checked bound), and a fourth static control failing any lock-holding path without the declared deadline (F15). +Revision 9 (sol r8 residual F15 + new F16): the hold deadline +becomes independently enforceable and the external store gains a +fence. §3.7's deadline is now enforced by a deadline supervisor +whose liveness does not depend on the stalled holder — the +database's timeout mechanisms are defense in depth, not the +enforcement path, since step 1's brain commit is not a database +statement — and every brain mutation performed under a locator +lock is FENCED: applied conditionally on the locator's brain state +still equaling the value recorded at lock acquisition, so a +terminated holder's in-flight external operation, resuming after +the lock has passed on, is refused atomically and can never remove +or replace a later holder's committed content (F16). §4.5's +deferred repair now RESERVES its queue place at the moment its +try-acquire fails, fixing the predecessor set at skip time and +closing the window between skip and queue entry; the cycle maximum +becomes the computed formula `M = S + Σᵢ(D·(Qᵢ+1)+R)` over +snapshots taken at or before each reservation, fixed when the last +reservation is placed and never enlarged; and exceedance is an +explicit transition — the cycle reports failure, never completion, +withdraws its pending reservations, and persists a durable +carry-over set the next cycle processes first — so the orphan +bound binds every completing cycle and every non-completing path +is a surfaced error, and the grant-order rule must be bound to a +documented mechanism guarantee or an application FIFO queue, not +assumed (F15). §7.10 adds two external-step deadline controls (a +stalled managed deletion and a stalled ordinary-writer brain +commit, each terminated by the supervisor mid-external-operation, +resumed after a fresh holder's acknowledged write, and asserted +refused by the fence), the reservation control, the exceedance +control, and static controls binding the grant-order guarantee and +the fenced deletion path; §7.16 adds the static fenced-commit +control (F15, F16). + 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 @@ -354,19 +387,37 @@ with contract 6 (`mode-conversion.md`), identity with locator lock — ordinary writer, managed deleter (§4.7), reconciliation repair (§4.5) — runs under a declared finite hold deadline: the implementing PR declares the value and enforces it - with the database's session-level timeout mechanisms (statement - timeout and idle-in-transaction timeout, plus an application - deadline covering step 1's brain commit), so a holder that - stalls past the deadline has its session TERMINATED — session - end then releases the lock automatically per the rule above, and - the terminated holder's partial protocol is exactly the crash - window the tokens already make harmless (§4.5, §4.7). No holder + through a **deadline supervisor** whose liveness is INDEPENDENT + of the holder — a separate live component, not a timer inside + the possibly-stalled holder process, that observes lock + acquisition times and terminates the database session of any + holder past its deadline. The database's session-level timeout + mechanisms (statement timeout, idle-in-transaction timeout) are + configured as defense in depth but are not the enforcement + path: a statement timeout aborts a statement without ending the + session, an idle-in-transaction timeout fires only on an idle + open transaction, and step 1's brain commit is not a database + statement at all — so a session stalled inside an external + operation is terminated by the supervisor, which no + database-side mechanism replaces. A holder that stalls past the + deadline thus has its session TERMINATED — session end then + releases the lock automatically per the rule above. The + terminated holder's partial DATABASE protocol is exactly the + crash window the tokens already make harmless (§4.5, §4.7); its + possibly still in-flight EXTERNAL operation is made harmless by + the brain fence below. No holder — live, stalled, or crashed — holds a locator lock longer than the declared deadline. **Grant order:** conflicting waiters on one locator lock are granted in arrival order — a requester that - starts waiting after another never acquires before it (the - queue-order grant of the pinned advisory-lock mechanism, - witnessed §7.10) — so a blocked waiter's wait is bounded by the + starts waiting after another never acquires before it. The + implementing PR MUST bind this property to a mechanism, not + assume it: either it cites the pinned advisory-lock mechanism's + documented queue-order grant guarantee, or, absent such a + documented guarantee, it routes every conflicting locator-lock + acquisition through an application-level FIFO queue whose order + invariant is itself part of the implementation; the binding is + checked by a §7.10 static control and the property is exercised + dynamically (§7.10) — so a blocked waiter's wait is bounded by the current holder's remaining deadline plus the deadline-bounded holds of the waiters already ahead of it, never by later arrivals. Writers to one locator are therefore @@ -377,8 +428,38 @@ with contract 6 (`mode-conversion.md`), identity with (writer A's pointer over writer B's content) is unschedulable, not merely unlikely. + **Brain fence.** Deadline termination releases the lock while an + external brain operation started by the terminated holder may + still be in flight; lock release is therefore not, by itself, a + fence for the external store. Every brain mutation performed + under a locator lock — an ordinary write's step-1 content + commit (below), a managed deletion's step-1 content deletion + (§4.7) — is FENCED: immediately after acquiring the lock the + holder records the locator's current brain state (the content + version at `L(category_key, slot)`, or its recorded absence) as + its **fence value**, and the mutation is applied conditionally — + the brain store applies it only if the locator's state at + application time still equals the recorded fence value, with the + check and the application atomic with respect to other brain + mutations (compare-and-swap semantics; an atomic ref update in a + git-backed store satisfies this, with the per-locator condition + re-validated inside the swap loop when the swap is taken on a + coarser head). A later holder that commits at the locator + changes the locator's state, so a stale operation — one whose + session was terminated and whose lock has passed to a newer + holder — no longer matches its recorded fence value and is + REFUSED atomically: it cannot remove or replace content + committed by any later holder, whether it resumes microseconds + or hours after the termination. A live holder's own mutation + passes its fence trivially: it holds the locator lock, so no + other mutator has changed the locator since it recorded the + value. Every brain-mutation path carries the fence; a path + applying an unfenced brain mutation fails the §7.10 and §7.16 + static controls. + **Steps.** Step 1 commits the content to the owning user's brain - at `L(category_key, slot)`; step 2, only after step 1 has + at `L(category_key, slot)` as a fenced mutation (Brain fence + above); step 2, only after step 1 has committed, upserts the pointer row (insert, or update of the existing row for that (user, category, brain_ref), incrementing `generation` per §4.1) in its own database transaction inside the @@ -471,39 +552,71 @@ with contract 6 (`mode-conversion.md`), identity with user's pointers runs on two triggers: every profile write for that user, and a periodic sweep whose interval the implementing PR declares (at most daily). A pointer whose content is absent is - deleted by the next triggered reconciliation — an orphan survives - at most one cycle, and repair performs no database content write. + deleted by the next triggered reconciliation cycle that reports + COMPLETION — no orphan survives a completed cycle; a cycle that + cannot complete within its computed maximum fails as an explicit + surfaced error with durable carry-over (below), never silently — + and repair performs no database content write. Per-pointer repair runs under the §3.7 locator lock: before observing content absence for a pointer, reconciliation acquires the pointer's locator lock, holds it across the observation and the DELETE, and releases it after. The sweep pass MAY use a try-acquire so it never blocks mid-pass behind a writer, but a - failed try-acquire defers the locator, never discharges it: the - skipped locator is queued within the SAME cycle, and before the - cycle completes the sweep revisits every queued locator with a - blocking acquire and runs the same locked observation and - token-conditioned repair, retrying until the lock is acquired. A + failed try-acquire defers the locator, never discharges it — and + the deferral RESERVES its place: at the moment the try-acquire + fails, the sweep submits the locator's blocking lock request + (the **reservation point**), so the request enters the lock + queue immediately and its predecessor set — the current holder + plus the waiters already queued at that instant — is fixed at + skip time; contenders arriving afterward are later arrivals + that §3.7 grant order places behind it, and there is no window + between the try-acquire failure and queue entry in which a new + contender can join the predecessor set. The sweep proceeds with + the pass while the reservation waits, and before the cycle + completes it returns to every reserved locator, completes the + blocking acquire, and runs the same locked observation and + token-conditioned repair. A triggered reconciliation cycle is COMPLETE only when every in-scope pointer has been processed under its locator lock; deferral moves work to the end of the cycle, never into a later - one, so the one-cycle bound above holds unconditionally on lock + one except through the exceedance transition below — which + fails the cycle loudly, never completes it — so the + completed-cycle bound above holds unconditionally on lock contention. The blocking retry terminates within a WALL-CLOCK BOUND, not merely eventually: every holder is subject to the - §3.7 hold deadline (a stalled live session is terminated at the - deadline and session end releases the lock — voluntary release - is not assumed), and grants follow §3.7 arrival order (no later - requester overtakes the queued retry), so once the retry blocks - it acquires within the current holder's remaining deadline plus - the deadline-bounded holds of the waiters already queued ahead — - a finite quantity fixed when the retry starts waiting, - independent of how many contenders arrive later. Cycle - completion is therefore itself bounded: the implementing PR - derives and declares a maximum cycle duration from the declared - hold deadline, the sweep scope, and the retry queue, and a cycle - exceeding its declared maximum is surfaced as an error — never a - silent hang. A cycle whose retry is still blocked has not - completed, and the orphan is deleted before the cycle reports - completion, within the declared bound. + §3.7 hold deadline (a stalled live session is terminated by the + independent supervisor at the deadline and session end releases + the lock — voluntary release is not assumed), and grants follow + §3.7 arrival order (no later requester overtakes the queued + reservation), so the reservation is granted within the current + holder's remaining deadline plus the deadline-bounded holds of + the waiters already queued ahead — a finite quantity fixed at + its reservation point, independent of how many contenders + arrive later. Cycle completion is therefore itself bounded by a + COMPUTED maximum fixed within the cycle: the implementing PR + declares the hold deadline `D`, a sweep-pass bound `S` over the + cycle's scope (the scope is snapshotted when the cycle starts), + and a per-locator repair bound `R` (at most `D`), and computes + the cycle maximum `M = S + Σᵢ (D·(Qᵢ+1) + R)` over the reserved + locators `i`, where `Qᵢ` is the number of waiters queued ahead + of reservation `i` at its reservation point. Every quantity in + the formula is a snapshot taken at or before the corresponding + reservation, so `M` is fully determined when the last + reservation is placed and is NEVER enlarged afterward — queue + growth after a reservation cannot extend it, because later + arrivals are not predecessors. **Exceedance transition.** A + cycle that reaches its computed `M` with reserved locators + unprocessed stops: it withdraws its pending lock reservations, + reports FAILURE — never completion — surfaces the error, and + PERSISTS its unprocessed reserved locators as a durable + CARRY-OVER set. The next triggered reconciliation cycle takes + the carry-over set into its scope FIRST — fresh reservations, + its own computed maximum — before its own sweep. An orphan is + deleted before its cycle reports completion, within that + cycle's computed `M`; the only path past a cycle boundary is + the exceedance transition, an explicit surfaced failure with + durable carry-over — never a silent hang and never a silent + drop. The repair DELETE is additionally token-conditioned like §4.7 step 2: it records the row's `id` and `generation` when it observes the content absent and conditions the DELETE on BOTH — @@ -541,8 +654,10 @@ with contract 6 (`mode-conversion.md`), identity with lock-holding writer has committed in its step 1 but not yet pointed, is unschedulable, because the deleter cannot enter its protocol while the writer holds the lock. **Steps**: step 1 - commits the content deletion to the user's brain repository, - recording the target row's `id` AND `generation` as read after + commits the content deletion to the user's brain repository as + a FENCED mutation (§3.7 Brain fence — the fence value recorded + immediately after lock acquisition), recording the target row's + `id` AND `generation` as read after lock acquisition and 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 @@ -563,7 +678,14 @@ with contract 6 (`mode-conversion.md`), identity with harmless — a deleter whose session dies after step 1 releases the lock (§3.7 rule), and whatever runs afterward, its never-run step 2 has no effect and a later deletion request is a NEW - protocol run reading current state. Interruption before step 1 + protocol run reading current state. A deleter TERMINATED at the + §3.7 hold deadline while its step-1 brain deletion is still in + flight is likewise harmless on the brain side: the in-flight + deletion is fenced, so once a later holder commits at the + locator, the stale deletion's conditional application fails and + removes nothing — content committed and acknowledged after the + termination cannot be destroyed by the terminated deleter's + resuming operation (witness §7.10). 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 @@ -952,13 +1074,47 @@ declared profile table" means `profile_answers` (§3.5). blocking retry then deletes the orphan under the recorded token before the cycle reports completion — proving termination does not depend on a cooperative holder. A **grant-order - control** blocks the end-of-cycle retry behind a holder, keeps + control** blocks the reserved repair behind a holder, keeps a stream of later contenders requesting the same locator lock - while the retry waits, and asserts the retry acquires before - every later contender and the cycle completes within its - declared maximum duration — proving later arrivals cannot - starve the queued repair, and exercising the declared - cycle-duration limit as a checked bound rather than prose. A + while the reservation waits, and asserts the reservation is + granted before every later contender and the cycle completes + within its computed maximum `M` — proving later arrivals cannot + starve the queued repair, and exercising the computed + cycle maximum as a checked bound rather than prose. A + **reservation control** has the sweep's try-acquire fail + against a holder and asserts the blocking lock request is + enqueued AT the try-acquire failure (the §4.5 reservation + point), not at end of pass — then starts contenders + immediately after the failed try-acquire and asserts every one + is granted after the reservation, and that the cycle's `M` was + fixed when the last reservation was placed and is not enlarged + while the late contenders queue. Two **external-step deadline + controls** exercise termination at the real external boundary, + not an idle transaction. First, the stalled managed deletion: + a §4.7 deleter acquires the locator lock and starts its step-1 + brain deletion, which is suspended mid-operation — the session + neither idle in a transaction nor executing a database + statement; the control asserts the independent §3.7 supervisor + terminates the session at the deadline with no cooperation + from the holder and the lock releases; a fresh §3.7 writer + then acquires the lock, commits new content at the locator + with its pointer upsert, and is acknowledged; the suspended + stale deletion is then RESUMED and asserted REFUSED by the + brain fence — the fresh content is still present, its pointer + resolves and verifies, and no mismatch is stamped. Second, the + stalled ordinary writer: a §3.7 writer stalls inside its + step-1 brain commit, is terminated at the deadline, a fresh + writer completes a full acknowledged write at the same + locator, and the stale commit is resumed and asserted refused + — the locator serves the fresh writer's bytes and the fresh + pointer's hash verifies. An **exceedance control** contrives a + cycle that cannot finish within its computed `M` (an injected + sequence of deadline-length holders on a reserved locator) and + asserts the cycle reports FAILURE — never completion — + surfaces the error, withdraws its pending reservations, and + persists the carry-over set; the next triggered cycle is + asserted to take the carry-over locators into scope first and + delete the orphan before reporting completion. A static control asserts the deletion implementation conditions on BOTH the recorded row `id` and `generation` — not on `content_hash` and not on `generation` alone — a second @@ -966,14 +1122,24 @@ declared profile table" means `profile_answers` (§3.5). reconciliation-repair implementations each acquire the §3.7 locator lock before their observation reads (§4.5, §4.7), and a third static control asserts the reconciliation implementation - queues a failed try-acquire for a within-cycle blocking retry — - an implementation that defers a skipped locator to a later - cycle, or that reports cycle completion with a queued locator - unprocessed, FAILS. A fourth static control asserts every + reserves a failed try-acquire's queue place at skip time for a + within-cycle blocking acquire — an implementation that defers + a skipped locator to a later cycle other than through the §4.5 + exceedance transition (which fails the cycle and persists the + carry-over set), or that reports cycle completion with a + reserved locator unprocessed, FAILS. A fourth static control asserts every lock-holding path (ordinary write §3.7, managed deletion §4.7, reconciliation repair §4.5) is configured with the declared §3.7 hold deadline — an implementation with any unbounded - lock-holding path FAILS. + lock-holding path FAILS. A fifth static control asserts the + §3.7 grant-order binding: the implementation either cites the + pinned advisory-lock mechanism's documented queue-order grant + guarantee or routes conflicting locator-lock acquisitions + through an application-level FIFO queue — an implementation + relying on an undocumented grant order FAILS. A sixth static + control asserts the managed-deletion brain step applies its + mutation conditionally on the recorded §3.7 fence value — an + unfenced brain-deletion path FAILS. 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 @@ -1079,7 +1245,10 @@ declared profile table" means `profile_answers` (§3.5). fails the witness. A static control asserts the implementation orders content before pointer: no enumerated sensitive-write route creates a pointer row before its brain commit has been - confirmed (§3.7). + confirmed (§3.7). A second static control asserts every + enumerated sensitive-write route applies its step-1 brain + commit as a fenced mutation conditioned on the recorded §3.7 + fence value — an unfenced brain-commit path FAILS. ## 8. Drafting additions (PRD §12.1 disclosure) @@ -1156,8 +1325,19 @@ UPDATE` as the first statement of its transaction, and the never past it; per revision 8 every hold runs under a declared finite hold deadline with arrival-order grants, so the deferred repair — and with it cycle completion — is wall-clock bounded, - not merely eventual), so live interleavings between any two - mutators are excluded by one serialization protocol (§3.7, + not merely eventual; per revision 9 the deadline is enforced + by a supervisor independent of the holder, the deferred repair + reserves its queue place at the moment its try-acquire fails, + the cycle maximum is a computed formula over + reservation-point snapshots that is fixed when the last + reservation is placed, exceedance is an explicit failed-cycle + transition with a durable carry-over set the next cycle + processes first — so the one-cycle orphan bound binds every + cycle that reports completion — and every brain mutation under + a locator lock is fenced by a conditional application the next + holder's commit invalidates), so live interleavings between + any two mutators are excluded by one serialization protocol, + and stale external tails are excluded by the fence (§3.7, §4.5, §4.7, §7.16). 17. The compare-and-delete managed-deletion step: deletion step 2 is conditioned on the PAIR of the target row's uuid `id` (the -- 2.54.0 From b10cfb1e6924a12c4457d335549a83b7f95f4411 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 03:04:52 -0500 Subject: [PATCH 10/13] =?UTF-8?q?docs:=20custody-schema=20contract=20revis?= =?UTF-8?q?ion=2010=20(sol=20r9=20F16=20residual=20+=20F17:=20ABA-resistan?= =?UTF-8?q?t=20per-locator=20fence=20epoch=20=E2=80=94=20fresh=20mint=20on?= =?UTF-8?q?=20every=20mutation=20incl.=20byte-identical=20and=20absence=20?= =?UTF-8?q?tombstone,=20fence-advance=20at=20lock=20acquisition=20invalida?= =?UTF-8?q?ting=20stale=20tails=20before=20first=20mutation,=20repair=20ad?= =?UTF-8?q?vances=20before=20observing;=20distinct-locator=20no-contention?= =?UTF-8?q?=20narrowed=20to=20locator=20locks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/requirements/custody-schema.md | 209 ++++++++++++++++++++++------ 1 file changed, 166 insertions(+), 43 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index 56ffa283..d965a665 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -187,6 +187,33 @@ control, and static controls binding the grant-order guarantee and the fenced deletion path; §7.16 adds the static fenced-commit control (F15, F16). +Revision 10 (sol r9 residual F16 + new F17): the brain fence becomes +ABA-resistant. §3.7's fence value is now a per-locator fence EPOCH +with two invariants: every successful brain mutation mints a fresh, +never-reused value — byte-identical recommits and transitions to +absence included, absence being a unique tombstone state rather than +a recurrable bare-missing-path value — and every holder performs a +FENCE-ADVANCE immediately after lock acquisition, minting a fresh +epoch before its first brain observation or mutation, so every +earlier holder's recorded fence is invalidated at handoff, before the +new holder relies on exclusivity. A stale external tail is therefore +refused unconditionally: neither a byte-identical recommit, nor a +return to absence, nor resumption before the new holder's first +mutation can re-present a consumed epoch (F16). The live holder's +fence-pass now follows from handoff invalidation rather than from +lock possession, and a defect-only refusal has a specified +abort-and-surface transition. §4.5 repair performs the advance before +the absence observation it relies on. The unqualified distinct-locator +no-contention claim is narrowed to locator locks: a shared atomic +update point may cost swap-loop retries across locators but never +invalidates a distinct locator's condition — a liveness cost, never a +correctness coupling (F17). §7.10 adds two ABA fence controls +(byte-identical stale deletion; absence→write→delete→stale-writer +resurrection), a handoff-invalidation control with a +repair-observation variant, and a static fence-epoch-invariant +control; §7.16's static fenced-commit control is restated over the +epoch (F16, F17). + 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 @@ -421,7 +448,15 @@ with contract 6 (`mode-conversion.md`), identity with current holder's remaining deadline plus the deadline-bounded holds of the waiters already ahead of it, never by later arrivals. Writers to one locator are therefore - totally ordered; writes to distinct locators do not contend. + totally ordered; writes to distinct locators do not contend FOR + LOCATOR LOCKS. The brain store's atomic update point MAY be + shared across locators (a single head compare-and-swap in a + git-backed store serializes physically unrelated writes): losing + that swap costs a retry of the swap loop, and the retry + re-validates only the writer's own per-locator condition (Brain + fence below), which an unrelated locator's mutation never + changes — so cross-locator contention at a shared update point + is a liveness cost, never a correctness coupling. Because no two same-locator writes interleave, a completed write always leaves the pointer's `content_hash` describing the blob currently committed at the locator — the divergent interleaving @@ -431,31 +466,60 @@ with contract 6 (`mode-conversion.md`), identity with **Brain fence.** Deadline termination releases the lock while an external brain operation started by the terminated holder may still be in flight; lock release is therefore not, by itself, a - fence for the external store. Every brain mutation performed - under a locator lock — an ordinary write's step-1 content - commit (below), a managed deletion's step-1 content deletion - (§4.7) — is FENCED: immediately after acquiring the lock the - holder records the locator's current brain state (the content - version at `L(category_key, slot)`, or its recorded absence) as - its **fence value**, and the mutation is applied conditionally — - the brain store applies it only if the locator's state at - application time still equals the recorded fence value, with the - check and the application atomic with respect to other brain - mutations (compare-and-swap semantics; an atomic ref update in a - git-backed store satisfies this, with the per-locator condition - re-validated inside the swap loop when the swap is taken on a - coarser head). A later holder that commits at the locator - changes the locator's state, so a stale operation — one whose - session was terminated and whose lock has passed to a newer - holder — no longer matches its recorded fence value and is - REFUSED atomically: it cannot remove or replace content + fence for the external store. The fence is a per-locator **fence + epoch**: a value stored in the brain store, atomically readable + and comparable together with the locator's content state, whose + defining property is that every change to it MINTS a fresh value + never before used at that locator. A per-locator monotonic + counter satisfies this, as does a unique commit identity in a + git-backed store; the locator's content BYTES do not — two + byte-identical states at different times are distinct epochs — + and ABSENCE is itself an epoch-bearing state, recorded as a + unique tombstone, never a bare missing path whose "value" can + recur. Two rules bind the epoch. **Minting:** every successful + brain mutation under a locator lock — a content commit, a + byte-identical recommit, a content deletion (transition to + absence) — advances the locator's epoch to a fresh never-reused + value, atomically with the mutation. **Fence-advance at + acquisition:** immediately after acquiring the locator lock, + before its first brain observation or mutation at the locator, + the holder performs a fence-advance — an atomic epoch mint that + leaves the locator's content presence and bytes unchanged — and + records the resulting epoch as its **fence value**; the advance + invalidates every fence recorded by any earlier holder before + the new holder relies on anything it reads or on its + exclusivity. Every brain mutation performed under a locator + lock — an ordinary write's step-1 content commit (below), a + managed deletion's step-1 content deletion (§4.7) — is applied + conditionally: the brain store applies it only if the locator's + epoch at application time still equals the holder's recorded + fence value, with the check and the application atomic with + respect to other brain mutations (compare-and-swap semantics; an + atomic ref update in a git-backed store satisfies this, with the + per-locator epoch condition re-validated inside the swap loop + when the swap is taken on a coarser head). A stale operation — + one whose session was terminated and whose lock has passed to a + newer holder — is REFUSED unconditionally: its recorded epoch + predates the next holder's fence-advance, so the condition fails + whether the intervening history changed the bytes, recommitted + byte-identical content, returned the locator to absence, or + consists of the advance alone — refusal does not wait for the + new holder's first mutation, and no sequence of mutations can + re-present a consumed epoch. It cannot remove or replace content committed by any later holder, whether it resumes microseconds - or hours after the termination. A live holder's own mutation - passes its fence trivially: it holds the locator lock, so no - other mutator has changed the locator since it recorded the - value. Every brain-mutation path carries the fence; a path - applying an unfenced brain mutation fails the §7.10 and §7.16 - static controls. + or hours after the termination. A live holder's own fenced + mutation cannot fail its condition — not because it holds the + lock, but because handoff invalidation plus minting mean the + only epochs minted since its fence-advance are its own; a + holder whose fenced mutation is nonetheless refused (reachable + only under a defect breaching these invariants) MUST abort its + protocol run, release the lock, and surface the error — it + never retries the mutation under its stale fence. Every + brain-mutation path carries the fence, and every brain-RELYING + observation path (reconciliation repair's absence observation, + §4.5) performs its fence-advance before the observation it + relies on; a path applying an unfenced brain mutation fails the + §7.10 and §7.16 static controls. **Steps.** Step 1 commits the content to the owning user's brain at `L(category_key, slot)` as a fenced mutation (Brain fence @@ -559,8 +623,12 @@ with contract 6 (`mode-conversion.md`), identity with and repair performs no database content write. Per-pointer repair runs under the §3.7 locator lock: before observing content absence for a pointer, reconciliation acquires - the pointer's locator lock, holds it across the observation and - the DELETE, and releases it after. The sweep pass MAY use a + the pointer's locator lock, performs the §3.7 fence-advance — + invalidating any stale in-flight operation's fence BEFORE the + observation repair relies on, so no terminated holder's resuming + mutation can change the locator between repair's observation and + its DELETE — holds the lock across the observation and the + DELETE, and releases it after. The sweep pass MAY use a try-acquire so it never blocks mid-pass behind a writer, but a failed try-acquire defers the locator, never discharges it — and the deferral RESERVES its place: at the moment the try-acquire @@ -655,8 +723,10 @@ with contract 6 (`mode-conversion.md`), identity with pointed, is unschedulable, because the deleter cannot enter its protocol while the writer holds the lock. **Steps**: step 1 commits the content deletion to the user's brain repository as - a FENCED mutation (§3.7 Brain fence — the fence value recorded - immediately after lock acquisition), recording the target row's + a FENCED mutation (§3.7 Brain fence — conditioned on the fence + epoch recorded at the deleter's post-acquisition fence-advance, + and itself minting the locator's unique absence tombstone + epoch), recording the target row's `id` AND `generation` as read after lock acquisition and before step 1; step 2, only after step 1 has committed, deletes the pointer row in its own database @@ -681,11 +751,14 @@ with contract 6 (`mode-conversion.md`), identity with protocol run reading current state. A deleter TERMINATED at the §3.7 hold deadline while its step-1 brain deletion is still in flight is likewise harmless on the brain side: the in-flight - deletion is fenced, so once a later holder commits at the - locator, the stale deletion's conditional application fails and - removes nothing — content committed and acknowledged after the - termination cannot be destroyed by the terminated deleter's - resuming operation (witness §7.10). Interruption before step 1 + deletion is fenced, so once a later holder performs its §3.7 + fence-advance — before that holder's first mutation, and + regardless of whether its eventual commit is byte-identical to + the state the deleter recorded — the stale deletion's + conditional application fails and removes nothing: content + committed and acknowledged after the termination cannot be + destroyed by the terminated deleter's resuming operation + (witness §7.10). 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 @@ -1107,7 +1180,37 @@ declared profile table" means `profile_answers` (§3.5). writer completes a full acknowledged write at the same locator, and the stale commit is resumed and asserted refused — the locator serves the fresh writer's bytes and the fresh - pointer's hash verifies. An **exceedance control** contrives a + pointer's hash verifies. Two **ABA fence controls** exercise + value recurrence, the cases a state-equality fence misses. + Byte-identical stale deletion: a §4.7 deleter records its + fence, is suspended inside its step-1 brain deletion and + terminated by the supervisor; a fresh §3.7 writer then + recommits BYTE-IDENTICAL content (unchanged §4.4 hash) with + its pointer upsert and is acknowledged; the stale deletion is + resumed and asserted REFUSED — the identical bytes carry a + fresh epoch — with the content still present, resolving, and + verifying, and no mismatch stamped. Absence resurrection: an + ordinary writer records the locator's ABSENCE as its fence, is + suspended inside step 1 and terminated; a fresh writer + completes an acknowledged write at the locator; a managed + deletion then removes that content and its pointer, returning + the locator to absence; the stale writer is resumed and + asserted REFUSED — the post-deletion absence is a fresh + tombstone epoch, not a recurrence of the recorded one — with + the locator still absent and NO unpointed content existing + anywhere afterward. A **handoff-invalidation control**: a + holder is terminated mid-external-operation; the next holder + acquires the lock and performs its §3.7 fence-advance but has + NOT yet applied any brain mutation when the stale operation is + resumed — the stale operation is asserted refused (the advance + alone invalidated it, before any fresh mutation), and the new + holder's own subsequent fenced mutation is asserted to + succeed. A repair-observation variant runs the same schedule + with reconciliation as the new holder: repair advances, + observes content absence, and the stale writer resuming before + repair's DELETE is asserted refused — the DELETE then + completes under its recorded (`id`, `generation`) token and + NO unpointed content is resurrected at the locator. An **exceedance control** contrives a cycle that cannot finish within its computed `M` (an injected sequence of deadline-length holders on a reserved locator) and asserts the cycle reports FAILURE — never completion — @@ -1138,8 +1241,20 @@ declared profile table" means `profile_answers` (§3.5). through an application-level FIFO queue — an implementation relying on an undocumented grant order FAILS. A sixth static control asserts the managed-deletion brain step applies its - mutation conditionally on the recorded §3.7 fence value — an - unfenced brain-deletion path FAILS. + mutation conditionally on the recorded §3.7 fence epoch — an + unfenced brain-deletion path FAILS. A seventh static control + asserts the §3.7 fence-epoch invariants: every fenced-mutation + path mints a fresh never-reused per-locator epoch atomically + with its mutation — byte-identical recommits and transitions + to absence (the unique tombstone) included — every + lock-acquiring path that mutates or relies on brain state + (ordinary write §3.7, managed deletion §4.7, reconciliation + repair §4.5) performs the fence-advance immediately after + acquisition and records the post-advance epoch as its fence + value, and no path represents the fence by content bytes, + content hash, or bare path absence — an implementation whose + fence value can recur, or that observes before advancing, + FAILS. 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 @@ -1248,7 +1363,9 @@ declared profile table" means `profile_answers` (§3.5). confirmed (§3.7). A second static control asserts every enumerated sensitive-write route applies its step-1 brain commit as a fenced mutation conditioned on the recorded §3.7 - fence value — an unfenced brain-commit path FAILS. + fence epoch — recorded at the route's post-acquisition + fence-advance, the commit itself minting a fresh epoch — an + unfenced brain-commit path FAILS. ## 8. Drafting additions (PRD §12.1 disclosure) @@ -1334,11 +1451,17 @@ UPDATE` as the first statement of its transaction, and the transition with a durable carry-over set the next cycle processes first — so the one-cycle orphan bound binds every cycle that reports completion — and every brain mutation under - a locator lock is fenced by a conditional application the next - holder's commit invalidates), so live interleavings between - any two mutators are excluded by one serialization protocol, - and stale external tails are excluded by the fence (§3.7, - §4.5, §4.7, §7.16). + a locator lock is fenced by a conditional application; per + revision 10 the fence value is an ABA-resistant per-locator + fence EPOCH — minted fresh by every mutation, byte-identical + recommits and transitions to absence (unique tombstone) + included, and advanced by every new holder immediately after + acquisition, before it relies on brain state — so a stale + tail is refused even when bytes or absence recur and even + before the new holder's first mutation), so live interleavings + between any two mutators are excluded by one serialization + protocol, and stale external tails are excluded by the fence + (§3.7, §4.5, §4.7, §7.16). 17. The compare-and-delete managed-deletion step: deletion step 2 is conditioned on the PAIR of the target row's uuid `id` (the immutable row-incarnation identity) and its monotonic -- 2.54.0 From 2c026313242c688363298add120e0215debf53cc Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 03:47:21 -0500 Subject: [PATCH 11/13] custody contract revision 11: acquisition-token guard on the fence-advance (sol r10 F16 residual, F18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fence-advance is itself fenced against an older acquisition: a per-locator acquisition token minted by the lock authority at every lock grant, carried unchanged across the acquisition's brain operations and swap-loop retries, compared and recorded by the brain store atomically with every brain mutation (advance included). A superseded acquisition's delayed advance is refused, mints nothing, and yields no usable fence (mandatory abort-release-surface, expected on deadline supersession). The 'refused unconditionally' claim is precisified to the exact linearization boundary; the pre-step-1 interruption predicates are restated over exact content/database state, expressly permitting the advance's epoch/token change (F18). New §7.10 stale-advance refusal and pre-advance linearization controls; token invariants added to the fence static controls (§7.10, §7.16); §8 item 16 and preamble extended. --- docs/requirements/custody-schema.md | 216 ++++++++++++++++++++++++---- 1 file changed, 186 insertions(+), 30 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index d965a665..b5663299 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -214,6 +214,39 @@ repair-observation variant, and a static fence-epoch-invariant control; §7.16's static fenced-commit control is restated over the epoch (F16, F17). +Revision 11 (sol r10 residual F16 + new F18): the fence-advance +itself is fenced against an older acquisition. §3.7 adds a +per-locator ACQUISITION TOKEN minted by the lock authority at every +lock grant — fresh, strictly increasing, existing before the +holder's first brain operation and carried unchanged through every +brain operation and swap-loop retry of that acquisition — and the +brain store records the greatest accepted token atomically with the +locator's epoch and content state, refusing any brain mutation, +the fence-advance included, that carries an older token. The +advance's acceptance condition is exactly this comparison (an epoch +condition would be circular, an unconditional mint would let a +terminated holder's delayed advance supersede a later holder's); a +refused advance mints nothing and returns no usable fence, with a +mandatory abort-release-surface transition that is an EXPECTED +outcome after deadline supersession, not a defect. Revision 10's +"refused unconditionally" claim is precisified to an exact +boundary: after the newer holder's advance linearizes, every +older-acquisition operation — its delayed advance included — is +refused; a stale content mutation linearizing BEFORE that advance +serializes entirely before anything the new holder relies on (the +advance preserves the content state it finds and mints after it), +indistinguishable from completion before the previous holder's +lock release — the defined legal outcome. The pre-step-1 +interruption predicates in §3.7, §4.7, and the §7.16 fault +injection are restated exactly — content presence, content bytes, +and database rows unchanged, the advance's epoch mint and token +recording expressly permitted — resolving their contradiction with +the mandatory advance (F18). §7.10 adds a stale-advance refusal +control (fresh-writer and repair-observation variants) and a +pre-advance linearization control, and extends the fence-invariant +static control with the token invariants; §7.16's static +fenced-commit control carries the token (F16, F18). + 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 @@ -466,8 +499,17 @@ with contract 6 (`mode-conversion.md`), identity with **Brain fence.** Deadline termination releases the lock while an external brain operation started by the terminated holder may still be in flight; lock release is therefore not, by itself, a - fence for the external store. The fence is a per-locator **fence - epoch**: a value stored in the brain store, atomically readable + fence for the external store. Two per-locator values fence it: + an **acquisition token** ordering holders and a **fence epoch** + ordering mutations. The acquisition token is minted by the lock + authority at EVERY grant of the locator lock: a fresh, strictly + increasing per-locator value handed to the holder with the + grant — it therefore exists before the holder's first brain + operation, independently of any brain state — and carried + UNCHANGED through every brain operation of that acquisition, + including every retry of a swap loop; a holder never re-mints + or refreshes its token. The fence epoch is a per-locator value + stored in the brain store, atomically readable and comparable together with the locator's content state, whose defining property is that every change to it MINTS a fresh value never before used at that locator. A per-locator monotonic @@ -476,11 +518,18 @@ with contract 6 (`mode-conversion.md`), identity with byte-identical states at different times are distinct epochs — and ABSENCE is itself an epoch-bearing state, recorded as a unique tombstone, never a bare missing path whose "value" can - recur. Two rules bind the epoch. **Minting:** every successful + recur. Three rules bind them. **Minting:** every successful brain mutation under a locator lock — a content commit, a byte-identical recommit, a content deletion (transition to absence) — advances the locator's epoch to a fresh never-reused - value, atomically with the mutation. **Fence-advance at + value, atomically with the mutation. **Token recording:** the + brain store records, per locator and atomically with the + locator's epoch and content state, the greatest acquisition + token it has accepted; EVERY brain mutation under a locator + lock — the fence-advance below included — presents its holder's + acquisition token, is REFUSED atomically when that token is + less than the recorded value, and records it on acceptance. + **Fence-advance at acquisition:** immediately after acquiring the locator lock, before its first brain observation or mutation at the locator, the holder performs a fence-advance — an atomic epoch mint that @@ -488,37 +537,74 @@ with contract 6 (`mode-conversion.md`), identity with records the resulting epoch as its **fence value**; the advance invalidates every fence recorded by any earlier holder before the new holder relies on anything it reads or on its - exclusivity. Every brain mutation performed under a locator + exclusivity. The advance's OWN acceptance condition is exactly + the token comparison — not the epoch (no fence exists for the + acquisition until the advance returns, so an epoch condition + would be circular) and not nothing (an unconditional mint would + let a terminated holder's delayed advance supersede a later + holder's): the token is minted at lock grant, so it exists + before the advance, and grants for one locator are totally + ordered, so the comparison encodes acquisition order. A refused + advance returns NO usable fence: the holder MUST NOT perform + any brain observation or mutation, MUST abort its protocol run + before any later step, release the lock if it still holds it, + and surface the error — and this refusal is an EXPECTED outcome + for an acquisition whose lock was deadline-released and + re-granted before its delayed advance applied, not a defect. + Every other brain mutation performed under a locator lock — an ordinary write's step-1 content commit (below), a managed deletion's step-1 content deletion (§4.7) — is applied conditionally: the brain store applies it only if the locator's epoch at application time still equals the holder's recorded - fence value, with the check and the application atomic with + fence value AND the token-recording comparison accepts, with + the checks and the application atomic with respect to other brain mutations (compare-and-swap semantics; an atomic ref update in a git-backed store satisfies this, with the - per-locator epoch condition re-validated inside the swap loop + per-locator conditions re-validated inside the swap loop when the swap is taken on a coarser head). A stale operation — one whose session was terminated and whose lock has passed to a - newer holder — is REFUSED unconditionally: its recorded epoch - predates the next holder's fence-advance, so the condition fails - whether the intervening history changed the bytes, recommitted - byte-identical content, returned the locator to absence, or - consists of the advance alone — refusal does not wait for the - new holder's first mutation, and no sequence of mutations can - re-present a consumed epoch. It cannot remove or replace content - committed by any later holder, whether it resumes microseconds + newer holder — is refused at an exact boundary. Once the newer + holder's fence-advance has linearized, EVERY operation of the + older acquisition is refused: its epoch condition fails (the + advance consumed its epoch, and no sequence of mutations can + re-present a consumed one — whether the intervening history + changed the bytes, recommitted byte-identical content, returned + the locator to absence, or consists of the advance alone), its + token comparison fails, and its own delayed fence-advance is + refused by the token comparison, minting nothing — a superseded + acquisition cannot manufacture a fresh fence. Before the newer + advance linearizes, a stale content mutation whose conditions + still hold MAY linearize first: the advance then finds the + post-mutation state, expressly preserves the content presence + and bytes it finds, mints its epoch after that mutation, and + the new holder's first relied-on observation follows its own + advance — so the stale effect serializes entirely before + anything the new holder relies on, indistinguishable from the + same operation completing before the previous holder's lock + release, an outcome the protocol already defines as legal (its + database half remains governed by the (`id`, `generation`) + pair, §4.5, §4.7). These are the only two outcomes: a stale + operation either serializes wholly before the new holder's + advance or is refused; it never applies after the advance, and + it cannot remove or replace content committed by any later + holder, whether it resumes microseconds or hours after the termination. A live holder's own fenced - mutation cannot fail its condition — not because it holds the + mutation cannot fail its conditions — not because it holds the lock, but because handoff invalidation plus minting mean the - only epochs minted since its fence-advance are its own; a + only epochs minted since its fence-advance are its own, and + because lock grants are serialized by the lock itself, no + greater acquisition token can be recorded while it holds the + lock; a holder whose fenced mutation is nonetheless refused (reachable only under a defect breaching these invariants) MUST abort its protocol run, release the lock, and surface the error — it never retries the mutation under its stale fence. Every - brain-mutation path carries the fence, and every brain-RELYING + brain-mutation path carries the fence and the acquisition + token, and every brain-RELYING observation path (reconciliation repair's absence observation, §4.5) performs its fence-advance before the observation it - relies on; a path applying an unfenced brain mutation fails the + relies on; a path applying an unfenced or token-free brain + mutation fails the §7.10 and §7.16 static controls. **Steps.** Step 1 commits the content to the owning user's brain @@ -528,8 +614,11 @@ with contract 6 (`mode-conversion.md`), identity with existing row for that (user, category, brain_ref), incrementing `generation` per §4.1) in its own database transaction inside the §2.6 fence. The write is acknowledged to the caller only after - step 2 commits. Interruption before step 1 leaves both stores - unchanged. Interruption between the steps leaves committed brain + step 2 commits. Interruption before step 1 leaves content + presence, content bytes, and every database row unchanged — the + post-acquisition fence-advance may already have minted an epoch + and recorded an acquisition token, the only brain change the + protocol permits before step 1. 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. @@ -623,10 +712,13 @@ with contract 6 (`mode-conversion.md`), identity with and repair performs no database content write. Per-pointer repair runs under the §3.7 locator lock: before observing content absence for a pointer, reconciliation acquires - the pointer's locator lock, performs the §3.7 fence-advance — + the pointer's locator lock, performs the §3.7 fence-advance + under repair's own acquisition token — invalidating any stale in-flight operation's fence BEFORE the observation repair relies on, so no terminated holder's resuming - mutation can change the locator between repair's observation and + mutation — its delayed fence-advance included, refused by the + §3.7 token comparison — can change the locator between repair's + observation and its DELETE — holds the lock across the observation and the DELETE, and releases it after. The sweep pass MAY use a try-acquire so it never blocks mid-pass behind a writer, but a @@ -725,6 +817,7 @@ with contract 6 (`mode-conversion.md`), identity with commits the content deletion to the user's brain repository as a FENCED mutation (§3.7 Brain fence — conditioned on the fence epoch recorded at the deleter's post-acquisition fence-advance, + carrying the deleter's acquisition token, and itself minting the locator's unique absence tombstone epoch), recording the target row's `id` AND `generation` as read after @@ -759,7 +852,11 @@ with contract 6 (`mode-conversion.md`), identity with committed and acknowledged after the termination cannot be destroyed by the terminated deleter's resuming operation (witness §7.10). Interruption before step 1 - commits leaves both stores unchanged. Interruption between the + commits leaves content presence, content bytes, and every + database row unchanged — the deleter's post-acquisition + fence-advance may already have minted an epoch and recorded an + acquisition token, the only brain change the protocol permits + before step 1. 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, @@ -1210,7 +1307,39 @@ declared profile table" means `profile_answers` (§3.5). observes content absence, and the stale writer resuming before repair's DELETE is asserted refused — the DELETE then completes under its recorded (`id`, `generation`) token and - NO unpointed content is resurrected at the locator. An **exceedance control** contrives a + NO unpointed content is resurrected at the locator. A + **stale-advance refusal control** exercises the §3.7 token + guard on the advance itself: writer W1 acquires the locator + lock and its mandatory fence-advance is suspended BEFORE its + atomic application; the independent supervisor terminates W1's + session at the deadline and the lock releases; the next holder + acquires the lock (receiving a greater acquisition token) and + completes its own fence-advance; W1's suspended advance is + then RESUMED and asserted REFUSED by the token comparison — + it mints no epoch and returns no usable fence — and W1's + protocol run is asserted to abort with the error surfaced, + performing no further brain observation or mutation, while + the next holder's fence remains valid and its subsequent + fenced mutation is asserted to succeed. A fresh-writer + variant runs the next holder as an ordinary §3.7 writer + through a full acknowledged write; a repair-observation + variant runs it as §4.5 reconciliation — repair advances, + observes content absence, W1's late advance resumed before + the DELETE is asserted refused and fenceless, and the DELETE + completes under its recorded (`id`, `generation`) token with + no unpointed content resurrected. A **pre-advance + linearization control** exercises the §3.7 legal-outcome + boundary: a terminated holder's stale content mutation is + scheduled to linearize immediately BEFORE the next holder's + fence-advance swap; the control asserts the store serializes + the stale mutation first, the advance preserves the + post-mutation content presence and bytes and mints its epoch + after it, the new holder's first relied-on observation + reflects the committed stale effect, and the end state is + consistent — equivalent to the stale operation having + completed before the previous holder's lock release — with + the stale operation's database half still governed by the + (`id`, `generation`) pair. An **exceedance control** contrives a cycle that cannot finish within its computed `M` (an injected sequence of deadline-length holders on a reserved locator) and asserts the cycle reports FAILURE — never completion — @@ -1254,7 +1383,18 @@ declared profile table" means `profile_answers` (§3.5). value, and no path represents the fence by content bytes, content hash, or bare path absence — an implementation whose fence value can recur, or that observes before advancing, - FAILS. + FAILS. The same control asserts the §3.7 acquisition-token + invariants: the lock authority mints a fresh, strictly + increasing per-locator token at every lock grant; every + brain-mutation path — the fence-advance included — presents + its holder's token unchanged across every swap-loop retry; + the brain store's token comparison and recording are atomic + with the mutation they guard; the advance is refused when its + token is less than the recorded value; and a refused advance + yields no usable fence — an implementation whose advance + applies unconditionally, that re-mints or refreshes a token + within an acquisition, or whose holder proceeds to any brain + observation or mutation after a refused advance, FAILS. 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 @@ -1326,7 +1466,10 @@ declared profile table" means `profile_answers` (§3.5). violates the §6.3 singleton constraint. 16. **Ordered-write protocol witnesses:** the §3.7 protocol is fault-injected at its interruption points: killed before the - brain commit, both stores are unchanged and the caller receives + brain commit, content presence, content bytes, and every + database row are unchanged — the post-acquisition + fence-advance's epoch mint and token recording are the only + permitted brain change — 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 @@ -1364,8 +1507,9 @@ declared profile table" means `profile_answers` (§3.5). enumerated sensitive-write route applies its step-1 brain commit as a fenced mutation conditioned on the recorded §3.7 fence epoch — recorded at the route's post-acquisition - fence-advance, the commit itself minting a fresh epoch — an - unfenced brain-commit path FAILS. + fence-advance, the commit itself minting a fresh epoch and + carrying the route's §3.7 acquisition token — an + unfenced or token-free brain-commit path FAILS. ## 8. Drafting additions (PRD §12.1 disclosure) @@ -1458,7 +1602,19 @@ UPDATE` as the first statement of its transaction, and the included, and advanced by every new holder immediately after acquisition, before it relies on brain state — so a stale tail is refused even when bytes or absence recur and even - before the new holder's first mutation), so live interleavings + before the new holder's first mutation; per revision 11 the + fence-advance itself is guarded by an acquisition token — + minted fresh and strictly increasing by the lock authority at + every lock grant, carried unchanged across an acquisition's + brain operations and swap-loop retries, compared and recorded + by the brain store atomically with every brain mutation, the + advance included — so a superseded acquisition's delayed + advance is refused and mints nothing, a refused advance + yields no usable fence with a mandatory + abort-release-surface transition, and a stale operation + either serializes wholly before the new holder's advance — + the defined legal pre-release-completion-equivalent outcome — + or is refused), so live interleavings between any two mutators are excluded by one serialization protocol, and stale external tails are excluded by the fence (§3.7, §4.5, §4.7, §7.16). -- 2.54.0 From 8085982396e7f5c64a943b0febf6ad6b30175140 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 04:19:19 -0500 Subject: [PATCH 12/13] =?UTF-8?q?contract(custody-schema):=20revision=2012?= =?UTF-8?q?=20=E2=80=94=20exact=20token=20comparison=20base=20case,=20spli?= =?UTF-8?q?t=20stale=20database-half=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sol r11 APPROVE minors: - F19: strictly-less-than refusal made explicit; absent recorded token compares lower than every minted token, first acceptance creates the record; §7.10 virgin-locator control + delayed-first-grant race. - F18 residual closed: §7.10 managed-deletion fault predicate restated as the exact §4.7 predicate (advance's epoch mint and token record permitted). - F20: pre-advance linearization boundary distinguishes database halves — pair-conditioned step 2 for deletion/repair; terminated ordinary writer performs no pointer upsert, retry is a fresh acquisition; §7.10 control runs both branches. - §8 item 16 revision-12 clause; preamble Revision 12 paragraph. --- docs/requirements/custody-schema.md | 88 ++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index b5663299..612d2ae6 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -247,6 +247,27 @@ pre-advance linearization control, and extends the fence-invariant static control with the token invariants; §7.16's static fenced-commit control carries the token (F16, F18). +Revision 12 (sol r11 minors F18 residual, F19, F20): three +exactness corrections to the revision-11 mechanism, no behavioral +change. §3.7's token comparison is made exact at both ends: +strictly less-than refuses (an already-recorded token re-presented +by a swap-loop retry or a later mutation of the same acquisition is +accepted), and an absent recorded token compares lower than every +minted token, so a virgin locator's first advance is accepted and +atomically creates the token record — with a §7.10 virgin-locator +control and a delayed-first-grant race variant (F19). §7.10's +remaining managed-deletion fault predicate ("both stores +unchanged") is restated as the exact §4.7 predicate — content +presence, content bytes, and database rows unchanged, the +advance's epoch mint and token recording expressly permitted +(F18 closed). The pre-advance-linearization outcome no longer +assigns the (`id`, `generation`) pair to every stale operation: +the §3.7 boundary text and the §7.10 control now distinguish the +database halves — deletion's and repair's step 2 stays +pair-conditioned, while a terminated ordinary writer performs no +pointer upsert and can continue only through a fresh acquisition +under a new token (F20). + 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 @@ -529,6 +550,16 @@ with contract 6 (`mode-conversion.md`), identity with lock — the fence-advance below included — presents its holder's acquisition token, is REFUSED atomically when that token is less than the recorded value, and records it on acceptance. + The comparison is exact at both ends: it is strictly + less-than — re-presenting the already-recorded token, as a + swap-loop retry or a later mutation of the same acquisition + does, is accepted — and a locator with NO recorded token + compares as lower than every minted token, so a virgin + locator's first presented token is never refused by this + comparison and its acceptance atomically CREATES the token + record; an implementation whose absent-token comparison can + refuse or fail to record the first holder is nonconforming + (witness §7.10). **Fence-advance at acquisition:** immediately after acquiring the locator lock, before its first brain observation or mutation at the locator, @@ -581,9 +612,16 @@ with contract 6 (`mode-conversion.md`), identity with advance — so the stale effect serializes entirely before anything the new holder relies on, indistinguishable from the same operation completing before the previous holder's lock - release, an outcome the protocol already defines as legal (its - database half remains governed by the (`id`, `generation`) - pair, §4.5, §4.7). These are the only two outcomes: a stale + release, an outcome the protocol already defines as legal. What + the stale operation's DATABASE half may then do depends on its + protocol: a managed deletion's or repair's step 2 remains + conditioned on the recorded (`id`, `generation`) pair (§4.5, + §4.7) and cannot unpoint any later row incarnation; an ordinary + write's terminated run performs NO pointer upsert — its + database session is dead, the committed-but-unpointed content + falls into the existing step-1-to-step-2 interruption window + repaired by §4.5, and any retry is a NEW lock acquisition under + a fresh token, never a continuation of the dead run. These are the only two outcomes: a stale operation either serializes wholly before the new holder's advance or is refused; it never applies after the advance, and it cannot remove or replace content committed by any later @@ -1184,7 +1222,11 @@ declared profile table" means `profile_answers` (§3.5). 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 + brain commit, content presence, content bytes, and every + database row are unchanged — the deleter's post-acquisition + fence-advance may already have minted an epoch and recorded an + acquisition token, the only brain change the protocol permits + before step 1; killed between the brain commit and the pointer delete, the dangling pointer is deleted by the next reconciliation — and at no point is content written to the database or restored to the brain. **Concurrent-rewrite @@ -1327,7 +1369,20 @@ declared profile table" means `profile_answers` (§3.5). observes content absence, W1's late advance resumed before the DELETE is asserted refused and fenceless, and the DELETE completes under its recorded (`id`, `generation`) token with - no unpointed content resurrected. A **pre-advance + no unpointed content resurrected. A **virgin-locator token + control** exercises the §3.7 absent-token base case: the FIRST + lock grant ever issued for a locator (no recorded acquisition + token exists in the brain store) performs its mandatory + fence-advance and is asserted ACCEPTED — the advance mints the + locator's epoch and its acceptance atomically creates the + token record — and the holder's subsequent fenced mutation + succeeds; a delayed-first-grant race variant suspends that + first holder's advance before application, terminates the + session at the deadline, lets a second holder acquire the + lock (receiving a greater token) and complete its own advance, + then resumes the first holder's advance and asserts it REFUSED + by the token comparison — the base case never admits a + superseded first grant. A **pre-advance linearization control** exercises the §3.7 legal-outcome boundary: a terminated holder's stale content mutation is scheduled to linearize immediately BEFORE the next holder's @@ -1337,9 +1392,17 @@ declared profile table" means `profile_answers` (§3.5). after it, the new holder's first relied-on observation reflects the committed stale effect, and the end state is consistent — equivalent to the stale operation having - completed before the previous holder's lock release — with - the stale operation's database half still governed by the - (`id`, `generation`) pair. An **exceedance control** contrives a + completed before the previous holder's lock release. The + control runs BOTH database-half branches: with the stale + mutation a managed deletion's step-1 brain half, its stale + step-2 DELETE remains conditioned on the recorded + (`id`, `generation`) pair and is asserted to match ZERO rows + against a later row incarnation; with the stale mutation an + ordinary §3.7 writer's step-1 content commit, the terminated + run is asserted to perform NO pointer upsert — the unpointed + content is repaired by §4.5 within one cycle, and a retry of + the write is asserted to run as a fresh lock acquisition + under a new token. An **exceedance control** contrives a cycle that cannot finish within its computed `M` (an injected sequence of deadline-length holders on a reserved locator) and asserts the cycle reports FAILURE — never completion — @@ -1614,7 +1677,14 @@ UPDATE` as the first statement of its transaction, and the abort-release-surface transition, and a stale operation either serializes wholly before the new holder's advance — the defined legal pre-release-completion-equivalent outcome — - or is refused), so live interleavings + or is refused; per revision 12 the token comparison is exact + at both ends — strictly less-than refuses, an absent recorded + token compares lower than every minted token so a virgin + locator's first advance is accepted and creates the record — + and the pre-advance-linearization outcome distinguishes the + stale database halves: pair-conditioned step 2 for deletion + and repair, no pointer upsert ever for a terminated ordinary + writer), so live interleavings between any two mutators are excluded by one serialization protocol, and stale external tails are excluded by the fence (§3.7, §4.5, §4.7, §7.16). -- 2.54.0 From 8a03d99e3194d3f36cfb088d23b59ebc2beea010 Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 27 Aug 2026 04:48:07 -0500 Subject: [PATCH 13/13] =?UTF-8?q?contract(custody-schema):=20revision=2013?= =?UTF-8?q?=20=E2=80=94=20withdraw=20wrong=20=C2=A74.5=20repair=20attribut?= =?UTF-8?q?ion=20(sol=20r12=20F20=20residual)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sol's r12 confirm closed F18/F19 and found one MINOR residual on F20: rev 12 assigned the terminated ordinary writer's committed-but-unpointed content to §4.5 repair, but §4.5 enumerates pointers and repairs the opposite state (pointer present, content absent) — unpointed content is outside its input, so the §7.10 assertion had no conforming transition. Per the prescribed correction: the §4.5 repair assertion is removed from §3.7 and §7.10; the no-upsert assertion stays; the state is what §3.7 and §7.16 already define — inert unpointed content (no pointer, no read path, no repair transition; a §4.5 cycle run against it changes nothing), never acknowledged, converging only through the client's retry as a fresh lock acquisition under a new token that recommits and completes the pointer upsert. Preamble Revision 13 paragraph added. --- docs/requirements/custody-schema.md | 38 +++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/docs/requirements/custody-schema.md b/docs/requirements/custody-schema.md index 612d2ae6..6308a17f 100644 --- a/docs/requirements/custody-schema.md +++ b/docs/requirements/custody-schema.md @@ -268,6 +268,20 @@ pair-conditioned, while a terminated ordinary writer performs no pointer upsert and can continue only through a fresh acquisition under a new token (F20). +Revision 13 (sol r12 confirm, F20 residual): revision 12's +ordinary-writer branch wrongly assigned the terminated writer's +committed-but-unpointed content to §4.5 repair — §4.5 enumerates +pointers and repairs the opposite state (a pointer whose content +is absent); unpointed content is outside its input. The §3.7 +boundary text and the §7.10 control now state what §3.7 and §7.16 +already specified: the state is inert (no pointer references it, +no read path serves it, no repair transition exists for it — a +§4.5 cycle run against it changes nothing), the terminated run is +never acknowledged, and convergence comes only through the +client's retry as a fresh acquisition under a new token, +recommitting and completing the pointer upsert. No behavioral +change; the wrong repair attribution is withdrawn. + 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 @@ -619,9 +633,14 @@ with contract 6 (`mode-conversion.md`), identity with §4.7) and cannot unpoint any later row incarnation; an ordinary write's terminated run performs NO pointer upsert — its database session is dead, the committed-but-unpointed content - falls into the existing step-1-to-step-2 interruption window - repaired by §4.5, and any retry is a NEW lock acquisition under - a fresh token, never a continuation of the dead run. These are the only two outcomes: a stale + is exactly the inert step-1-to-step-2 interruption state this + section already defines (no pointer references it, no read + path serves it, and no automatic repair transition exists for + it — §4.5 repairs the opposite state, a pointer whose content + is absent), and the write converges only through the unacknowledged + client's retry, which is a NEW lock acquisition under + a fresh token — recommitting the locator and completing the + pointer upsert — never a continuation of the dead run. These are the only two outcomes: a stale operation either serializes wholly before the new holder's advance or is refused; it never applies after the advance, and it cannot remove or replace content committed by any later @@ -1399,10 +1418,15 @@ declared profile table" means `profile_answers` (§3.5). (`id`, `generation`) pair and is asserted to match ZERO rows against a later row incarnation; with the stale mutation an ordinary §3.7 writer's step-1 content commit, the terminated - run is asserted to perform NO pointer upsert — the unpointed - content is repaired by §4.5 within one cycle, and a retry of - the write is asserted to run as a fresh lock acquisition - under a new token. An **exceedance control** contrives a + run is asserted to perform NO pointer upsert and to receive + NO acknowledgment; the committed content is asserted + unpointed and inert — referenced by no pointer, served by no + read path, and subject to no repair transition (a §4.5 cycle + run against the state is asserted to change nothing) — and + convergence is asserted only through the client's retry, + which acquires the lock as a fresh acquisition + under a new token, recommits the locator, and completes the + pointer upsert to the pointed state (§3.7, witness §7.16). An **exceedance control** contrives a cycle that cannot finish within its computed `M` (an injected sequence of deadline-length holders on a reserved locator) and asserts the cycle reports FAILURE — never completion — -- 2.54.0