// #1873 Phase A slice A2: the root agent file's writer and the project-agent writer verb (design // docs/plans/2026-11-10_agent-levels-design.md sections 6, 7 and 9 item 1). import test from "node:test"; import assert from "node:assert/strict"; import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { loadRootAgents } from "../src/agent-levels.mjs"; import { ABSENT_REVISION, ROOT_LOCK, ROOT_RECEIPTS, changeRootAgent, patchRootEntry, projectAgentProblem, readRootAgentsFile, rootAgentSummary, } from "../src/agent-levels-write.mjs"; import * as oldPath from "../../business/src/agent-levels-write.mjs"; import { organizationWriter } from "../src/organization.mjs"; import { businessDoc, REPO_ROLES, rolesCopy, tmp, writeJson } from "./helpers.mjs"; const rolesDir = REPO_ROLES; const SECRET = "sk-test-NOT-A-SECRET-1234"; const OWNER = Object.freeze({ provider: "local-dev", iss: "local-dev", sub: "local-dev-owner" }); const as = (actor = OWNER) => ({ actor, owner: OWNER, rolesDir }); function config() { const dir = join(tmp(), "config"); mkdirSync(dir, { recursive: true, mode: 0o700 }); chmodSync(dir, 0o700); return dir; } const file = dir => join(dir, "agents.json"); const bytes = dir => existsSync(file(dir)) ? readFileSync(file(dir), "utf8") : null; const receipts = dir => existsSync(join(dir, ROOT_RECEIPTS)) ? readFileSync(join(dir, ROOT_RECEIPTS), "utf8").trim().split("\n").filter(Boolean).map(line => JSON.parse(line)) : []; const seeded = async dir => { await changeRootAgent(dir, { revision: ABSENT_REVISION, name: "researcher", entry: { definition: "research", vars: { model: "claude-opus-5-5" } } }, as()); return readRootAgentsFile(dir); }; const refuses = (code, check = () => true) => error => error.code === code && check(error); test("the old business path re-exports the writer", () => { assert.equal(oldPath.changeRootAgent, changeRootAgent); }); test("root writer: an owner write lands, reads back through A1's loader, and its receipt names the change", async () => { const dir = config(); const receipt = await changeRootAgent(dir, { revision: ABSENT_REVISION, name: "researcher", entry: { definition: "research", vars: { model: "claude-opus-5-5" } } }, as()); assert.equal(receipt.verb, "root.agent.set"); assert.equal(receipt.agent, "researcher"); assert.equal(receipt.definition, "research"); assert.deepEqual(receipt.changed, ["added"]); assert.equal(receipt.revisionBefore, ABSENT_REVISION); const st = lstatSync(file(dir)); assert.equal(st.mode & 0o777, 0o600); assert.equal(st.nlink, 1); const loaded = loadRootAgents(dir, { rolesDir }); assert.equal(loaded.agents.researcher.vars.model, "claude-opus-5-5"); const read = await readRootAgentsFile(dir); assert.equal(read.revision, receipt.revisionAfter); // A patch keeps the keys it leaves out and names, by key, what it changed. const second = await changeRootAgent(dir, { revision: read.revision, name: "researcher", entry: old => patchRootEntry(old, { vars: { "limits.network": "api-only" }, excludeOrgs: ["home"] }) }, as()); assert.deepEqual(second.changed, ["vars.limits.network", "excludeOrgs"]); assert.deepEqual(rootAgentSummary((await readRootAgentsFile(dir)).document), [{ name: "researcher", definition: "research", model: "claude-opus-5-5", varKeys: ["limits.network"], excludeOrgs: ["home"] }]); const removed = await changeRootAgent(dir, { revision: second.revisionAfter, name: "researcher", entry: null }, as()); assert.equal(removed.verb, "root.agent.remove"); assert.deepEqual(removed.changed, ["removed"]); assert.deepEqual(loadRootAgents(dir, { rolesDir }).agents, {}); const lines = receipts(dir); assert.equal(lines.length, 3); // Receipts carry names and key paths only: no var value, no host path. const text = readFileSync(join(dir, ROOT_RECEIPTS), "utf8"); assert.ok(!text.includes("claude-opus-5-5") && !text.includes("api-only") && !text.includes(dir)); assert.ok(lines.every(line => line.actor === "owner" && line.uid === process.geteuid())); assert.equal(lstatSync(join(dir, ROOT_RECEIPTS)).mode & 0o777, 0o600); assert.equal(existsSync(join(dir, "agents.lock")), false); assert.deepEqual(readdirSync(dir).filter(n => n.startsWith(".agents-")), []); }); test("root writer: every actor but the owner refuses human-required and nothing is written", async () => { const dir = config(); const others = [ { provider: "local-dev", iss: "local-dev", sub: "chief" }, { provider: "local-dev", iss: "local-dev", sub: "chief-of-staff.chief" }, { provider: "local-dev", iss: "local-dev", sub: "tester" }, { ...OWNER, provider: "tester" }, { provider: "https-idp", iss: "https://idp.example", sub: "local-dev-owner" }, "local-dev-owner", "pm", null, undefined, {}, ]; for (const actor of others) { await assert.rejects(changeRootAgent(dir, { revision: ABSENT_REVISION, name: "researcher", entry: { definition: "research" } }, { actor, owner: OWNER, rolesDir }), refuses("human-required"), JSON.stringify(actor ?? null)); } // An owner context that names the tester is not an owner either. const tester = { provider: "local-dev", iss: "local-dev", sub: "tester" }; await assert.rejects(changeRootAgent(dir, { revision: ABSENT_REVISION, name: "researcher", entry: { definition: "research" } }, { actor: tester, owner: tester, rolesDir }), refuses("human-required")); await assert.rejects(changeRootAgent(dir, { revision: ABSENT_REVISION, name: "researcher", entry: { definition: "research" } }, { actor: OWNER, rolesDir }), refuses("human-required")); assert.equal(bytes(dir), null); assert.deepEqual(receipts(dir), []); }); test("root writer: a stale revision refuses and the file is unchanged", async () => { const dir = config(); const first = await seeded(dir); const before = bytes(dir); await assert.rejects(changeRootAgent(dir, { revision: ABSENT_REVISION, name: "reviewer", entry: { definition: "reviewer" } }, as()), refuses("revision-conflict")); await changeRootAgent(dir, { revision: first.revision, name: "reviewer", entry: { definition: "reviewer" } }, as()); const after = bytes(dir); // The first revision is now stale too. await assert.rejects(changeRootAgent(dir, { revision: first.revision, name: "pm", entry: { definition: "pm" } }, as()), refuses("revision-conflict")); assert.notEqual(before, after); assert.equal(bytes(dir), after); await assert.rejects(changeRootAgent(dir, { revision: "nope", name: "pm", entry: { definition: "pm" } }, as()), refuses("revision-conflict")); }); test("root writer: a symbolic link, a group-writable file and a foreign uid refuse and the file is unchanged", async () => { const dir = config(); const { revision } = await seeded(dir); const original = bytes(dir); // A symbolic link in place of the file: the target is never followed or written. const target = join(dir, "elsewhere.json"); writeFileSync(target, original); chmodSync(target, 0o600); const real = join(dir, "real.json"); writeFileSync(real, original); chmodSync(real, 0o600); const moved = readFileSync(file(dir)); writeFileSync(real, moved); const { renameSync } = await import("node:fs"); renameSync(file(dir), join(dir, "kept.json")); symlinkSync(target, file(dir)); await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, as()), refuses("agents-unsafe", e => !e.message.includes(dir))); assert.equal(readFileSync(target, "utf8"), original); assert.ok(lstatSync(file(dir)).isSymbolicLink()); await assert.rejects(readRootAgentsFile(dir), refuses("agents-unsafe")); // Restore, then make it group-writable. const { unlinkSync } = await import("node:fs"); unlinkSync(file(dir)); renameSync(join(dir, "kept.json"), file(dir)); chmodSync(file(dir), 0o620); await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, as()), refuses("agents-unsafe", e => /group or other/.test(e.message))); assert.equal(bytes(dir), original); assert.equal(lstatSync(file(dir)).mode & 0o777, 0o620); chmodSync(file(dir), 0o600); // A second link to the file refuses: a write through one name would not be the file another reads. const { linkSync } = await import("node:fs"); linkSync(file(dir), join(dir, "alias.json")); await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, as()), refuses("agents-unsafe", e => /one link/.test(e.message))); unlinkSync(join(dir, "alias.json")); // Not this process's uid: the directory checks refuse first (an ancestor this uid owns is foreign). await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, { ...as(), uid: process.geteuid() + 1 }), refuses("agents-unsafe", e => /config directory/.test(e.message) && !/root agent file/.test(e.message))); await assert.rejects(readRootAgentsFile(dir, { uid: process.geteuid() + 1 }), refuses("agents-unsafe", e => /config directory/.test(e.message) && !/root agent file/.test(e.message))); assert.equal(bytes(dir), original); // A file another uid owns, in a directory that is this uid's: the file's own owner check refuses. // Changing a file's owner needs root, so the descriptor stat reports a foreign uid through the seam. const foreign = async fd => { const st = await fd.stat(); return Object.assign(Object.create(Object.getPrototypeOf(st)), st, { uid: st.uid + 1 }); }; const receiptsBefore = existsSync(join(dir, ROOT_RECEIPTS)) ? readFileSync(join(dir, ROOT_RECEIPTS), "utf8") : null; await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, { ...as(), statFile: foreign }), refuses("agents-unsafe", e => /root agent file must belong to uid/.test(e.message))); await assert.rejects(readRootAgentsFile(dir, { statFile: foreign }), refuses("agents-unsafe", e => /root agent file must belong to uid/.test(e.message))); assert.equal(bytes(dir), original); assert.equal(lstatSync(file(dir)).mode & 0o777, 0o600); assert.equal(existsSync(join(dir, ROOT_RECEIPTS)) ? readFileSync(join(dir, ROOT_RECEIPTS), "utf8") : null, receiptsBefore); assert.equal(existsSync(join(dir, ROOT_LOCK)), false, "the lock is released"); // A group-writable config directory refuses before any lock or read. chmodSync(dir, 0o770); await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, as()), refuses("agents-unsafe", e => /config directory/.test(e.message))); chmodSync(dir, 0o700); // An ancestor another user can write without the sticky bit refuses: that user could rename the // config directory between the checks and the write. With the sticky bit, as /tmp has, it can't. const parent = dirname(dir); const mode = lstatSync(parent).mode & 0o7777; chmodSync(parent, 0o777); try { await assert.rejects(changeRootAgent(dir, { revision, name: "pm", entry: { definition: "pm" } }, as()), refuses("agents-unsafe", e => /ancestor/.test(e.message) && !e.message.includes(dir))); await assert.rejects(readRootAgentsFile(dir), refuses("agents-unsafe", e => /ancestor/.test(e.message))); chmodSync(parent, 0o1777); assert.equal((await readRootAgentsFile(dir)).revision, revision); } finally { chmodSync(parent, mode); } assert.equal(bytes(dir), original); assert.equal(receipts(dir).length, 1); assert.equal(existsSync(join(dir, "agents.lock")), false); }); test("root writer: an invalid document refuses with the key named and the value absent", async () => { const dir = config(); const { revision } = await seeded(dir); const original = bytes(dir); const cases = [ [{ definition: "research", vars: { model: SECRET.repeat(10) } }, /variable model/], [{ definition: "research", vars: { "limits.network": SECRET } }, /variable limits\.network/], [{ definition: SECRET }, /root agents\.leaky\.definition/], [{ definition: "research", excludeOrgs: [SECRET.toUpperCase()] }, /root agents\.leaky\.excludeOrgs/], [{ definition: "research", vars: { [SECRET]: "x" } }, /root agents\.leaky\.vars/], [{ definition: "research", credentials: { gitea: SECRET } }, /root agents\.leaky\.credentials/], [{ definition: "chief-of-staff" }, /chief-of-staff can't be a root agent/], [{ definition: "assistant" }, /assistant definition is the root agent named assistant only/], ]; for (const [entry, pattern] of cases) { await assert.rejects(changeRootAgent(dir, { revision, name: "leaky", entry }, as()), refuses("agents-invalid", e => pattern.test(e.message) && !e.message.includes(SECRET) && !e.message.includes(SECRET.toUpperCase()) && !e.message.includes(dir)), JSON.stringify(Object.keys(entry))); } await assert.rejects(changeRootAgent(dir, { revision, name: SECRET.toUpperCase(), entry: { definition: "research" } }, as()), refuses("agents-invalid", e => !e.message.includes(SECRET.toUpperCase()))); await assert.rejects(changeRootAgent(dir, { revision, name: "absent", entry: null }, as()), refuses("agent-not-found")); await assert.rejects(changeRootAgent(dir, { revision, name: "researcher", entry: { definition: "research", vars: { model: "claude-opus-5-5" } } }, as()), refuses("no-change")); assert.equal(bytes(dir), original); assert.equal(receipts(dir).length, 1); }); test("root writer: a crash between the temp file and the rename leaves the old file whole", async () => { const dir = config(); const { revision } = await seeded(dir); const original = bytes(dir); await assert.rejects(changeRootAgent(dir, { revision, name: "reviewer", entry: { definition: "reviewer" } }, { ...as(), beforeReplace: () => { throw new Error("crash"); } }), /crash/); assert.equal(bytes(dir), original); assert.deepEqual(readdirSync(dir).filter(n => n.startsWith(".agents-")), []); assert.equal(existsSync(join(dir, "agents.lock")), false); assert.equal(receipts(dir).length, 1); // A writer that ignores the lock and changes the file between the temp and the rename loses. await assert.rejects(changeRootAgent(dir, { revision, name: "reviewer", entry: { definition: "reviewer" } }, { ...as(), beforeReplace: () => writeFileSync(file(dir), original.replace("research\"", "research\" ")) }), refuses("revision-conflict")); // A new file is never published over one created meanwhile. const fresh = config(); await assert.rejects(changeRootAgent(fresh, { revision: ABSENT_REVISION, name: "pm", entry: { definition: "pm" } }, { ...as(), beforeReplace: () => writeJson(file(fresh), { agentsVersion: 1, agents: {} }) }), refuses("revision-conflict")); assert.deepEqual(JSON.parse(bytes(fresh)), { agentsVersion: 1, agents: {} }); // A failure after the rename says the change landed. const landed = config(); await assert.rejects(changeRootAgent(landed, { revision: ABSENT_REVISION, name: "pm", entry: { definition: "pm" } }, { ...as(), syncDirectory: async () => { throw new Error("disk"); } }), e => e.committed === true); assert.ok(loadRootAgents(landed, { rolesDir }).agents.pm); }); test("root writer: a held lock refuses agents-locked", async () => { const dir = config(); writeFileSync(join(dir, "agents.lock"), ""); await assert.rejects(changeRootAgent(dir, { revision: ABSENT_REVISION, name: "pm", entry: { definition: "pm" } }, as()), refuses("agents-locked")); assert.equal(bytes(dir), null); }); // The project-agent verb: the organization file's projects.

