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.
130 lines
8.0 KiB
JavaScript
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);
|
|
});
|