Files
jason.woltje 5f8079a972 feat(bus): copy launch gate CB3, verified bound copies only (#1873)
Materialize only verified bound copies: a binding approved after boot
answers restart-required. Boot and every launch re-resolve the binding
and prove the slot live against its service; unavailable verification,
a partial binding or a stale file refuses. The A3 revision fence and
the launch authority digest ride through prepare, final admission and
bindLaunch. Pending revocation blocks admission and no fence is
released on an uncertain outcome.

Every copy credential call checks session, incarnation, scopes,
generation and liveness, takes the slot from the session, goes only to
the approved origin with redirects refused, and rechecks the generation
at completion. Prepare and boot proofs run inside a time budget so a
silent service cannot stall the host channel. The launcher maps a
copyLaunch refusal to its own code instead of launch-failed.

Tests: copy-launch unit suite, CLI launch and race cases (prepare and
bind races assert no repo call, empty project manifest included), and a
docker acceptance on gitea 1.27.1.
2026-11-12 13:37:46 -06:00

361 lines
23 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);
});
// #1873 CB3: a copy the bus verified at boot joins as an instance with no credential reference, only
// the services it was bound for; a partial or extra binding never promotes.
test("CB3: a verified copy joins with copyBound and no reference; a partial or extra binding stays unbound", async () => {
const { promoteCopies } = await import("../src/agent-copies.mjs");
const dir = tmp();
const doc = org(dir, "acme");
const r = root({ researcher: { definition: "research" }, reviewer2: { definition: "reviewer" } });
const snapshot = materializeCopies({ orgId: "acme", doc, root: r, rolesDir });
const promoted = promoteCopies(snapshot, { researcher: ["gitea"], reviewer2: ["gitea"], nobody: ["gitea"] });
assert.equal(promoted.copies.researcher.state, "instance");
assert.deepEqual(promoted.copies.researcher.bound, ["gitea"]);
assert.equal(Object.hasOwn(promoted.copies.researcher, "missing"), false);
// reviewer needs gitea and vikunja: gitea alone is a partial binding.
assert.equal(promoted.copies.reviewer2.state, "unbound");
assert.equal(promoteCopies(snapshot, { researcher: ["gitea", "vikunja"] }).copies.researcher.state, "unbound", "an extra service is no promotion");
assert.equal(promoteCopies(snapshot, { reviewer2: ["gitea", "vikunja"] }).copies.reviewer2.state, "unbound", "vikunja has no copy verifier");
assert.ok(Object.isFrozen(promoted.copies.researcher));
assert.equal(snapshot.copies.researcher.state, "unbound", "the boot snapshot is unchanged");
const business = withCopies(doc, businessFilePath("acme", dir), { rolesDir, snapshot: promoted });
assert.deepEqual(business.roles.researcher.credentials, {});
assert.deepEqual(business.roles.researcher.copyBound, ["gitea"]);
assert.equal(business.copies.researcher.state, "instance");
assert.deepEqual(business.copies.researcher.bound, ["gitea"]);
assert.equal(copyRefusal(business, "researcher"), null);
assert.equal(copyRefusal(business, "reviewer2").code, "credential-unbound");
assert.equal(JSON.stringify(business).includes(join("secrets", "agents")), false, "no store path reaches the business");
assert.equal(JSON.stringify(business.roles.researcher).includes(dir), false);
const resolved = resolveInstance({ system: systemFor(dir), business, instance: "researcher" });
assert.deepEqual(resolved.credentials, {});
});