Skip to content

feat(manager): mint a DID from the whole task surface, and read its log - #247

Merged
stormer78 merged 2 commits into
mainfrom
feat/did-create-and-log-console
Sep 15, 2026
Merged

stormer78 merged 2 commits into
mainfrom
feat/did-create-and-log-console

Conversation

@stormer78

@stormer78 stormer78 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

The New DID form sent 3 of the 20 members vta/webvh/dids/create/1.0
accepts, and the DIDs pane showed logEntryCount with no way to read an
entry — a number with nothing behind it. Anything past a server id and
Portable meant leaving the console for the CLI.

The form

Grouped by the decision each member makes rather than by the payload's field
order:

Where it is published server, domain, label, and the existing PathPicker with its live DID preview and the hosting server's own path rules
What it commits to portable and preRotationCount as choices with their consequence beside them, since both are fixed in the first log entry
Which keys it is built on signingKeyId / kaKeyId, offering only active keys of the right type in the right context
What it advertises addMediatorService / addTspService, plus a structured id/type/endpoint editor for additionalServices
What it is stamped from templates from didTemplateList, a field per requiredVars, defaults seeding the controls above

setPrimary is now sent explicitly as false. The agent defaults it to
true, so the quiet path through the old form replaced the context's identity
on every mint — which anything resolving a context's DID to find its keys,
a room host at startup, would then follow.

additionalServices is validated here because the agent does not validate it:
it is written into the document verbatim, and the document is a log entry,
which is append-only. A bad entry is published, not refused. http:// and
credentials-in-URL are rejected; did: endpoints are accepted, since a
mediator entry names a DID.

Reading a log

Clicking the DID expands the row into vta/webvh/dids/get with includeLog,
fetched on open rather than with the listing — the log is the DID's whole
history, and a listing that pulled every one would be every history, to draw a
table. Each entry shows version id, time, proof count, parameters,
verification methods and services, with copy buttons at three levels: whole
log, one entry, the DID.

A response carrying no log is drawn as a refusal to read, never as an absence
of history: includeLog's own spec says absence means it was not asked for.
One unreadable line is one unreadable entry, with its raw text still copyable,
rather than a blank panel.

DID templates pane

Full CRUD; built-ins are inspect-only because the agent refuses the write and
a button that refuses first beats a refusal after the form was filled in.

Update is a REPLACE, so the editor carries optionalVars and defaults it
never draws — without that a save cleared them while the agent reported
success. Render the stored version calls didTemplateRender so the operator
sees the agent's own substitution rather than this console's guess at it; the
button says it renders the stored version, because render takes a name and
cannot see unsaved edits.

Three defects the new tests caught before they shipped

  1. templateDefaults coerced its hint with Number(), and Number(null) is
    0
    — a null preRotationCount hint would have silently switched
    pre-rotation off, the one setting that makes a stolen key unrecoverable.
  2. The agent answers a context-scoped template listing with global templates
    too
    , so asking both namespaces listed every global template twice, under
    one React key. mergeTemplates keys on scope and name, which still keeps
    two genuinely different templates of one name apart.
  3. The key pickers said "Reading…" with no context selected, when nothing
    had been asked for.

Reuse of a key is offered but never quiet: two DIDs publishing one key are
provably one holder, so the form says so when a key is chosen and clears the
choice after a mint — leaving it set would build the next DID on the same key
by inaction.

Core

webvhDidCreate threads the members it was dropping. pathMode is exposed
without the bare path shorthand, because sending both is an error and one way
to say it cannot be said twice. preRotationCount and addMediatorService use
!== undefined rather than a truthy test: 0 and false are the caller's
answers, not absence.

Guards

