Files
stack/packages/organization/tests/agent-copies.test.mjs
jason.woltje 29f7c3af85 fix(agents): A3 excluded checkouts hidden, launch boundary rechecks unions, reload collisions and changed defines refuse (#1873)
- A checkout an earlier launch left for a project the instance may no longer
  work in is covered by an empty tmpfs inside the sandbox; the host copy stays.
- The launcher rechecks every prepared project against the reloaded document
  after preparation, and the bus's final admission takes the instance and
  checks the broker's current unions.
- A project define whose vars changed since boot refuses its copy until the
  next start.
- Instance-name collisions are recomputed at each reload from the frozen root
  claims and the current project defines; every claimant refuses.
2026-11-10 20:27:33 -06:00

330 lines
20 KiB
JavaScript

// #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);
});