// #1873 T1: organization definitions and templates (roles-and-rails design sections 4, 5 and 13). import test from "node:test"; import assert from "node:assert/strict"; import * as fs from "node:fs"; import { syncBuiltinESMExports } from "node:module"; import { appendFileSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { organizationWriter } from "../src/organization.mjs"; import { validateBusinessDocument } from "../src/organization-config.mjs"; import { definitionVerbs, DEFINITION_VERBS } from "../src/definition-verbs.mjs"; import { readTemplate, templateHash, templateHashes, diffDefinition, lineDiff } from "../src/definitions.mjs"; import { resolveInstance } from "../src/resolve.mjs"; import { ROLE_RIGHTS, VERB_RIGHTS, normalizeRights } from "../src/org-rights.mjs"; import { businessDoc, rolesCopy, systemFor, tmp, writeJson } from "./helpers.mjs"; const SECRET = "sk-test-NOT-A-SECRET-1234"; // The credential store's check, as if it held SECRET. const assertClean = (value) => { if (String(value).includes(SECRET)) throw new Error(`credential-leak ${value}`); }; const OWNER = { actor: "jason", rights: [...ROLE_RIGHTS.owner], agent: false }; const MEMBER = { actor: "alice", rights: [...ROLE_RIGHTS.member], agent: false }; const HIRER = { actor: "alice", rights: normalizeRights(ROLE_RIGHTS.member, { add: ["org.hire"] }), agent: false }; const PLAIN = { role: { tools: ["read"], network: "none", authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: "Read and report.\n" }; function fixture(t, edit = (doc) => doc) { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const dir = join(root, "config"); const rolesDir = rolesCopy(root); const file = writeJson(join(dir, "businesses/acme.json"), edit(businessDoc(root))); const writer = organizationWriter({ dir, rolesDir }); return { root, dir, file, rolesDir, writer, verbs: definitionVerbs({ writer, rolesDir, assertClean }) }; } const rev = (f) => f.writer.read("acme").revision; const doc = (f) => JSON.parse(readFileSync(f.file, "utf8")); test("T1 import pins the template hash and copies its primitives and contract", (t) => { const f = fixture(t); const hash = templateHash(f.rolesDir, "coder"); assert.match(hash, /^[0-9a-f]{64}$/); const receipt = f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER); assert.deepEqual(Object.keys(receipt).sort(), ["definition", "from", "organization", "rev", "revision", "transaction", "verb"]); assert.equal(receipt.rev, 1); assert.deepEqual(receipt.from, { template: "coder", sha256: hash }); const copy = doc(f).definitions.coder; assert.deepEqual(copy.from, { template: "coder", sha256: hash }); assert.equal(copy.rev, 1); assert.equal(copy.instructions, readFileSync(join(f.rolesDir, "coder.md"), "utf8")); const template = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8")); assert.deepEqual(copy.role.tools, template.tools); assert.equal(copy.role.network, template.network); assert.equal(receipt.revision, rev(f)); // The hash read lists every version 2 template, and only those. const hashes = templateHashes(f.rolesDir); assert.ok(hashes.some((h) => h.template === "coder" && h.sha256 === hash)); assert.ok(!hashes.some((h) => h.template === "conductor-policy")); // --as names the copy. const second = f.verbs.import("acme", { revision: rev(f), template: "coder", as: "coder-two" }, OWNER); assert.equal(second.definition, "coder-two"); assert.throws(() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER), { code: "definition-exists" }); }); test("T1 the hash covers the template JSON and its contract together", (t) => { const f = fixture(t); const before = templateHash(f.rolesDir, "coder"); appendFileSync(join(f.rolesDir, "coder.md"), "\nOne more line.\n"); const contractChanged = templateHash(f.rolesDir, "coder"); assert.notEqual(contractChanged, before); const json = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8")); writeJson(join(f.rolesDir, "coder.json"), { ...json, title: `${json.title} (renamed)` }, 0o644); assert.notEqual(templateHash(f.rolesDir, "coder"), contractChanged); }); test("T1 editing a template file changes no copy, and the diff read shows the change", (t) => { const f = fixture(t); f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER); const bytes = readFileSync(f.file); const pinned = doc(f).definitions.coder.from.sha256; assert.deepEqual(f.verbs.diff("acme", "coder"), { organization: "acme", revision: rev(f), definition: "coder", rev: 1, from: { template: "coder", sha256: pinned }, current: pinned, changed: false, primitives: [], instructions: [] }); appendFileSync(join(f.rolesDir, "coder.md"), "Always run the suites twice.\n"); const json = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8")); writeJson(join(f.rolesDir, "coder.json"), { ...json, network: "none" }, 0o644); assert.notEqual(json.network, "none", "the fixture must change network"); // No copy changed, and the file still loads. assert.deepEqual(readFileSync(f.file), bytes); assert.equal(f.writer.read("acme").document.definitions.coder.from.sha256, pinned); const diff = f.verbs.diff("acme", "coder"); assert.equal(diff.changed, true); assert.equal(diff.current, templateHash(f.rolesDir, "coder")); assert.notEqual(diff.current, pinned); assert.deepEqual(diff.primitives, ["network", "instructions"]); assert.deepEqual(diff.instructions.filter((l) => l.op !== " "), [{ op: "+", line: "Always run the suites twice." }]); // Nothing applied. assert.deepEqual(readFileSync(f.file), bytes); }); test("T1 diff read: an invented definition has nothing to compare, a missing template is named", (t) => { const f = fixture(t); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); assert.equal(f.verbs.diff("acme", "scout").changed, false); assert.equal(f.verbs.diff("acme", "scout").current, null); f.verbs.import("acme", { revision: rev(f), template: "proofreader" }, OWNER); rmSync(join(f.rolesDir, "proofreader.json")); const gone = diffDefinition(f.writer.read("acme").document, "proofreader", f.rolesDir); assert.equal(gone.templateMissing, true); assert.throws(() => f.verbs.diff("acme", "nobody"), { name: "BusinessError" }); assert.deepEqual(lineDiff("a\nb", "a\nc"), [{ op: " ", line: "a" }, { op: "-", line: "b" }, { op: "+", line: "c" }]); }); test("T1 rev counts up on every write, and a stale write refuses", (t) => { const f = fixture(t); assert.equal(f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER).rev, 1); const edited = { ...PLAIN, instructions: "Read, then report.\n" }; assert.equal(f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: edited }, OWNER).rev, 2); assert.equal(f.verbs.edit("acme", { revision: rev(f), rev: 2, name: "scout", definition: { ...edited, personality: { summary: "Terse." } } }, OWNER).rev, 3); assert.equal(doc(f).definitions.scout.rev, 3); const bytes = readFileSync(f.file); // A stale rev, a stale file revision, an unchanged edit. assert.throws(() => f.verbs.edit("acme", { revision: rev(f), rev: 2, name: "scout", definition: PLAIN }, OWNER), { code: "definition-rev-conflict" }); const old = rev(f); assert.throws(() => f.verbs.edit("acme", { revision: "0".repeat(64), rev: 3, name: "scout", definition: PLAIN }, OWNER), { code: "revision-conflict" }); assert.throws(() => f.verbs.edit("acme", { revision: old, rev: 3, name: "scout", definition: { ...edited, personality: { summary: "Terse." } } }, OWNER), { code: "definition-unchanged" }); assert.throws(() => f.verbs.edit("acme", { revision: old, name: "scout", definition: PLAIN }, OWNER), { code: "definition-rev-required" }); assert.throws(() => f.verbs.remove("acme", { revision: old, rev: 1, name: "scout" }, OWNER), { code: "definition-rev-conflict" }); assert.deepEqual(readFileSync(f.file), bytes); // Edit keeps from; remove takes the entry out. f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER); const from = doc(f).definitions.coder.from; f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "coder", definition: PLAIN }, OWNER); assert.deepEqual(doc(f).definitions.coder.from, from); const removed = f.verbs.remove("acme", { revision: rev(f), rev: 3, name: "scout" }, OWNER); assert.equal(removed.removedRev, 3); assert.ok(!Object.hasOwn(doc(f).definitions, "scout")); // Each write is logged with its verb, name and rev, never role text. const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map(JSON.parse).filter((e) => e.state === "committed"); assert.deepEqual(log.map((e) => [e.detail.verb, e.detail.definition, e.detail.rev]), [ ["definition.invent.console", "scout", 1], ["definition.edit.console", "scout", 2], ["definition.edit.console", "scout", 3], ["definition.import.console", "coder", 1], ["definition.edit.console", "coder", 2], ["definition.remove.console", "scout", null], ]); }); test("T1 invent and customize refuse what checkComposition refuses, with no value in the error", (t) => { const f = fixture(t); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); const bytes = readFileSync(f.file); const bad = [ ["held credential in instructions", { ...PLAIN, instructions: `Use ${SECRET} to log in.\n` }], ["held credential in personality", { ...PLAIN, personality: { voice: `say ${SECRET}` } }], ["unregistered tool", { ...PLAIN, role: { ...PLAIN.role, tools: [SECRET] } }], ["unregistered service", { ...PLAIN, role: { ...PLAIN.role, credentials: [{ service: SECRET }] } }], ["hire chain", { ...PLAIN, role: { ...PLAIN.role, authority: { withinRole: ["role.hire"], crossRole: [] } } }], ["over bound", { ...PLAIN, instructions: "x".repeat(16385) }], ["unknown entry key", { ...PLAIN, [SECRET]: 1 }], ["unknown role key", { ...PLAIN, role: { ...PLAIN.role, [SECRET]: [] } }], ]; for (const [what, definition] of bad) { for (const call of [ () => f.verbs.invent("acme", { revision: rev(f), name: "other", definition }, OWNER), () => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition }, OWNER), ]) { assert.throws(call, (e) => !e.message.includes(SECRET) && !e.message.includes("x".repeat(64)) && !String(e.stack).includes(SECRET), what); } } assert.deepEqual(readFileSync(f.file), bytes); // Import refuses a template that holds a hire right (R9): the chief of staff. assert.throws(() => f.verbs.import("acme", { revision: rev(f), template: "chief-of-staff" }, OWNER), /cannot hire or fire/); assert.deepEqual(readFileSync(f.file), bytes); }); test("T1 a bad entry in the file refuses the whole file, naming the definition and key, never the value", (t) => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root); const file = join(root, "config/businesses/acme.json"); const base = businessDoc(root); const entry = { rev: 1, ...PLAIN }; const refusal = (definitions) => { try { validateBusinessDocument({ ...base, definitions }, file, { rolesDir, assertClean }); } catch (e) { assert.equal(e.name, "BusinessError"); return e.message; } assert.fail("expected a refusal"); }; assert.match(refusal({ scout: { ...entry, instructions: `see ${SECRET}` } }), /definitions\.scout: composition instructions holds a credential/); assert.match(refusal({ scout: { ...entry, role: { ...PLAIN.role, tools: [SECRET] } } }), /definitions\.scout: .*tools/); assert.match(refusal({ scout: { ...entry, rev: 0 } }), /definitions\.scout\.rev must be a positive integer/); assert.match(refusal({ scout: { ...entry, from: { template: "coder", sha256: SECRET } } }), /definitions\.scout\.from\.sha256/); assert.match(refusal({ scout: { ...entry, [SECRET]: true } }), /definitions\.scout has an unknown key \(key \d+;/); assert.match(refusal({ [SECRET]: entry }), /definitions key 1 must be a definition name/); for (const d of [{ scout: { ...entry, instructions: `see ${SECRET}` } }, { scout: { ...entry, role: { ...PLAIN.role, tools: [SECRET] } } }, { [SECRET]: entry }, { scout: { ...entry, [SECRET]: true } }]) { assert.ok(!refusal(d).includes(SECRET)); } // A good definition validates; the resolved map holds only what instances name. const ok = validateBusinessDocument({ ...base, definitions: { scout: entry } }, file, { rolesDir }); assert.ok(!Object.hasOwn(ok.definitions, "org:scout")); }); test("T1 an org: reference resolves to the definition, and a missing one refuses naming instance and definition", (t) => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root); const file = join(root, "config/businesses/acme.json"); const base = businessDoc(root); const named = { ...base, definitions: { scout: { rev: 4, ...PLAIN, personality: { summary: "Terse." } } }, roles: { ...base.roles, helper: { definition: "org:scout" } } }; const business = validateBusinessDocument(named, file, { rolesDir }); const role = business.definitions["org:scout"]; assert.equal(role.name, "org:scout"); assert.equal(role.contractPath, null); assert.equal(role.instructions, PLAIN.instructions); assert.equal(role.rev, 4); const resolved = resolveInstance({ system: systemFor(root), business, instance: "helper" }); assert.equal(resolved.definition, "org:scout"); assert.equal(resolved.contract, null); assert.equal(resolved.instructions, PLAIN.instructions); assert.deepEqual(resolved.limits.tools, ["read"]); const missing = { ...base, roles: { ...base.roles, helper: { definition: "org:ghost" } } }; assert.throws(() => validateBusinessDocument(missing, file, { rolesDir }), (e) => /definition-missing: .*roles\.helper\.definition names org:ghost/.test(e.message)); assert.throws(() => validateBusinessDocument({ ...base, roles: { ...base.roles, helper: { definition: `org:${SECRET.toLowerCase()}!` } } }, file, { rolesDir }), (e) => !e.message.includes(SECRET.toLowerCase())); // Template names work exactly as before, including one shaped like a definition name. assert.throws(() => validateBusinessDocument({ ...base, roles: { ...base.roles, helper: { definition: "scout" } } }, file, { rolesDir }), { name: "BusinessError" }); }); test("T1 an org whose instances name only templates loads exactly as before", (t) => { const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true })); const rolesDir = rolesCopy(root); const file = join(root, "config/businesses/acme.json"); const base = businessDoc(root); const plain = validateBusinessDocument(base, file, { rolesDir }); const withUnused = validateBusinessDocument({ ...base, definitions: { scout: { rev: 1, ...PLAIN } } }, file, { rolesDir }); assert.deepEqual(withUnused, plain); assert.deepEqual(Object.keys(plain).sort(), ["arbiters", "businessVersion", "definitions", "file", "human", "id", "launch", "plugins", "projects", "roles", "tracker", "vars"]); assert.deepEqual(Object.keys(plain.definitions).sort(), ["chief-of-staff", "coder", "cto", "pm", "reviewer"]); const system = systemFor(root); for (const instance of Object.keys(base.roles)) { const a = resolveInstance({ system, business: plain, instance }); const b = resolveInstance({ system, business: withUnused, instance }); assert.equal(a.digest, b.digest); assert.ok(!Object.hasOwn(a, "instructions")); assert.equal(a.contract, plain.definitions[base.roles[instance].definition].contractPath); } }); test("T1 definition-in-use: an edit or removal of a definition an instance names refuses, naming it", (t) => { const f = fixture(t); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); // A human adds the instance through the ordinary writer; definitions stay as they are. const next = doc(f); next.roles.helper = { definition: "org:scout" }; f.writer.write("acme", next, { revision: rev(f), actor: "jason" }); const bytes = readFileSync(f.file); assert.throws(() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "Changed.\n" } }, OWNER), (e) => e.code === "definition-in-use" && /named by helper/.test(e.message)); assert.throws(() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, OWNER), (e) => e.code === "definition-in-use" && /named by helper/.test(e.message)); assert.deepEqual(readFileSync(f.file), bytes); assert.deepEqual(f.verbs.list("acme").definitions, [{ name: "scout", reference: "org:scout", rev: 1, from: null, usedBy: ["helper"] }]); assert.deepEqual(f.verbs.show("acme", "scout").usedBy, ["helper"]); }); test("T1 the ordinary writer cannot change definitions, and setDefinition changes nothing else", (t) => { const f = fixture(t); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); const next = doc(f); next.definitions.scout.instructions = "Sneaked in.\n"; next.definitions.scout.rev = 2; assert.throws(() => f.writer.write("acme", next, { revision: rev(f), actor: "jason" }), { code: "organization-definitions-protected" }); const added = doc(f); added.definitions.other = { rev: 1, ...PLAIN }; assert.throws(() => f.writer.write("acme", added, { revision: rev(f), actor: "jason" }), { code: "organization-definitions-protected" }); // The writer itself checks rev and the composition, whatever builds the entry. assert.throws(() => f.writer.setDefinition("acme", "scout", () => ({ ...PLAIN, role: { ...PLAIN.role, tools: ["no-such-tool"] } }), { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" }); }); test("T1 an agent cannot call the edit verbs", (t) => { const f = fixture(t); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); const bytes = readFileSync(f.file); const calls = (caller) => [ () => f.verbs.import("acme", { revision: rev(f), template: "coder" }, caller), () => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, caller), () => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, caller), () => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, caller), ]; // An agent session, even one holding every right; an agent's name or the tester as actor. for (const caller of [ { ...OWNER, agent: true }, { actor: "coder", rights: OWNER.rights }, { ...OWNER, actor: "coder" }, { ...OWNER, actor: "coder.coder" }, { ...OWNER, actor: "tester" }, { ...OWNER, actor: "chief" }, ]) { for (const call of calls(caller)) assert.throws(call, { code: "human-required" }); } // The writer refuses an agent actor by itself too. assert.throws(() => f.writer.setDefinition("acme", "other", () => PLAIN, { revision: rev(f), actor: "pm" }), { code: "human-required" }); assert.deepEqual(readFileSync(f.file), bytes); }); test("T1 a member without org.hire refuses on each verb; the owner and a member granted org.hire succeed", (t) => { const f = fixture(t); for (const verb of Object.values(DEFINITION_VERBS)) assert.equal(VERB_RIGHTS[verb], "org.hire"); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); const bytes = readFileSync(f.file); const refused = [ () => f.verbs.import("acme", { revision: rev(f), template: "coder" }, MEMBER), () => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, MEMBER), () => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, MEMBER), () => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, MEMBER), () => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, { actor: "alice", agent: false }), ]; for (const call of refused) assert.throws(call, (e) => e.code === "right-required" && /needs org\.hire/.test(e.message)); assert.deepEqual(readFileSync(f.file), bytes); // Owner: every verb. f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER); f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, OWNER); f.verbs.remove("acme", { revision: rev(f), rev: 2, name: "scout" }, OWNER); // A member granted org.hire: every verb, logged as that member. f.verbs.import("acme", { revision: rev(f), template: "reviewer" }, HIRER); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, HIRER); f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "y\n" } }, HIRER); f.verbs.remove("acme", { revision: rev(f), rev: 2, name: "scout" }, HIRER); const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map(JSON.parse).filter((e) => e.state === "committed"); assert.deepEqual(log.slice(-4).map((e) => e.actor), ["alice", "alice", "alice", "alice"]); }); test("T1 the template files on disk are never written", (t) => { const f = fixture(t); const before = readFileSync(join(f.rolesDir, "coder.json")); f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER); f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "coder", definition: PLAIN }, OWNER); assert.deepEqual(readFileSync(join(f.rolesDir, "coder.json")), before); writeFileSync(join(f.root, "touched"), ""); }); test("T1 a write that adds or changes a definition refuses without the credential store's secret check", (t) => { const f = fixture(t); f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER); const unchecked = definitionVerbs({ writer: f.writer, rolesDir: f.rolesDir }); const before = readFileSync(f.file); const required = { code: "definition-secret-check-required" }; assert.throws(() => unchecked.import("acme", { revision: rev(f), template: "coder" }, OWNER), required); assert.throws(() => unchecked.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, OWNER), required); assert.throws(() => unchecked.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "Report.\n" } }, OWNER), required); assert.throws(() => f.writer.setDefinition("acme", "other", () => PLAIN, { revision: rev(f), actor: "jason" }), required); assert.deepEqual(readFileSync(f.file), before); // A removal writes no role text, so it needs no secret check. assert.equal(unchecked.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, OWNER).removedRev, 1); }); test('A4 T1 hand-off: a failed second contract read names the template, never the path', t => { const root=tmp(),rolesDir=rolesCopy(root),read=fs.default.readFileSync; let contracts=0; fs.default.readFileSync=function(file,...args){ if(file===join(rolesDir,'reviewer.md') && ++contracts===2)throw new Error(`cannot read ${root}/sk-test-NOT-A-SECRET-1234`); return read(file,...args); }; syncBuiltinESMExports(); try { assert.throws(()=>readTemplate(rolesDir,'reviewer'),{message:'template reviewer contract is unavailable'}); } finally { fs.default.readFileSync=read;syncBuiltinESMExports();rmSync(root,{recursive:true,force:true}); } });