bus: row 236, grants.status is a read under the kill switch, as hosts.status is (sage-2453, #1742)

Row 191's #processingGate refuses agent verbs off its read list while
processing is halted. grants.status joins hosts.status on that list, per
the kill-switch plan (2026-10-20_kill-switch.md:16): reads still work.
An agent's grants.revoke stays refused while halted, as hosts.revoke is.
This commit is contained in:
jason.woltje committed 2026-10-21 23:50:17 -05:00
1 parent c8e90f5f03
commit 08840f3c15
3 files changed
+27 -1

No files matched your search

+12
View File
@@ -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.
+1 -1
View File
@@ -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()); }
+14
View File
@@ -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;