diff --git a/agents/megavolt/work/queue-236/BUILD.md b/agents/megavolt/work/queue-236/BUILD.md index 9c361089e..6765994cb 100644 --- a/agents/megavolt/work/queue-236/BUILD.md +++ b/agents/megavolt/work/queue-236/BUILD.md @@ -12,6 +12,10 @@ runs on Sage's gate. - Build carry: `agents/megavolt/work/436b/BUILD-CARRY.md` at 3330c901b (N9, N10). - Schema label 3j-3k per sage-2237. If 264a or 206 lands first, this rebases onto a new branch name and reruns the whole bus suite, the schema.test counts and the T46 lists. +- Rebases: onto 86f95bb27 after row 273 (sage-2403, no conflicts), then onto 0fa7bf421 after row 191 + (sage-2452, sage-2457). The 191 rebase took three unions in commit 1, 191's `processing` first and then the + grant options: the Broker constructor (broker.mjs:317), the runtime's Broker options (runtime.mjs:173) and + the cli case line in `scripts/mosaic` (191's `halt|resume|processing|` plus `grants`). ## What changed @@ -78,6 +82,11 @@ Each is a reading of the design where it left room. Dewey may rule otherwise. revoke check. A revoke during the whoami ends the grant `revoked`, not `grant-lost`, and the token-unknown branch neither holds the agent again nor writes `grant-token-unknown`, so no start names a file the revoke unlinked. +21. sage-2453, ruling (a) on megavolt-0123: 191's `#processingGate` refuses every agent verb off its read list + while processing is halted. `grants.status` is a read like `hosts.status`, and the kill-switch plan + (`docs/plans/2026-10-20_kill-switch.md`:16) says "Reads and the human's own verbs, including the clear, + still work", so it joins the list. This touches row 191's list (one entry). An agent's `grants.revoke` + stays refused while halted, as `hosts.revoke` does. ## Named for DEFERRED (r2, Dewey N1-N4) @@ -135,3 +144,6 @@ r2 tests, each red on 8bf442be9 by reading: - N5 (r3, sage-2388): "X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not held" and "X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost". On 77dd43c9e they end `grant-token-unknown` (the agent back in tokenUnknown) and `grant-lost`. +- Choice 21 (sage-2453): "kill switch (plan :16): while halted the chief still reads grants.status, and an + agent grants.revoke refuses". M38 drops `'grants.status'` from the read list; the chief's status then + refuses `processing-halted` and the test fails. diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs index 31069a41b..1a8d7bb23 100644 --- a/packages/bus/src/broker.mjs +++ b/packages/bus/src/broker.mjs @@ -419,7 +419,7 @@ export class Broker { if (s.human || s.reader || s.tester) return; if (!['inbox', 'agents', 'tasks', 'trail', 'decisions', 'replies', 'messages', 'state', 'usage', 'requirements', 'spec.read', 'project.list', 'project.files', 'project.file', 'library.file', 'library.status', - 'thread.list', 'thread.messages', 'names', 'instances', 'routes', 'hosts.status', + 'thread.list', 'thread.messages', 'names', 'instances', 'routes', 'hosts.status', 'grants.status', 'processing.status', 'role.release', 'usage.end'].includes(verb)) this.#processingWork(); } reconcileProcessing() { this.#processingMirror(this.#processingState()); } diff --git a/packages/bus/tests/gitea-grant.test.mjs b/packages/bus/tests/gitea-grant.test.mjs index 067be95a3..2834919fe 100644 --- a/packages/bus/tests/gitea-grant.test.mjs +++ b/packages/bus/tests/gitea-grant.test.mjs @@ -88,6 +88,7 @@ async function setup(t, opts = {}) { secretCheck: (value) => credentials.assertClean(value), giteaGrant: grantApi, giteaKeys: keys, + processing: opts.processing, credentialVerbs: (id, verb, args, actor) => { calls.push([verb, actor]); if (verb === 'credentials.revoke' || verb === 'credentials.delete') { @@ -895,6 +896,19 @@ test('X-G11: run refuses every caller but the human, an unapproved, a spent and assert.deepEqual(f.grantEvents().filter((e) => 'decision' in e).map((e) => e.operation), ['grant.gitea']); }); +test('kill switch (plan :16): while halted the chief still reads grants.status, and an agent grants.revoke refuses', async (t) => { + const control = { halted: false, version: 1, generation: randomUUID() }; + const f = await setup(t, { processing: { read: () => ({ ...control }) } }); + await f.approve(); + control.halted = true; + const status = await f.call('human', 'grants.status', { service: 'gitea' }); + assert.deepEqual(await f.call('chief', 'grants.status', { service: 'gitea' }), status); + // The gate throws before dispatch, as in processing.test.mjs, so the call is wrapped. + await rejects(async () => f.call('pm', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'r' }), 'processing-halted'); + assert.equal(f.record().agent, 'coder'); + assert.equal(f.state(), 'valid'); +}); + test('X-G11: a revoke while the executor runs aborts it and the grant ends revoked', async (t) => { const f = await setup(t); let release;