docs(custody): revision 2 — classifier registry, grantee model, exact schemas, witness closure (sol r1 F1-F6)
ci/woodpecker/pr/ci Pipeline is running

This commit is contained in:
fred
2026-08-26 19:39:04 -05:00
parent caa5b04096
commit 8079d8dc91
+348 -110
View File
@@ -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 F1F6): 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:<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.
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
`<grantee_type>:<grantee_id>` (e.g. `agent:<agent id>`). The
discriminant set is closed at the three CHECK values. Referential
integrity is per type: `agent` ids reference the enrolled-agent
registry of `identity-lifecycle.md`; `connector` ids reference the
platform connector registry; `feature` ids reference a closed
feature-key list owned by amendments to this contract, and that
list is EMPTY at version 1 (no feature grantee exists until an
amendment names one). A grant to one agent confers nothing on
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/<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.
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 17 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?