Files
jason.woltje 13560660ae fix(packs): SP8 review round, single-pass placeholders, CLI hints, pins (#1873)
- template-save: one alternation pass replaces every literal, so an
  instance named root, org or agent never rewrites a placeholder already
  written in. checkPack's refusal message is passed on as the reason. A
  write or fsync failure removes the partial file it created.
- broker #templateFail drops a reason the credential check refuses.
- cli template words: humanTransport throws a CliError named by the bus
  code; a known code now gains its hint (the test transport throws the
  same CliError).
- template-http: the header notes serve.mjs strips /api/orgs/<id>.
- pins: the two .console verbs in console-decide and request-console;
  the packages/business/src re-export for template-save.mjs.
- tests: placeholder collision, a held-value key masked as [key], and a
  save after a real pack.apply.console keeping {id, version}.
2026-11-12 20:05:32 -06:00

179 lines
12 KiB
JavaScript

// #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:<name> 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:<name> 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 === 'invalid-request' && e.reason === 'a template of this name is already saved');
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));
});