.agents, one entry per call. function org(mutate = () => {}) { const root = tmp(); const doc = businessDoc(root); mutate(doc); const dir = join(root, "config"); const orgFile = writeJson(join(dir, "businesses/acme.json"), doc); chmodSync(dir, 0o700); const roles = rolesCopy(root); return { dir, orgFile, roles, writer: organizationWriter({ dir, rolesDir: roles }) }; } const agentsOf = (writer, project = "stack") => writer.read("acme").document.projects[project].agents; const history = (writer) => readFileSync(writer.logFile("acme"), "utf8"); test("project verb: each entry form lands, the log names paths only, and remove drops the key", () => { const { writer } = org(); let { revision } = writer.read("acme"); const step = (name, entry) => ({ revision } = writer.setProjectAgent("acme", { revision, project: "stack", name, entry }, "jason")); step("checker", { definition: "fact-checker", vars: { model: "claude-sonnet-5-5" } }); assert.deepEqual(agentsOf(writer), { checker: { definition: "fact-checker", vars: { model: "claude-sonnet-5-5" } } }); step("coder", { vars: { "limits.network": "none" } }); step("reviewer", { exclude: true }); assert.deepEqual(Object.keys(agentsOf(writer)).sort(), ["checker", "coder", "reviewer"]); step("checker", null); step("coder", null); step("reviewer", null); assert.equal(writer.read("acme").document.projects.stack.agents, undefined); const events = history(writer).trim().split("\n").map(line => JSON.parse(line)).filter(e => e.state === "committed"); assert.deepEqual(events.map(e => e.detail.form), ["define", "narrow", "exclude", "remove", "remove", "remove"]); assert.ok(events.every(e => e.detail.verb === "project.agents.set" && e.detail.project === "stack" && e.actor === "jason")); assert.ok(events.every(e => e.changes.every(c => !("old" in c) && !("new" in c)))); assert.ok(!history(writer).includes("claude-sonnet-5-5")); }); test("project verb: a narrow that widens refuses, and refusals never quote a value", () => { const { writer, orgFile } = org(); const { revision } = writer.read("acme"); const before = readFileSync(orgFile, "utf8"); const refusals = [ [{ vars: { model: SECRET } }, "project-agents-invalid", /can only narrow/], [{ vars: { "limits.network": SECRET } }, "project-agents-invalid", /variable limits\.network/], [{ definition: "fact-checker", vars: { model: SECRET.repeat(10) } }, "project-agents-invalid", /variable model/], [{ definition: SECRET }, "project-agents-invalid", /projects\.stack\.agents\.coder\.definition/], [{ definition: "fact-checker", credentials: { gitea: SECRET } }, "project-agents-invalid", /credentials/], [{ exclude: false }, "project-agents-invalid", /exclude entry/], [{}, "project-agents-invalid", /defines \(definition\), narrows \(vars\) or excludes/], ]; for (const [entry, code, pattern] of refusals) { assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name: "coder", entry }, "jason"), e => e.code === code && pattern.test(e.message) && !e.message.includes(SECRET), JSON.stringify(entry)); } // A narrow or exclude that names nothing it could act on refuses (the union check). assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name: "ghost", entry: { exclude: true } }, "jason"), e => e.code === "project-agents-invalid" && /names no root or organization agent/.test(e.message)); assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name: "ghost", entry: null }, "jason"), { code: "agent-not-found" }); assert.equal(readFileSync(orgFile, "utf8"), before); }); test("project verb: the chief and the assistant refuse", () => { const { writer, orgFile } = org(doc => { doc.roles.assistant = { definition: "assistant" }; }); const { document, revision } = writer.read("acme"); assert.equal(document.roles.assistant.definition, "assistant", "the fixture has an organization assistant"); assert.equal(document.roles.chief.definition, "chief-of-staff", "the fixture has a chief"); const before = readFileSync(orgFile, "utf8"); const protectedAgent = /a project can't change the organization's (chief-of-staff|assistant)/; const notProjectAgent = /can't be a project agent/; for (const [name, entry, reason] of [ ["chief", { exclude: true }, protectedAgent], ["chief", { vars: { "limits.network": "none" } }, protectedAgent], ["chief", { definition: "reviewer" }, protectedAgent], ["assistant", { exclude: true }, protectedAgent], ["assistant", { vars: { "limits.network": "none" } }, protectedAgent], ["assistant", { definition: "reviewer" }, protectedAgent], ["helper", { definition: "chief-of-staff" }, notProjectAgent], ["helper", { definition: "assistant" }, notProjectAgent], ]) { assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name, entry }, "jason"), e => e.code === "project-agents-invalid" && reason.test(e.message), `${name} ${JSON.stringify(entry)}`); } assert.equal(readFileSync(orgFile, "utf8"), before); }); test("project verb: every agent actor and every non-owner refuses human-required", () => { const { writer, orgFile } = org(); const { revision } = writer.read("acme"); const before = readFileSync(orgFile, "utf8"); for (const actor of ["pm", "pm.pm", "chief", "chief-of-staff.chief", "coder", "tester", "local-dev-owner", "someone"]) { assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name: "checker", entry: { definition: "fact-checker" } }, actor), { code: "human-required" }, actor); } assert.equal(readFileSync(orgFile, "utf8"), before); }); test("project verb: only projects.