The four new vta/did-templates/* URIs join the admin guard in ci.yml
operator surface outside that list is unchecked. Verified after a build that
all four appear only in manager.js, that the seeds guard is untouched, that
manager.js is still a single chunk, and that background.js still has no
dynamic import().

Second commit: a pre-existing CI race, fixed en route

The first push went red on both Node versions, and it was not this change —
the identical failure took out main at db16fbb on 2026-09-14.

tests/task-surface-sync.mjs drove sync-task-surface.mjs against a fixture
SDK, let it write the real checked-in packages/core/task-surface.json, and
restored the saved copy in a finally. That is a race rather than a cleanup:
node --test runs test files in parallel processes, and
tests/task-surface.mjs reads that same file at module scope. Watching the file
while the sync test ran against the original code:

task counts observed:  256  1  3  2  0

— which is exactly the two shapes CI reported: task-surface.json looks truncated plus implements 0 of 3 canonical task families on one runner, and
SyntaxError: Unexpected end of JSON input on the other. One cause, two
symptoms, which is why it read like a corrupt checkout. The blob is
byte-identical in the tree; I checked the remote SHA against local before
suspecting the tests.

It reproduces only where the two files overlap — rarely on a many-core dev
machine, often on a two-core runner — so npm test stayed green locally through
every one of these failures.

sync-task-surface.mjs now takes the output path as an optional second
argument and the fixture points it inside its own temp directory, so no test
opens the checked-in snapshot for writing. With the fix the same observation
loop sees only 256. One assertion changed with it: "the real snapshot is
untouched after a failed sync"
was the only statement available while the
fixture shared that file; with a path of its own, a stopped sync leaves no
snapshot at all, which says it directly.

This bug is on main and will keep flaking other PRs until this merges. If
you would rather it landed separately it lifts out cleanly — it is the last
commit and touches only those two files.

Verification

tsc -b clean, 1452 tests pass / 0 fail (77 new across 7 files), build
clean. Also driven in a real Chrome tab against a shim — confirmed the template
defaults seeding Portable + pre-rotation 3, the render round-trip, the log
expansion, and that the key pickers exclude an x25519 key from the signing
list, a revoked key from both, and a key belonging to another context.

Pre-merge checklist

- [x] No new reqwest::Client::new() / bare fetch(); all clients have finite timeouts (R1.2) — no new fetch; every call goes through the console relay
- [x] No lock held across a network await (R1.3) — n/a, no locks
- [x] No local state committed before its remote effect, or the flow is resumable with an idempotency key (R2.1) — all writes are single Trust Tasks; the form's own state is cleared only after the agent answers
- [x] Every retry is bounded + backed off; non-idempotent ops are not blind-retried (R1.4) — no retries added
- [x] Accept/poll/listen loops survive transient errors (R1.5) — n/a, no loops
- [x] Acks/deletes happen only after durable handoff (R1.6) — n/a, inbound path untouched
- [x] New/changed wire types: camelCase, deny_unknown_fields where security-relevant, schema registered, all consumers (incl. JS) updated (R3.*) — no new wire types; existing spec members threaded with `===`-exact spelling, no compat folds
- [x] Config absence = most restrictive; fail-closed if enforcement can't start (R5.*) — a key's absent `contextId` is read as restrictive, never as a wildcard; an unreachable template registry does not fall back to a compiled copy
- [x] Logs/status claim only what was verified; background-job failures are surfaced (R6.*) — a missing log is a refusal to read, not "no history"; a failed server/key/template listing says so rather than rendering empty
- [x] "Process dies on the next line" answered for every mutation touched (R2.1) — a serverless mint keeps its `logEntry` on screen after the form resets, because it is the only copy of the thing that must be served
- [ ] Deviations from this guide flagged explicitly with rule numbers — none

The New DID form sent 3 of the 20 members `vta/webvh/dids/create/1.0`
accepts, and the DIDs pane showed `logEntryCount` with no way to read an
entry — a number with nothing behind it. Anything past a server id and
Portable meant leaving the console for the CLI.

The form now covers the surface, grouped by the decision each member
makes rather than by the payload's field order:

  - where it is published — server, domain, label, and the existing
    `PathPicker` with its live DID preview and the hosting server's own
    path rules
  - what it commits to — portable and pre-rotation as choices with their
    consequence beside them, since both are fixed in the first log entry
  - which keys it is built on — `signingKeyId` / `kaKeyId`, offering only
    active keys of the right type in the right context
  - what it advertises — `addMediatorService` / `addTspService`, plus a
    structured editor for `additionalServices`
  - what it is stamped from — templates from `didTemplateList`, a field
    per `requiredVars`, `defaults` seeding the controls above

`setPrimary` is now sent explicitly as `false`. The agent defaults it to
`true`, so the quiet path through the old form replaced the context's
identity on every mint — which anything resolving a context's DID to find
its keys, a room host at startup, would then follow.

Clicking a DID expands the row into `vta/webvh/dids/get` with
`includeLog`, fetched on open rather than with the listing: the log is the
DID's whole history and a listing that pulled every one would be every
history, to draw a table. A response carrying no log is drawn as a refusal
to read, never as an absence of history, and one unreadable line is one
unreadable entry rather than a blank panel.

New DID templates pane, full CRUD, built-ins inspect-only. Update is a
REPLACE, so the editor carries `optionalVars` and `defaults` it never
draws; without that a save cleared them while the agent reported success.
"Render the stored version" calls `didTemplateRender` so the operator sees
the agent's own substitution rather than this console's guess at it.

Three defects the new tests caught before they shipped:

  - `templateDefaults` coerced its hint with `Number()`, and `Number(null)`
    is 0 — a null `preRotationCount` hint would have silently switched
    pre-rotation off, the one setting that makes a stolen key
    unrecoverable
  - the agent answers a context-scoped template listing with global
    templates too, so asking both namespaces listed every global template
    twice, under one React key; `mergeTemplates` keys on scope AND name,
    which keeps two genuinely different templates of one name apart
  - the key pickers said "Reading…" with no context selected, when nothing
    had been asked for

Reuse of a key is offered but never quiet: two DIDs publishing one key are
provably one holder, so the form says so when a key is chosen and clears
the choice after a mint — leaving it set would build the next DID on the
same key by inaction.

`webvhDidCreate` threads the members it was dropping. `pathMode` is
exposed without the bare `path` shorthand, because sending both is an
error and one way to say it cannot be said twice. `preRotationCount` and
`addMediatorService` use `!== undefined` rather than a truthy test: 0 and
false are the caller's answers, not absence.

The four new `vta/did-templates/*` URIs join the admin guard in ci.yml —
operator surface outside that list is unchecked. Verified after a build
that all four appear only in `manager.js`, that the seeds guard is
untouched, and that the console is still one chunk.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…d-in one

`tests/task-surface-sync.mjs` drove `sync-task-surface.mjs` against a
fixture SDK, let it write the real `packages/core/task-surface.json`, and
restored the saved copy in a `finally`. That is a race, not a cleanup:
`node --test` runs test FILES in parallel processes, and
`tests/task-surface.mjs` reads that same file at module scope.

So the two raced, and the reader saw whatever the writer had got to:

    task counts observed while the sync test ran:  256  1  3  2  0

Which is exactly the two shapes CI reported — "task-surface.json looks
truncated" plus "implements 0 of 3 canonical task families" on one
runner, and `SyntaxError: Unexpected end of JSON input` on the other.

It reproduced only where the two files overlap, which on a developer
machine with cores to spare is rarely and on a two-core CI runner is
often: it took out main at db16fbb on 2026-09-14 and both jobs of two
consecutive runs on this branch, while `npm test` stayed green locally
through all of it. The file is byte-identical in the tree either way,
which is what made it read as a corrupt checkout rather than a test
writing where it shouldn't.

`sync-task-surface.mjs` now takes the output path as an optional second
argument, and the fixture points it inside its own temp directory. The
checked-in snapshot is never opened for writing by a test.

One assertion changed with it. "The real snapshot is untouched after a
failed sync" was the only statement available while the fixture shared
that file; with a path of its own, a stopped sync leaves no snapshot at
all, which says the same thing directly rather than resting on a run not
having reached its write.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 272a96c into main Sep 15, 2026
4 checks passed
@stormer78
stormer78 deleted the feat/did-create-and-log-console branch September 15, 2026 09:16
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