// C2-03b (#1791, design 7.2): the credential the library fetch runs with. The chief's grant store file // once a chief has Gitea grant evidence, slot library-gitea before that, and a refusal otherwise. // C2-21 replaces sage-4100's read fixture seed: T7/T9 raise the real chief library grant, the human // resolves yes and the executor mints it. Revocation and fire keep using the same real verbs. // The choice function's other checks (listed, a fire under way, held, two chiefs, another project) // use fixtures: the evidence and the gitea_grants row go in by SQL, as gitea-grant.test.mjs does for // decisions. Tokens are made up or the fake's, and only ever compared. import test from 'node:test'; import { hireServiceFixture, fixtureConsole, approveFixtureFire } from './hire-fixtures.mjs'; import assert from 'node:assert/strict'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { randomBytes } from 'node:crypto'; import { Store } from '../src/store.mjs'; import { Broker, GRANT_EVIDENCE, LIBRARY_FETCH_FIX, writeGrantEvent } from '../src/broker.mjs'; import { Credentials } from '../src/credentials.mjs'; import { giteaGrant, giteaKeys, giteaStart, giteaReference } from '../src/gitea-grant.mjs'; import { startBroker } from '../src/runtime.mjs'; import { gitEnv } from '../../seat/src/checkout.mjs'; import { mappingEntry, repo } from '../../library/tests/helpers.mjs'; import * as library from '../../library/src/index.mjs'; import { fakeGiteaApi } from './fake-gitea-api.mjs'; import { grantDecision } from './host-fixtures.mjs'; import { systemFor } from '../../organization/tests/helpers.mjs'; const AT = '2026-10-22T00:00:00.000Z'; const REV = 'b'.repeat(64); const SLOT_TOKEN = 'fx286-slot-q7-r6-a9'; const role = { authority: { withinRole: [], crossRole: [] } }; const none = { authority: { withinRole: ['message.send'], crossRole: [] } }; const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }]; const chiefAuthority = JSON.parse(readFileSync(fileURLToPath(new URL('../../../roles/chief-of-staff.json', import.meta.url)), 'utf8')).authority; const SWITCH = { version: 1, halted: false, generation: '0b6e7f6e-4c1d-4a8e-9a51-6d2f3c4b5a69' }; const dayFrom = (n) => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function until(fn, ms = 10000) { const end = Date.now() + ms; while (!fn()) { if (Date.now() > end) throw new Error('timed out'); await sleep(10); } } const makeBusiness = ({ root, chief = true, instances = null } = {}) => ({ id: 'acme', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, // sage-4097 / mos-1584: the chief's declared read credential; no-chief negative fixture stays absent. roles: { cto: { definition: 'cto', authority: { withinRole: ['decision.resolve.technical'], crossRole: [] } }, pm: { definition: 'pm', authority: { withinRole: ['decision.resolve.delivery'], crossRole: [] } }, ...(chief ? { chief: { definition: 'chief-of-staff', ...role, credentials: { gitea: giteaReference(join(root, 'secrets', 'agents'), 'acme', 'chief', '2099-01-01') } } } : {}), coder: { definition: 'coder', ...role } }, projects: { books: { ...mappingEntry(), user: 'books-bot' } }, ...(instances ? { launch: { instances } } : {}), }); // A Gitea grant for `agent` as row 236 leaves it: the decision, the user and grant rows, and // grant.applied. `project` is the grant's target; the evidence check names no project. function grant(store, agent, { user = agent + '-bot', project = 'books', row = true } = {}) { const decision = grantDecision(store, { business: 'acme', grant: { method: 'gitea', agent, target: { projects: [project] }, rotateBy: '2099-01-01' } }); writeGrantEvent(store, AT, 'acme', { operation: 'grant.gitea', decision }, decision); writeGrantEvent(store, AT, 'acme', { action: 'grant.applied', method: 'gitea', agent, grantDecision: decision, user, projects: [project] }, decision); if (!row) return decision; store.run('INSERT INTO gitea_users(business,agent,user,id,nonce,decision) VALUES(?,?,?,?,?,?)', 'acme', agent, user, 7, randomBytes(8).toString('hex'), decision); store.run('INSERT INTO gitea_grants(business,agent,user,token_id,token_name,projects,rotate_by,decision) VALUES(?,?,?,?,?,?,?,?)', 'acme', agent, user, 11, 'mosaic-' + agent, JSON.stringify([project]), '2099-01-01', decision); return decision; } function world(t, options) { const root = mkdtempSync(join(tmpdir(), 'bus-library-credential-')), store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const business = makeBusiness({ ...options, root }); const held = new Set(); const broker = new Broker({hires:hireServiceFixture, store, businesses: { acme: business }, giteaKeys: { held: (b, a) => held.has(b + '/' + a) } }); return { store, business, broker, held, choose: () => broker.libraryFetchCredential('acme') }; } // The business for the real paths. `boss` is launch.by: instance.fire is the launch.by role's verb and // refuses its own name (organization-instance-referenced), so the fired chief is the one listed // chief-of-staff instance, fired by boss. The real read grant targets the library project books. function verbDoc(url, root) { return { id: 'acme', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: { boss: { definition: 'pm', authority: chiefAuthority }, // sage-4097 / mos-1584: fixture projection carries the chief's own Gitea store reference. chief: { definition: 'chief-of-staff', ...none, credentials: { gitea: giteaReference(join(root, 'secrets', 'agents'), 'acme', 'chief', '2099-01-01') }, identity: { name: 'Chief', email: 'chief@example.test', gitea: { url, user: 'acme-chief' } }, publish: ['books'] }, coder: { definition: 'coder', ...none }, }, definitions: { 'chief-of-staff': { roleVersion: 2, credentials: [{ service: 'gitea', scopes: ['read:repository', 'read:user'] }] }, pm: { roleVersion: 2, credentials: [] }, coder: { roleVersion: 2, credentials: [] }, }, // sage-4054 / mos-1272: runtime validates every code project's absolute checkout root. projects: { app: { root: join(root, 'app-checkout'), remote: `${url}/acme/app` }, books: { ...mappingEntry(), remote: `${url}/acme/books`, user: 'books-bot' } }, launch: { by: 'boss', instances: ['chief', 'coder'] }, }; } // A bare broker on the real grant paths, as gitea-grant.test.mjs and fire-revokes-grants.test.mjs run // it. A second one on the same data root and store picks the grant up as runtime.mjs does: giteaStart, // then Credentials with its references and held list. The admin slot sits outside storeRoot's config // dir, so a runtime started on that dir finds no admin slot. async function verbWorld(t, opts = {}) { const dir = mkdtempSync(join(tmpdir(), 'bus-library-verbs-')); t.after(() => rmSync(dir, { recursive: true, force: true })); let gitea = opts.gitea; if (!gitea) { gitea = await fakeGiteaApi(t, join(dir, 'tls')); gitea.addUser('acme'); gitea.addRepo('acme', 'app'); gitea.addRepo('acme', 'books'); } const adminDir = join(dir, 'admin'); mkdirSync(adminDir, { mode: 0o700 }); const dataRoot = opts.dataRoot ?? join(dir, 'data'); mkdirSync(dataRoot, { recursive: true, mode: 0o700 }); const storeRoot = opts.storeRoot ?? join(dir, 'secrets', 'agents'); const doc = opts.doc ?? verbDoc(gitea.url, dir); const store = new Store(dataRoot); const references = {}; const { held } = giteaStart(store, storeRoot, { acme: doc }, references); const credentials = new Credentials({ references, storeRoot, dataRoot, held }); let open = true; const close = () => { if (!open) return; open = false; credentials.close(); store.close(); }; t.after(close); const grantApi = giteaGrant({ configDir: adminDir, ca: gitea.ca, timeoutMs: 5000 }); grantApi.writeAdmin('acme', gitea.adminToken, { url: gitea.url, account: 'mosaic-grant', rotateBy: dayFrom(60), setAt: AT }); let broker; const organization = (business, op, args) => { if (op === 'instance.fire') { doc.launch.instances = doc.launch.instances.filter((n) => n !== args.name); broker.reloadBusiness('acme', doc); return { revision: REV, transaction: 't-fire' }; } return { revision: REV }; }; broker = new Broker({hires:hireServiceFixture, store, businesses: { acme: structuredClone(doc) }, secretCheck: (value) => credentials.assertClean(value), organization, processing: { read: () => SWITCH }, giteaGrant: grantApi, giteaKeys: giteaKeys({ credentials, storeRoot }), }); const boss = broker.bindLaunch({ business: 'acme', role: 'boss', run: 'boss-run', harness: 'pi', address: 'boss' }); broker.request(boss, { verb: 'role.claim' }); // sage-4054 / mos-1272: grant raises require the chief definition, independent of launch.by's fire authority. const chief = broker.bindLaunch({ business: 'acme', role: 'chief', run: 'chief-run', harness: 'pi', address: 'chief' }); broker.request(chief, { verb: 'role.claim' }); const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true }); const events = () => store.all(`SELECT body FROM events WHERE business='acme' AND ${GRANT_EVIDENCE} ORDER BY seq`).map((e) => JSON.parse(e.body)); const row = () => store.get("SELECT 1 FROM gitea_grants WHERE business='acme' AND agent='chief'"); const mintChiefReadGrant = async () => { const decision = await broker.request(chief, { verb: 'decision.raise', args: { action: 'credential.mint', question: 'Grant chief library read?', options, recommendation: 'yes', blocking: false, grant: { method: 'gitea', agent: 'chief', target: { projects: ['books'] }, rotateBy: dayFrom(30) }, }, }); broker.request(human, { verb: 'decision.resolve', args: { id: decision.id, choice: 'yes' } }); await until(() => events().some((e) => e.grantDecision === decision.id && ['grant.applied', 'grant.failed'].includes(e.action))); await broker.giteaIdle(); const end = events().find((e) => e.grantDecision === decision.id && ['grant.applied', 'grant.failed'].includes(e.action)); assert.equal(end.action, 'grant.applied', JSON.stringify(end)); assert.deepEqual(end.repositories, [{ project: 'books', repository: `${gitea.url}/acme/books`, level: 'read' }]); assert.deepEqual(gitea.tokens('acme-chief')[0].scopes, ['read:repository', 'read:user']); assert.ok(row(), 'real grant wrote its gitea_grants row'); }; const revoke = async () => { await broker.request(human, { verb: 'grants.revoke', args: { service: 'gitea', agent: 'chief', reason: 'test' } }); await broker.giteaIdle(); assert.equal(row(), undefined, 'grants.revoke deleted the row'); assert.ok(events().some((e) => e.action === 'grant.revoked' && e.agent === 'chief')); }; const fire = async () => { await approveFixtureFire(broker,fixtureConsole(broker,'acme'),'chief'); await until(() => !doc.launch.instances.includes('chief')); await broker.grantsIdle(); assert.equal(row(), undefined, 'the fire\'s step G deleted the row (row 281)'); }; // sage-4054 / mos-1272: a second fixture broker on this store needs the previous runs released. const release = () => { broker.request(chief, { verb: 'role.release' }); broker.request(boss, { verb: 'role.release' }); }; return { gitea, doc, store, broker, storeRoot, row, mintChiefReadGrant, revoke, fire, close, release, choose: () => broker.libraryFetchCredential('acme') }; } const REFUSED = { refused: 'library-fetch-no-grant', fix: LIBRARY_FETCH_FIX }; test('C2-03b T6: the slot before any chief evidence, then the listed chief\'s live grant', t => { const w = world(t); assert.deepEqual(w.choose(), { kind: 'slot' }); grant(w.store, 'coder'); assert.deepEqual(w.choose(), { kind: 'slot' }, 'a seat grant is not the chief\'s: the chief entry keeps the fallback'); grant(w.store, 'chief', { user: 'chief-bot' }); assert.deepEqual(w.choose(), { kind: 'chief', agent: 'chief', user: 'chief-bot' }); assert.deepEqual(new Broker({hires:hireServiceFixture, store: w.store, businesses: { acme: w.business } }).libraryFetchCredential('acme'), REFUSED, 'a bus with no grant store refuses rather than guess'); assert.throws(() => w.broker.libraryFetchCredential('other'), { code: 'unknown-business' }); assert.equal(LIBRARY_FETCH_FIX, 'add the chief of staff, or grant the library through C2-21'); }); test('C2-03b T7: a chief revoked through grants.revoke or fired through instance.fire refuses, and the fallback stays ended', { timeout: 60000 }, async t => { const revoked = await verbWorld(t); assert.deepEqual(revoked.choose(), { kind: 'slot' }); await revoked.mintChiefReadGrant(); assert.deepEqual(revoked.choose(), { kind: 'chief', agent: 'chief', user: 'acme-chief' }); await revoked.revoke(); assert.deepEqual(revoked.choose(), REFUSED, 'revoked'); const fired = await verbWorld(t); await fired.mintChiefReadGrant(); assert.deepEqual(fired.choose(), { kind: 'chief', agent: 'chief', user: 'acme-chief' }); await fired.fire(); assert.equal(fired.doc.launch.instances.includes('chief'), false, 'delisted'); assert.ok(fired.doc.roles.chief, 'roles[chief] remains after a fire, so its evidence still ends the fallback'); assert.deepEqual(fired.choose(), REFUSED, 'fired'); }); test('C2-03b T7 choice checks: delisted, a fire under way and held each refuse; a rehire lists the chief again', t => { const delisted = world(t, { instances: ['coder'] }); grant(delisted.store, 'chief'); assert.deepEqual(delisted.choose(), REFUSED, 'delisted with the row still there'); delisted.business.launch.instances.push('chief'); // sage-4054 / mos-1272: Broker clones input; publish the fixture's organization change. delisted.broker.reloadBusiness('acme', delisted.business); assert.deepEqual(delisted.choose(), { kind: 'chief', agent: 'chief', user: 'chief-bot' }, 'a rehire lists it again'); delisted.store.run("INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES('f1',?,?,'action.allowed','chief','run','chief',?)", AT, 'acme', JSON.stringify({ action: 'fire.stopped' })); assert.deepEqual(delisted.choose(), REFUSED, 'a fire under way'); const held = world(t); grant(held.store, 'chief'); held.held.add('acme/chief'); assert.deepEqual(held.choose(), REFUSED, 'held'); }); test('C2-03b T8: a chief grant on another project ends the fallback, and two listed chiefs refuse library-fetch-no-grant', t => { const w = world(t); grant(w.store, 'chief', { project: 'other-project', row: false }); assert.deepEqual(w.choose(), REFUSED); const two = world(t); grant(two.store, 'chief'); assert.deepEqual(two.choose(), { kind: 'chief', agent: 'chief', user: 'chief-bot' }); two.business.roles.second = { definition: 'chief-of-staff', ...role }; two.broker.reloadBusiness('acme', two.business); // sage-4054 / mos-1272: publish the fixture change assert.deepEqual(two.choose(), REFUSED, 'two chief-of-staff entries, both listed: one chief only'); two.business.launch = { instances: ['coder'] }; two.broker.reloadBusiness('acme', two.business); // sage-4054 / mos-1272 assert.deepEqual(two.choose(), REFUSED, 'neither listed'); }); test('C2-03b departure (sage-2928): with no chief entry, any Gitea grant evidence refuses', t => { const w = world(t, { chief: false }); assert.deepEqual(w.choose(), { kind: 'slot' }); grant(w.store, 'coder'); assert.deepEqual(w.choose(), REFUSED); }); // T9 and the runtime wiring. The timer and library.sync both take the broker's choice: the chief's // store file, the slot, or a refusal that runs no git even with the slot at 0000. The grant and its // revoke run through the real paths on the runtime's own data root and store. test('C2-03b T9 runtime: the chief\'s store file and user, then a real revoke that runs no git with the slot at 0000', { timeout: 60000 }, async t => { const f = repo(t), root = f.root, dataRoot = join(root, 'data'); mkdirSync(dataRoot, { mode: 0o700 }); const storeRoot = join(root, 'secrets', 'agents'); // sage-4100: row285 ships the chief's read declaration; use the real roles without a scratch copy. const rolesDir = fileURLToPath(new URL('../../../roles', import.meta.url)); const slot = join(root, 'secrets', 'acme', library.SLOT); mkdirSync(join(root, 'secrets', 'acme'), { recursive: true, mode: 0o700 }); writeFileSync(slot, SLOT_TOKEN + '\n', { mode: 0o600 }); const granted = await verbWorld(t, { dataRoot, storeRoot }); await granted.mintChiefReadGrant(); granted.release(); granted.close(); const gitea = granted.gitea; const chiefFile = join(storeRoot, 'acme', 'chief', 'gitea'); assert.ok(existsSync(chiefFile), 'the grant wrote the chief\'s store file'); const tokens = () => [SLOT_TOKEN, ...gitea.issued]; const calls = [], lines = [], write = process.stderr.write; const command = (kind, url, opts) => { calls.push({ kind, opts }); return { argv: ['--version'], env: gitEnv() }; }; process.stderr.write = function (chunk, ...rest) { if (String(chunk).startsWith('library: ')) { lines.push(String(chunk)); return true; } return write.call(this, chunk, ...rest); }; t.after(() => { process.stderr.write = write; }); const start = (intervalMs = 50) => startBroker({ dataRoot, businesses: { acme: verbDoc(gitea.url, root) }, organization: { dir: root, rolesDir, system: systemFor(root) }, librarySync: { command, intervalMs } }); const untilAsync = async (fn) => { const end = Date.now() + 10000; while (!fn() && Date.now() < end) await sleep(10); }; let runtime = await start(); t.after(async () => { await runtime.close(); }); // sage-4054 / mos-1272: close even on assertion failure // sage-4054 / mos-1272: command construction precedes publication; wait for the completed sync log. await untilAsync(() => lines.length >= 1); const human = runtime.broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true }); await runtime.broker.request(human, { verb: 'library.sync', args: { project: 'books' } }).catch(e => e); // webby-0151 R3: library.status names the kind and user of the last fetch, never the file or value. const shown = await runtime.broker.request(human, { verb: 'library.status', args: { project: 'books' } }); assert.deepEqual(shown.credential, { kind: 'chief', user: 'acme-chief' }); assert.equal(JSON.stringify(shown).includes(chiefFile) || tokens().some(tok => JSON.stringify(shown).includes(tok)), false); await runtime.close(); assert.ok(calls.length >= 2, 'the timer and the verb'); assert.ok(calls.every(c => c.kind === 'fetch' && c.opts.slot === chiefFile && c.opts.user === 'acme-chief'), 'the chief\'s file and user, never the slot'); // Revoked through grants.revoke: the next start refuses before git, and the log names the fix. const revoking = await verbWorld(t, { dataRoot, storeRoot, gitea }); await revoking.revoke(); revoking.release(); revoking.close(); chmodSync(slot, 0o000); calls.length = 0; lines.length = 0; // mos-1594: TIMEOUT_MAX keeps later timer ticks from racing the revoked verb sync. runtime = await start(2147483647); await untilAsync(() => lines.length >= 1); const human2 = runtime.broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true }); await assert.rejects(runtime.broker.request(human2, { verb: 'library.sync', args: { project: 'books' } }), { code: 'library-fetch-no-grant' }); const after = await runtime.broker.request(human2, { verb: 'library.status', args: { project: 'books' } }); assert.equal(after.lastError.code, 'library-fetch-no-grant'); assert.equal(after.credential, null); await runtime.close(); assert.equal(calls.length, 0, 'no git with a refusal'); assert.equal(statSync(slot).mode & 0o777, 0, 'the slot was never fixed up'); assert.deepEqual(JSON.parse(lines[0].slice('library: '.length)), { event: 'library-sync-held', business: 'acme', project: 'books', code: 'library-fetch-no-grant', fix: LIBRARY_FETCH_FIX }); for (const line of lines) assert.equal(tokens().some(tok => line.includes(tok)), false); const mirror = library.mirrorPath({ business: verbDoc(gitea.url, root), project: 'books', dataRoot }); const state = await library.readSyncState(mirror, { branch: 'main' }); assert.equal(state.lastError.code, 'library-fetch-no-grant'); assert.equal(state.credential, null); });