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.
139 lines
9.7 KiB
JavaScript
139 lines
9.7 KiB
JavaScript
// #1873 T1: org:<name> definitions at launch, and `mosaic org definitions`.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { join } from 'node:path';
|
|
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { createLauncher } from '../src/launcher.mjs';
|
|
import { main } from '../src/cli.mjs';
|
|
import { orgCommand } from '../src/org.mjs';
|
|
import { bootConfig, loadSystem } from '../src/config.mjs';
|
|
import { BusError } from '../../bus/src/broker.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, templateHash } from '../../organization/src/definitions.mjs';
|
|
import { REPO_ROLES, writeJson } from '../../organization/tests/helpers.mjs';
|
|
import { fixture, io, tmp, withoutHostCredentials } from './helpers.mjs';
|
|
|
|
const SCOUT = { rev: 1, role: { tools: ['read'], network: 'none', authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: 'Read and report.\n' };
|
|
const stop = () => Object.assign(Error('fixture authorization stop'), { code: 'fixture-policy' });
|
|
const host = { business: 'acme', beforeClose: () => {}, op: async (x) => {
|
|
if (x.op === 'identity') return { business: 'acme', role: 'pm' };
|
|
if (x.op === 'mayLaunch') return { allow: true };
|
|
if (x.op === 'authorizeLaunch') throw stop();
|
|
if (x.op === 'definitionScan') return { held: null };
|
|
return {};
|
|
} };
|
|
|
|
test('T1 an org: instance passes every launch check, and a missing definition refuses naming instance and definition', async (t) => {
|
|
const root = tmp(t);
|
|
// The org's own copy of the coder template, so the instance's credentials and tracker still match.
|
|
const builder = entryFromTemplate(readTemplate(REPO_ROLES, 'coder'));
|
|
const f = fixture(root, 'acme', (doc) => ({ ...doc, definitions: { builder }, roles: { ...doc.roles, coder: { ...doc.roles.coder, definition: 'org:builder' } } }));
|
|
const file = join(root, 'config/businesses/acme.json');
|
|
withoutHostCredentials(f.env, { piKey: true });
|
|
initializeControl(f.dataRoot);
|
|
const launcher = createLauncher({ host, system: JSON.parse(readFileSync(f.config, 'utf8')), env: f.env, bwrap: '/usr/bin/true', log: () => {} });
|
|
// Resolve, route, contract (the definition's instructions) and skills all pass; the fixture stops at authorization.
|
|
assert.equal((await launcher.fromSession('fixture-cap', 'coder')).error, 'fixture-policy');
|
|
|
|
// The chief names a definition the file doesn't declare: the whole file refuses, so a session's
|
|
// launch and the chief's launch both refuse.
|
|
writeJson(file, { ...f.doc, roles: { ...f.doc.roles, chief: { ...f.doc.roles.chief, definition: 'org:ghost' } } });
|
|
assert.deepEqual(await launcher.fromSession('fixture-cap', 'coder'), { ok: false, error: 'organization-refused' });
|
|
await assert.rejects(launcher.launchPm(), (e) => e.code === 'organization-refused' && /roles\.chief\.definition names org:ghost/.test(e.message));
|
|
const event = JSON.parse(readFileSync(launchLogFile(f.dataRoot, 'acme'), 'utf8').trim().split('\n').at(-1));
|
|
assert.equal(event.code, 'organization-refused');
|
|
assert.equal(event.instance, 'chief');
|
|
assert.match(event.why, /definition-missing: .*roles\.chief\.definition names org:ghost/);
|
|
await launcher.close?.();
|
|
|
|
// A host does not start on a file that names a missing definition.
|
|
assert.throws(() => createLauncher({ host, system: JSON.parse(readFileSync(f.config, 'utf8')), env: f.env, bwrap: '/usr/bin/true', log: () => {} }), /org:ghost/);
|
|
});
|
|
|
|
// The fixture bus holds SENTINEL as coder's gitea token, so the bus's secret check refuses it.
|
|
const SENTINEL = 'sk-test-NOT-A-SECRET-1234';
|
|
function files(dir) {
|
|
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? files(join(dir, e.name)) : e.isFile() ? [join(dir, e.name)] : []);
|
|
}
|
|
|
|
test('T1 mosaic org definitions: writes go through the bus and its secret check, each prints its receipt, and an agent shell refuses', async (t) => {
|
|
const root = tmp(t);
|
|
const builder = entryFromTemplate(readTemplate(REPO_ROLES, 'coder'));
|
|
const f = fixture(root, 'acme', (doc) => ({ ...doc, definitions: { builder }, roles: { ...doc.roles, coder: { ...doc.roles.coder, definition: 'org:builder' } } }));
|
|
const file = join(root, 'config/businesses/acme.json');
|
|
const changes = join(root, 'config/businesses/.history/acme/changes.jsonl');
|
|
writeFileSync(f.doc.roles.coder.credentials.gitea.file, SENTINEL);
|
|
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: 'acme', env: f.env });
|
|
const b = await startBroker({ dataRoot: f.dataRoot, businesses: boot.businesses, organization: boot.organization });
|
|
t.after(() => b.close());
|
|
const human = b.broker.bindHuman({ business: 'acme', human: f.doc.human, via: 'cli', outsideAgent: true });
|
|
const calls = [];
|
|
// The human transport's shape: a bus refusal arrives as its code.
|
|
const deps = { system: { dataRoot: f.dataRoot }, transport: () => async (verb, args = {}) => {
|
|
calls.push(verb);
|
|
try { return structuredClone(b.broker.request(human, { verb, args })); }
|
|
catch (e) { if (!(e instanceof BusError)) throw e; throw Object.assign(new Error(e.code), { code: e.code }); }
|
|
} };
|
|
const run = async (args, env = f.env) => { const x = io(env); await main(['org', 'definitions', ...args], x, deps); return JSON.parse(x.out.text); };
|
|
const hash = templateHash(f.roles, 'coder');
|
|
assert.ok((await run(['templates'])).some((h) => h.template === 'coder' && h.sha256 === hash));
|
|
|
|
const imported = await run(['acme', 'import', 'coder', '--as', 'copy']);
|
|
assert.deepEqual({ ...imported, revision: 0, transaction: 0 }, { verb: 'import', organization: 'acme', definition: 'copy', rev: 1, revision: 0, transaction: 0, from: { template: 'coder', sha256: hash } });
|
|
const spec = join(root, 'scout.json');
|
|
writeFileSync(spec, JSON.stringify({ role: SCOUT.role, instructions: SCOUT.instructions }));
|
|
assert.equal((await run(['acme', 'invent', 'scout', '--from', spec])).rev, 1);
|
|
writeFileSync(spec, JSON.stringify({ role: SCOUT.role, instructions: 'Read, then report.\n' }));
|
|
const edited = await run(['acme', 'edit', 'scout', '--from', spec, '--if-rev', '1']);
|
|
assert.equal(edited.verb, 'edit');
|
|
assert.equal(edited.rev, 2);
|
|
assert.deepEqual((await run(['acme', 'list'])).definitions.map((d) => [d.name, d.rev]), [['builder', 1], ['copy', 1], ['scout', 2]]);
|
|
assert.equal((await run(['acme', 'show', 'scout'])).instructions, 'Read, then report.\n');
|
|
assert.equal((await run(['acme', 'diff', 'copy'])).changed, false);
|
|
assert.equal((await run(['acme', 'remove', 'scout', '--if-rev', '2'])).removedRev, 2);
|
|
assert.deepEqual(calls, ['definition.import', 'definition.invent', 'definition.edit', 'definition.remove']);
|
|
const committed = readFileSync(changes, 'utf8').trim().split('\n').map((l) => JSON.parse(l)).filter((e) => e.state === 'committed').slice(-4);
|
|
assert.deepEqual(committed.map((e) => e.actor), Array(4).fill(f.doc.human));
|
|
|
|
// A definition holding a token the bus holds refuses on invent and on edit, in valid JSON, and
|
|
// the token reaches no message, no organization file and nothing the bus wrote.
|
|
const before = readFileSync(file);
|
|
const beforeChanges = readFileSync(changes);
|
|
writeFileSync(spec, JSON.stringify({ role: SCOUT.role, instructions: `Use ${SENTINEL} to report.\n` }));
|
|
for (const args of [['acme', 'invent', 'leak', '--from', spec], ['acme', 'edit', 'copy', '--from', spec, '--if-rev', '1']]) {
|
|
await assert.rejects(run(args), (e) => e.code === 'credential-leak' && e.exitCode === 2 && !e.message.includes(SENTINEL) && !String(e.stack).includes(SENTINEL));
|
|
}
|
|
assert.deepEqual(readFileSync(file), before);
|
|
assert.deepEqual(readFileSync(changes), beforeChanges);
|
|
for (const path of files(f.dataRoot)) assert.ok(!readFileSync(path).includes(SENTINEL), path);
|
|
|
|
// An unparsable file never echoes; a key the file's checks refuse is named before the bus is
|
|
// called; a stale --if-rev and an in-use definition refuse with what they mean.
|
|
writeFileSync(spec, `{"role": ${SENTINEL}`);
|
|
await assert.rejects(run(['acme', 'invent', 'other', '--from', spec]), (e) => e.exitCode === 2 && !e.message.includes('sk-test'));
|
|
writeFileSync(spec, JSON.stringify({ role: { ...SCOUT.role, network: 'everywhere' }, instructions: 'x\n' }));
|
|
const asked = calls.length;
|
|
await assert.rejects(run(['acme', 'invent', 'other', '--from', spec]), /definitions\.other/);
|
|
assert.equal(calls.length, asked);
|
|
await assert.rejects(run(['acme', 'remove', 'copy', '--if-rev', '7']), (e) => /^definition-rev-conflict: definition copy is not at rev 7/.test(e.message));
|
|
await assert.rejects(run(['acme', 'remove', 'builder', '--if-rev', '1']), (e) => /^definition-in-use: definition builder is named by coder/.test(e.message));
|
|
assert.deepEqual(readFileSync(file), before);
|
|
|
|
// Inside an agent run every write refuses before it reads anything or calls the bus; reads still work.
|
|
const agent = { ...f.env, MOSAIC_RUN_ID: 'run-1' };
|
|
const sent = calls.length;
|
|
for (const args of [['acme', 'import', 'reviewer'], ['acme', 'invent', 'x', '--from', spec], ['acme', 'edit', 'copy', '--from', spec, '--if-rev', '1'], ['acme', 'remove', 'copy', '--if-rev', '1']]) {
|
|
await assert.rejects(run(args, agent), (e) => e.exitCode === 3 && /human shell/.test(e.message));
|
|
}
|
|
assert.equal(calls.length, sent);
|
|
assert.deepEqual(readFileSync(file), before);
|
|
assert.equal((await run(['acme', 'list'], agent)).definitions.length, 2);
|
|
|
|
// With no bus call to make, a write refuses rather than writing without the secret check.
|
|
const x = io(f.env);
|
|
await assert.rejects(orgCommand(['definitions', 'acme', 'remove', 'copy', '--if-rev', '1'], x), (e) => e.exitCode === 3 && /organization bus/.test(e.message));
|
|
assert.deepEqual(readFileSync(file), before);
|
|
});
|