// #1873 T1: org: 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(); 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); });