// #1873 SP8: saving a template at the bus. Only the organization's owner saves or lists: the CLI's // human cap on the unsuffixed names, the owner's console cap on the .console ones. The saved file // applies through SP4 as saved: for its saver. import test from 'node:test'; import assert from 'node:assert/strict'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { CONSOLE_VERBS, TEMPLATE_BUS_KEYS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs'; import { REFUSALS } from '../src/refusals.mjs'; import { VERB_RIGHTS } from '../../organization/src/org-rights.mjs'; const TOKEN = 'fixture-held-token-sp8-bus-3a9d'; const SUBJECT = { provider: 'test', iss: 'test', sub: 'owner' }; async function harness(t, { mutate = () => {} } = {}) { const { startBroker } = await import('../src/runtime.mjs'); const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../organization/tests/helpers.mjs'); const { entryFromTemplate, readTemplate } = await import('../../organization/src/definitions.mjs'); const root = mkdtempSync(join(tmpdir(), 'tmpl-')); const rolesDir = rolesCopy(root); const doc = businessDoc(root, 'demo'); doc.plugins = { harness: ['pi'] }; const writer = entryFromTemplate(readTemplate(rolesDir, 'coder')); writer.instructions = 'Write code for demo. Ask jason.'; doc.definitions = { writer }; // An instance of the org definition, with its own token files and tracker bot. doc.roles.ana = { ...structuredClone(doc.roles.coder), definition: 'org:writer', tracker: { bot: 'bot-demo-ana', botId: 11 } }; mutate(doc, root); writeJson(join(root, 'businesses/demo.json'), doc); const data = join(root, 'data'); mkdirSync(data, { mode: 0o700 }); const token = join(root, 'held.token'); writeFileSync(token, TOKEN, { mode: 0o600 }); chmodSync(token, 0o600); const roles = { chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] }, credentials: { gitea: { file: token, rotateBy: '2099-01-01' } } } }; const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles } }; const bus = await startBroker({ dataRoot: data, businesses, organization: { dir: root, rolesDir, system: systemFor(root) } }); t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); }); const b = bus.broker; const consoleOwner = b.bindConsole({ business: 'demo', human: 'jason', subject: SUBJECT }); const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }); const call = async (verb, args = {}, cap = consoleOwner) => b.request(cap, { verb, args }); const path = join(root, 'businesses/demo.json'); const saved = (name, account = 'jason') => join(root, 'templates', account, `${name}.json`); return { bus, b, root, rolesDir, path, consoleOwner, human, call, saved }; } // Preview, then save with the preview's digest: the console names by default, the cli names for cap. const save = async (call, name, extra = {}, cap) => { const verb = cap ? 'template.save' : 'template.save.console'; const preview = await call(verb, { name, preview: true, ...extra }, cap); return { preview, out: await call(verb, { name, digest: preview.digest, ...extra }, cap) }; }; test('SP8 red before: the owner saves an organization whose instance holds credentials; neither credentials nor copy material reach the file', async t => { const { call, saved, root } = await harness(t); const { preview, out } = await save(call, 'demo-shape'); assert.deepEqual(out, { name: 'demo-shape', scope: 'org', digest: preview.digest }); const text = readFileSync(saved('demo-shape'), 'utf8'); assert.equal(text, preview.json); for (const k of ['copyBindings', 'copyId', 'orgIncarnation', 'generation', 'tracker', 'holder']) assert.ok(!text.includes(`"${k}"`), k); assert.ok(!text.includes(join(root, 'secrets')), 'a token file path reached the template'); assert.ok(!text.includes('"file"')); // The role's credential request lines are kept. assert.ok(JSON.parse(text).definitions.writer.role.credentials.length > 0); }); test('SP8 a saved template applies through SP4 as saved: for its saver', async t => { const { call, path } = await harness(t); await save(call, 'demo-shape'); const listed = await call('template.list.console'); assert.deepEqual(listed.templates.map(x => [x.name, x.scope]), [['demo-shape', 'org']]); const preview = await call('pack.preview.console', { pack: 'saved:demo-shape' }); assert.equal(preview.pack.origin, 'saved'); assert.ok(preview.hires.some(h => h.name === 'coder-1'), JSON.stringify(preview.hires.map(h => h.name))); const before = JSON.parse(readFileSync(path, 'utf8')); const out = await call('pack.apply.console', { pack: 'saved:demo-shape', digest: preview.digest }); const after = JSON.parse(readFileSync(path, 'utf8')); assert.equal(after.packs.length, (before.packs ?? []).length + 1); assert.equal(after.packs.at(-1).origin, 'saved'); assert.equal(after.packs.at(-1).id, 'demo-shape'); // writer is already the organization's own (same); the template references the coder template. assert.ok(out.added.includes('definitions.coder'), JSON.stringify(out.added)); assert.equal(preview.hires.find(h => h.name === 'coder-1').definition, 'writer'); }); test('SP8 the owner\'s CLI cap saves on the unsuffixed name and writes the file', async t => { const { call, human, saved } = await harness(t); const { out } = await save(call, 'cli-shape', {}, human); assert.equal(out.name, 'cli-shape'); assert.ok(existsSync(saved('cli-shape'))); assert.deepEqual((await call('template.list', {}, human)).templates.map(x => x.name), ['cli-shape']); }); test('SP8 who may call: a member\'s session, an agent and the tester refuse; the owner\'s console cap cannot call the cli name', async t => { const { b, call, human, root } = await harness(t); assert.deepEqual(TESTER_ORGANIZATION_VERBS, ['limits', 'plugins', 'instances', 'plugins.set', 'instances.set', 'routes.set', 'routes']); const member = b.bindHireConsole({ business: 'demo', actor: 'alice', subject: { provider: 'test', iss: 'test', sub: 'alice' }, rights: ['org.hire', 'board.read', 'org.setup'] }); for (const verb of ['template.save', 'template.save.console']) assert.throws(() => b.request(member, { verb, args: { name: 'x', preview: true } }), /human-required/); for (const verb of ['template.list', 'template.list.console']) assert.throws(() => b.request(member, { verb, args: {} }), /human-required/); const tester = b.bindTester({ business: 'demo' }); assert.throws(() => b.request(tester, { verb: 'template.save', args: { name: 'x', preview: true } }), /tester-refused/); const agent = b.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi', address: 'coder-run' }); b.request(agent, { verb: 'role.claim' }); for (const verb of ['template.save', 'template.save.console']) assert.throws(() => b.request(agent, { verb, args: { name: 'x', preview: true } }), /human-required/); await assert.rejects(call('template.save', { name: 'x', preview: true }), /human-required/); await assert.rejects(call('template.save.console', { name: 'x', preview: true }, human), /human-required/); assert.ok(!existsSync(join(root, 'templates'))); }); test('SP8 the real credential store: a held value in kept text refuses template-secret-suspected with the field path as its reason', async t => { // A definition holding one never loads (definition-holds-credential); workflow text is what save // must catch itself. const workflow = { description: `Ship it, key ${TOKEN}`, steps: [{ title: 'Write', state: 'todo', roles: ['writer'] }] }; const { call, root } = await harness(t, { mutate: doc => { doc.workflows = { change: workflow }; } }); await assert.rejects(call('template.save.console', { name: 'leaky', preview: true }), (e) => { assert.equal(e.code, 'template-secret-suspected'); assert.equal(e.reason, 'workflows.change.description'); assert.ok(!String(e.message).includes(TOKEN) && !JSON.stringify(e).includes(TOKEN)); return true; }); assert.ok(!existsSync(join(root, 'templates'))); }); test('SP8 a key that holds a held value: the refusal reason masks it as [key], never the value', async t => { // A definition keyed by one never loads (definition-holds-credential); a workflow keyed by one is // what save sees. The broker also drops any reason its credential check refuses. const workflow = { description: 'Ship it.', steps: [{ title: 'Write', state: 'todo', roles: ['writer'] }] }; const { call, root } = await harness(t, { mutate: doc => { doc.workflows = { [TOKEN]: workflow }; } }); await assert.rejects(call('template.save.console', { name: 'leaky', preview: true }), (e) => { assert.equal(e.code, 'template-secret-suspected'); assert.equal(e.reason, 'workflows[key]'); assert.ok(!String(e.message).includes(TOKEN) && !JSON.stringify(e).includes(TOKEN)); return true; }); assert.ok(!existsSync(join(root, 'templates'))); }); test('SP8 a save after a real pack.apply.console keeps each packs entry as {id, version}', async t => { const { call, saved } = await harness(t); const packs = await call('packs.console'); const shipped = packs.packs.find(p => p.origin === 'shipped'); assert.ok(shipped, JSON.stringify(packs.packs.map(p => [p.id, p.origin]))); const pack = await call('pack.preview.console', { pack: shipped.id }); await call('pack.apply.console', { pack: shipped.id, digest: pack.digest }); await save(call, 'after-apply'); const kept = JSON.parse(readFileSync(saved('after-apply'), 'utf8')).packs; assert.deepEqual(kept, [{ id: shipped.id, version: pack.pack.version }]); }); test('SP8 argument checks: a second save, a .. name, an unknown key, preview with a digest, and a stale digest', async t => { const { call, saved } = await harness(t); const { preview } = await save(call, 'demo-shape'); const first = readFileSync(saved('demo-shape')); await assert.rejects(call('template.save.console', { name: 'demo-shape', digest: preview.digest }), (e) => e.code === 'template-exists' && e.reason === 'a template of this name is already saved; save under another name'); assert.deepEqual(readFileSync(saved('demo-shape')), first); for (const name of ['..', '../x', 'a/b']) await assert.rejects(call('template.save.console', { name, preview: true }), { code: 'invalid-request' }); for (const args of [{ name: 'x', preview: true, account: 'alex' }, { name: 'x', preview: true, digest: preview.digest }, { name: 'x' }, { preview: true }, { name: 'x', preview: false }]) await assert.rejects(call('template.save.console', args), { code: 'invalid-request' }); await assert.rejects(call('template.list.console', { account: 'alex' }), { code: 'invalid-request' }); await assert.rejects(call('template.save.console', { name: 'other', digest: preview.digest }), { code: 'pack-plan-stale' }); }); test('SP8 the schema pins: both .console verbs are console verbs, owner-only in VERB_RIGHTS, and template-secret-suspected has a rail', () => { for (const verb of ['template.save.console', 'template.list.console']) { assert.ok(CONSOLE_VERBS.includes(verb), verb); assert.ok(Object.hasOwn(VERB_RIGHTS, verb) && VERB_RIGHTS[verb] === null, verb); assert.ok(Object.hasOwn(TEMPLATE_BUS_KEYS, verb)); } assert.ok(!CONSOLE_VERBS.includes('template.save') && !CONSOLE_VERBS.includes('template.list')); const c = 'template-secret-suspected'; assert.ok(Object.hasOwn(REFUSALS, c)); assert.ok(REFUSALS[c].rail?.length > 5); assert.ok(REFUSALS[c].nextStep?.length > 20); assert.ok(!/(^|[\s("'`])(\/|~\/)/.test(REFUSALS[c].nextStep)); });