// Row 298 (#1804, Q228): the chief of staff's grant scope at the broker. The human's grants.scope and // grants.scope.set (sage-3594 Q3), the in-scope close via 'scope' with grant.scope.used (Q4), the // spend recheck's scope-changed (Q5), and the out-of-scope raise that still waits for the human. // Gitea grants run against the fake Gitea in fake-gitea-api.mjs on 127.0.0.1; host grants stop at the // spend, as no host key store is configured, and nothing connects to a host. Every token is a fake the // fake made and none is printed. The organization writer is a fake here; the real one is // packages/organization/tests/grant-scope.test.mjs. import test from 'node:test'; import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs'; import { Credentials } from '../src/credentials.mjs'; import { giteaGrant, giteaKeys } from '../src/gitea-grant.mjs'; import { hostGrant, grantKeys, fingerprint } from '../src/host-grant.mjs'; import { Store } from '../src/store.mjs'; import { checkGrantScope, grantScopeOf } from '../../organization/src/vocabulary.mjs'; import { fakeGiteaApi } from './fake-gitea-api.mjs'; import { hostRecord, grantDecision } from './host-fixtures.mjs'; const USER = 'demo-coder'; const ENTRY = { address: '192.0.2.10', port: 22, user: 'jarvis', hostKeys: ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHostKeyForTests'] }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const sha256 = (x) => createHash('sha256').update(x).digest('hex'); const dayFrom = (n) => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10); const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }]; const none = { authority: { withinRole: ['message.send'], crossRole: [] } }; const DEFINITIONS = { 'chief-of-staff': { roleVersion: 2, credentials: [] }, pm: { roleVersion: 2, credentials: [] }, coder: { roleVersion: 2, credentials: [{ service: 'gitea' }] }, ops: { roleVersion: 2, credentials: [{ service: 'host' }] }, }; const GITEA = { gitea: { agents: ['coder'], projects: ['app'] } }; const HOST = { host: { agents: ['ops'], hosts: ['web1'], commands: [['systemctl', 'status', 'nginx']] } }; const REVISION = 'a'.repeat(64); async function until(fn, ms = 10000) { const end = Date.now() + ms; while (!fn()) { if (Date.now() > end) throw new Error('timed out'); await sleep(20); } } const rejects = (p, code) => assert.rejects(Promise.resolve().then(p), (e) => (assert.equal(e.code, code), true)); // opts.scope: the value grantScope answers, or a function called at each read. async function setup(t, opts = {}) { const dir = mkdtempSync(join(tmpdir(), 'g298-')); t.after(() => rmSync(dir, { recursive: true, force: true })); const gitea = await fakeGiteaApi(t, join(dir, 'tls')); gitea.addUser('acme'); gitea.addRepo('acme', 'app'); gitea.addRepo('acme', 'lib'); const configDir = join(dir, 'cfg'); mkdirSync(configDir, { mode: 0o700 }); const storeRoot = join(configDir, 'secrets', 'agents'); mkdirSync(join(dir, 'data'), { mode: 0o700 }); const store = new Store(join(dir, 'data')); const credentials = new Credentials({ references: {}, storeRoot, dataRoot: join(dir, 'data') }); t.after(() => { credentials.close(); store.close(); }); const business = { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles: { chief: { definition: 'chief-of-staff', ...none }, // A second chief-of-staff instance, as an instances.set or the tester could add. It isn't launch.by. chief2: { definition: 'chief-of-staff', ...none }, pm: { definition: 'pm', ...none }, coder: { definition: 'coder', ...none, identity: { name: 'Coder', email: 'coder@example.test', gitea: { url: gitea.url, user: USER } }, publish: ['app', 'lib'] }, ops: { definition: 'ops', ...none }, }, definitions: structuredClone(DEFINITIONS), projects: Object.fromEntries(['app', 'lib'].map((p) => [p, { remote: `${gitea.url}/acme/${p}` }])), launch: { by: 'chief', instances: ['pm', 'coder', 'ops'] }, vars: { 'bus.tester': true }, }; const grantApi = giteaGrant({ configDir, ca: gitea.ca, timeoutMs: 5000 }); grantApi.writeAdmin('demo', gitea.adminToken, { url: gitea.url, account: 'mosaic-grant', rotateBy: dayFrom(60), setAt: '2026-10-21T00:00:00.000Z' }); // The fake organization writer: the scope in a box, checked as the real writer checks it. const box = { value: opts.scope }; const orgCalls = []; const organization = (id, verb, args, actor) => { orgCalls.push([verb, actor]); if (verb === 'grants.scope') return { business: id, revision: REVISION, ...grantScopeOf(box.value) }; if (verb === 'grants.scope.set') { const scope = checkGrantScope(args.scope); if (scope === null) throw Object.assign(new Error('scope is not a grant scope'), { code: 'invalid-request' }); const old = grantScopeOf(box.value).scope; box.value = scope; return { business: id, scope, old, transaction: 'tx-298' }; } return { revision: REVISION }; }; // Keep the real key generation/store/records; replace only remote host calls. const hostLines = new Set(); const hostApi = { ...hostGrant({ configDir, runRoot: join(configDir, 'secrets', 'run') }), keyscan: async () => ENTRY.hostKeys, install: async (_business, _host, _record, tag) => { hostLines.add(tag); }, verify: async () => {}, remove: async (_business, _host, _record, tag) => { hostLines.delete(tag); }, }; const broker = new Broker({ store, businesses: { demo: business }, secretCheck: (value) => credentials.assertClean(value), giteaGrant: grantApi, giteaKeys: giteaKeys({ credentials, storeRoot }), hostGrant: hostApi, hostKeys: opts.hostKeys ? grantKeys({ credentials, storeRoot }) : null, organization, grantScope: () => ({ value: typeof box.value === 'function' ? box.value() : box.value, revision: Object.hasOwn(opts, 'revision') ? opts.revision : REVISION }), }); const caps = {}; for (const role of ['chief', 'chief2', 'pm', 'ops']) { caps[role] = broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi', address: role }); broker.request(caps[role], { verb: 'role.claim' }); } caps.human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); caps.tester = broker.bindTester({ business: 'demo' }); caps.reader = broker.bindReader({ business: 'demo' }); caps.console = broker.bindConsole({ business: 'demo', human: 'jason', subject: { provider: 'p', iss: 'https://idp.example.test', sub: 's' } }); hostRecord(store, { host: 'web1', entry: ENTRY }); const call = (who, verb, args = {}) => broker.request(caps[who], { verb, args }); const raiseGitea = (over = {}, who = 'chief') => call(who, 'decision.raise', { action: 'credential.mint', question: 'Grant coder Gitea?', options, recommendation: 'yes', blocking: false, grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30), ...over }, }); const raiseHost = (over = {}, who = 'chief') => call(who, 'decision.raise', { action: 'credential.mint', question: 'Grant ops web1?', options, recommendation: 'yes', blocking: false, grant: { method: 'host', agent: 'ops', host: 'web1', commands: [['systemctl', 'status', 'nginx']], rotateBy: dayFrom(30), ...over }, }); const grantEvents = () => store.all(`SELECT body FROM events WHERE business='demo' AND ${GRANT_EVIDENCE} ORDER BY seq`).map((e) => JSON.parse(e.body)); const ended = (decision) => grantEvents().findLast((e) => e.grantDecision === decision && ['grant.applied', 'grant.failed'].includes(e.action)); const closes = (decision) => store.all('SELECT * FROM decision_events WHERE decision=? ORDER BY seq', decision); const spent = (decision) => grantEvents().filter((e) => e.decision === decision && e.operation?.startsWith('grant.')).map((e) => e.operation); const record = () => store.get("SELECT * FROM gitea_grants WHERE business='demo' AND agent='coder'"); return { store, broker, caps, box, orgCalls, call, raiseGitea, raiseHost, grantEvents, ended, closes, spent, record, gitea, hostLines }; } test('row 298 (Q3): only the human at the cli reads or sets the scope; agents, the tester, a reader and the Console refuse', async (t) => { const f = await setup(t); for (const verb of ['grants.scope', 'grants.scope.set']) { const args = verb === 'grants.scope' ? {} : { revision: REVISION, scope: GITEA }; for (const who of ['chief', 'chief2', 'pm', 'tester']) await rejects(() => f.call(who, verb, args), 'human-required'); await rejects(() => f.call('reader', verb, args), 'read-only'); await rejects(() => f.call('console', verb, args), 'human-required'); } assert.deepEqual(f.orgCalls, []); assert.equal(f.box.value, undefined); assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.scope'), []); }); test('row 298 (Q1): organization.set and limits.set refuse a grantScope key, and nothing is written', async (t) => { const f = await setup(t); await rejects(() => f.call('human', 'limits.set', { revision: REVISION, grantScope: GITEA }), 'invalid-request'); await rejects(() => f.call('chief', 'organization.set', { revision: REVISION, grantScope: GITEA }), 'invalid-request'); assert.deepEqual(f.orgCalls, []); assert.equal(f.box.value, undefined); }); test('row 298 (Q3): the human verb writes the scope and a grant.scope record with the human proof and both sha256', async (t) => { const f = await setup(t); assert.deepEqual(await f.call('human', 'grants.scope'), { business: 'demo', revision: REVISION, scope: {}, fallback: false }); const r = await f.call('human', 'grants.scope.set', { revision: REVISION, scope: GITEA }); assert.deepEqual(r.scope, GITEA); assert.deepEqual(f.orgCalls, [['grants.scope', 'jason'], ['grants.scope.set', 'jason']]); const changes = f.grantEvents().filter((e) => e.action === 'grant.scope'); assert.deepEqual(changes, [{ action: 'grant.scope', by: 'jason', via: 'cli', outsideAgent: true, oldSha256: sha256(JSON.stringify({})), newSha256: sha256(JSON.stringify(GITEA)), transaction: 'tx-298', }]); await rejects(() => f.call('human', 'grants.scope.set', { revision: REVISION, scope: { gitea: {} } }), 'invalid-request'); await rejects(() => f.call('human', 'grants.scope.set', { revision: REVISION }), 'invalid-request'); await rejects(() => f.call('human', 'grants.scope.set', { revision: REVISION, scope: {}, extra: 1 }), 'invalid-request'); assert.equal(f.grantEvents().filter((e) => e.action === 'grant.scope').length, 1); }); test('row 298 (Q4): an in-scope Gitea grant is issued with no inbox item, logged with 236 records, and revocable', async (t) => { const f = await setup(t, { scope: GITEA }); const d = await f.raiseGitea(); const [close] = f.closes(d.id); assert.deepEqual([close.op, close.by, close.choice, close.via], ['resolved', 'grant-scope', 'yes', 'scope']); assert.ok(f.grantEvents().some(e => e.action === 'grant.scope.used' && e.grantDecision === d.id), 'scope close must have protected evidence'); const inbox = await f.call('human', 'inbox'); assert.ok(!JSON.stringify(inbox).includes(d.id), 'the decision is not in the human inbox'); await until(() => f.ended(d.id)); await f.broker.giteaIdle(); assert.equal(f.ended(d.id).action, 'grant.applied'); const used = f.grantEvents().find((e) => e.action === 'grant.scope.used'); assert.deepEqual(used, { action: 'grant.scope.used', grantDecision: d.id, decisionSeq: close.seq, method: 'gitea', agent: 'coder', scope: GITEA, revision: REVISION, scopeSha256: sha256(JSON.stringify(GITEA)), }); assert.deepEqual(f.spent(d.id), ['grant.gitea']); assert.equal(f.record().decision, d.id); // The spend is row 236's: a second start finds it consumed. assert.equal(await f.broker.runGiteaGrant('demo', d.id), 'decision-consumed'); const out = await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'test' }); assert.deepEqual(out.serviceLeft, []); assert.ok(f.grantEvents().some((e) => e.action === 'grant.revoked' && e.agent === 'coder')); assert.equal(f.record(), undefined); }); test('row 298: an out-of-scope Gitea grant waits for the human, whose Approve still issues it', async (t) => { const f = await setup(t, { scope: GITEA }); const d = await f.raiseGitea({ target: { projects: ['app', 'lib'] } }); assert.deepEqual(f.closes(d.id), []); assert.ok(JSON.stringify(await f.call('human', 'inbox')).includes(d.id)); assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.scope.used'), []); await f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' }); await until(() => f.ended(d.id)); await f.broker.giteaIdle(); assert.equal(f.ended(d.id).action, 'grant.applied'); }); test('row 298: with no scope, a bad scope or an unreadable file, an in-scope-looking grant waits', async (t) => { const bad = { gitea: { agents: ['coder'], projects: ['app'], note: 'not a key' } }; for (const scope of [undefined, bad, () => { throw new Error('unreadable'); }]) { const f = await setup(t, { scope }); const d = await f.raiseGitea(); assert.deepEqual(f.closes(d.id), [], String(scope)); assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.scope.used'), []); } }); test('row 298 (Q6): a chief-of-staff instance that is not launch.by never raises in scope', async (t) => { const f = await setup(t, { scope: GITEA }); const d = await f.raiseGitea({}, 'chief2'); assert.deepEqual(f.closes(d.id), []); }); test('row 298 (Q5): a scope narrowed between the raise and the spend ends the grant scope-changed', async (t) => { let reads = 0; const f = await setup(t, { scope: () => (reads++ === 0 ? GITEA : {}) }); const d = await f.raiseGitea(); assert.equal(f.closes(d.id)[0].via, 'scope'); await until(() => f.ended(d.id)); await f.broker.giteaIdle(); assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'scope-changed']); assert.equal(f.record(), undefined); assert.deepEqual(f.gitea.tokens(USER) ?? [], []); assert.deepEqual(f.spent(d.id), ['grant.gitea']); assert.equal(await f.broker.runGiteaGrant('demo', d.id), 'decision-consumed'); }); test('row 298 (Q2): an in-scope host grant is spent with no Approve; ls never covers lsblk, nor a scope command extra arguments', async (t) => { const scope = { host: { agents: ['ops'], hosts: ['web1'], commands: [['ls'], ['systemctl', 'status', 'nginx']] } }; const f = await setup(t, { scope }); const d = await f.raiseHost(); assert.equal(f.closes(d.id)[0].via, 'scope'); await until(() => f.ended(d.id)); // The spend is row 235's. No host key store is configured, so the recheck after it ends here. assert.deepEqual(f.spent(d.id), ['grant.host']); assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'host-not-configured']); for (const commands of [[['lsblk']], [['ls', '-l']], [['systemctl', 'restart']]]) { const w = await f.raiseHost({ commands }); assert.deepEqual(f.closes(w.id), [], JSON.stringify(commands)); } }); test('row 298 (Q5): a host scope narrowed before the spend ends scope-changed', async (t) => { let reads = 0; const f = await setup(t, { scope: () => (reads++ === 0 ? HOST : {}) }); const d = await f.raiseHost(); assert.equal(f.closes(d.id)[0].via, 'scope'); await until(() => f.ended(d.id)); assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'scope-changed']); assert.deepEqual(f.spent(d.id), ['grant.host']); }); // Break caught: accepting scope closes with no evidence or unbound evidence. test('row 298 Q4: missing scope evidence or a bad scope digest cannot authorize execution', async (t) => { const f = await setup(t, { scope: GITEA }); const good = { scope: GITEA, scopeSha256: sha256(JSON.stringify(GITEA)), revision: REVISION }; for (const evidence of [ null, { ...good, scopeSha256: '0'.repeat(64) }, { ...good, revision: null }, { ...good, scope: { gitea: 'all' } }, { ...good, agent: 'other' }, { ...good, method: 'host' }, { ...good, decisionSeq: 999999 }, ]) { const grant = { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) }; const id = grantDecision(f.store, { grant, close: { op: 'resolved', by: 'grant-scope', choice: 'yes', via: 'scope' } }); if (evidence) writeGrantEvent(f.store, new Date().toISOString(), 'demo', { action: 'grant.scope.used', grantDecision: id, decisionSeq: f.closes(id)[0].seq, method: 'gitea', agent: 'coder', ...evidence, }, id); assert.equal(await f.broker.runGiteaGrant('demo', id), 'not-authorized'); assert.deepEqual(f.spent(id), []); assert.equal(f.record(), undefined); } // Positive control: the same fixture with valid scope evidence reaches the // next grant check. Its intentionally absent definition binding refuses there. const id = grantDecision(f.store, { grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) }, close: { op: 'resolved', by: 'grant-scope', choice: 'yes', via: 'scope' }, }); writeGrantEvent(f.store, new Date().toISOString(), 'demo', { action: 'grant.scope.used', grantDecision: id, decisionSeq: f.closes(id)[0].seq, method: 'gitea', agent: 'coder', ...good, }, id); assert.equal(await f.broker.runGiteaGrant('demo', id), 'decision-mismatch'); assert.deepEqual(f.spent(id), []); assert.equal(f.record(), undefined); }); // Break caught: host grants bypassing records or losing the ordinary revoke path. test('row 298 T4/T8: an in-scope host grant issues, records and revokes through the existing executor', async (t) => { const f = await setup(t, { scope: HOST, hostKeys: true }); const d = await f.raiseHost(); assert.equal(f.closes(d.id)[0].via, 'scope'); assert.ok(!JSON.stringify(await f.call('human', 'inbox')).includes(d.id)); await until(() => f.ended(d.id)); await f.broker.grantsIdle(); assert.equal(f.ended(d.id).action, 'grant.applied'); assert.equal(f.store.get("SELECT decision FROM host_grants WHERE agent='ops'").decision, d.id); assert.equal(f.hostLines.size, 1); await f.call('human', 'grants.scope.set', { revision: REVISION, scope: {} }); // Historical approval still passes #hostAccess after scope is narrowed; // only the absent command executor refuses this call, before any sudo decision. await rejects(() => f.broker.requestHost(f.caps.ops, { verb: 'host.run', args: { host: 'web1', argv: ['systemctl', 'status', 'nginx'] } }, null), 'host-not-configured'); await f.call('human', 'hosts.revoke', { agent: 'ops', host: 'web1', reason: 'test' }); assert.equal(f.store.get("SELECT decision FROM host_grants WHERE agent='ops'"), undefined); assert.equal(f.hostLines.size, 0); assert.ok(f.grantEvents().some(e => e.action === 'grant.revoked' && e.agent === 'ops')); }); // Break caught: a new host enrollment entering the scope shortcut. test('row 298 T6: host enrollment stays open for human Approve even with both methods in scope', async (t) => { const f = await setup(t, { scope: { ...GITEA, ...HOST } }); const d = await f.call('chief', 'decision.raise', { action: 'credential.mint', question: 'Enroll host?', options, recommendation: 'yes', blocking: false, grant: { method: 'host-enroll', host: 'web2', address: ENTRY.address, port: ENTRY.port, user: ENTRY.user, sudo: true, fingerprints: ENTRY.hostKeys.map(fingerprint) }, }); assert.deepEqual(f.closes(d.id), []); assert.ok(JSON.stringify(await f.call('human', 'inbox')).includes(d.id)); assert.deepEqual(f.grantEvents().filter(e => e.action === 'grant.scope.used'), []); }); // Break caught: using a scope without a verifiable organization revision. test('row 298: scope without an organization revision never closes automatically', async (t) => { const f = await setup(t, { scope: GITEA, revision: null }); const d = await f.raiseGitea(); assert.deepEqual(f.closes(d.id), []); assert.deepEqual(f.spent(d.id), []); });