24 KiB
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.
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), 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
- 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.
- Sensitive content: any profile answer or derived text in a §2
category classified
sensitive. - Pointer: a database record referencing sensitive content that lives in a user brain, carrying no content (§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).
- Consent record: a database record granting one grantee access to one category of one user's data (§5).
- Registry version: the monotonically increasing integer identifying the active state of the §2 registry.
- 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. Profile-category registry
-
Model. The registry is the single classifier for every profile category, closed and versioned, in the platform database. Table
profile_category_registry, columns exactly:Column Type Constraints category_keytext primary key classificationtext NOT NULL, CHECK in ( sensitive,non-sensitive)since_versioninteger NOT NULL (registry version that introduced this row) created_attimestamptz NOT NULL 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. -
Initial registry (version 1). Drawn from the D4 profile step and D14's "disabilities, family, communication style, and similar":
Category key Classification Covers disabilitiessensitive disabilities including ADHD/autism/PDA/vision family-socialsensitive family, pets, friends communication-stylesensitive desired agent communication style, voice-matching interview product personal-interestssensitive hobbies, likes/dislikes connector-contentsensitive email and drive content reached through user connectors professional-backgroundnon-sensitive (per ruling) professional background summary used for agent configuration educationnon-sensitive (per ruling) education summary used for agent configuration The
Coverscolumn 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. -
Unknown category — refusal. A profile write naming a
category_keywith 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. -
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.
-
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
- 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 encodings, and not as embeddings or other derived representations that reconstruct content.
- The database MAY hold, about sensitive content: the pointer records of §4, the consent records of §5, the registry of §2, and the custody configuration of §6.3. Nothing else.
- 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. - 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
-
Exact columns. Table
profile_pointers, columns exactly:Column Type Constraints iduuid primary key user_iduuid NOT NULL, FK → users(id) ON DELETE CASCADE category_keytext NOT NULL, FK → profile_category_registry(category_key) brain_reftext NOT NULL, CHECK against the §4.3 grammar content_hashtext NOT NULL (§4.4 construction) created_attimestamptz NOT NULL updated_attimestamptz NOT NULL audit_event_iduuid 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. -
Opacity.
brain_refand every other pointer column MUST NOT embed content or content-derived text (no titles, snippets, or free-text descriptions). A locator is structural, not descriptive. -
brain_refgrammar and owner binding.brain_refis 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'suser_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. -
content_hashconstruction.content_hashishmac-sha256:<hex>— HMAC-SHA-256 over the canonical content bytes, keyed with a platform integrity key held in the secrets backend and never stored in the database or any repository. Because the key is external, the stored value is not an offline dictionary oracle for low-entropy answers. On read, a hash mismatch refuses the read and flags the pointer for §4.5 reconciliation. -
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
-
Exact columns. Table
profile_consents, columns exactly:Column Type Constraints iduuid primary key user_iduuid NOT NULL, FK → users(id) ON DELETE CASCADE (the data subject) grantee_typetext NOT NULL, CHECK in ( agent,connector,feature)grantee_idtext NOT NULL (§5.2 per-type referential integrity) category_keytext NOT NULL, FK → profile_category_registry(category_key) statetext NOT NULL, CHECK in ( granted,revoked)granted_attimestamptz NOT NULL revoked_attimestamptz CHECK ((state = 'granted') = (revoked_at IS NULL)) actortext NOT NULL (the authenticated principal that recorded the change) audit_event_iduuid NOT NULL, FK → the platform audit log table plus the partial unique index UNIQUE (
user_id,grantee_type,grantee_id,category_key) WHEREstate = 'granted'— at most one active grant per (user, concrete grantee, category), as a database constraint. -
Grantee reference model. A grantee is identified by the pair (
grantee_type,grantee_id); its canonical display form is<grantee_type>:<grantee_id>(e.g.agent:<agent id>). The discriminant set is closed at the three CHECK values. Referential integrity is per type:agentids reference the enrolled-agent registry ofidentity-lifecycle.md;connectorids reference the platform connector registry;featureids 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 includesgrantee_id, so two same-type grantees are distinct rows. -
Default deny. Absence of an active
grantedrow 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. -
Mutation authority. Only the authenticated data subject may create, grant, or revoke consent rows for their own data: the server enforces that
actoris the subject's principal and equals the row'suser_idbefore 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 underidentity-lifecycle.md), the platform auto-revokes its active rows, recording a system principal asactor. -
Revocation and re-grant. Revocation flips exactly one active row to
revokedand stampsrevoked_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. -
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), 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. -
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
- The §2–§5 schemas are mode-independent: Standalone and Enterprise use the same tables and the same routing rule.
- 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/<user id>/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. - 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), defaultsplit),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. - 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).
- Conversion precondition. An operator electing
unsplitaccepts 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. - 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. 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.
- 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.
- 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.
- Closed write-route witness (hierarchy contract §6.3 style): a
static, re-export-aware inventory over
apps/andpackages/(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. - 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.
- 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; 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.
- Registry witnesses: (a) the version-1 registry state is exactly
the seven §2.2 rows with their classifications; (b) a
professional-backgroundanswer routes relationally (or per the ruling's alternative); (c) an unknowncategory_keyis refused with nothing stored (§2.3); (d) with the registry unreadable, the write is refused (§2.5); (e) a runtime insert/update/delete againstprofile_category_registryoutside 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). - Server-side classification witness: a client-supplied classification or routing override on a profile write is ignored; the registry decision is applied (§3.3).
- Pointer-constraint witnesses: inserting a second pointer for the
same (user, category, brain_ref) violates the §4.1 unique
constraint; a
brain_reffailing the §4.3 grammar (leading/,..segment, empty segment) is rejected by the CHECK; resolution of a validbrain_refunder user A's row never reads user B's brain (owner binding, §4.3). - Hash witnesses:
content_hashverifies 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). - 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.
- Default-deny and granularity witnesses: an agent grantee with
no active row is refused; with a
grantedrow 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 —featurevia its refusal path, since the version-1 feature list is empty (§5.2). - 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;
actorequals the subject's principal on every committed row (§5.4). - 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).
- Self-access witness: the data subject reads their own content without consent rows (§5.6).
- 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 recordsunsplitwith actor and audit linkage (§6.3); conversion from anunsplitinstall 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:
- The
featuregrantee type, with a closed feature-key list that is empty at version 1 (§5.2). - Append-only consent history: re-grants insert new rows; revoked rows are retained unmutated (§5.5).
- System-actor auto-revocation when a grantee ceases to exist (§5.4).
- The
custody_configelection record for the Standalone layout (§6.3). - The keyed
content_hashconstruction and its mismatch handling (§4.4). - The bounded dangling-pointer reconciliation policy (§4.5).
- 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
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?