Skip to content

feat(console): the persona pane, and the guard it changes - #163

Merged
stormer78 merged 1 commit into
mainfrom
feat/persona-console
Sep 7, 2026
Merged

feat(console): the persona pane, and the guard it changes#163
stormer78 merged 1 commit into
mainfrom
feat/persona-console

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

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/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:

Unlike the seeds family, these may legitimately ship one day — 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.

What lands

packages/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 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.

Panel Tasks
Attributes attribute/{list,put,delete}
Profiles profile/{list,put,get,delete}
What each persona presents binding/set, plus binding/list from the wallet half
Where your identities link correlation/analyze
What has left, and to whom disclosure/history

The guard is narrowed, not deleted

Banned everywhere in dist/ except manager.js — the same shape as the admin/* 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.ts needed no change: it already refuses the whole persona/ prefix to pages, so the new tasks are covered.

isUnscopedHolder, and why it is not hasRole(authority, "admin")

The agent gates all ten on require_super_adminAdmin and unrestricted scope — deliberately not on role == 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 scopes is why the two tests are not redundant: an empty context list is unrestricted for an admin and nothing at all for every other role, so scopes.length === 0 alone would promote a monitor to the most privileged caller there is.

profile-entries.ts is a pure module because it had to be

What 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:

  • Rebuild from the ticks alone and every pinned, overridden and inline entry disappears.
  • Seed the ticks from every 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: setCorrelation followed by if (!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.
  • Every Assert step in ci.yml run locally against a real build, including the two new ones, plus their negative cases.
  • packages/core/tests/admin.persona.mjs pins the client shapes, each structural claim paired with a positive one. Two are load-bearing on the wire: a value is 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-sdk types it Value), and a profileId of null is an unbind rather than an omission.
  • task-surface.mjs coverage 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

  • Deleting a profile is deliberately not a Destructive. Nothing can preview "which personas present this profile" — that spans every context and only persona/profile/delete computes 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.
  • A rejection's code and details do not survive the offscreen bridge, which flattens to e.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 carries sharedWithProfileCount on each finding, which the published schema does not permit (findings.items is additionalProperties: false). A VTI-side drift; the pane reads only schema members, so it renders correctly either way.

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
stormer78 force-pushed the feat/persona-console branch from 0ef5034 to 50f6b84 Compare September 7, 2026 08:39
@stormer78
stormer78 merged commit d52a89d into main Sep 7, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/persona-console branch September 7, 2026 08:42
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