Skip to content

feat(persona): read the claim-type table from the agent - #196

Merged
stormer78 merged 1 commit into
mainfrom
feat/read-claim-types-from-agent
Sep 8, 2026
Merged

feat(persona): read the claim-type table from the agent#196
stormer78 merged 1 commit into
mainfrom
feat/read-claim-types-from-agent

Conversation

@stormer78

@stormer78 stormer78 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The payoff for persona/claim-types/list: the console reads the claim-type
table from the agent instead of shipping its own.

The copy was correct. That was never the problem.

Worth being precise, because "removed a vendored table" sounds like a bug fix
and this mostly is not one. manager/claim-sensitivity.ts implemented §4
faithfully, prefix walk included.

What a copy costs is a re-sync pull request against two repositories on every
registry change — the argument CLAIM-TYPES.md §1 makes for the registry
existing at all, one layer down — and it can only ever describe the tokens its
own build knew about. An agent serving an extension type is invisible to a
client shipping its own table.

Rebuilt on top of #195, not merged with it

#195 (the holder decides what happens to a value) landed in these same files
while this was open, and a textual conflict resolution would have quietly
dropped it. This branch was reset onto main and the change re-applied on top
of its code instead.

Two things worth saying plainly:

#195 already fixed the §3.3 masking bug, independently and before this — the
maskedFact early return on sensitivity !== "high" that drew email.* in the
clear. An earlier draft of this PR claimed that fix; it is not mine and the
claim is withdrawn.

#195's override layer is preserved unchanged. treatmentFor still applies
the holder's own sensitivity over the registry's answer, still moves only the
axis they decided, and still leaves a declared token's mask alone — the
narrowness that PR argued for. What changed underneath it is where the
registry's answer comes from: isRegisteredType is now a walk over the served
table rather than a local one over a compiled record.

What moved where

Resolution → @openvtc/pnm-core/persona. resolveTreatment is §4 in full,
in one place, next to the served table. The parts that look like detail are the
parts that get got wrong: dot boundaries, a proper prefix, x: borrowing
nothing, and a prefix that can only tighten. A caller walking prefixes itself
has to get all four right and the loosening direction fails quietly.

Orderings come from the agent's strictness, not a constant here — so "more
protective" means the same thing on both sides, and a maintainer adding a
stricter mask style is honoured without a rebuild. An axis value this build does
not recognise is treated as most protective.

Drawing stays in the console, because §3.3 gives the style and says the
width is the client's: a run that tracked the length would publish it.

null is a real state, and it fails closed

While the table is in flight every value resolves to the floor and is masked;
grouping answers unregistered. There is deliberately no compiled fallback —
that is the copy this replaced, and a stale copy resolving a token the agent has
since tightened is the failure the registry exists to end.

The render fixture proved this by accident: before I passed a registry, the
"one control per hidden attribute" test went 3 → 4, because everything had
fallen to the floor. That is the fail-closed path, observed.

listClaimTypes takes no contextId

Every other task in core/src/persona/ takes PersonaCallerParams, which
requires one. This must not: the payload is empty and the agent gates it as
reachable by any authenticated caller, precisely so the holder's own tooling —
which has no context to name — can read it. The typechecker caught this the
moment the console called it. A context invented to satisfy a signature is a lie
in an audit trail.

Tests

Moved to core (persona.claim-types.mjs, 10): the §4 rules, against a
served-shaped fixture — exact entry beats family, a bare name is a token, a
gated family is not escapable, a family entry cannot loosen, x: borrows
nothing, prefixes are dot-bounded, and an unrecognised axis value never resolves
to "no mask".

Kept in the console (manager-claim-sensitivity.test.mts, 8): the drawing —
fixed-width runs, a value no longer than its own tail, emailLocal refusing to
guess at a non-address, and the §3.3 regression as its own case.

Reframed (manager-attribute-family.test.mts): "every root the registry
declares is placed" cannot survive reading the table from the agent — a
maintainer may serve a family this build predates, and unregistered is the
honest answer for one, not a gap. It now asserts the mapping is internally
complete (PLACED_ROOTS), plus a new case pinning the unknown-family answer.

950 tests across four workspaces, 0 failures. tsc -b and npm run build
clean.

Guide checklist (§9)

  • R1.2 — the new call goes through the same channel as every other persona task, timeout included
  • R1.3/R1.4/R1.5/R1.6 — no lock, retry, loop or ack path touched
  • R2.1 — read-only
  • R3.* — no new wire type; claim-types/list shipped in the agent (VTI #1315) before this reads it, and task-surface.json records it
  • R5.* — absence is the most restrictive answer: no registry ⇒ floor ⇒ masked; unknown axis value ⇒ most protective; no compiled fallback
  • R6.* — the console renders the table the agent actually serves, not a claim about it
  • Deviations — none

The payoff for `persona/claim-types/list`: the console stops shipping its own
copy of the table. The copy was correct — that was never the problem. A copy of
a table two repositories do not own costs a re-sync PR against each on every
change, and can only describe the tokens its own build knew about.

Resolution moves to `@openvtc/pnm-core/persona` beside the served table, and the
strictness orderings come from the agent, so "more protective" means the same
thing on both sides. An axis value this build does not recognise is treated as
most protective, never least.

#195's work is preserved as-is: `treatmentFor` still applies the holder's own
decision over the registry's answer, only the axis they decided moves, and a
declared token's mask never does. What changed underneath it is where the
registry's answer comes from — `isRegisteredType` is now the served table's
walk rather than a local one over a compiled record.

`null` is a real state and fails closed: masked, `unregistered`, attributed to
the registry rather than the holder, with no compiled fallback — a stale copy
resolving a token the agent has since tightened is the failure the registry
exists to end. A render test pins it, found by leaving the fixture out and
watching the reveal-control count go 3 → 4.

Drawing stays here, because §3.3 gives the style and says the width is the
client's. `listClaimTypes` takes no contextId: the payload is empty and the task
is reachable by any authenticated caller, so the holder's own tooling can read
it.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 force-pushed the feat/read-claim-types-from-agent branch from e4d167c to 105bd2c Compare September 8, 2026 11:14
@stormer78

Copy link
Copy Markdown
Contributor Author

Rebuilt on top of #195 rather than resolving the conflict textually — it landed in the same files and a mechanical merge would have dropped it. Branch reset to main, change re-applied over its code.

Correcting one claim from the earlier description: I had written that this fixed the §3.3 masking bug — maskedFact returning early on sensitivity !== "high", so email.* was drawn in the clear. #195 already fixed that, independently and first. Not mine; withdrawn from the body.

#195's override layer is preserved as-is: treatmentFor still applies the holder's decision over the registry's, still moves only the axis they decided, and still leaves a declared token's mask alone. Only the source of the registry's answer changed.

976 tests, 0 failures.

@stormer78
stormer78 merged commit 6960ce9 into main Sep 8, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/read-claim-types-from-agent branch September 8, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant