// #1873 Phase A slice A3: root and project agent copies (design // docs/plans/2026-11-10_agent-levels-design.md sections 4 and 9 item 2). import test from "node:test"; import assert from "node:assert/strict"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { checkRootAgents, loadRootAgents } from "../src/agent-levels.mjs"; import { copiesView, copyInstanceName, copyRefusal, loadWithCopies, materializeCopies, projectRefusal, withCopies, } from "../src/agent-copies.mjs"; import { businessFilePath, validateBusinessDocument } from "../src/organization-config.mjs"; import { resolveInstance } from "../src/resolve.mjs"; import { REPO, REPO_ROLES, businessDoc, systemFor, tmp, tokenFile, writeJson } from "./helpers.mjs"; const rolesDir = REPO_ROLES; const SECRET = "sk-test-NOT-A-SECRET-1234"; const root = (agents) => ({ state: "ok", ...checkRootAgents({ agentsVersion: 1, agents }, { rolesDir }) }); // An organization `id` under `dir`, written where loadBusiness and the writer read it. function org(dir, id, edit = (d) => d) { const doc = edit(businessDoc(dir, id)); writeJson(businessFilePath(id, dir), doc); return doc; } const boot = (dir, id, doc, r) => { const snapshot = materializeCopies({ orgId: id, doc, root: r, rolesDir }); return { snapshot, business: withCopies(doc, businessFilePath(id, dir), { rolesDir, snapshot }) }; }; test("A3: two root agents, one excluded: the other becomes an instance in each organization, each its own", () => { const dir = tmp(); const r = root({ gate: { definition: "merge-gate" }, sec: { definition: "security-reviewer", excludeOrgs: ["acme"] }, assistant: { definition: "assistant" } }); const acme = boot(dir, "acme", org(dir, "acme"), r); assert.deepEqual(Object.keys(acme.snapshot.copies), ["gate"]); assert.equal(acme.business.roles.gate.definition, "merge-gate"); assert.equal(Object.hasOwn(acme.business.roles, "sec"), false, "the excluded root agent is no instance"); // The root assistant stays shown, not launched (A6): never a copy. assert.equal(Object.hasOwn(acme.business.roles, "assistant"), false); assert.equal(acme.business.copies.gate.state, "instance"); // A second organization makes its own copies from the same root agents. const beta = boot(dir, "beta", org(dir, "beta"), r); assert.deepEqual(Object.keys(beta.snapshot.copies).sort(), ["gate", "sec"]); assert.equal(beta.snapshot.org, "beta"); assert.notEqual(beta.snapshot.copies.gate, acme.snapshot.copies.gate); assert.ok(Object.isFrozen(acme.snapshot.copies.gate), "the boot snapshot can't be changed in place"); // Each copy carries only its own organization's entry: the credential map is the organization's // own, and a copy holds no credential. assert.deepEqual(acme.business.roles.gate.credentials, {}); assert.deepEqual(beta.business.roles.gate.credentials, {}); }); test("A3: an unbound copy refuses by name and binding, and no value reaches the refusal or the view", () => { const dir = tmp(); const doc = org(dir, "acme", (d) => { // The secret is bound elsewhere in this organization: the coder's gitea token file is named it // and holds it. const file = tokenFile(join(dir, "secrets"), SECRET); writeFileSync(file, SECRET); d.roles.coder.credentials.gitea = { file, rotateBy: "2099-01-01" }; return d; }); const r = root({ researcher: { definition: "research" }, reviewer2: { definition: "reviewer" } }); const { snapshot, business } = boot(dir, "acme", doc, r); assert.equal(snapshot.copies.researcher.state, "unbound"); assert.deepEqual(snapshot.copies.researcher.missing, ["gitea"]); assert.deepEqual(snapshot.copies.reviewer2.missing, ["gitea", "vikunja"]); assert.equal(Object.hasOwn(business.roles, "researcher"), false, "an unbound copy is no instance"); const refusal = copyRefusal(business, "researcher"); assert.equal(refusal.code, "credential-unbound"); assert.match(refusal.message, /researcher/); assert.match(refusal.message, /gitea/); assert.equal(copyRefusal(business, "coder"), null, "an org instance is no copy"); for (const text of [refusal.message, copyRefusal(business, "reviewer2").message, JSON.stringify(business.copies), JSON.stringify(copiesView(snapshot.copies))]) { assert.equal(text.includes(SECRET), false); assert.equal(text.includes(dir), false, "no host path"); } }); test("A3: a copy whose var the organization refuses is refused alone, and its value stays out of the reason", () => { const dir = tmp(); const doc = org(dir, "acme"); const r = root({ gate: { definition: "merge-gate" }, wide: { definition: "merge-gate", vars: { model: SECRET } } }); const { business } = boot(dir, "acme", doc, r); assert.equal(business.roles.gate.definition, "merge-gate", "the other copy still joins"); assert.equal(Object.hasOwn(business.roles, "wide"), false); assert.equal(business.copies.wide.state, "refused"); const refusal = copyRefusal(business, "wide"); assert.equal(refusal.code, "copy-refused"); assert.match(refusal.message, /^wide can't launch in acme: /); assert.equal(refusal.message.includes(SECRET), false); assert.equal(JSON.stringify(business.copies).includes(SECRET), false); }); test("A3: a copy's tools and actions equal an org instance's of the same role; a copy that skips checkRoles fails this", () => { const dir = tmp(); const doc = org(dir, "acme", (d) => ({ ...d, roles: { ...d.roles, "gate-org": { definition: "merge-gate", vars: { model: "glm-5.3-flash" } } } })); const r = root({ gate: { definition: "merge-gate", vars: { model: "glm-5.3-flash" } }, wide: { definition: "merge-gate", vars: { model: "not-a-listed-model" } } }); const snapshot = materializeCopies({ orgId: "acme", doc, root: r, rolesDir }); const file = businessFilePath("acme", dir); const system = systemFor(dir); const same = (business) => { assert.deepEqual(business.roles.gate, business.roles["gate-org"]); const copy = resolveInstance({ system, business, instance: "gate" }); const own = resolveInstance({ system, business, instance: "gate-org" }); assert.deepEqual(copy.limits.tools, own.limits.tools); assert.deepEqual(copy.limits.authority, own.limits.authority); assert.deepEqual(copy.credentials, own.credentials); // A copy the organization's own checks refuse never joins. assert.equal(Object.hasOwn(business.roles, "wide"), false); }; same(withCopies(doc, file, { rolesDir, snapshot })); // The counterfactual: the copies joined as their raw entries, with no checkRoles. const plain = validateBusinessDocument(doc, file, { rolesDir }); const skipped = { ...plain, roles: { ...plain.roles, gate: { definition: "merge-gate", vars: { model: "glm-5.3-flash" } }, wide: { definition: "merge-gate", vars: { model: "not-a-listed-model" } } } }; assert.throws(() => same(skipped), assert.AssertionError); }); test("A3: a root file change after boot changes nothing until the next boot", () => { const dir = tmp(); const doc = org(dir, "acme"); writeJson(join(dir, "agents.json"), { agentsVersion: 1, agents: { gate: { definition: "merge-gate" } } }); const snapshot = materializeCopies({ orgId: "acme", doc, root: loadRootAgents(dir, { rolesDir }), rolesDir }); const first = loadWithCopies("acme", { dir, rolesDir, snapshot }); assert.equal(first.roles.gate.definition, "merge-gate"); // The root file now drops gate, adds one and excludes acme; the bus holds its snapshot. writeJson(join(dir, "agents.json"), { agentsVersion: 1, agents: { sec: { definition: "security-reviewer" }, gate: { definition: "merge-gate", excludeOrgs: ["acme"] } } }); const later = loadWithCopies("acme", { dir, rolesDir, snapshot }); assert.deepEqual(Object.keys(later.copies), ["gate"]); assert.equal(later.roles.gate.definition, "merge-gate"); assert.equal(Object.hasOwn(later.roles, "sec"), false); // A broken root file is no different: nothing after boot reads it. writeFileSync(join(dir, "agents.json"), "{ not json"); assert.equal(loadWithCopies("acme", { dir, rolesDir, snapshot }).roles.gate.definition, "merge-gate"); // The next boot reads it again. writeJson(join(dir, "agents.json"), { agentsVersion: 1, agents: { sec: { definition: "security-reviewer" } } }); const next = materializeCopies({ orgId: "acme", doc, root: loadRootAgents(dir, { rolesDir }), rolesDir }); assert.deepEqual(Object.keys(next.copies), ["sec"]); // Nothing that runs after boot reads the root file: only the cli's boot step names loadRootAgents. for (const src of ["packages/bus/src/runtime.mjs", "packages/bus/src/broker.mjs", "packages/cli/src/launcher.mjs", "packages/organization/src/agent-copies.mjs"]) assert.equal(/loadRootAgents\s*\(/.test(readFileSync(join(REPO, src), "utf8")), false, src); }); test("A3: project agents copy into their own project, and a refused union refuses every agent it defines", () => { const dir = tmp(); mkdirSync(join(dir, "web"), { recursive: true }); const doc = org(dir, "acme", (d) => { d.projects.stack.agents = { checker: { definition: "merge-gate" } }; // web excludes an agent nothing defines, so its union refuses. d.projects.web = { root: join(dir, "web"), agents: { checker: { definition: "merge-gate" }, ghost: { exclude: true } } }; return d; }); const { snapshot, business } = boot(dir, "acme", doc, root({})); assert.equal(copyInstanceName("checker", "stack"), "checker.stack"); assert.deepEqual({ ...snapshot.copies["checker.stack"], vars: undefined }, { name: "checker", level: "project", project: "stack", definition: "merge-gate", vars: undefined, state: "instance" }); assert.equal(business.roles["checker.stack"].definition, "merge-gate"); assert.equal(Object.hasOwn(business.roles, "checker.web"), false); const refusal = copyRefusal(business, "checker.web"); assert.equal(refusal.code, "project-refused"); assert.match(refusal.message, /^checker\.web can't launch in acme: project web refuses its agents: /); assert.match(refusal.message, /ghost/); // A project that stops being declared after boot takes its copy with it. const { stack, ...rest } = doc.projects; const later = withCopies({ ...doc, projects: rest }, businessFilePath("acme", dir), { rolesDir, snapshot }); assert.equal(Object.hasOwn(later.roles, "checker.stack"), false); assert.equal(copyRefusal(later, "checker.stack").code, "project-refused"); }); test("A3: an organization instance of the same name wins over a root or project copy, at boot and after it, and the copy is gone, not refused", () => { const dir = tmp(); const doc = org(dir, "acme", (d) => { d.projects.stack.agents = { checker: { definition: "merge-gate" } }; // A raw org instance already named like the project copy wins at boot. d.roles["checker.stack"] = { definition: "security-reviewer" }; return d; }); const { snapshot, business } = boot(dir, "acme", doc, root({ gate: { definition: "merge-gate" } })); assert.equal(business.copies.gate.state, "instance"); assert.equal(Object.hasOwn(snapshot.copies, "checker.stack"), false); assert.equal(business.roles["checker.stack"].definition, "security-reviewer"); assert.equal(copyRefusal(business, "checker.stack"), null); // An org instance added after boot replaces the root copy: the copy leaves the lookup, so nothing // refuses the organization's own instance. const later = withCopies({ ...doc, roles: { ...doc.roles, gate: { definition: "security-reviewer" } } }, businessFilePath("acme", dir), { rolesDir, snapshot }); assert.equal(later.roles.gate.definition, "security-reviewer", "the organization file's own instance"); assert.equal(Object.hasOwn(later.copies, "gate"), false); assert.equal(copyRefusal(later, "gate"), null); assert.equal(projectRefusal(later, "gate", "stack"), null); }); test("A3: two agents whose instance names come out the same both refuse, naming each; neither overwrites the other", () => { const dir = tmp(); mkdirSync(join(dir, "a.b"), { recursive: true }); mkdirSync(join(dir, "b"), { recursive: true }); const doc = org(dir, "acme", (d) => { d.projects.stack.agents = { checker: { definition: "merge-gate" } }; // Project a.b's c and project b's c.a both come out c.a.b. d.projects["a.b"] = { root: join(dir, "a.b"), agents: { c: { definition: "merge-gate" } } }; d.projects.b = { root: join(dir, "b"), agents: { "c.a": { definition: "merge-gate" } } }; return d; }); // Root checker.stack is a different agent from project stack's checker. const { snapshot, business } = boot(dir, "acme", doc, root({ "checker.stack": { definition: "security-reviewer" } })); for (const [instance, claims] of [["checker.stack", /root agent checker\.stack and project stack's checker/], ["c.a.b", /project a\.b's c and project b's c\.a/]]) { assert.equal(snapshot.copies[instance].state, "refused", instance); assert.match(snapshot.copies[instance].reason, claims); assert.equal(Object.hasOwn(business.roles, instance), false, instance); assert.ok(copyRefusal(business, instance), instance); } // The view lists every claimant, so a reader finds each agent's copy by its name and project. assert.deepEqual(business.copies["checker.stack"].claims, [{ name: "checker.stack", project: null }, { name: "checker", project: "stack" }]); assert.deepEqual(business.copies["c.a.b"].claims, [{ name: "c", project: "a.b" }, { name: "c.a", project: "b" }]); }); test("A3: a collision the current file brings in after boot refuses every claimant it reaches, and lifts when the file drops it", () => { const dir = tmp(); mkdirSync(join(dir, "a.b"), { recursive: true }); mkdirSync(join(dir, "b"), { recursive: true }); const file = businessFilePath("acme", dir); const doc = org(dir, "acme", (d) => { d.projects["a.b"] = { root: join(dir, "a.b"), agents: { c: { definition: "merge-gate" } } }; d.projects.b = { root: join(dir, "b") }; return d; }); const { snapshot, business } = boot(dir, "acme", doc, root({ "new.stack": { definition: "security-reviewer" } })); assert.equal(business.copies["c.a.b"].state, "instance"); assert.equal(business.copies["new.stack"].state, "instance"); // Project b's c.a and project stack's new now come out as the names of copies made at boot. const edited = structuredClone(doc); edited.projects.b.agents = { "c.a": { definition: "merge-gate" } }; edited.projects.stack.agents = { new: { definition: "merge-gate" } }; const later = withCopies(edited, file, { rolesDir, snapshot }); for (const [instance, claims, reason] of [ ["c.a.b", [{ name: "c", project: "a.b" }, { name: "c.a", project: "b" }], "the instance name c.a.b is claimed by project a.b's c and project b's c.a"], ["new.stack", [{ name: "new.stack", project: null }, { name: "new", project: "stack" }], "the instance name new.stack is claimed by root agent new.stack and project stack's new"], ]) { assert.equal(later.copies[instance].state, "refused", instance); assert.deepEqual(later.copies[instance].claims, claims, instance); assert.equal(later.copies[instance].reason, reason, instance); assert.equal(Object.hasOwn(later.roles, instance), false, instance); assert.ok(copyRefusal(later, instance), instance); } // Without the new defines, both copies are instances again. const back = withCopies(doc, file, { rolesDir, snapshot }); assert.equal(back.copies["c.a.b"].state, "instance"); assert.equal(back.copies["new.stack"].state, "instance"); }); test("A3: a project define whose vars change after boot refuses its copy until the next boot", () => { const dir = tmp(); const file = businessFilePath("acme", dir); const doc = org(dir, "acme", (d) => { d.projects.stack.agents = { checker: { definition: "merge-gate" } }; return d; }); const { snapshot, business } = boot(dir, "acme", doc, root({})); assert.equal(business.copies["checker.stack"].state, "instance"); const edited = structuredClone(doc); edited.projects.stack.agents.checker.vars = { "limits.tools": ["read"] }; const later = withCopies(edited, file, { rolesDir, snapshot }); const r = copyRefusal(later, "checker.stack"); assert.equal(r.code, "project-refused"); assert.match(r.message, /project stack changed checker's vars since the bus started; the change takes effect at the next start$/); assert.equal(Object.hasOwn(later.roles, "checker.stack"), false); // The next boot makes the copy from the new vars. const next = boot(dir, "acme", edited, root({})).business; assert.equal(next.copies["checker.stack"].state, "instance"); assert.deepEqual(next.roles["checker.stack"].vars["limits.tools"], ["read"]); }); test("A3: each project's union is recomputed from the current file: a refused union refuses its own copy and every agent in it; exclude and narrow reach the instance", () => { const dir = tmp(); mkdirSync(join(dir, "web"), { recursive: true }); const file = businessFilePath("acme", dir); const doc = org(dir, "acme", (d) => { d.projects.stack.agents = { checker: { definition: "merge-gate" } }; d.projects.web = { root: join(dir, "web") }; d.roles["gate-org"] = { definition: "merge-gate" }; return d; }); const { snapshot, business } = boot(dir, "acme", doc, root({ gate: { definition: "merge-gate" } })); assert.deepEqual(business.unions.stack, { ok: true, agents: [...Object.keys(doc.roles), "checker.stack", "gate"].sort() }); assert.equal(projectRefusal(business, "checker.stack", "web").code, "project-agent-elsewhere"); assert.equal(projectRefusal(business, "gate", "web"), null); const system = systemFor(dir); const tools = (b, instance) => resolveInstance({ system, business: b, instance }).limits.tools; assert.ok(tools(business, "gate").includes("grep")); // After boot: stack excludes something nothing defines, so its union refuses; web excludes gate and // narrows gate-org's tools. const edited = structuredClone(doc); edited.projects.stack.agents.ghost = { exclude: true }; edited.projects.web.agents = { gate: { exclude: true }, "gate-org": { vars: { "limits.tools": ["read"] } } }; const later = withCopies(edited, file, { rolesDir, snapshot }); assert.equal(later.unions.stack.ok, false); assert.match(later.unions.stack.reason, /ghost/); // stack's own copy refuses, naming the project and why. assert.equal(Object.hasOwn(later.roles, "checker.stack"), false); const own = copyRefusal(later, "checker.stack"); assert.equal(own.code, "project-refused"); assert.match(own.message, /^checker\.stack can't launch in acme: project stack refuses its agents: .*ghost/); // Every inherited agent refuses there too. for (const instance of ["gate", "coder", "gate-org"]) { const r = projectRefusal(later, instance, "stack"); assert.equal(r.code, "project-refused", instance); assert.match(r.message, new RegExp(`^${instance} can't work in project stack: project stack refuses its agents: .*ghost`)); } assert.match(projectRefusal(later, "gate", "web").message, /the project excludes it$/); assert.equal(projectRefusal(later, "coder", "web"), null); // The narrow reaches the instance itself, so its resolved tools and actions are narrowed. assert.deepEqual(later.roles["gate-org"].vars["limits.tools"], ["read"]); assert.deepEqual(tools(later, "gate-org"), ["read"]); // A project that stops defining its agent takes the copy with it. const dropped = structuredClone(doc); dropped.projects.stack.agents = { other: { definition: "merge-gate" } }; assert.match(copyRefusal(withCopies(dropped, file, { rolesDir, snapshot }), "checker.stack").message, /project stack no longer defines checker as merge-gate$/); }); test("A3: a project narrowing a root copy narrows that copy; an organization with only project entries still gets its unions", () => { const dir = tmp(); const doc = org(dir, "acme", (d) => { d.projects.stack.agents = { gate: { vars: { "limits.tools": ["read", "ls"] } } }; return d; }); const { business } = boot(dir, "acme", doc, root({ gate: { definition: "merge-gate" } })); assert.deepEqual(resolveInstance({ system: systemFor(dir), business, instance: "gate" }).limits.tools, ["read", "ls"]); // No root agents and no copy: the project's entries still shape the business. const only = org(dir, "beta", (d) => { d.projects.stack.agents = { coder: { vars: { "limits.tools": ["read"] } } }; return d; }); const plain = loadWithCopies("beta", { dir, rolesDir, snapshot: null }); assert.deepEqual(plain.unions.stack.agents, Object.keys(only.roles).sort()); assert.deepEqual(plain.roles.coder.vars["limits.tools"], ["read"]); assert.equal(Object.hasOwn(plain, "copies"), false); });