Skip to content

feat(health): print the DID this install authenticates to the VTA as - #283

Merged
stormer78 merged 1 commit into
mainfrom
worktree-health-vta-auth-did
Sep 8, 2026
Merged

feat(health): print the DID this install authenticates to the VTA as#283
stormer78 merged 1 commit into
mainfrom
worktree-health-vta-auth-did

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The VTA keys its ACL entry on a did:key minted during setup, so that DID is
what an operator has to name in pnm acl get / pnm acl update. It was
reachable from nowhere but the TUI's VTA panel, truncated to fit the pane —
while the identity pane's own refusal hint told the reader that openvtc health
prints it. It did not.

openvtc health — a VTA access section

Printed after Local configuration, before the network map. Live against
glenn-vta:

VTA access
  VTA              did:webvh:QmWoJD2kpP…:webvh.storm.ws:glenn-vta
  context          openvtc
  transport        DIDComm
  mediator         did:webvh:QmTS3a3H9D…:webvh.storm.ws:mediator
  authenticates as did:key:z6MkjRAaiFHdQko1cKy9vXnzDM4yeXwqcmZjk6VTwVHhYJ9x

  The VTA's ACL is keyed on that last DID: it is what to name when reading
  or changing what this install may do. From your PNM session:

    pnm acl get did:key:z6Mkj…
    pnm acl update did:key:z6Mkj… --capabilities persona-holder

  `persona-holder` is the exception that grants rather than narrows: it adds
  authority over your own identity — the facts and faces that sit above every
  context — without widening this install's reach into any other context.

Three choices worth naming:

  • The DID is printed in full. The TUI panel must truncate it to fit; a
    truncated did:key names nobody, and this line exists to be copied into
    another terminal.
  • The transport is derived, not guessed — the same mediator_did.is_some()
    rule build_runtime_vta_client branches on, so it names the leg actually in
    use rather than inferring REST from a non-empty URL (which stays populated on
    the DIDComm path as the fallback). Sending an operator to debug the leg that
    is not being used is the failure this avoids (R6.4).
  • No network. All of it is read from the loaded config, so it answers on
    exactly the run where the network leg is the broken thing.

--json carries it as top-level vta_access, and as null for a BIP32 profile
so a script need not branch on the backend first.

The identity pane's grant hint, fixed alongside

It named a --did flag that pnm acl update does not have — the DID is
positional (pnm-cli/src/cli.rs, AclCommands::Update) — so pasting it would
have failed. And it left the DID as <this install's DID>, pointing at openvtc health, which did not print it either.

It now carries the real DID, from the config the pane is already rendering from
(IdentityState::agent_credential_did, set in the same sync_from_config arm
that fills VtaState::credential_did). The placeholder survives only for a
BIP32 account, which has no agent credential to name and will not have produced
that refusal in the first place.

Testing

Six new unit tests: the JSON shape (the ACL subject arrives whole, absent
profiles serialise as null), the transport rule across all four
mediator/URL combinations, and the completed command (real DID present,
placeholder gone, no --did flag).

  • cargo fmt --all --check, cargo clippy --all-targets --all-features -D warnings
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
  • cargo test --all-features --workspace — green
  • Run live against the deployment above; both the rendered and --json forms
    verified.

Pre-merge checklist

- [x] No new reqwest::Client::new() / bare fetch(); all clients have finite timeouts (R1.2) — no new clients; the section makes no network calls
- [x] No lock held across a network await (R1.3) — n/a
- [x] No local state committed before its remote effect, or the flow is resumable with an idempotency key (R2.1) — read-only, no mutations
- [x] Every retry is bounded + backed off; non-idempotent ops are not blind-retried (R1.4) — n/a
- [x] Accept/poll/listen loops survive transient errors (R1.5) — n/a
- [x] Acks/deletes happen only after durable handoff (R1.6) — n/a
- [x] New/changed wire types: camelCase, deny_unknown_fields where security-relevant, schema registered, all consumers (incl. JS) updated (R3.*) — no wire types; `vta_access` is an additive key in this tool's own JSON output
- [x] Config absence = most restrictive; fail-closed if enforcement can't start (R5.*) — a non-VTA backend renders nothing rather than a partial claim
- [x] Logs/status claim only what was verified; background-job failures are surfaced (R6.*) — the transport is named by the rule the client branches on, not inferred; the `pnm` commands were verified against pnm-cli's current arg definitions (R3.6)
- [x] "Process dies on the next line" answered for every mutation touched (R2.1) — no mutations
- [x] Deviations from this guide flagged explicitly with rule numbers — none

The VTA keys its ACL entry on a `did:key` minted during setup, so that DID
is what an operator has to name in `pnm acl get` / `pnm acl update`. It was
reachable from nowhere but the TUI's VTA panel, truncated to fit the pane —
while the identity pane's own refusal hint told the reader that `openvtc
health` prints it. It did not.

A new *VTA access* section now does: the agent, the context, the transport
that would be opened (the same mediator-vs-REST rule
`build_runtime_vta_client` branches on, rather than a guess from the URL
being non-empty), its mediator or REST endpoint, and the authenticating DID
in full. Both `pnm acl` commands are printed ready to run, with a line on
why `--capabilities persona-holder` grants rather than narrows — it reads
as a narrowing everywhere else the flag appears.

All of it comes from the loaded config, so it answers on the run where the
network leg is the broken thing. `--json` carries it as `vta_access`, and
as null for a BIP32 profile so a script need not branch on the backend.

The identity pane's hint is fixed alongside it: it named a `--did` flag
`pnm acl update` does not have (the DID is positional) and left the DID as
a placeholder. It now carries the real one, from the config the pane is
already rendering from — the placeholder survives only for a BIP32 account,
which has no agent credential to name and will not have produced the
refusal in the first place.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner September 8, 2026 07:21
@stormer78
stormer78 merged commit 6fa7a87 into main Sep 8, 2026
14 checks passed
@stormer78
stormer78 deleted the worktree-health-vta-auth-did branch September 8, 2026 07:29
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

🔎 A manual security review is recommended before merging. Please contact the Security team for specifics and remediation guidance.

ℹ️ Detailed findings are not published on public repositories; the Security team holds the complete report.

stormer78 added a commit that referenced this pull request Sep 8, 2026
…284)

The wipe-profile screen ends by saying the VTA-side context, persona DID
and keys survive, and to run `pnm contexts delete` if you want those gone
too. It named no context — on the one screen that is about to remove the
config file and the keyring entry that hold the id, which is why the advice
says "first". Sending the operator away to look it up was sending them
somewhere that stops existing a keypress later.

The id is now carried on `SettingsState` from `account.top_context_id`, set
in the same `sync_from_config` pass as the rest of the pane, and the command
is printed complete on its own row — the treatment every other command this
TUI hands over already gets, because it is meant to be retyped in another
terminal.

Positional, as `pnm-cli`'s `ContextCommands` defines it; the `--did` flag
that had to come off the identity pane's `pnm acl update` hint in #283 is
the same mistake one repo boundary away, so the shape is asserted rather
than assumed. A nested context keeps its whole `<parent>/<id>` path, since
`pnm` addresses a sub-context that way and the leaf alone names a different
context or none. An unloaded account keeps a placeholder: no id is better
than a wrong one in a command that deletes a context and everything under it.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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.

2 participants