feat(persona): the holder decides what happens to a value, and "not decided" is an answer - #195
Merged
Merged
Conversation
…ecided" is an answer `sensitivity` and `release` are per-attribute members the holder sets: how carefully a value is shown to them, and what it takes to let it leave. Absent means they decided nothing and the claim-type registry answers. The console read neither and wrote neither, so the only visibility rule in force was a vendored table — which is why an attribute typed `profile.github` was masked and withheld with no way to say otherwise. The editor now asks both questions, each with three options, because "let your agent decide" is a state and not a synonym for the registry's current answer. Choosing it writes the member **absent**: `persona/attribute/put` warns that sending back a *resolved* value freezes the attribute to today's table, so a later tightening of the registry would protect every new attribute and leave this one exposed. That same replace semantics had already broken something quietly. A put replaces the whole record, and the editor never mentioned these members — so every save cleared whatever the holder had decided, and nothing in the response said so. The editor now reads them off the record it loaded and sends them back untouched. - `AttributePutParams.sensitivity` / `.release` in `@openvtc/pnm-core/admin`, spread conditionally, with the reason for the conditional written where the next person will edit it. - `treatmentFor` in `claim-sensitivity.ts` applies the holder's decision over the registry's and reports which spoke, so a pane can say "you decided" without putting the registry's words in the holder's mouth. `treatmentOf` stays the registry-only answer, exactly where its docstring said this rule would go. - One narrow exception, and it is why the feature does anything visible: a declared token keeps the registry's mask (§3.3 — the axes are independent, so `phone.mobile` stays `•• 25` however it is marked), while an unregistered one's mask follows the holder. `UNREGISTERED` is a single conservative answer covering both axes *because nobody had reasoned about the token*; the holder deciding is the decision it stood in for. Without this, marking your own `profile.github` as showable still drew four bullets. - `maskedFact` now gates on the mask style rather than on `sensitivity`. They are independent per §3.3, and gating on `high` meant `email.*` (`normal` / `emailLocal`) was called hidden by `isSensitive` and drawn in full by the renderer — a promised *Show* button that never appeared. - The map carries both decisions through to `FactValue`, and the detail strip names them: "You decided: kept back until you ask · approved again every time it leaves." Two rows added to `design-docs/persona-vocabulary.md` for the on-screen words — *showing it to you*, *letting it leave*, *let your agent decide* — per that table's own rule about adding copy. 13 new tests (352 extension, 552 core), `tsc -b` clean, `npm run build` clean. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
added a commit
that referenced
this pull request
Sep 8, 2026
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>
🛡️ AI Agentic Security Code Review🔎 A manual security review is recommended before merging. Please contact the Security team for specifics and remediation guidance.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #194, which fixed the reading of sensitive values and closed with "there is still no way to change an attribute's visibility from the console". This is that.
The two questions, asked
sensitivityandreleaseare per-attribute members on the wire — how carefully a value is shown to the holder, and what it takes to let it leave. The console read neither and wrote neither, so the only rule in force was the vendored claim-type table, and an attribute typedprofile.githubwas masked and withheld with no way to say otherwise.The editor now asks both, each with three options:
The default option names what the registry answers today, so the person can see what they would be changing away from.
"Let your agent decide" is a state, not a synonym
Choosing it writes the member absent.
persona/attribute/putis explicit about why: sending back a resolved value freezes the attribute to today's table, so a later tightening of the registry would protect every new attribute and leave this one exposed.The same replace semantics had already broken something quietly. A put replaces the whole record, and the editor never mentioned these members — so every save cleared whatever the holder had decided, and nothing in the response said so. A holder who gated their passport and then fixed its label lost the gate. The editor now reads both off the record it loaded and sends them back untouched;
manager-claim-sensitivityand the render tests pin all three directions (carried, set, cleared).Why marking something showable now actually shows it
treatmentForapplies the holder's decision over the registry's and reports which spoke. One narrow exception makes the feature do anything visible:phone.mobilestays•• 25however it is marked. The registry has an opinion there and this console does not overrule it.UNREGISTEREDis one conservative answer covering both axes precisely because nobody had reasoned about the token (§4 rule 3's own words). The holder deciding is the decision it was standing in for. Without this, marking your ownprofile.githubas showable still drew four bullets, by a rule justified only by nobody having looked at it.A masking bug found on the way
maskedFactgated onsensitivity === "high";isSensitivegated on the mask style. §3.3 made those independent, andemail.*isnormal/emailLocal— so the strip promised "hidden until you press Show" while the address sat on screen in full with no button to press. Two functions, one question, two answers. The mask style decides now.Words
Three rows added to
design-docs/persona-vocabulary.md(that table's own rule: add copy in those words, or change the document first) — showing it to you, letting it leave, and let your agent decide for a member the holder left absent. "Sensitivity" stays off the screen: it reads as a judgement about the value rather than a setting about a screen. That file lives outside this repo, so it is not in this diff.Checks
tsc -bclean across workspaces;npm run buildclean,manager.jsa single chunk.x:stays unregistered; a decision is carried through an unrelated edit; clearing writes absence rather than a resolved default; and an attribute the holder marked showable is drawn in the clear on the map.