.agents. may change; the projects fence keeps every other writer out", () => { const { writer, orgFile, roles } = org(); const { document, revision } = writer.read("acme"); const before = readFileSync(orgFile, "utf8"); // The general writer can't touch project agents, or anything else under projects. const withAgents = structuredClone(document); withAgents.projects.stack.agents = { checker: { definition: "fact-checker" } }; assert.throws(() => writer.write("acme", withAgents, { revision, actor: "jason" }), { code: "organization-projects-protected" }); // The verb's guard refuses any candidate that changes another projects key. const root = { state: "absent", agents: {}, assistant: null }; const args = { orgId: "acme", project: "stack", name: "checker", entry: { definition: "fact-checker" }, rolesDir: roles, root }; const good = structuredClone(document); good.projects.stack.agents = { checker: { definition: "fact-checker" } }; assert.equal(projectAgentProblem(document, good, args), null); const mutations = [ d => { d.projects.stack.root = "/elsewhere"; }, d => { d.projects.other = { root: "/elsewhere" }; }, d => { d.projects.stack.agents.extra = { definition: "reviewer" }; }, d => { d.human = "someone"; }, d => { d.projects.stack.agents.checker = { definition: "reviewer" }; }, ]; for (const mutate of mutations) { const next = structuredClone(good); mutate(next); assert.equal(projectAgentProblem(document, next, args)?.[0], "organization-projects-protected", mutate.toString()); } // A missing project and a library project refuse. assert.throws(() => writer.setProjectAgent("acme", { revision, project: "nope", name: "checker", entry: { definition: "fact-checker" } }, "jason"), { code: "project-not-found" }); const library = structuredClone(document); library.projects.stack.kind = "library"; assert.equal(projectAgentProblem(library, good, args)?.[0], "project-agents-library"); // A stale revision refuses. writer.setProjectAgent("acme", { revision, project: "stack", name: "checker", entry: { definition: "fact-checker" } }, "jason"); assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name: "other", entry: { definition: "reviewer" } }, "jason"), { code: "revision-conflict" }); assert.notEqual(readFileSync(orgFile, "utf8"), before); }); test("project verb: a root file that refuses refuses the project write", () => { const { writer, dir, orgFile } = org(); writeJson(join(dir, "agents.json"), { agentsVersion: 2 }); const { revision } = writer.read("acme"); const before = readFileSync(orgFile, "utf8"); assert.throws(() => writer.setProjectAgent("acme", { revision, project: "stack", name: "checker", entry: { definition: "fact-checker" } }, "jason"), e => e.code === "project-agents-invalid" && /root agent file/.test(e.message)); assert.equal(readFileSync(orgFile, "utf8"), before); }); test("project verb: a narrow of a root agent checks against the root level", async () => { const { writer, dir, roles } = org(); await changeRootAgent(dir, { revision: ABSENT_REVISION, name: "researcher", entry: { definition: "research" } }, { actor: OWNER, owner: OWNER, rolesDir: roles }); const { revision } = writer.read("acme"); writer.setProjectAgent("acme", { revision, project: "stack", name: "researcher", entry: { vars: { "limits.network": "none" } } }, "jason"); assert.deepEqual(agentsOf(writer).researcher, { vars: { "limits.network": "none" } }); });