Files
stack/packages/bus/tests/definition-scan.test.mjs
jason.woltje 16cdf5cdc1 fix(organization): an approved hire and the boot copy definitions run the definition check (#1873)
The hire service takes the bus's definition check as scanDefinition and runs
it on the stored proposal entry before preview, check or apply goes further,
and on the approved entry again just before the writers. An approval whose
definition holds a credential the store came to hold after the raise refuses
definition-holds-credential before the decision is consumed; nothing is
written. Without the hook the entry still goes through assertClean whole.

Bus start and credentials.issue also check the boot copy snapshot's
definitions (A4 root agents file definitions, re-applied on every reload),
which no organization file holds.
2026-11-11 08:49:15 -06:00

337 lines
23 KiB
JavaScript

// #1873 T1 follow-up (org-defscan): bus start runs the live credential store's held-secret check on
// every organization definition, and the launch check asks the bus the same question. A hit names
// the definition and the key, never the text there or the credential's name.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { BusError } from '../src/broker.mjs';
import { REFUSALS } from '../src/refusals.mjs';
import { DEFINITION_HOLDS_CREDENTIAL, DEFINITION_SCAN_UNREADABLE, heldPath, scanDefinitions, scanOrganizations } from '../src/definition-scan.mjs';
import { startBroker } from '../src/runtime.mjs';
import { organizationWriter } from '../../organization/src/organization.mjs';
import { bootConfig, loadSystem } from '../../cli/src/config.mjs';
import { fixture, tmp } from '../../cli/tests/helpers.mjs';
import { entryFromTemplate, readTemplate } from '../../organization/src/definitions.mjs';
import { REPO_ROLES, writeJson } from '../../organization/tests/helpers.mjs';
// The fixture bus holds SENTINEL as coder's gitea token.
const SENTINEL = 'sk-test-NOT-A-SECRET-1234';
const UNHELD = 'sk-test-UNHELD-LOOKS-LIKE-A-TOKEN-0000';
function files(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? files(join(dir, e.name)) : e.isFile() ? [join(dir, e.name)] : []);
}
// An organization whose coder names org:builder, a copy of the coder template; `edit` changes the
// builder entry before the file is written. `held` is the coder's gitea token at start.
function org(t, edit = (entry) => entry, { held = SENTINEL } = {}) {
const root = tmp(t, 'defscan-');
const builder = edit(entryFromTemplate(readTemplate(REPO_ROLES, 'coder')));
const f = fixture(root, 'acme', (doc) => ({ ...doc, definitions: { builder }, roles: { ...doc.roles, coder: { ...doc.roles.coder, definition: 'org:builder' } } }));
writeFileSync(f.doc.roles.coder.credentials.gitea.file, held);
const file = join(root, 'config/businesses/acme.json');
const boot = () => bootConfig({ system: loadSystem({ env: f.env }), businessId: 'acme', env: f.env });
const start = (b = boot()) => startBroker({ dataRoot: f.dataRoot, businesses: b.businesses, organization: b.organization });
return { root, f, file, boot, start };
}
function refusedWithout(e, reason) {
assert.ok(e instanceof BusError, String(e));
assert.equal(e.code, DEFINITION_HOLDS_CREDENTIAL);
assert.equal(e.reason, reason);
for (const text of [e.message, e.reason, String(e.stack), JSON.stringify(e)]) {
assert.ok(!text.includes(SENTINEL), 'the refusal never carries the value');
assert.ok(!/gitea/.test(text), 'the refusal never names the credential');
}
return true;
}
// The start's outcome: the refusal, or "started" after closing a bus that should not have started.
const outcome = (start) => start.then(async (bus) => { await bus.close(); return 'started'; }, (e) => e);
function nothingWritten(root) {
for (const path of files(root)) {
if (path.endsWith('.token') || path.endsWith('acme.json')) continue;
assert.ok(!readFileSync(path).includes(SENTINEL), path);
}
}
test('org-defscan: the refusal code is in the TI refusal table with a next step', () => {
const entry = REFUSALS[DEFINITION_HOLDS_CREDENTIAL];
assert.ok(entry, 'definition-holds-credential is a known refusal');
assert.match(entry.rail, /^R\d+:/);
assert.match(entry.nextStep, /definition/);
assert.match(entry.nextStep, /issue the credential again/);
assert.match(entry.nextStep, /For a hire, deny it/);
// The issue-time check's fail-closed code for an organization file it cannot read.
const unreadable = REFUSALS[DEFINITION_SCAN_UNREADABLE];
assert.ok(unreadable, 'definition-scan-unreadable is a known refusal');
assert.match(unreadable.rail, /^R\d+/);
assert.match(unreadable.nextStep, /organization file/);
assert.equal(new BusError(DEFINITION_SCAN_UNREADABLE).nextStep, unreadable.nextStep);
});
test('org-defscan: a held credential in a definition\'s instructions refuses bus start, naming definition and key only', async (t) => {
const o = org(t, (entry) => ({ ...entry, instructions: `${entry.instructions}\nThe forge token is ${SENTINEL}.\n` }));
refusedWithout(await outcome(o.start()), 'definitions.builder instructions holds a credential this bus holds');
nothingWritten(o.root);
});
test('org-defscan: a held credential in a personality field refuses bus start the same way', async (t) => {
const o = org(t, (entry) => ({ ...entry, personality: { summary: `Careful. Key ${SENTINEL}.`, traits: ['plain'] } }));
refusedWithout(await outcome(o.start()), 'definitions.builder personality.summary holds a credential this bus holds');
const p = org(t, (entry) => ({ ...entry, personality: { summary: 'Careful.', traits: ['plain', `x${SENTINEL}`] } }));
refusedWithout(await outcome(p.start()), 'definitions.builder personality.traits[1] holds a credential this bus holds');
nothingWritten(o.root);
nothingWritten(p.root);
});
test('org-defscan: a definition in the file but not in the boot copy is checked too', async (t) => {
const o = org(t);
const boot = o.boot();
// Written after the load and before the bus starts: no instance names it, so only the raw read sees it.
const doc = JSON.parse(readFileSync(o.file, 'utf8'));
doc.definitions.spare = { ...doc.definitions.builder, instructions: `Spare: ${SENTINEL}\n` };
writeJson(o.file, doc);
refusedWithout(await outcome(o.start(boot)), 'definitions.spare instructions holds a credential this bus holds');
});
test('org-defscan: a clean file starts, and token-looking text the store does not hold starts', async (t) => {
for (const edit of [(entry) => entry, (entry) => ({ ...entry, instructions: `${entry.instructions}\nExample: ${UNHELD}\n`, personality: { summary: `Says ${UNHELD}.` } })]) {
const o = org(t, edit);
const bus = await o.start();
try {
// The launch check's question: a clean entry answers null, a held one names the key only.
const entry = o.boot().businesses.acme.definitions['org:builder'];
assert.deepEqual(bus.definitionScan({ business: 'acme', definition: 'org:builder', entry }), { held: null });
const hit = bus.definitionScan({ business: 'acme', definition: 'org:builder', entry: { ...entry, instructions: `x ${SENTINEL}` } });
assert.deepEqual(hit, { held: 'instructions' });
// A malformed question refuses rather than answering clean.
for (const bad of [{}, { business: 'acme', definition: 'builder', entry }, { business: 'other', definition: 'org:builder', entry },
{ business: 'acme', definition: 'org:builder', entry: null }, { business: 'acme', definition: 'org:builder', entry: [entry] }])
assert.throws(() => bus.definitionScan(bad), (e) => e.code === 'invalid-request');
} finally {
await bus.close();
}
}
});
// The coder's gitea credential as the human verb issues or rotates it.
function verbs(o, bus) {
const human = bus.broker.bindHuman({ business: 'acme', human: o.f.doc.human, via: 'cli', outsideAgent: true });
return {
issue: (token) => bus.broker.request(human, { verb: 'credentials.issue', args: { agent: 'coder', service: 'gitea', token, date: '2099-01-01' } }),
revoke: () => bus.broker.request(human, { verb: 'credentials.revoke', args: { agent: 'coder', service: 'gitea', reason: 'test' } }),
};
}
// Whether the bus holds `value` now, asked the way the launch check asks.
const holds = (bus, value) => bus.definitionScan({ business: 'acme', definition: 'org:builder', entry: { instructions: `x ${value}` } }).held !== null;
const state = (bus) => bus.credentials.status().find((e) => e.instance === 'acme/coder' && e.service === 'gitea')?.state;
function issueRefused(e, value, reason) {
assert.ok(e instanceof BusError, String(e));
assert.equal(e.code, DEFINITION_HOLDS_CREDENTIAL);
assert.equal(e.reason, reason);
for (const text of [e.message, e.reason, String(e.stack), JSON.stringify(e)]) {
assert.ok(!text.includes(value), 'the refusal never carries the value');
assert.ok(!/gitea/.test(text), 'the refusal never names the credential');
}
return true;
}
// No file under the root holds `value` except the organization file it was written into and that
// file's own history copies.
function onlyInOrganization(root, value) {
for (const path of files(root))
if (!path.endsWith('acme.json') && !path.includes('/businesses/.history/acme/')) assert.ok(!readFileSync(path).includes(value), path);
}
test('org-defscan: a credential issued between the final launch admission and spawn is refused at issue; the store stays as it was', async (t) => {
// The definition carries text that is not held at start. After the launch's final admission answers
// ready, the human rotates the coder's token to that text with the same date. On the bus before this
// fix the rotate succeeded and the agent spawned with a held value in its contract.
const FIRST = 'fake_first_token_0123456789';
const o = org(t, (entry) => ({ ...entry, instructions: `${entry.instructions}\nExample ${SENTINEL}\n` }), { held: 'fake_boot_token_5555555555' });
const boot = o.boot();
const bus = await o.start(boot);
t.after(() => bus.close());
const v = verbs(o, bus);
// A clean issue still works.
assert.equal(v.issue(FIRST).state, 'valid');
const writer = organizationWriter({ dir: boot.organization.dir, rolesDir: boot.organization.rolesDir, dataRoot: o.f.dataRoot });
const revision = writer.read('acme').revision;
const changes = files(o.root).filter((p) => p.endsWith('changes.jsonl')).map((p) => [p, readFileSync(p, 'utf8')]);
const entry = o.boot().businesses.acme.definitions['org:builder'];
assert.deepEqual(bus.definitionScan({ business: 'acme', definition: 'org:builder', entry }), { held: null });
const final = () => bus.admission({ business: 'acme', projects: [], instance: 'coder', revision });
assert.equal(final().ready, true);
// Between the admission's answer and the spawn: the rotate.
assert.throws(() => v.issue(SENTINEL), (e) => issueRefused(e, SENTINEL, 'organization acme definitions.builder instructions holds the credential being issued'));
// Nothing changed: the old token is held, the new one is not, the file and its receipts are as they were.
assert.ok(holds(bus, FIRST) && !holds(bus, SENTINEL));
assert.equal(state(bus), 'valid');
assert.equal(writer.read('acme').revision, revision);
for (const [p, text] of changes) assert.equal(readFileSync(p, 'utf8'), text, p);
onlyInOrganization(o.root, SENTINEL);
assert.equal(final().ready, true, 'the contract the agent gets holds no held value');
});
test('org-defscan: issue over a revoked entry refuses the same way for a personality field, and a clean issue then works', async (t) => {
const o = org(t, (entry) => ({ ...entry, personality: { summary: 'Careful.', traits: ['plain', `uses ${SENTINEL}`] } }), { held: 'fake_boot_token_5555555555' });
const bus = await o.start();
t.after(() => bus.close());
const v = verbs(o, bus);
v.revoke();
assert.equal(state(bus), 'revoked');
assert.throws(() => v.issue(SENTINEL), (e) => issueRefused(e, SENTINEL, 'organization acme definitions.builder personality.traits[1] holds the credential being issued'));
assert.equal(state(bus), 'revoked');
assert.ok(!holds(bus, SENTINEL));
onlyInOrganization(o.root, SENTINEL);
const issued = v.issue('fake_clean_token_4444444444');
assert.deepEqual([issued.verb, issued.state], ['issue', 'valid']);
});
test('org-defscan: a value held without the issue verb still refuses the launch at its final admission, even when the file revision is unchanged', async (t) => {
// Defence behind the issue check: the store comes to hold text already in the definition by a path
// other than the human verb, which leaves the organization file's bytes, so its revision, as they were.
const ROTATED = 'fake_rotated_token_9876543210';
const o = org(t, (entry) => ({ ...entry, instructions: `${entry.instructions}\nExample ${ROTATED}\n` }));
const boot = o.boot();
const bus = await o.start(boot);
t.after(() => bus.close());
const writer = organizationWriter({ dir: boot.organization.dir, rolesDir: boot.organization.rolesDir, dataRoot: o.f.dataRoot });
const revision = writer.read('acme').revision;
const final = () => bus.admission({ business: 'acme', projects: [], instance: 'coder', revision });
assert.equal(final().ready, true);
bus.credentials.issue('acme/coder', 'gitea', ROTATED, '2099-01-01');
assert.equal(writer.read('acme').revision, revision, 'the store change left the file as it was');
assert.deepEqual(final(), { ready: false, revision: null, code: DEFINITION_HOLDS_CREDENTIAL });
// An instance on a shipped template is not asked about a definition.
assert.equal(bus.admission({ business: 'acme', projects: [], instance: 'reviewer', revision }).ready, true);
});
test('org-defscan: issue checks the other organizations the bus serves, and refuses when one\'s file cannot be read', async (t) => {
// The bus serves acme and beta. beta's file has a definition holding text acme's coder then tries to
// be issued; later beta's file stops being JSON. Neither issue changes acme's store or file.
const o = org(t, (entry) => entry, { held: 'fake_boot_token_5555555555' });
const boot = o.boot();
const betaFile = join(o.root, 'config/businesses/beta.json');
const acme = JSON.parse(readFileSync(o.file, 'utf8'));
const spare = { ...acme.definitions.builder, personality: { summary: 'Spare.', voice: `quotes ${SENTINEL}` } };
writeJson(betaFile, { ...acme, id: 'beta', definitions: { spare } });
const bus = await o.start({ ...boot, businesses: { ...boot.businesses, beta: { ...boot.businesses.acme, id: 'beta' } } });
t.after(() => bus.close());
const v = verbs(o, bus);
const writer = organizationWriter({ dir: boot.organization.dir, rolesDir: boot.organization.rolesDir, dataRoot: o.f.dataRoot });
const revision = writer.read('acme').revision;
assert.throws(() => v.issue(SENTINEL), (e) => issueRefused(e, SENTINEL, 'organization beta definitions.spare personality.voice holds the credential being issued'));
writeFileSync(betaFile, '{ not json');
const CLEAN = 'fake_clean_token_4444444444';
assert.throws(() => v.issue(CLEAN), (e) => e instanceof BusError && e.code === DEFINITION_SCAN_UNREADABLE && !JSON.stringify(e).includes(CLEAN) && !String(e.stack).includes(CLEAN));
for (const value of [SENTINEL, CLEAN]) assert.ok(!holds(bus, value));
assert.equal(state(bus), 'valid');
assert.equal(writer.read('acme').revision, revision);
onlyInOrganization(o.root, CLEAN);
// With beta's file readable again, the clean issue goes through.
writeJson(betaFile, { ...acme, id: 'beta', definitions: {} });
assert.equal(v.issue(CLEAN).state, 'valid');
assert.ok(holds(bus, CLEAN));
});
test('org-defscan: the issue check covers every organization the bus serves, and an unreadable file refuses', () => {
const held = 'HELD-VALUE-77';
const assertClean = (value) => { if ((JSON.stringify(value) ?? '').includes(held)) throw new BusError('credential-leak'); };
const own = { id: 'acme', document: { definitions: { builder: { instructions: 'fine' } } } };
const files = { beta: { readable: true, document: { definitions: { spare: { instructions: 'fine', personality: { voice: `say ${held}` } } } } },
gamma: { readable: true, document: {} }, broken: { readable: false } };
const reads = [];
const readRaw = (id) => { reads.push(id); if (id === 'throws') throw new Error('EACCES'); return files[id]; };
assert.doesNotThrow(() => scanOrganizations({ organizations: ['acme', 'gamma'], own, readRaw }, assertClean));
assert.deepEqual(reads, ['gamma'], 'the issuing organization is the document already read');
assert.throws(() => scanOrganizations({ organizations: ['acme', 'gamma', 'beta'], own, readRaw }, assertClean),
(e) => e.code === DEFINITION_HOLDS_CREDENTIAL && e.reason === 'organization beta definitions.spare personality.voice holds the credential being issued' && !JSON.stringify(e).includes(held));
assert.throws(() => scanOrganizations({ organizations: ['acme', { ...own, id: 'x' }.id], own: { id: 'acme', document: { definitions: { [held]: {} } } }, readRaw: () => ({ readable: true, document: {} }) }, assertClean),
(e) => e.reason === 'organization acme definitions.* key holds the credential being issued' && !e.reason.includes(held));
for (const bad of ['broken', 'throws', 'missing'])
assert.throws(() => scanOrganizations({ organizations: ['acme', bad], own, readRaw }, assertClean), (e) => e.code === DEFINITION_SCAN_UNREADABLE && e.reason === undefined);
});
test('org-defscan: heldPath names the first place, a key by its parent, and never the text', () => {
const held = 'HELD-VALUE-77';
const assertClean = (value) => { if ((JSON.stringify(value) ?? '').includes(held)) throw new BusError('credential-leak'); };
assert.equal(heldPath({ instructions: 'fine', personality: { traits: ['a', 'b'] } }, assertClean), null);
assert.equal(heldPath({ instructions: 'fine', personality: { traits: ['a', `b${held}`] } }, assertClean), 'personality.traits[1]');
assert.equal(heldPath({ role: { tools: [held] } }, assertClean), 'role.tools[0]');
assert.equal(heldPath({ personality: { [held]: 'x' } }, assertClean), 'personality key');
assert.equal(heldPath({ [`${held}`]: 'x' }, assertClean), 'key');
assert.throws(() => scanDefinitions({ 'org:a': { instructions: 'ok' }, 'org:b': { instructions: held } }, assertClean, { prefix: 'org:' }),
(e) => e.code === DEFINITION_HOLDS_CREDENTIAL && e.reason === 'definitions.b instructions holds a credential this bus holds');
// A name outside the loader's pattern is not quoted.
assert.throws(() => scanDefinitions({ 'Bad Name': { instructions: held } }, assertClean), (e) => e.reason === 'definitions.* instructions holds a credential this bus holds');
assert.throws(() => scanDefinitions({ [held]: { instructions: 'ok' } }, assertClean), (e) => e.reason === 'definitions.* key holds a credential this bus holds' && !e.reason.includes(held));
assert.doesNotThrow(() => scanDefinitions(undefined, assertClean));
});
test('org-defscan: an approved hire whose definition holds a credential issued after the raise refuses before the decision is consumed', async (t) => {
// The chief raises a hire whose instructions carry text the store does not hold yet. The proposal is
// private, so the issue-time check cannot see it, and the store then comes to hold that text. The
// approval runs the same definition check as start, launch and issue: the refusal names the code, the
// decision stays open, and the organization file is as it was.
const LATER = 'fake_later_token_0123456789';
const o = org(t, (entry) => entry, { held: 'fake_boot_token_5555555555' });
const boot = o.boot();
const bus = await o.start(boot);
t.after(() => bus.close());
const chief = bus.broker.bindLaunch({ business: 'acme', role: 'chief', run: 'chief-run', harness: 'pi' });
bus.broker.request(chief, { verb: 'role.claim' });
const inline = { tools: ['read'], network: 'none', authority: { withinRole: [], crossRole: [] }, credentials: [], instructions: `Read and report.\nExample ${LATER}\n` };
const raised = await bus.broker.request(chief, { verb: 'hire.raise', args: { op: 'hire', level: 'org', name: 'scout', role: { inline }, route: { harness: 'pi', model: 'glm-5.3-flash' }, reason: 'Need help' } });
// Issued through the store, so the organization file and its revision stay as they were and the
// approval reaches the definition check rather than the revision check.
bus.credentials.issue('acme/coder', 'gitea', LATER, '2099-01-01');
assert.ok(holds(bus, LATER));
const writer = organizationWriter({ dir: boot.organization.dir, rolesDir: boot.organization.rolesDir, dataRoot: o.f.dataRoot });
const before = readFileSync(o.file);
const revision = writer.read('acme').revision;
const owner = bus.broker.bindConsole({ business: 'acme', human: o.f.doc.human, subject: { provider: 'test', iss: 'https://issuer.test', sub: 'owner' } });
const refused = (e) => {
assert.equal(e.code, DEFINITION_HOLDS_CREDENTIAL);
for (const text of [e.message, String(e.reason), String(e.stack), JSON.stringify(e)]) assert.ok(!text.includes(LATER), 'the refusal never carries the value');
return true;
};
await assert.rejects(bus.broker.request(owner, { verb: 'hire.resolve.console', args: { id: raised.id, choice: 'yes', digest: raised.hire.digest } }), refused);
// Nothing was written.
assert.deepEqual(readFileSync(o.file), before);
assert.equal(writer.read('acme').revision, revision);
assert.ok(!Object.hasOwn(writer.read('acme').document.roles, 'scout'));
// The decision was not consumed: a second approval meets the same check, not decision-closed.
await assert.rejects(bus.broker.request(owner, { verb: 'hire.resolve.console', args: { id: raised.id, choice: 'yes', digest: raised.hire.digest } }), refused);
assert.deepEqual(readFileSync(o.file), before);
// A hire with clean text, raised and approved on the same bus, is written as before.
const clean = await bus.broker.request(chief, { verb: 'hire.raise', args: { op: 'hire', level: 'org', name: 'helper', role: { inline: { ...inline, instructions: 'Read and report.\n' } }, route: { harness: 'pi', model: 'glm-5.3-flash' }, reason: 'Need help' } });
assert.equal((await bus.broker.request(owner, { verb: 'hire.resolve.console', args: { id: clean.id, choice: 'yes', digest: clean.hire.digest } })).choice, 'yes');
assert.ok(Object.hasOwn(writer.read('acme').document.roles, 'helper'));
onlyInOrganization(join(o.root, 'config/businesses'), LATER);
});
test('org-defscan: the boot copy snapshot\'s definitions (the root agents file\'s) are checked at start and at issue', async (t) => {
// A4: a root definition reaches an organization through the boot copy snapshot, which every reload
// re-applies. Its text is in no organization file, so the start and issue checks read the snapshot.
const LATER = 'fake_later_token_0123456789';
const snapshot = (instructions) => ({ org: 'acme', root: 'ok', rootAgents: {}, copies: {},
definitions: { rooted: { ...entryFromTemplate(readTemplate(REPO_ROLES, 'coder')), instructions } } });
const startWith = (o, copies, b = o.boot()) => startBroker({ dataRoot: o.f.dataRoot, businesses: b.businesses, organization: b.organization, copies });
// Held at start: the start refuses, naming the snapshot's definition and key.
const held = org(t);
refusedWithout(await outcome(startWith(held, snapshot(`Root copy ${SENTINEL}\n`))), 'boot copy definitions.rooted instructions holds a credential this bus holds');
// Not held at start: the bus starts, then issuing that text refuses and the store is as it was.
const o = org(t, (entry) => entry, { held: 'fake_boot_token_5555555555' });
const bus = await startWith(o, snapshot(`Root copy ${LATER}\n`));
t.after(() => bus.close());
assert.throws(() => verbs(o, bus).issue(LATER), (e) => issueRefused(e, LATER, 'organization acme boot copy definitions.rooted instructions holds the credential being issued'));
assert.ok(!holds(bus, LATER));
assert.equal(state(bus), 'valid');
assert.equal(verbs(o, bus).issue('fake_clean_token_4444444444').state, 'valid');
});