Files
stack/packages/cli/tests/definition-scan.test.mjs
jason.woltje 8fec494694 fix(organization): bus start and launch check org definitions against the held credentials (#1873)
T1 follow-up. A definition's text becomes an agent's contract, but a load ran the
held-secret check with an empty store. Bus start now checks every definitions entry
(the boot copy's org: entries and the file's own) with the live store, and the
launch check asks the bus the same question for the org: definition it just read,
before a contract exists. A hit refuses as definition-holds-credential, a new code
in the refusal table; the refusal names the definition and key, never the text or
the credential. bus start records the refusal in the boot status.
2026-11-11 08:49:15 -06:00

130 lines
8.0 KiB
JavaScript

// #1873 T1 follow-up (org-defscan): a definition that holds a credential the bus holds refuses at
// launch (the launch check asks the bus before a contract exists) and at `mosaic bus start`, and the
// value reaches no refusal, record or log.
import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { createLauncher } from '../src/launcher.mjs';
import { bootConfig, loadSystem } from '../src/config.mjs';
import { readBootStatus, bootEventsFile } from '../src/boot-status.mjs';
import { hostFile } from '../src/host.mjs';
import { startBroker } from '../../bus/src/runtime.mjs';
import { initializeControl } from '../../processing-control/src/state.mjs';
import { launchLogFile } from '../../seat/src/session.mjs';
import { entryFromTemplate, readTemplate } from '../../organization/src/definitions.mjs';
import { reserveOrg, activateOrg } from '../../organization/src/org-registry.mjs';
import { REPO_ROLES, writeJson } from '../../organization/tests/helpers.mjs';
import { fixture, notifyConfig, tmp, withoutHostCredentials } from './helpers.mjs';
// The fixture bus holds SENTINEL as coder's gitea token.
const SENTINEL = 'sk-test-NOT-A-SECRET-1234';
const CLI = fileURLToPath(new URL('../src/cli.mjs', import.meta.url));
function files(dir) {
if (!existsSync(dir)) return [];
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? files(join(dir, e.name)) : e.isFile() ? [join(dir, e.name)] : []);
}
const withBuilder = (builder) => (doc) => ({ ...doc, definitions: { builder }, roles: { ...doc.roles, coder: { ...doc.roles.coder, definition: 'org:builder' } } });
test('org-defscan: a file edited after start to hold a held credential refuses the next launch of an instance naming it; a template launch still works', async (t) => {
const root = tmp(t);
const f = fixture(root, 'acme', withBuilder(entryFromTemplate(readTemplate(REPO_ROLES, 'coder'))));
const file = join(root, 'config/businesses/acme.json');
writeFileSync(f.doc.roles.coder.credentials.gitea.file, SENTINEL);
withoutHostCredentials(f.env, { piKey: true });
initializeControl(f.dataRoot);
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: 'acme', env: f.env });
const bus = await startBroker({ dataRoot: f.dataRoot, businesses: boot.businesses, organization: boot.organization });
t.after(() => bus.close());
// The real host's op path for the scan, over a structured clone as IPC carries it; the fixture
// stops at authorization, so "reached authorizeLaunch" is "would have launched".
const ops = [];
const host = { business: 'acme', beforeClose: () => {}, op: async (x) => {
ops.push(x.op);
if (x.op === 'identity') return { business: 'acme', role: 'pm' };
if (x.op === 'mayLaunch') return { allow: true };
if (x.op === 'authorizeLaunch') throw Object.assign(Error('fixture authorization stop'), { code: 'fixture-policy' });
if (x.op === 'definitionScan') return structuredClone(bus.definitionScan(structuredClone({ business: x.business, definition: x.definition, entry: x.entry })));
return {};
} };
const launcher = createLauncher({ host, system: JSON.parse(readFileSync(f.config, 'utf8')), env: f.env, bwrap: '/usr/bin/true', log: () => {} });
t.after(() => launcher.close?.());
// Clean: the scan passes and the launch reaches authorization.
assert.equal((await launcher.fromSession('fixture-cap', 'coder')).error, 'fixture-policy');
assert.ok(ops.includes('definitionScan'));
// Edited by hand while the bus runs.
const doc = JSON.parse(readFileSync(file, 'utf8'));
doc.definitions.builder.instructions += `\nThe forge token is ${SENTINEL}.\n`;
writeJson(file, doc);
ops.length = 0;
assert.deepEqual(await launcher.fromSession('fixture-cap', 'coder'), { ok: false, error: 'definition-holds-credential' });
assert.ok(!ops.includes('authorizeLaunch'), 'nothing launches');
const event = JSON.parse(readFileSync(launchLogFile(f.dataRoot, 'acme'), 'utf8').trim().split('\n').at(-1));
assert.equal(event.code, 'definition-holds-credential');
assert.equal(event.instance, 'coder');
assert.match(event.why, /coder's definition org:builder holds a credential in instructions/);
// A personality field is checked the same way.
doc.definitions.builder.instructions = doc.definitions.builder.instructions.replace(SENTINEL, 'REDACTED');
doc.definitions.builder.personality = { summary: 'Plain.', voice: `Signs off with ${SENTINEL}` };
writeJson(file, doc);
assert.deepEqual(await launcher.fromSession('fixture-cap', 'coder'), { ok: false, error: 'definition-holds-credential' });
assert.match(JSON.parse(readFileSync(launchLogFile(f.dataRoot, 'acme'), 'utf8').trim().split('\n').at(-1)).why, /in personality\.voice$/);
// An instance on a shipped template launches as before, and asks no scan.
ops.length = 0;
assert.equal((await launcher.fromSession('fixture-cap', 'reviewer')).error, 'fixture-policy');
assert.ok(!ops.includes('definitionScan'));
// The value reached no record the bus or the launcher wrote.
for (const path of files(f.dataRoot)) assert.ok(!readFileSync(path).includes(SENTINEL), path);
});
// A bootstrapped instance with an active organization whose file names org:builder.
async function orgFixture(t, builder) {
const root = tmp(t, 'o-');
const f = fixture(root, 'acme', withBuilder(builder));
const configDir = join(root, '.config', 'mosaic-boot');
const env = { ...f.env, HOME: root, MOSAIC_CONFIG: join(configDir, 'config.json') };
const bootstrap = spawnSync(join(process.cwd(), 'scripts/bootstrap.sh'), ['--instance', 'boot'], { env, encoding: 'utf8' });
assert.equal(bootstrap.status, 0, bootstrap.stderr);
mkdirSync(join(configDir, 'businesses'), { recursive: true, mode: 0o700 });
const file = join(configDir, 'businesses', 'acme.json');
writeFileSync(file, readFileSync(join(root, 'config/businesses/acme.json')), { mode: 0o600 });
writeFileSync(f.doc.roles.coder.credentials.gitea.file, SENTINEL);
const dataRoot = join(root, '.mosaic-boot');
const ctx = { configDir, dataRoot, instance: 'boot' };
const org = await reserveOrg(ctx, 'acme');
await activateOrg(ctx, 'acme');
// A notifier config, so a start that passes the load goes on to start the broker.
notifyConfig(org.root, 'acme', null);
return { env, file, dataRoot, orgRoot: org.root };
}
test('org-defscan: mosaic bus start refuses a held credential in a definition, records why, and the value is nowhere', async (t) => {
const builder = entryFromTemplate(readTemplate(REPO_ROLES, 'coder'));
const o = await orgFixture(t, { ...builder, instructions: `${builder.instructions}\nThe forge token is ${SENTINEL}.\n` });
const run = (...args) => spawnSync(process.execPath, [CLI, ...args], { env: o.env, encoding: 'utf8', timeout: 60000 });
const r = run('bus', 'start', 'acme', '--detach', '--no-launch');
// Should a start get through (it must not), stop it before the fixture's directory goes.
if (r.status === 0 || existsSync(hostFile(o.orgRoot))) run('bus', 'stop', 'acme');
assert.equal(r.status, 3, r.stderr);
assert.ok(!existsSync(join(o.orgRoot, 'bus', 'broker.sock')), 'no socket');
const s = readBootStatus(o.orgRoot);
assert.equal(s.ok, false);
assert.equal(s.business, 'acme');
assert.match(s.reason, /definition-holds-credential: definitions\.builder instructions holds a credential this bus holds/);
assert.equal(s.lastGood, null, 'the load that the broker then refused is not a good load');
const events = readFileSync(bootEventsFile(o.orgRoot), 'utf8').trim().split('\n').map(JSON.parse);
assert.equal(events.at(-1).event, 'bus.boot.refused');
const status = run('bus', 'status', 'acme', '--json');
assert.equal(JSON.parse(status.stdout).boot.ok, false);
for (const text of [r.stdout, r.stderr, status.stdout, status.stderr]) assert.ok(!text.includes(SENTINEL));
for (const path of files(o.dataRoot)) assert.ok(!readFileSync(path).includes(SENTINEL), path);
});