195 lines
14 KiB
JavaScript
195 lines
14 KiB
JavaScript
// #1873 Phase A slice A1: the three agent levels and their union (design
|
|
// docs/plans/2026-11-10_agent-levels-design.md sections 2, 3 and 6). Read-only: nothing here writes.
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import {
|
|
checkProjectAgents, checkRootAgents, loadRootAgents, projectUnion, resolveAgentLevels, validateOrganizationDocument,
|
|
} from "../src/index.mjs";
|
|
import { businessDoc, REPO_ROLES, tmp, writeJson } from "./helpers.mjs";
|
|
|
|
const rolesDir = REPO_ROLES;
|
|
const NONE = Object.freeze({ state: "absent", agents: {}, assistant: null });
|
|
const root = (agents) => checkRootAgents({ agentsVersion: 1, agents }, { rolesDir });
|
|
const project = (agents) => checkProjectAgents(agents, { rolesDir, where: "projects.web.agents" });
|
|
const ORG = { pm: { definition: "pm", vars: {} }, reviewer: { definition: "reviewer", vars: {} }, chief: { definition: "chief-of-staff", vars: {} } };
|
|
const names = (union) => union.agents.map((a) => `${a.level}:${a.name}`);
|
|
const refuses = (fn, pattern) => assert.throws(fn, (error) => error.name === "BusinessError" && pattern.test(error.message));
|
|
|
|
test("root level alone: each root agent runs in the organization it is used from", () => {
|
|
const r = root({ researcher: { definition: "research", vars: { model: "claude-opus-5-5" } }, assistant: { definition: "assistant" } });
|
|
assert.equal(r.assistant, "assistant");
|
|
const union = projectUnion({ orgId: "acme", root: r, org: {}, project: {}, projectId: "web" });
|
|
assert.deepEqual(names(union), ["root:researcher"]);
|
|
assert.deepEqual(union.agents[0].scope, { kind: "org", org: "acme" });
|
|
assert.equal(union.agents[0].vars.model, "claude-opus-5-5");
|
|
// The same root agent used from another organization is scoped to that one, never to both.
|
|
assert.deepEqual(projectUnion({ orgId: "home", root: r, org: {} }).agents[0].scope, { kind: "org", org: "home" });
|
|
});
|
|
|
|
test("organization level alone: its instances, scoped to it", () => {
|
|
const union = projectUnion({ orgId: "acme", root: NONE, org: ORG });
|
|
assert.deepEqual(names(union), ["org:chief", "org:pm", "org:reviewer"]);
|
|
assert.ok(union.agents.every((a) => a.scope.org === "acme"));
|
|
assert.equal(union.project, null);
|
|
});
|
|
|
|
test("project level alone: a project-defined agent", () => {
|
|
const union = projectUnion({ orgId: "acme", root: NONE, org: {}, project: project({ checker: { definition: "fact-checker" } }), projectId: "web" });
|
|
assert.deepEqual(names(union), ["project:checker"]);
|
|
assert.equal(union.agents[0].project, "web");
|
|
assert.equal(union.agents[0].definition, "fact-checker");
|
|
});
|
|
|
|
test("clash: the most specific level wins and the shadowed definitions are recorded", () => {
|
|
const r = root({ reviewer: { definition: "reviewer", vars: { model: "glm-5.3-flash" } } });
|
|
const p = project({ reviewer: { definition: "security-reviewer" } });
|
|
const union = projectUnion({ orgId: "acme", root: r, org: ORG, project: p, projectId: "web" });
|
|
const reviewer = union.agents.find((a) => a.name === "reviewer");
|
|
assert.equal(reviewer.level, "project");
|
|
assert.equal(reviewer.definition, "security-reviewer");
|
|
assert.deepEqual(reviewer.overrides, [{ level: "root", definition: "reviewer" }, { level: "org", definition: "reviewer" }]);
|
|
// The whole entry wins: the root's model does not leak into the project definition.
|
|
assert.equal(reviewer.vars.model, undefined);
|
|
assert.equal(union.agents.filter((a) => a.name === "reviewer").length, 1);
|
|
});
|
|
|
|
test("project narrowing: intersect keys only, never wider than what it inherits", () => {
|
|
const org = { ...ORG, coder: { definition: "coder", vars: { model: "claude-sonnet-5-5", "limits.network": "api-only", "limits.tools": ["read", "bash"] } } };
|
|
const p = project({ coder: { vars: { "limits.network": "open", "limits.tools": ["read", "edit"] } } });
|
|
const coder = projectUnion({ orgId: "acme", root: NONE, org, project: p, projectId: "web" }).agents.find((a) => a.name === "coder");
|
|
assert.equal(coder.level, "org");
|
|
assert.equal(coder.vars["limits.network"], "api-only"); // asking for open stays at the narrower api-only
|
|
assert.deepEqual(coder.vars["limits.tools"], ["read"]);
|
|
assert.equal(coder.vars.model, "claude-sonnet-5-5");
|
|
assert.deepEqual(coder.narrowed, ["limits.network", "limits.tools"]);
|
|
assert.equal(coder.vars["tracker.kind"], undefined); // no registry defaults leak in
|
|
refuses(() => project({ coder: { vars: { model: "claude-opus-5-5" } } }), /can only narrow limits\.tools, limits\.network, limits\.authority, not "model"/);
|
|
});
|
|
|
|
test("exclusions: a root opt-out per organization and a project exclude", () => {
|
|
const r = root({ reviewer: { definition: "reviewer", excludeOrgs: ["home"] }, researcher: { definition: "research" } });
|
|
assert.deepEqual(names(projectUnion({ orgId: "home", root: r, org: {} })), ["root:researcher"]);
|
|
assert.deepEqual(projectUnion({ orgId: "home", root: r, org: {} }).excluded, [{ name: "reviewer", level: "root", by: "root" }]);
|
|
const union = projectUnion({ orgId: "acme", root: r, org: {}, project: project({ researcher: { exclude: true } }), projectId: "web" });
|
|
assert.deepEqual(names(union), ["root:reviewer"]);
|
|
assert.deepEqual(union.excluded, [{ name: "researcher", level: "root", by: "project" }]);
|
|
});
|
|
|
|
test("a name the root turned off, or a project excluded, that a more specific level defines is available, not excluded", () => {
|
|
const r = root({ reviewer: { definition: "reviewer", excludeOrgs: ["home"] } });
|
|
// The organization's own reviewer: available under the organization's definition, no opt-out listed.
|
|
const home = projectUnion({ orgId: "home", root: r, org: { reviewer: { definition: "reviewer", vars: {} } } });
|
|
assert.deepEqual(names(home), ["org:reviewer"]);
|
|
assert.deepEqual(home.excluded, []);
|
|
// A project that defines the name the root turned off for its organization.
|
|
const web = projectUnion({ orgId: "home", root: r, org: {}, project: project({ reviewer: { definition: "security-reviewer" } }), projectId: "web" });
|
|
assert.deepEqual(names(web), ["project:reviewer"]);
|
|
assert.deepEqual(web.excluded, []);
|
|
// Without a definition of its own, the opt-out is still reported.
|
|
assert.deepEqual(projectUnion({ orgId: "home", root: r, org: {}, project: {}, projectId: "web" }).excluded, [{ name: "reviewer", level: "root", by: "root" }]);
|
|
});
|
|
|
|
test("the root assistant is cross-organization and never in one organization's union", () => {
|
|
const r = root({ assistant: { definition: "assistant" } });
|
|
assert.deepEqual(projectUnion({ orgId: "acme", root: r, org: {} }).agents, []);
|
|
// An organization's own assistant instance works inside that organization only.
|
|
const own = projectUnion({ orgId: "acme", root: r, org: { assistant: { definition: "assistant", vars: {} } } });
|
|
assert.deepEqual(names(own), ["org:assistant"]);
|
|
assert.deepEqual(own.agents[0].scope, { kind: "org", org: "acme" });
|
|
refuses(() => root({ helper: { definition: "assistant" } }), /assistant definition is the root agent named assistant only/);
|
|
});
|
|
|
|
test("refusals in a project's union: unknown targets, the chief, the assistant", () => {
|
|
refuses(() => projectUnion({ orgId: "acme", root: NONE, org: ORG, project: project({ ghost: { exclude: true } }), projectId: "web" }), /names no root or organization agent to exclude/);
|
|
refuses(() => projectUnion({ orgId: "acme", root: NONE, org: ORG, project: project({ ghost: { vars: { "limits.network": "none" } } }), projectId: "web" }), /to narrow/);
|
|
refuses(() => projectUnion({ orgId: "acme", root: NONE, org: ORG, project: project({ chief: { exclude: true } }), projectId: "web" }), /can't change the organization's chief-of-staff/);
|
|
refuses(() => projectUnion({ orgId: "acme", root: NONE, org: ORG, project: project({ chief: { definition: "reviewer" } }), projectId: "web" }), /can't change the organization's chief-of-staff/);
|
|
});
|
|
|
|
test("malformed root definitions refuse", () => {
|
|
refuses(() => checkRootAgents({ agentsVersion: 2, agents: {} }, { rolesDir }), /agentsVersion/);
|
|
refuses(() => checkRootAgents({ agentsVersion: 1, agents: {}, extra: 1 }, { rolesDir }), /unsupported root agents key: "extra"/);
|
|
refuses(() => root({ boss: { definition: "chief-of-staff" } }), /can't be a root agent/);
|
|
refuses(() => root({ old: { definition: "researcher" } }), /version 1/);
|
|
refuses(() => root({ x: { definition: "no-such-role" } }), /no usable role definition named no-such-role/);
|
|
refuses(() => root({ x: { definition: "reviewer", credentials: {} } }), /can't carry credentials/);
|
|
refuses(() => root({ x: { definition: "reviewer", vars: { "tracker.project": 3 } } }), /can't be set at the agent layer/);
|
|
refuses(() => root({ x: { definition: "reviewer", excludeOrgs: ["a", "a"] } }), /duplicate entry/);
|
|
refuses(() => root({ "Bad Name": { definition: "reviewer" } }), /agent name must match/);
|
|
});
|
|
|
|
test("a refused var names its key, never its value", () => {
|
|
const secret = "sk-secret-marker-7731";
|
|
const hides = (fn, key) => assert.throws(fn, (error) => error.name === "BusinessError" && error.message.includes(`variable ${key}`) && !error.message.includes(secret));
|
|
hides(() => root({ x: { definition: "reviewer", vars: { thinking: secret } } }), "thinking");
|
|
hides(() => root({ x: { definition: "reviewer", vars: { model: "opus-5.5", harness: `${secret}!` } } }), "harness");
|
|
hides(() => root({ x: { definition: "reviewer", vars: { "limits.tools": ["read", secret] } } }), "limits.tools");
|
|
hides(() => project({ x: { definition: "reviewer", vars: { thinking: secret } } }), "thinking");
|
|
hides(() => project({ x: { vars: { "limits.network": secret } } }), "limits.network");
|
|
// A message that quotes no value is kept whole, so the owner still reads the rule.
|
|
refuses(() => root({ x: { definition: "reviewer", vars: { "tracker.project": 3 } } }), /variable tracker\.project can't be set at the agent layer/);
|
|
refuses(() => root({ x: { definition: "reviewer", vars: { "fallback.model": "m" } } }), /a fallback sets all of/);
|
|
});
|
|
|
|
test("malformed project definitions refuse", () => {
|
|
refuses(() => project({ x: { exclude: true, vars: {} } }), /exactly \{"exclude": true\}/);
|
|
refuses(() => project({ x: { exclude: false } }), /exactly/);
|
|
refuses(() => project({ x: {} }), /defines \(definition\), narrows \(vars\) or excludes/);
|
|
refuses(() => project({ x: { definition: "chief-of-staff" } }), /can't be a project agent/);
|
|
refuses(() => project({ x: { definition: "assistant" } }), /can't be a project agent/);
|
|
refuses(() => project({ x: { definition: "reviewer", hosts: [] } }), /can't carry hosts/);
|
|
refuses(() => project({ x: { definition: "reviewer", other: 1 } }), /unsupported projects\.web\.agents\.x key/);
|
|
});
|
|
|
|
test("root file: absent, owner and mode checks, symbolic link, bad JSON, and no host path in a message", () => {
|
|
const dir = tmp();
|
|
assert.equal(loadRootAgents(dir, { rolesDir }).state, "absent");
|
|
const file = writeJson(join(dir, "agents.json"), { agentsVersion: 1, agents: { reviewer: { definition: "reviewer" } } });
|
|
const ok = loadRootAgents(dir, { rolesDir });
|
|
assert.equal(ok.state, "ok");
|
|
assert.deepEqual(Object.keys(ok.agents), ["reviewer"]);
|
|
chmodSync(file, 0o620);
|
|
refuses(() => loadRootAgents(dir, { rolesDir }), /writable by group or other/);
|
|
chmodSync(file, 0o600);
|
|
writeFileSync(file, "{ nope");
|
|
assert.throws(() => loadRootAgents(dir, { rolesDir }), (error) => /not valid JSON \(agents\.json\)/.test(error.message) && !error.message.includes(dir));
|
|
// The parser's own text can quote the file's bytes; the refusal never carries it.
|
|
writeFileSync(file, '{"agentsVersion": 1, "agents": {"x": secret-marker-7731}}');
|
|
assert.throws(() => loadRootAgents(dir, { rolesDir }), (error) => error.message === "root agent file is not valid JSON (agents.json)" && error.exitCode !== undefined);
|
|
// An id check's quote of what it was given is dropped too.
|
|
writeFileSync(file, JSON.stringify({ agentsVersion: 1, agents: { x: { definition: "Secret Marker 7731" } } }));
|
|
assert.throws(() => loadRootAgents(dir, { rolesDir }), (error) => /root agents\.x\.definition must match/.test(error.message) && !error.message.includes("Marker"));
|
|
const linked = tmp();
|
|
symlinkSync(file, join(linked, "agents.json"));
|
|
assert.throws(() => loadRootAgents(linked, { rolesDir }), (error) => error.exitCode === 4 && !error.message.includes(linked));
|
|
});
|
|
|
|
test("organization file: project agents are checked but not loaded; a malformed one refuses the file", () => {
|
|
const dir = tmp();
|
|
const doc = businessDoc(dir);
|
|
doc.projects.stack.agents = { coder: { vars: { "limits.network": "none" } }, checker: { definition: "fact-checker" } };
|
|
const loaded = validateOrganizationDocument(structuredClone(doc), "acme.json", { rolesDir });
|
|
assert.equal(loaded.projects.stack.agents, undefined);
|
|
const levels = resolveAgentLevels({ orgId: "acme", doc, root: NONE, rolesDir });
|
|
assert.equal(levels.unions.stack.ok, true);
|
|
assert.ok(levels.unions.stack.agents.some((a) => a.name === "checker" && a.level === "project"));
|
|
assert.deepEqual(levels.unions.stack.agents.find((a) => a.name === "coder").narrowed, ["limits.network"]);
|
|
doc.projects.stack.agents = { boss: { definition: "chief-of-staff" } };
|
|
refuses(() => validateOrganizationDocument(doc, "acme.json", { rolesDir }), /can't be a project agent/);
|
|
});
|
|
|
|
test("one project's bad union refuses only that project", () => {
|
|
const dir = tmp();
|
|
const doc = businessDoc(dir);
|
|
mkdirSync(join(dir, "other"));
|
|
doc.projects.other = { root: join(dir, "other"), agents: { ghost: { exclude: true } } };
|
|
const levels = resolveAgentLevels({ orgId: "acme", doc, root: NONE, rolesDir });
|
|
assert.equal(levels.unions.stack.ok, true);
|
|
assert.equal(levels.unions.other.ok, false);
|
|
assert.match(levels.unions.other.reason, /names no root or organization agent to exclude/);
|
|
assert.ok(levels.base.agents.length > 0);
|
|
});
|