// Row 252 (#1753, Gander's design approved by Webby at 5d8f0f4b5, N1-N5): the broker side of the // Console's Approve and Deny for a grant request. A console cap, made by bindConsole after the // transport checked the wrapper and the signed-in owner, reaches only decision.preview.console and // decision.resolve.console, and no other cap reaches those. The answer records via 'console' and a // human.input that names the owner, written before the grant check (N2), and only that record lets // an approve start the executor (J13, J14). Q188: no agent caller answers. The grant uses row 234's // host-enroll raise with a fake keyscan and fake enroll and check steps, so nothing connects anywhere. import test from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Broker, CONSOLE_VERBS } from '../src/broker.mjs'; import { Client } from '../src/client.mjs'; import { serve } from '../src/server.mjs'; import { Store } from '../src/store.mjs'; import { hostGrant, fingerprint } from '../src/host-grant.mjs'; import { grantDecision } from './host-fixtures.mjs'; const options = [{ key: 'yes', text: 'Yes, enroll it' }, { key: 'no', text: 'No' }]; const SUBJECT = { provider: 'fake-oidc', iss: 'https://idp.test', sub: 'owner-1' }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const keyLine = (dir, name) => { execFileSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'x', '-f', join(dir, name)]); return readFileSync(join(dir, name + '.pub'), 'utf8').trim().split(' ').slice(0, 2).join(' '); }; const refusesSync = (fn, code) => assert.throws(fn, (e) => (assert.equal(e.code, code), true)); function setup(t) { const root = mkdtempSync(join(tmpdir(), 'c252-')); const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); const hostKeys = [keyLine(root, 'hk1'), keyLine(root, 'hk2')]; const configDir = join(root, 'config'); const real = hostGrant({ configDir, runRoot: join(configDir, 'secrets', 'run'), scan: async () => hostKeys }); const enrolls = []; const hg = { ...real, enroll: async (...args) => (enrolls.push(args), real.enroll(...args)), check: async (...args) => ({ installerFingerprint: args[2].installer_fingerprint, exit: 0 }), }; const business = { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send', 'decision.resolve.delivery'], crossRole: [] } }, pm: { definition: 'pm', authority: { withinRole: ['message.send', 'decision.resolve.delivery'], crossRole: [] } }, }, vars: { 'bus.tester': true }, }; const make = () => new Broker({ store, businesses: { demo: business }, hostGrant: hg }); const broker = make(); const caps = {}; for (const role of ['chief', 'pm']) { 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: SUBJECT }); const grant = { method: 'host-enroll', host: 'web1', address: '192.0.2.10', port: 22, user: 'jarvis', sudo: true, fingerprints: hostKeys.map(fingerprint) }; const call = (who, verb, args = {}) => broker.request(caps[who], { verb, args }); const raise = (extra = {}) => call('chief', 'decision.raise', { action: 'credential.mint', question: 'Enroll web1?', options, recommendation: 'yes', blocking: false, grant, ...extra }); const events = () => store .all("SELECT seq, kind, actor_role, actor_run, subject, body FROM events WHERE business='demo' ORDER BY seq") // The row's subject is `about`, since a console answer's body has its own `subject`. The row's kind // is `event`, set after the body, since an answer's body has its own `kind`. .map((e) => ({ seq: e.seq, kind: e.kind, actor_role: e.actor_role, actor_run: e.actor_run, about: e.subject, ...JSON.parse(e.body), event: e.kind })); const grantEvents = (decision) => events().filter((e) => e.actor_run === 'grant' && (e.decision === decision || e.grantDecision === decision)); const settle = async (decision) => { for (let i = 0; i < 250; i++) { if (grantEvents(decision).some((e) => ['grant.host.enrolled', 'grant.failed'].includes(e.action))) return; await sleep(20); } throw new Error('enrollment did not end'); }; const closes = (decision) => store.all('SELECT seq, op, by, choice, via FROM decision_events WHERE decision=? ORDER BY seq', decision); return { store, broker, make, caps, call, raise, events, grantEvents, settle, closes, enrolls, grant }; } // #797 / #1839 adds the owner-only artifact read to the existing decision and request controls; // no unsuffixed verb is ever a console verb. test('N4: owner Console verbs have an explicit immutable whitelist and exclude decision.resolve', () => { assert.deepEqual([...CONSOLE_VERBS], ['system-types.console', 'project.remote.get.console', 'project.remote.set.console', 'autonomy.get.console', 'autonomy.set.console', 'autonomy.review.console', 'decision.confirm.console', 'decision.reverse.console', 'request.workspace.console', 'request.artifact.console', 'decision.preview.console', 'decision.resolve.console', 'hire.preview.console', 'hire.resolve.console', 'hire.start.console', 'hire.templates.console', 'prd.revoke.console', 'request.mode.console', 'request.draft.console', 'request.get.console', 'request.projects.console', 'request.answer.console', 'request.attach.console', 'request.cancel.console']); assert.ok(CONSOLE_VERBS.every(verb => verb.endsWith('.console'))); assert.equal(CONSOLE_VERBS.includes('decision.resolve'), false); assert.ok(Object.isFrozen(CONSOLE_VERBS)); }); test('bindConsole takes only the business human and a three-string owner subject', (t) => { const f = setup(t); const bind = (record) => () => f.broker.bindConsole(record); refusesSync(bind({ business: 'demo', human: 'someone', subject: SUBJECT }), 'human-required'); refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, name: 'Jason' } }), 'human-required'); refusesSync(bind({ business: 'demo', human: 'jason', subject: { provider: 'p', iss: 'i' } }), 'human-required'); refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, sub: '' } }), 'human-required'); refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, sub: 'x'.repeat(513) } }), 'human-required'); refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, sub: 7 } }), 'human-required'); refusesSync(bind({ business: 'demo', human: 'jason', subject: 'owner-1' }), 'human-required'); assert.throws(bind({ business: 'demo', human: 'jason', subject: SUBJECT, via: 'cli' })); assert.throws(bind({ business: 'demo', human: 'jason' })); assert.throws(bind({ business: 'other', human: 'jason', subject: SUBJECT })); assert.deepEqual(f.broker.identity(f.caps.console), { business: 'demo', role: null, run: null, human: 'jason' }); }); test('J1, Q188: only a console cap reaches the console verbs, and a console cap reaches nothing else', async (t) => { const f = setup(t); const d = await f.raise(); const preview = f.call('console', 'decision.preview.console', { id: d.id }); const args = { id: d.id, choice: 'yes', digest: preview.digest }; // Agents, the cli human, the tester and the reader are refused before the verb runs. for (const who of ['chief', 'pm', 'human', 'tester', 'reader']) { refusesSync(() => f.call(who, 'decision.preview.console', { id: d.id }), 'human-required'); refusesSync(() => f.call(who, 'decision.resolve.console', args), 'human-required'); } // N4: the console cap answers nothing through decision.resolve, and reaches no other verb. for (const [verb, a] of [ ['decision.resolve', { id: d.id, choice: 'yes' }], ['decision.seen', { id: d.id }], ['decision.withdraw', { id: d.id }], ['decision.raise', { action: 'credential.mint', question: 'q', options, recommendation: 'yes', blocking: false, grant: f.grant }], ['inbox', {}], ['message.send', { to: 'chief', body: 'hi' }], ['role.claim', {}], ]) refusesSync(() => f.call('console', verb, a), 'human-required'); assert.deepEqual(f.closes(d.id), []); assert.equal(f.enrolls.length, 0); }); test('preview: the two real answers, a grant summary and the digest, for an open grant request only', async (t) => { const f = setup(t); const d = await f.raise(); const p = f.call('console', 'decision.preview.console', { id: d.id }); assert.equal(p.id, d.id); assert.equal(p.business, 'demo'); assert.match(p.question, /^Enroll web1\?/); assert.deepEqual(p.approve, { key: 'yes', text: 'Yes, enroll it' }); assert.deepEqual(p.deny, { key: 'no', text: 'No' }); assert.deepEqual(p.grant, { method: 'host-enroll', host: 'web1' }); assert.match(p.digest, /^[a-f0-9]{64}$/); // Read twice, the digest is the same; a preview writes nothing. assert.equal(f.call('console', 'decision.preview.console', { id: d.id }).digest, p.digest); assert.deepEqual(f.closes(d.id), []); refusesSync(() => f.call('console', 'decision.preview.console', { id: d.id, choice: 'yes' }), 'invalid-request'); refusesSync(() => f.call('console', 'decision.preview.console', { id: 'no-such-decision' }), 'decision-not-found'); // The inbox row says the same, for the human and the reader. The agent-facing views do not. for (const who of ['human', 'reader']) { const row = f.call(who, 'inbox').find((r) => r.id === d.id); assert.deepEqual(row.console, { kind: 'grant', options, approve: p.approve, deny: p.deny, grant: p.grant, digest: p.digest }); } }); test('J1, J2: a decision that is not an eligible grant request refuses both console verbs', async (t) => { const f = setup(t); const grant = { method: 'host', agent: 'pm', host: 'web1', commands: ['uptime'], rotateBy: '2099-01-01' }; const ineligible = [ grantDecision(f.store, { action: 'deploy', grant, close: null }), grantDecision(f.store, { route: 'pm', grant, close: null }), grantDecision(f.store, { grant: undefined, close: null }), ]; for (const id of ineligible) { refusesSync(() => f.call('console', 'decision.preview.console', { id }), 'decision-not-eligible'); refusesSync(() => f.call('console', 'decision.resolve.console', { id, choice: 'yes', digest: '0'.repeat(64) }), 'decision-not-eligible'); assert.equal(f.call('human', 'inbox').find((r) => r.id === id)?.console, undefined); } }); test('ordinary three-choice decisions expose authoritative options and resolve the third choice once with Console provenance', (t) => { const f = setup(t); const choices = [ { key: 'new/autonomous', text: 'New project — autonomous' }, { key: 'new/guided', text: 'New project — guided' }, { key: 'existing:project', text: 'Add to existing project' }, ]; const d = f.call('chief', 'decision.raise', { action: 'prd.approve', question: 'How should we handle this request?', options: choices, recommendation: choices[0].key, approvalChoice: choices[0].key, blocking: false }); const p = f.call('console', 'decision.preview.console', { id: d.id }); assert.deepEqual(p.options, choices); assert.equal(p.kind, 'decision'); assert.equal(p.grant, undefined); assert.equal(p.approve, undefined); for (const who of ['human', 'reader']) assert.deepEqual(f.call(who, 'inbox').find(r => r.id === d.id).console.options, choices); const r = f.call('console', 'decision.resolve.console', { id: d.id, choice: choices[2].key, digest: p.digest }); assert.deepEqual(r, { id: d.id, op: 'resolved', choice: choices[2].key }); assert.deepEqual(f.closes(d.id).map(c => [c.op, c.by, c.choice, c.via]), [['resolved', 'jason', choices[2].key, 'console']]); const inputs = f.events().filter(e => e.event === 'human.input' && e.about === d.id); assert.equal(inputs.length, 1); assert.deepEqual(inputs[0].subject, SUBJECT); assert.equal(inputs[0].via, 'console'); assert.equal(inputs[0].actor_role, null); assert.equal(inputs[0].actor_run, null); assert.equal(inputs[0].decisionSeq, f.closes(d.id)[0].seq); const notices = f.store.all("SELECT * FROM messages WHERE decision=? AND from_role='human' AND class='DECISION'", d.id); assert.equal(notices.length, 1); assert.equal(notices[0].to_role, 'chief'); assert.equal(notices[0].from_run, 'human-console'); assert.match(notices[0].body, /resolved by jason: existing:project \(Add to existing project\)/); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: choices[2].key, digest: p.digest }), 'decision-closed'); assert.equal(f.enrolls.length, 0); assert.deepEqual(f.grantEvents(d.id), []); assert.deepEqual(f.make().reconcileGrants(), []); }); test('ordinary PRD approval and nine non-yes/no choices use the same protocol without grant execution', (t) => { const f = setup(t); for (const choices of [options, Array.from({ length: 9 }, (_, i) => ({ key: `choice-${i}`, text: `Choice ${i}` }))]) { const d = f.call('chief', 'decision.raise', { action: 'prd.approve', question: 'Approve this PRD?', options: choices, recommendation: choices[0].key, approvalChoice: choices[0].key, blocking: false }); const p = f.call('console', 'decision.preview.console', { id: d.id }); assert.deepEqual(p.options, choices); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'outside', digest: p.digest }), 'decision-stale'); assert.equal(f.closes(d.id).length, 0); assert.deepEqual(f.call('console', 'decision.resolve.console', { id: d.id, choice: choices[0].key, digest: p.digest }), { id: d.id, op: 'resolved', choice: choices[0].key }); assert.deepEqual(f.grantEvents(d.id), []); } assert.equal(f.enrolls.length, 0); }); test('ordinary role routes refuse Console resolution and agent answers retain the actual role/run', (t) => { const f = setup(t); for (const route of ['pm', 'chief']) { const id = grantDecision(f.store, { action: 'policy.change', route, raiser:route==='chief'?'pm':'chief', close: null }); refusesSync(() => f.call('console', 'decision.preview.console', { id }), 'decision-not-eligible'); refusesSync(() => f.call('console', 'decision.resolve.console', { id, choice: 'yes', digest: '0'.repeat(64) }), 'decision-not-eligible'); f.call(route, 'decision.resolve', { id, choice: 'yes' }); assert.deepEqual(f.closes(id).map(c => [c.by, c.via]), [[route + '-run', 'broker']]); assert.equal(f.events().filter(e => e.event === 'human.input' && e.about === id).length, 0); } }); test('the digest binds ordinary question, action, options, business and raise authorization context', (t) => { const f = setup(t); const d = f.call('chief', 'decision.raise', { action: 'prd.approve', target: 'product-1', question: 'Approve draft?', options, recommendation: 'yes', blocking: false }); const p = f.call('console', 'decision.preview.console', { id: d.id }); const original = f.store.get('SELECT * FROM decisions WHERE id=?', d.id); const event = f.store.get("SELECT * FROM events WHERE subject=? AND kind='action.allowed' AND json_extract(body,'$.operation')='decision.raise'", d.id); // Each immutable variant is inserted into a fresh scratch store, never rewritten in place. for (const change of [ { question: 'Approve changed draft?' }, { action: 'deploy' }, { project: 'product-2' }, { spec_commit: 'f'.repeat(40) }, { options: JSON.stringify([{ key: 'yes', text: 'Changed' }, options[1]]) }, { authorization: { target: 'product-2' } }, { authorization: { approvalChoice: 'no' } }, ]) { const g = setup(t), { authorization, ...fields } = change; const row = { ...original, ...fields }; const columns = Object.keys(row); g.store.run(`INSERT INTO decisions(${columns.join(',')}) VALUES(${columns.map(() => '?').join(',')})`, ...Object.values(row)); const e = { ...event, body: JSON.stringify({ ...JSON.parse(event.body), ...authorization }) }; const ec = Object.keys(e); g.store.run(`INSERT INTO events(${ec.join(',')}) VALUES(${ec.map(() => '?').join(',')})`, ...Object.values(e)); const current = g.call('console', 'decision.preview.console', { id: d.id }); assert.notEqual(current.digest, p.digest); refusesSync(() => g.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest: p.digest }), 'decision-stale'); assert.deepEqual(g.closes(d.id), []); } const other = grantDecision(f.store, { business: 'other', action: 'prd.approve', close: null }); refusesSync(() => f.call('console', 'decision.preview.console', { id: other }), 'decision-not-found'); refusesSync(() => f.call('console', 'decision.resolve.console', { id: other, choice: 'yes', digest: p.digest }), 'decision-not-found'); }); test('missing and malformed grant bindings cannot become ordinary Console answers or start an executor', (t) => { const f = setup(t); for (const grant of [undefined, {}, { host: 'web1' }, { method: 7 }]) { const id = grantDecision(f.store, { grant, close: null }); refusesSync(() => f.call('console', 'decision.preview.console', { id }), 'decision-not-eligible'); refusesSync(() => f.call('console', 'decision.resolve.console', { id, choice: 'yes', digest: '0'.repeat(64) }), 'decision-not-eligible'); assert.deepEqual(f.closes(id), []); assert.deepEqual(f.grantEvents(id), []); } assert.equal(f.enrolls.length, 0); }); test('N2, J13: a Console approve records via console and the owner, before the executor starts, and the grant runs once', async (t) => { const f = setup(t); const d = await f.raise(); const { digest } = f.call('console', 'decision.preview.console', { id: d.id }); const r = f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest }); assert.deepEqual(r, { id: d.id, op: 'resolved', choice: 'yes', grant: 'started' }); const [close] = f.closes(d.id); assert.deepEqual({ ...close, seq: undefined }, { seq: undefined, op: 'resolved', by: 'jason', choice: 'yes', via: 'console' }); const input = f.events().filter((e) => e.event === 'human.input' && e.about === d.id); assert.equal(input.length, 1); assert.deepEqual( { ...input[0], seq: undefined }, { seq: undefined, kind: 'answer', actor_role: null, actor_run: null, about: d.id, decision: d.id, op: 'resolved', choice: 'yes', via: 'console', subject: SUBJECT, decisionSeq: close.seq, event: 'human.input' }, ); await f.settle(d.id); assert.equal(f.enrolls.length, 1); const spend = f.grantEvents(d.id).find((e) => e.operation === 'grant.host-enroll'); assert.ok(spend && input[0].seq < spend.seq, 'the human.input precedes the executor spend'); assert.ok(f.grantEvents(d.id).some((e) => e.action === 'grant.host.enrolled')); // N3: the close notice to the raiser comes from the human through the Console. const notices = f.store.all("SELECT from_run, body FROM messages WHERE business='demo' AND decision=? AND class='DECISION' AND from_role='human'", d.id); assert.equal(notices.length, 1); const [notice] = notices; assert.equal(notice.from_run, 'human-console'); assert.match(notice.body, /resolved by jason: yes \(Yes, enroll it\)/); // A second answer of either kind finds it closed, and nothing starts again. refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest }), 'decision-closed'); refusesSync(() => f.call('console', 'decision.preview.console', { id: d.id }), 'decision-closed'); refusesSync(() => f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' }), 'decision-closed'); await sleep(50); assert.equal(f.enrolls.length, 1); assert.equal(f.closes(d.id).length, 1); }); test('J15: a Console deny closes with the other key and starts nothing', async (t) => { const f = setup(t); const d = await f.raise(); const { digest } = f.call('console', 'decision.preview.console', { id: d.id }); const r = f.call('console', 'decision.resolve.console', { id: d.id, choice: 'no', digest }); assert.deepEqual(r, { id: d.id, op: 'resolved', choice: 'no' }); assert.deepEqual(f.closes(d.id).map((c) => [c.op, c.choice, c.via]), [['resolved', 'no', 'console']]); await sleep(100); assert.equal(f.enrolls.length, 0); assert.deepEqual(f.grantEvents(d.id), []); assert.deepEqual(f.make().reconcileGrants(), []); }); test('J2: approve and deny come from approvalChoice and the options, not a fixed yes and no', async (t) => { const f = setup(t); // Deny listed first, so a build that takes options[0] as approve fails too. const keyed = { options: [{ key: 'stop', text: 'Stop' }, { key: 'go', text: 'Go ahead' }], approvalChoice: 'go', recommendation: 'go' }; const denied = await f.raise({ ...keyed, question: 'Enroll web1? (deny)' }); const p = f.call('console', 'decision.preview.console', { id: denied.id }); assert.deepEqual([p.approve, p.deny], [{ key: 'go', text: 'Go ahead' }, { key: 'stop', text: 'Stop' }]); for (const choice of ['yes', 'no']) refusesSync(() => f.call('console', 'decision.resolve.console', { id: denied.id, choice, digest: p.digest }), 'decision-stale'); assert.deepEqual(f.call('console', 'decision.resolve.console', { id: denied.id, choice: 'stop', digest: p.digest }), { id: denied.id, op: 'resolved', choice: 'stop' }); await sleep(100); assert.equal(f.enrolls.length, 0); const approved = await f.raise({ ...keyed, question: 'Enroll web1? (approve)' }); const { digest } = f.call('console', 'decision.preview.console', { id: approved.id }); assert.deepEqual(f.call('console', 'decision.resolve.console', { id: approved.id, choice: 'go', digest }), { id: approved.id, op: 'resolved', choice: 'go', grant: 'started' }); await f.settle(approved.id); assert.equal(f.enrolls.length, 1); }); test('J11: a stale digest or a key outside the two refuses decision-stale and writes nothing', async (t) => { const f = setup(t); const d = await f.raise(); const { digest } = f.call('console', 'decision.preview.console', { id: d.id }); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest: 'f'.repeat(64) }), 'decision-stale'); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'maybe', digest }), 'decision-stale'); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes' }), 'invalid-request'); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest, note: 'x' }), 'invalid-request'); // Another decision's digest does not answer this one. const other = await f.raise({ question: 'Enroll web1 again?' }); const od = f.call('console', 'decision.preview.console', { id: other.id }).digest; assert.notEqual(od, digest); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest: od }), 'decision-stale'); assert.deepEqual(f.closes(d.id), []); assert.equal(f.events().filter((e) => e.event === 'human.input').length, 0); // A refusal is recorded, as for every verb. assert.ok(f.events().some((e) => e.event === 'action.refused' && e.code === 'decision-stale')); // The decision closed by the cli meanwhile: the console answer finds it closed. f.call('human', 'decision.resolve', { id: d.id, choice: 'no' }); refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest }), 'decision-closed'); }); test('J14: a via console close with no matching human.input approves nothing', (t) => { const f = setup(t); const grant = { method: 'host-enroll', host: 'web2', address: '192.0.2.11', port: 22, user: 'jarvis', sudo: true, fingerprints: [] }; const cli = grantDecision(f.store, { grant }); const bare = grantDecision(f.store, { grant: { ...grant, host: 'web3' }, close: { op: 'resolved', by: 'jason', choice: 'yes', via: 'console' } }); const wrong = grantDecision(f.store, { grant: { ...grant, host: 'web4' }, close: { op: 'resolved', by: 'jason', choice: 'yes', via: 'console' } }); const good = grantDecision(f.store, { grant: { ...grant, host: 'web5' }, close: { op: 'resolved', by: 'jason', choice: 'yes', via: 'console' } }); const seqOf = (id) => f.store.get('SELECT seq FROM decision_events WHERE decision=?', id).seq; const input = (id, body, role = null) => f.store.run( "INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'human.input',?,?,?,?)", 'e-' + id + '-' + Math.random(), '2026-10-22T00:00:00.000Z', 'demo', role, role && role + '-run', id, JSON.stringify(body), ); const answer = (id) => ({ kind: 'answer', decision: id, op: 'resolved', choice: 'yes', via: 'console', subject: SUBJECT, decisionSeq: seqOf(id) }); // `bare` has no human.input at all. `wrong` has records that each break one part: another seq, choice, empty subject, an agent actor. input(wrong, { ...answer(wrong), decisionSeq: seqOf(wrong) + 1000 }); input(wrong, { ...answer(wrong), choice: 'no' }); input(wrong, { ...answer(wrong), subject: { ...SUBJECT, sub: '' } }); input(wrong, answer(wrong), 'chief'); input(good, answer(good)); // reconcileGrants ends every approved, unspent host-enroll: exactly the cli one and the good console one. assert.deepEqual(f.make().reconcileGrants().sort(), [cli, good].sort()); assert.equal(f.enrolls.length, 0); assert.equal(f.store.get("SELECT count(*) AS n FROM events WHERE business='demo' AND actor_run='grant' AND json_extract(body,'$.decision') IN (?,?)", bare, wrong).n, 0); }); test('recordEvent keeps human.input reserved, so no agent forges a console answer', async (t) => { const f = setup(t); const d = await f.raise(); const forged = { kind: 'human.input', subject: d.id, body: { kind: 'answer', decision: d.id, op: 'resolved', choice: 'yes', via: 'console', subject: SUBJECT, decisionSeq: 1 } }; for (const who of ['chief', 'pm']) refusesSync(() => f.broker.recordEvent(f.caps[who], forged), 'reserved-event'); refusesSync(() => f.broker.recordEvent(f.caps.console, forged), 'agent-required'); assert.equal(f.events().filter((e) => e.event === 'human.input').length, 0); }); // J7: the socket transport binds a console proof's cap for its one call only. The reply and a refusal // both release it, so the owner cap can't outlive the request that proved it. test('J7: a console proof over the socket binds a cap for its one call and the reply releases it', async (t) => { const f = setup(t); const d = await f.raise(); const dir = mkdtempSync(join(tmpdir(), 'c252s-')); const bound = []; const server = await serve({ broker: f.broker, path: join(dir, 'bus.sock'), authenticateConsole: () => (bound.push(f.broker.bindConsole({ business: 'demo', human: 'jason', subject: SUBJECT })), bound.at(-1)), }); t.after(async () => { await server.close(); rmSync(dir, { recursive: true, force: true }); }); const client = new Client({ path: join(dir, 'bus.sock'), console: { fixture: true } }); const preview = await client.call('decision.preview.console', { id: d.id }); assert.equal(preview.id, d.id); await assert.rejects(client.call('decision.preview.console', { id: 'nope' }), { code: 'decision-not-found' }); assert.equal(bound.length, 2); for (const cap of bound) refusesSync(() => f.broker.identity(cap), 'unauthenticated'); });