Manifest pins 9 files on base 4100fa68; candidate source is not committed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
7.8 KiB
Row 60 (#1549): bus read, credential status on the broker, round 1
Filbert, 2026-10-11. Brief: docs/plans/2026-10-10_design-implementation.md,
"Bus read: credential status on the broker" (blob a360554c). Darkwing and
Dewey review. Sage commits after the gate; nothing is committed, staged or
pushed yet.
Base 4100fa68. build.patch applies there with git apply --index;
candidate-manifest.sha256 pins all 9 files after the patch. HEAD is now
c949f9ef, with docs and queue commits only; no file under packages/
changed. I applied the patch there and all 9 manifest lines check OK.
What the read is
A new socket verb, credentials. No args. Human and reader sessions only; a
role session gets human-required, as with decisions. It returns one row
per credential the running host loaded for the session's business:
{ service, account, role, dateKind, date, state }
service:giteaorvikunja.account: the role'stracker.botfor Vikunja, the sync identity'sbotfor thebusiness/@syncrow,nullfor Gitea (the business file names no Gitea account).role: the instance, ortracker syncfor@sync.dateKind,date:rotateByfor Gitea,expiresfor Vikunja.state:Credentials.status()against the host clock at the request:valid,expiring(under 7 days),expired, orrotation-duefor Gitea.
The order is status() load order: role instances, then the sync identity.
The row shape is the one the WebUI's Business view already uses
(credential() in packages/webui/src/reads.mjs), so a page can use either.
The difference is the source. The Business view reads the business file: what
the host should load. This read asks the host: what it did load, checked
against its own clock. A token file rotated after startup isn't reread until
restart, and this read is how to see what the host is running with.
How the broker gets it
The broker gets one new constructor option, credentialStatus, a function.
The runtime passes () => credentials.status(). The broker never gets the
Credentials object, so it can't reach use(). The default is () => [],
so a broker built without it (every existing test) lists none.
status() already returns metadata only (instance, service, date,
state). The broker filters it to business + '/', drops instance, and
adds account, role and dateKind from the business config. The result
also goes through the known-token check every read goes through.
The existing trusted launch op credentialStatus(business, role) in the
runtime is unchanged.
WebUI
GET /api/bus/credentials is a new bus route: credentials joins
BUS_VERBS in serve.mjs, and views.mjs gains credentials(). It runs
through the same human transport as the other bus reads, so the same 403,
503 and 502 mapping applies. GET only.
No page uses it yet. The brief names the endpoint and its tests, not a view change. The Business view still shows the file's metadata. See question 1.
Files
packages/bus/src/broker.mjs: thecredentialStatusoption, thecredentialscase, andcredentialsin the reader allowlist.packages/bus/src/runtime.mjs: passescredentialStatus.packages/bus/src/views.mjs:credentials().packages/bus/tests/credential-status.test.mjs(new, 4 tests):- Human and reader sessions get the exact rows and keys for
demo, with every state (valid, expiring, expired, rotation-due), a file ref and an env ref, and the sync row. A second business,demox, shares thedemoprefix without the slash and gets only its own row. - A role session gets
human-required; any argument isinvalid-request. - The status is read on each request (a changed state shows on the next one); a broker without the option lists none.
- The brief's acceptance test: the real broker process (
process.mjs, forked) boots on a seeded config with five token files and one env token. A reader callscredentials, a role session is refused, and the process closes. The rows, the host log (stdout and stderr) and the store files (bus.sqlite*) contain none of the six tokens, the token file paths, their directory, or the env variable name. The store check also confirms the refusal was written, so it read the right files.
- Human and reader sessions get the exact rows and keys for
packages/bus/README.md: a verb table row and acredentialsparagraph under "Human and reader paths".packages/webui/src/serve.mjs,packages/webui/src/bus.mjs: the verb and comments.packages/webui/tests/bus.test.mjs: the route returns the reader's rows only (one business of two),{ rows, at },no-store; the route is GET-only;busReadersends{verb:'credentials', args:{}}.packages/webui/README.md: the route, and the exposure note now names the credential status.
Mutants
16 mutants (run.mjs, kept in my scratch). Each edits one file, runs the
tests that own it, and restores the file:
- broker, runtime: the bus suite;
- views, serve:
packages/webui/tests/bus.test.mjs.
All 16 are killed, and the diff is unchanged afterwards:
- broker, 12: role session allowed, reader refused, prefix without the
slash, no business filter, arguments accepted, a Gitea account from
tracker.bot, the sync accountnull,@syncas the role, a fixeddateKind,instanceleft in the row, a fixed state, the status read once at construction. - runtime, 1: no
credentialStatuspassed (the process test). - views, 2: the wrong verb, an extra argument.
- serve, 1:
credentialsmissing fromBUS_VERBS.
Gate (my run on the candidate)
Worktree at 4100fa68 with the patch, run in sequence with TMPDIR in my
scratch and DOCKER_HOST pointed at a missing socket:
| Suite | Result |
|---|---|
| packages/bus | 84/0 (80 before; 4 new) |
| packages/control-board | 124/0 |
| packages/webui | 41/0 (40 before; 1 new), the browser tests included |
| packages/cli | 112/0, since it starts the runtime |
| scripts/test-*.sh | all nine exit 0: auth 15, conductor 17, config 24, discord 66 (see below), extension-package 18, foundation 44, queue 27, release 4, task 26 |
test-discord failed once in the sequential run, 65/1: the engine test
"when pi has not started a timed-out turn by the end of the abort grace…"
(packages/discord, a timing assertion). Load average was about 20 at the
time. This patch touches nothing under packages/discord. The rerun of the
whole script passed 66/0. Sage's gate rerun will show whether it recurs.
test-release and test-task skipped their Docker cases without a daemon;
Sage's gate rerun covers those. No test calls a model. Logs are in my
scratch (r60/gate/), not kept in the repository.
The mutants ran before one comment-only change: I rewrapped the header
comment of packages/webui/src/bus.mjs. The gate ran after it.
Overlap
- Row 59 (#1548, in review) edits the
agentsbranch ofbroker.mjs, adds#agentStateafter#dispatch, and replaces theinbox, tasks, agentsrow of the bus README verb table. With row 59's packet applied on4100fa68first, plaingit applyof this patch refuses onpackages/bus/README.md(both add a line after thetrailrow);git apply --3waymerges every file cleanly, with thecredentialsrow aftertrailand the new case beforedefault:. On that combined tree, the bus suite and the WebUI bus and browser tests pass, 100/0. - Row 58 (Darkwing) edits the
tasksbranch, which this patch doesn't touch.
Questions for reviewers
- Should the Business view use this read, or show it next to the file's metadata (for example, flag a difference)? I left the view alone because the brief lists only the endpoint, and the Business view works without a bus host.
- The bus README verb table has no
decisionsrow (row 57 added the verb and a paragraph). I added acredentialsrow only. Should a follow-up add the missing row?