// Row 199 (#1697, design section 6, T4, T5): credentials.issue, credentials.revoke and // credentials.delete through the runtime. Human-only; the tester revokes and never issues. Each verb // is live with no restart and leaves one receipt in changes.jsonl with no value in it. Fake tokens in a // scratch root only. import test from 'node:test'; import assert from 'node:assert/strict'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { startBroker } from '../src/runtime.mjs'; import { Broker, CREDENTIAL_VERBS } from '../src/broker.mjs'; import { Store } from '../src/store.mjs'; import { businessDoc, rolesCopy, systemFor, writeJson } from '../../organization/tests/helpers.mjs'; const TOKEN = 'fake_issued_token_0123456789'; const ROTATED = 'fake_rotated_token_9876543210'; const OTHER = 'fake_other_token_5555555555'; const DATE = '2099-01-01'; const role = { authority: { withinRole: ['message.send'], crossRole: [] } }; async function setup(t, { storeRoot = true } = {}) { const root = mkdtempSync(join(tmpdir(), 'bus-cred-verbs-')); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root); const doc = { ...businessDoc(root, 'demo'), plugins: { harness: ['pi'] } }; doc.roles.coder.credentials.gitea = { store: true, rotateBy: DATE }; writeJson(join(root, 'businesses/demo.json'), doc); mkdirSync(join(root, 'data'), { mode: 0o700 }); if (storeRoot) mkdirSync(join(root, 'secrets', 'agents'), { mode: 0o700 }); const store = (agent, service) => join(root, 'secrets', 'agents', 'demo', agent, service); // The bus's resolved business carries the same references; a store one names its derived file. const demo = { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { ...role, credentials: { gitea: { store: true, file: store('coder', 'gitea'), rotateBy: DATE }, vikunja: doc.roles.coder.credentials.vikunja } }, reviewer: { ...role, credentials: doc.roles.reviewer.credentials }, }, }; const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo }, organization: { dir: root, rolesDir, system: systemFor(root) } }); t.after(() => bus.close()); const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const tester = bus.broker.bindTester({ business: 'demo' }); const ask = (cap, verb, args) => bus.broker.request(cap, { verb, args }); const file = () => JSON.parse(readFileSync(join(root, 'businesses/demo.json'), 'utf8')); const receipts = () => { const log = join(root, 'businesses/.history/demo/changes.jsonl'); return existsSync(log) ? readFileSync(log, 'utf8').trim().split('\n').map(JSON.parse) : []; }; const committed = () => receipts().filter((r) => r.state === 'committed'); const status = (agent) => Object.fromEntries(bus.credentialStatus('demo', agent).map((e) => [e.service, e.state])); return { root, bus, human, tester, ask, file, receipts, committed, status, store }; } test('row 199: the lifecycle verbs refuse every session but the human, and the tester revokes only', (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-cred-gate-')); const s = new Store(root); t.after(() => { s.close(); rmSync(root, { recursive: true, force: true }); }); const calls = []; const b = new Broker({ store: s, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: role } } }, credentialVerbs: (id, verb, args, actor) => (calls.push([verb, actor]), { ok: true }), }); const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const tester = b.bindTester({ business: 'demo' }); const reader = b.bindReader({ business: 'demo' }); const coder = b.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi' }); const args = { 'credentials.issue': { agent: 'coder', service: 'gitea', token: TOKEN, date: DATE }, 'credentials.revoke': { agent: 'coder', reason: 'fired' }, 'credentials.delete': { agent: 'coder', reason: 'fired' }, }; assert.deepEqual(CREDENTIAL_VERBS, Object.keys(args)); for (const verb of CREDENTIAL_VERBS) { assert.throws(() => b.request(reader, { verb, args: args[verb] }), /human-required/, verb); assert.throws(() => b.request(coder, { verb, args: args[verb] }), /human-required/, verb); } assert.throws(() => b.request(tester, { verb: 'credentials.issue', args: args['credentials.issue'] }), /tester-refused/); assert.throws(() => b.request(tester, { verb: 'credentials.delete', args: args['credentials.delete'] }), /tester-refused/); // Keys: a token on revoke, a missing reason, an unknown service, an unknown agent, an @ agent. assert.throws(() => b.request(human, { verb: 'credentials.revoke', args: { ...args['credentials.revoke'], token: TOKEN } }), /invalid-request/); assert.throws(() => b.request(human, { verb: 'credentials.revoke', args: { agent: 'coder' } }), /invalid-request/); assert.throws(() => b.request(human, { verb: 'credentials.issue', args: { ...args['credentials.issue'], service: 'model' } }), /invalid-request/); assert.throws(() => b.request(human, { verb: 'credentials.issue', args: { ...args['credentials.issue'], file: '/x' } }), /invalid-request/); assert.throws(() => b.request(human, { verb: 'credentials.issue', args: { ...args['credentials.issue'], agent: 'pm' } }), /credential-unknown/); assert.throws(() => b.request(human, { verb: 'credentials.revoke', args: { agent: '@sync', reason: 'x' } }), /credential-unknown/); assert.equal(calls.length, 0); // A refusal records its code alone, never the token. const refused = s.all("SELECT body FROM events WHERE kind='action.refused'"); assert.ok(refused.length >= 10); for (const { body } of refused) assert.equal(body.includes(TOKEN), false); // delete waits for the agent's claim to end. b.request(coder, { verb: 'role.claim' }); assert.throws(() => b.request(human, { verb: 'credentials.delete', args: args['credentials.delete'] }), /agent-running/); b.request(coder, { verb: 'role.release' }); for (const verb of CREDENTIAL_VERBS) b.request(human, { verb, args: args[verb] }); b.request(tester, { verb: 'credentials.revoke', args: args['credentials.revoke'] }); assert.deepEqual(calls, [['credentials.issue', 'jason'], ['credentials.revoke', 'jason'], ['credentials.delete', 'jason'], ['credentials.revoke', 'tester']]); // No lifecycle without the runtime's store. const bare = new Broker({ store: s, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: role } } } }); const h2 = bare.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); assert.throws(() => bare.request(h2, { verb: 'credentials.revoke', args: args['credentials.revoke'] }), /not-configured/); }); test('T5: issue, rotate, revoke and delete are live, rewrite the business file and leave one receipt each, with no value anywhere', async (t) => { const c = await setup(t); const coder = { business: 'demo', role: 'coder' }; const read = (service) => c.bus.credentials.useFor(coder, service, async (token) => token === TOKEN ? 'issued' : token === ROTATED ? 'rotated' : 'other'); assert.deepEqual(c.status('coder'), { gitea: 'not-issued', vikunja: 'valid' }); // issue: the file, then the business file's reference, then memory. const issued = c.ask(c.human, 'credentials.issue', { agent: 'coder', service: 'gitea', token: TOKEN + '\n', date: DATE }); assert.deepEqual(issued, { agent: 'coder', verb: 'issue', service: 'gitea', date: DATE, state: 'valid' }); assert.equal(await read('gitea'), 'issued'); assert.equal(statSync(c.store('coder', 'gitea')).mode & 0o777, 0o600); assert.deepEqual(c.file().roles.coder.credentials.gitea, { store: true, rotateBy: DATE }); let last = c.committed().at(-1); assert.equal(last.actor, 'jason'); assert.deepEqual(last.detail, { verb: 'issue', agent: 'coder', kind: ['gitea'], date: DATE }); // The same token again is one the bus holds: the secret check refuses it before any write. const count = c.receipts().length; assert.throws(() => c.ask(c.human, 'credentials.issue', { agent: 'coder', service: 'gitea', token: TOKEN, date: DATE }), /credential-leak/); assert.throws(() => c.ask(c.human, 'credentials.issue', { agent: 'coder', service: 'gitea', token: 'short', date: DATE }), /credential-format/); assert.throws(() => c.ask(c.human, 'credentials.issue', { agent: 'coder', service: 'gitea', token: OTHER, date: '2099-02-30' }), /credential-date/); assert.equal(c.receipts().length, count); // rotate: an issue over an issued entry, used at once. const rotated = c.ask(c.human, 'credentials.issue', { agent: 'coder', service: 'gitea', token: ROTATED, date: '2099-06-01' }); assert.equal(rotated.verb, 'rotate'); assert.equal(await read('gitea'), 'rotated'); assert.deepEqual(c.file().roles.coder.credentials.gitea, { store: true, rotateBy: '2099-06-01' }); assert.deepEqual(c.committed().at(-1).detail, { verb: 'rotate', agent: 'coder', kind: ['gitea'], date: '2099-06-01' }); // revoke of a store entry by the tester: memory and file, no document change, a receipt. const bytes = readFileSync(join(c.root, 'businesses/demo.json'), 'utf8'); const revoked = c.ask(c.tester, 'credentials.revoke', { agent: 'coder', service: 'gitea', reason: 'test' }); assert.deepEqual(revoked, { agent: 'coder', verb: 'revoke', services: [{ service: 'gitea', state: 'revoked' }] }); await assert.rejects(read('gitea'), /credential-unavailable/); assert.equal(existsSync(c.store('coder', 'gitea')), false); assert.equal(readFileSync(join(c.root, 'businesses/demo.json'), 'utf8'), bytes); last = c.committed().at(-1); assert.equal(last.actor, 'tester'); assert.deepEqual(last.changes, []); assert.deepEqual(last.detail, { verb: 'revoke', agent: 'coder', kind: ['gitea'], reason: 'test' }); // revoke of a {file} reference: memory, the outside file kept, the reference becomes store. const outside = c.file().roles.coder.credentials.vikunja.file; c.ask(c.human, 'credentials.revoke', { agent: 'coder', service: 'vikunja', reason: 'moved' }); await assert.rejects(read('vikunja'), /credential-unavailable/); assert.equal(existsSync(outside), true); assert.deepEqual(c.file().roles.coder.credentials.vikunja, { store: true, expires: DATE }); assert.ok(c.committed().at(-1).changes.some((e) => e.path === 'roles.coder.credentials.vikunja.store' && e.new === true)); // delete: every kind revoked, the agent's dir gone, both references store. c.ask(c.human, 'credentials.issue', { agent: 'reviewer', service: 'gitea', token: OTHER, date: DATE }); assert.equal(existsSync(c.store('reviewer', 'gitea')), true); const deleted = c.ask(c.human, 'credentials.delete', { agent: 'reviewer', reason: 'fired' }); assert.deepEqual(deleted.services.map((e) => e.state), ['revoked', 'revoked']); assert.equal(existsSync(join(c.root, 'secrets', 'agents', 'demo', 'reviewer')), false); assert.deepEqual(c.file().roles.reviewer.credentials, { gitea: { store: true, rotateBy: DATE }, vikunja: { store: true, expires: DATE } }); assert.deepEqual(c.committed().at(-1).detail, { verb: 'delete', agent: 'reviewer', kind: ['gitea', 'vikunja'], reason: 'fired' }); // The status read never carries a value. assert.deepEqual(c.ask(c.human, 'credentials', {}).map((e) => e.state).sort(), ['revoked', 'revoked', 'revoked', 'revoked']); // T4: no token in any file under the root (receipts, business file, events, logs). const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(join(dir, e.name)) : e.isFile() ? [join(dir, e.name)] : []); for (const path of walk(c.root)) for (const token of [TOKEN, ROTATED, OTHER]) assert.equal(readFileSync(path).includes(token), false, path); }); test('#1886: a store reference with no store root yet starts the bus, makes the root private and issues', async (t) => { const c = await setup(t, { storeRoot: false }); const dir = join(c.root, 'secrets', 'agents'); assert.equal(statSync(dir).mode & 0o777, 0o700); assert.equal(statSync(join(c.root, 'secrets')).mode & 0o077, 0); assert.deepEqual(c.status('coder'), { gitea: 'not-issued', vikunja: 'valid' }); const issued = c.ask(c.human, 'credentials.issue', { agent: 'coder', service: 'gitea', token: TOKEN + '\n', date: DATE }); assert.deepEqual(issued, { agent: 'coder', verb: 'issue', service: 'gitea', date: DATE, state: 'valid' }); assert.equal(statSync(c.store('coder', 'gitea')).mode & 0o777, 0o600); }); test('#1886: a store root that exists with loose modes still refuses start', async (t) => { const root = mkdtempSync(join(tmpdir(), 'bus-cred-loose-')); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root); writeJson(join(root, 'businesses/demo.json'), { ...businessDoc(root, 'demo'), plugins: { harness: ['pi'] } }); mkdirSync(join(root, 'data'), { mode: 0o700 }); mkdirSync(join(root, 'secrets', 'agents'), { recursive: true, mode: 0o750 }); chmodSync(join(root, 'secrets', 'agents'), 0o750); const demo = { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: {}, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { ...role, credentials: { gitea: { store: true, file: join(root, 'secrets', 'agents', 'demo', 'coder', 'gitea'), rotateBy: DATE } } } }, }; await assert.rejects(startBroker({ dataRoot: join(root, 'data'), businesses: { demo }, organization: { dir: root, rolesDir, system: systemFor(root) } }), /credential-file/); });