feat(console): VTA management console in the wallet, on the Trust Context tree - #155
Merged
Merged
Conversation
… tree Everything an operator does *to* their agent — contexts, keys, ACL, approvals, policy, services — has only ever been reachable from the `pnm` CLI, which means from a laptop with a keyring. The client library for it already existed: `@openvtc/pnm-core/admin` covers all of the CLI's command tree bar backup, bootstrap and auth-credential, and its doc comments were written for a console it did not yet have. This is the spine of that console, plus Contexts end to end. Trust contexts are the navigation, not a section. `ContextRecord` already carries `parent`/`basePath` and `contextId` is a filter parameter on `keysList`, `aclList` and `auditList` — so the tree is a persistent column and the selection scopes every pane to its right. Two properties are load-bearing: The console holds no key material. It composes typed documents with the `admin/*` helpers and the offscreen document mints and signs them, so `RUNTIME_MANAGER_TASK` carries only `type` and `payload` — the rule that the device mints the envelope does not soften because the composer is an extension page. Reusing `OFFSCREEN_REQUEST_TASK` inherits transport selection, health recording and the approver ceremony; `offscreen.ts` is unchanged. This is also why `admin/*` now types its envelope parties as `TaskParty` (just a DID) rather than `Identity`: only `.did` was ever read, and a surface typed on `Identity` can only be called from somewhere holding a private key. The wallet still ships no operator authority. The CI guard is narrowed rather than dropped — admin task URIs are banned everywhere in `dist/` except `manager.js`, which is its own vite build with `codeSplitting: false` so Rollup cannot hoist a shared chunk into a wallet surface. A second assertion fails if it ever emits more than one chunk. The relay is gated on `sender.url`, since every content script carries this extension's id. It does not prompt per call — the operator is the caller, and twelve dialogs to draw one screen is dismissal, not consent; the agent's ACL and policy engine remain the authority, and a `requireConsent` renders as a match-code ceremony rather than a red error string. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…audit Fills out the console's remaining eight panes on the spine landed in the previous commit. Every one is scoped by the persistent context selection where the underlying task takes a `contextId`, so choosing `work/eng` once scopes Keys, DIDs, Access, Policy and Audit together. Shared vocabulary rather than nine copies of it: `table.tsx` separates the three states a list actually has — being fetched, refused, and answered with nothing — because written per-pane they collapse and an operator reads "you have no keys" off a permission error. `use-async.ts` keeps `data` null until the agent answers for the same reason. Where the agent draws a distinction, the pane draws it too: - Keys shows `internal` origin as unrecoverable, because such a key derives from no seed and nothing reconstitutes it. `keysSign` is deliberately not surfaced — signing is use, not administration, and a console that offers a "sign this" box turns key management into an oracle. - Access renders an entry with no scopes as "everywhere" and no expiry as "never", both in the caution colour. `aclChangeRole` carries `fromRole` from the row it was opened on, so a role someone else changed in between rejects instead of silently overwriting them. - Policy carries `expectedVersion` on upsert *and* delete — without it two operators editing one module is last-write-wins and the loser never finds out. - Transports pairs advertised state against observed, and presents `unknown` as "not observed" rather than a failure; a constructed REST channel is not evidence of anything. - Audit renders `truncated` above the table, not below: a warning under a long list is a warning nobody reads, and concluding "nothing else happened" from a partial page is the failure an audit trail exists to prevent. - Sessions is headed "your sessions" — the task returns only the caller's, and the agent's all-sessions route is deliberately not wired here. `webvh/dids.ts` takes the same `TaskParty` widening as `admin/*`: it too read only `.did` off its envelope parties. Verified by rendering the built bundle against stubbed extension APIs in Chrome, light and dark. Admin surface remains confined to manager.js. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
carrier.ts defines it; sender.ts re-exported it, so half the panes reached for one path and half the other. Two routes to one symbol invite the question of whether they could ever be different classes — which matters here, because every pane distinguishes this class from Error by identity to decide between rendering a consent ceremony and rendering a red string. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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.
Brings a full VTA management console into the wallet, with Trust Contexts as its navigation spine.
Targets
vta-management(working branch), notmain.Why now
Everything an operator does to their agent — contexts, keys, ACL, approvals, policy, services — has only been reachable from
pnm-cli, i.e. from a laptop with a keyring. Two things made this the moment:@openvtc/pnm-core/adminis 13 modules / 57 functions, and mapped againstpnm-cli's command tree the only gaps arebackup,bootstrapandauth-credential. Its doc comments were written for a console it did not yet have. This was a UI project, not a protocol one.chrome.runtime.idas the rpId, so no web origin can unlock it — andsignOutboundTaskalready attaches a proof at the channel, so every admin task inherits SPEC §7.2 item 7a signing for free.What's in it
A full-page console at
manager.html, opened from the options rail. Nine panes across three acts (the grouping and its colours follow the GDC workshop deck's rail):The context tree is a persistent column, not a page.
ContextRecordalready carriesparent/basePathandcontextIdis a filter parameter onkeysList,aclListandauditList— so selectingwork/engonce scopes every pane to its right. The tree also draws a placeholder for a parent the caller's ACL cannot reach: the obvious build (attach tobyId[parent], keep what attached) silently drops exactly the children you do administer, and renders a shorter, entirely plausible tree.The invariant this deliberately changes
packages/core/src/adminis operator authority, and CI grepped all ofpackages/extension/dist/for 17 of its task URIs on the grounds that "a wallet has no business shipping any of it". The console makes that false on purpose, so the guard is narrowed, not deleted: banned everywhere indist/exceptmanager.js.That exception is structural rather than a convention.
vite.config.manager.tsbuilds the console alone withcodeSplitting: false, so Rollup has no other entry to hoist a shared chunk into — and a second CI assertion fails if it ever emits more than one chunk, since the first guard names exactly one exception and an extra chunk is a file nothing checks.The one new idea: only
typeandpayloadcross the bridgerequest-task.tsis explicit that the device mints the envelope — a wallet that counter-signs a document composed elsewhere attests to fields it never checked. That does not soften because the composer is an extension page.So the envelope an
admin/*helper builds is a carrier:carrier.tstakes itstypeandpayloadand discards the rest, and the existingOFFSCREEN_REQUEST_TASKmints the real one.offscreen.tsneeded no change at all, and the console inherits transport selection and fallback,TransportHealthrecording, outbound signing, and the same-browser approver ceremony.Two consequences worth stating:
admin/*,vta/contexts.tsandwebvh/dids.tsnow type their envelope parties asTaskParty(just a DID) rather thanIdentity/RemoteDidcommEndpoint. Only.didwas ever read — and a surface typed onIdentitycan only be called from somewhere holding a private key, which is what would have forced key material into the console. The REST convenience wrappers keep the stricter pair, because they build a channel and a channel signs.The relay is gated on
sender.urlagainstchrome.runtime.getURL(""), since every content script carries this extension's id. It is absent fromPAGE_FACING_RUNTIME_TYPESand fromcontent.ts's dispatch table, and does not prompt per call — the operator is the caller, and twelve identical dialogs to draw one screen is dismissal, not consent.Where the agent draws a distinction, the pane draws it too
unknownas "not observed" — a constructed REST channel is not evidence of anything. Onlydownmeans the wallet tried and could not.truncatedabove the table: a warning under a long list is a warning nobody reads, and concluding "nothing else happened" from a partial page is the failure an audit trail exists to prevent.internalorigin as unrecoverable, and deliberately does not surfacekeysSign: signing is use, not administration, and a "sign this" box turns key management into an oracle.aclChangeRolecarriesfromRolefrom the row it opened on.expectedVersionon upsert and delete — without it, two operators editing one module is last-write-wins and the loser never finds out.contextPreviewDeleterenders the real keys and DIDs), withforceas its own tick.consentRequiredis a typedConsentRequiredErrorrendered as a match-code ceremony, never a red string.Verification
npm run clean && npm run lint && npm run build && npm testfrom cold — 718 tests pass. Both CI guards confirmed by hand: admin URIs appear only inmanager.js(and do appear there, so the guard is not passing on an empty file), and the console is a single bundle.New tests:
manager-sender.test.mts(a carrier with forgedissuer/recipient/idmust not reach the bridge; a consent refusal keeps its digest),manager-surface.test.mts(not page-facing, not in the content-script table, gated onsender.url),manager-context-tree.test.mts(an orphan under an unreachable parent is still drawn).Rendered the built bundle in Chrome against stubbed extension APIs, light and dark, walking every pane and a destructive preview. That caught three real bugs before commit — a filename collision, a crushed table column, an act rail too faint to do its job.
Not verified against a live VTA. That needs the unpacked extension and an onboarded agent. When doing so: check the
whoAmIbanner resolves against whichever host the wallet is actually onboarded with rather than an assumed one, and cross-check one list againstpnm contexts list/pnm acl list.Not built:
backup,bootstrap,auth-credential— the only gaps against the CLI, and they have nopnm-coreclient functions yet.Pre-merge checklist