feat(console): the persona pane, and the guard it changes - #163
Merged
Conversation
The `persona/*` family has two halves and only one of them shipped. The wallet's — disclosure, contacts, read-only bindings, context-local profiles — landed in #159..#162. The holder's own attribute pool, the profiles over it, `binding/set`, `correlation/analyze` and `disclosure/history` did not, and CI actively refused them: a guard banned all ten URIs from every extension bundle, with no exception. That guard's comment said why, and said what would change it: "a persona-management pane belongs beside the console's other holder-scoped panes, not in the wallet. That is why this guard has no exception yet rather than never: whoever builds that pane has to change this line, and the change is the review." This is that change. **`core/src/admin/persona.ts`** — the ten holder-scoped tasks, at the `admin` subpath rather than `./persona`, which stays wallet-safe. The split is what keeps the boundary greppable: one module holding both halves would be a root-barrel import away from putting the pool's URIs in the service worker. **`manager/panes/persona.tsx`** — attributes, profiles, bindings, correlation and disclosure history, agent-wide because its records sit *above* every context rather than outside them. A context column here would suggest the pool has compartments, which is the misreading the one-way boundary exists to prevent. **The guard is narrowed, not deleted** — banned everywhere in `dist/` except `manager.js`, the same shape as the `admin/*` one, because the console administers the agent while a wallet surface acts as a party inside a context. A second assertion checks the console still *has* the ten: narrowing gave a leak two shapes, and the new one — a dropped import or a tree-shake — is a pane whose buttons do nothing behind a smaller bundle and a green build. Both verified non-vacuous by mutation. **`profile-entries.ts`** is a pure module because what a profile presents is a security property, and it caught two of its own bugs: rebuilding entries from the tick list drops every pinned, overridden and inline entry silently, and seeding the ticks from every `ref` writes the same attribute twice — one live, one pinned — from an edit in which nothing was touched. Both are invisible until a disclosure. `isUnscopedHolder` is deliberately not `hasRole(authority, "admin")`: the agent gates these on `require_super_admin`, and an admin scoped to one context reading the pool would be reading every other context's identity data. Not exercised against a live agent — the pane has been typechecked, bundled and its client shapes pinned, but nobody has clicked it. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
force-pushed
the
feat/persona-console
branch
from
September 7, 2026 08:39
0ef5034 to
50f6b84
Compare
This was referenced Sep 7, 2026
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.
The
persona/*family has two halves and only one of them shipped. The wallet's — disclosure's two-call gate, contacts, read-only bindings, context-local profiles — landed in #159–#162. The holder's own attribute pool, the profiles over it,binding/set,correlation/analyzeanddisclosure/historydid not, and CI actively refused them: a guard banned all ten URIs from every extension bundle, with no exception.That guard's comment said why, and said what would change it:
This is that change.
What lands
packages/core/src/admin/persona.ts— the ten holder-scoped tasks, at theadminsubpath rather than./persona, which stays wallet-safe. The split is what keeps the boundary greppable: one module holding both halves would be one root-barrel import away from putting the pool's URIs in the service worker, and the boundary would then be a comment rather than something CI can check.packages/extension/src/manager/panes/persona.tsx— a new console section, in the Data & credentials act, agent-wide (contextScoped: false) because its records sit above every context rather than outside them. Five panels, in the order the argument runs: the facts, the projections over the facts, where a projection is presented, and then the two readings that only make sense once all three exist.attribute/{list,put,delete}profile/{list,put,get,delete}binding/set, plusbinding/listfrom the wallet halfcorrelation/analyzedisclosure/historyThe guard is narrowed, not deleted
Banned everywhere in
dist/exceptmanager.js— the same shape as theadmin/*guard, and for the same reason: the console administers the agent and its operator can hold the unscoped holder credential these tasks require, while every wallet surface acts as a party inside a context and cannot.A second assertion checks the console still has the ten. Narrowing gave a leak two shapes rather than one: a wallet gaining these URIs (which the first guard catches), and the console losing them to a dropped import or a tree-shake — a persona pane whose buttons do nothing, behind a smaller bundle and a green build. Both guards were verified non-vacuous by mutating
dist/and watching each one trip.page-task-policy.tsneeded no change: it already refuses the wholepersona/prefix to pages, so the new tasks are covered.isUnscopedHolder, and why it is nothasRole(authority, "admin")The agent gates all ten on
require_super_admin—Adminand unrestricted scope — deliberately not onrole == Admin. An administrator scoped to one context who could read the pool would be reading identity data belonging to every other context. The console mirrors that test to explain the refusal; the agent decides.The emptiness of
scopesis why the two tests are not redundant: an empty context list is unrestricted for an admin and nothing at all for every other role, soscopes.length === 0alone would promote a monitor to the most privileged caller there is.profile-entries.tsis a pure module because it had to beWhat a profile presents is a security property, and the composition caught two of its own bugs, both silent — the profile saves, keeps its name, keeps working, and presents a different set of claims than the operator ticked:
ref— pinned and overridden entries have one too — and saving writes the same attribute twice, once live and once pinned, from an edit in which nothing was touched.Ten tests, mutation-checked against both bugs (four fail on each). A third bug is fixed in the pane itself:
setCorrelationfollowed byif (!correlation)in the same closure closed the editor the instant the linkage warning was raised, showing it to nobody.Verification
npm run lint,npm run build,npm test— 785 tests green across the four workspaces.Assertstep inci.ymlrun locally against a real build, including the two new ones, plus their negative cases.packages/core/tests/admin.persona.mjspins the client shapes, each structural claim paired with a positive one. Two are load-bearing on the wire: avalueis sent as the JSON it is rather than wrapped in an object (the generated payload type renders an unconstrained JSON member as an index signature — a codegen artifact;vta-sdktypes itValue), and aprofileIdofnullis an unbind rather than an omission.task-surface.mjscoverage moves 177 → 187, and the note there is rewritten: it used to argue these ten "cannot be" implemented here, which was true of the wallet and wrong about the library.Not exercised against a live agent. The pane typechecks, bundles, and its client shapes are pinned, but nobody has clicked it.
Known, not fixed here
Destructive. Nothing can preview "which personas present this profile" — that spans every context and onlypersona/profile/deletecomputes it — so the agent's refusal is the preview, and the unbind choice is an ordinary option rather than a force tick that would make every operator authorise an unbind for the common case where nothing is bound.codeanddetailsdo not survive the offscreen bridge, which flattens toe.message. That is why nothing here matches on a code (R3.7 would require one) and why the unbind flow is shaped as above. Worth widening the relay separately.persona/correlation/analyze's response carriessharedWithProfileCounton each finding, which the published schema does not permit (findings.itemsisadditionalProperties: false). A VTI-side drift; the pane reads only schema members, so it renders correctly either way.