Files
jason.woltje bad760ce08 Merge refactor fbf2f6fc8 (rename stage B) into F2
Conflict in packages/webui/src/serve.mjs imports: kept F2's
THREAD_DEFINITION and onboarding imports on stage B's organization path.
The F2 assistant test edit followed the move to packages/organization;
onboarding-browser now imports the organization service directly.
2026-11-09 13:17:33 -06:00

178 lines
8.4 KiB
JavaScript

// The assistant role (REQ-ASSIST-1, row 119). The role file is policy: it
// holds message.send and record.write within role, no credentials, and tools
// as read-only as the chief of staff's, plus the web grant (REQ-HARN-2, vikunja:19/295). A widened file fails here.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { ACTIONS, BusinessError, classify, loadBusiness, loadRole, resolveInstance } from "../src/index.mjs";
import { businessDoc, REPO_ROLES, systemFor, tmp, writeJson } from "./helpers.mjs";
const APPROVED = Object.freeze({
tools: ["read", "grep", "find", "ls", "web"],
network: "api-only",
authority: Object.freeze({ withinRole: Object.freeze(["message.send", "record.write"]), crossRole: Object.freeze([]) }),
credentials: Object.freeze([]),
// REQ-ASSIST-2: the user and the chief of staff, both ways. The broker enforces these lists (row 120).
messaging: Object.freeze({
sendTo: Object.freeze(["human", "chief-of-staff"]),
receiveFrom: Object.freeze(["human", "chief-of-staff"]),
}),
});
const INSTANCE = "assistant";
function refuses(fn, pattern) {
assert.throws(fn, (error) => {
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
assert.equal(error.exitCode, 2);
assert.match(error.message, pattern);
return true;
});
}
function loadFixture(mutate = () => {}) {
const root = tmp();
const doc = businessDoc(root, "acme");
doc.roles[INSTANCE] = { definition: "assistant", holder: "mos" };
mutate(doc);
writeJson(join(root, "businesses", "acme.json"), doc);
return { root, business: loadBusiness("acme", { dir: root, rolesDir: REPO_ROLES }) };
}
function resolved({ root, business }) {
return resolveInstance({ system: systemFor(root), business, instance: INSTANCE });
}
test("the role file loads and resolves through loadBusiness", () => {
const role = loadRole(REPO_ROLES, "assistant");
assert.equal(role.roleVersion, 2);
assert.equal(role.name, "assistant");
assert.equal(role.contract, "assistant.md");
assert.equal(role.contractPath, join(REPO_ROLES, "assistant.md"));
const record = resolved(loadFixture());
assert.equal(record.definition, "assistant");
assert.equal(record.holder, "mos");
assert.equal(record.contract, join(REPO_ROLES, "assistant.md"));
assert.equal(record.launch, null);
assert.equal(record.tracker, null);
});
test("the shipped file is exactly the approved list", () => {
const raw = JSON.parse(readFileSync(join(REPO_ROLES, "assistant.json"), "utf8"));
const { title, ...rest } = raw;
assert.equal(typeof title, "string");
assert.deepEqual(rest, { roleVersion: 2, name: "assistant", contract: "assistant.md", ...APPROVED });
});
test("the assistant's authority is message.send and record.write within role", () => {
const role = loadRole(REPO_ROLES, "assistant");
assert.deepEqual(role.authority, { withinRole: ["message.send", "record.write"], crossRole: [] });
assert.deepEqual(role.credentials, []);
const record = resolved(loadFixture());
assert.deepEqual(record.limits.authority, { withinRole: ["message.send", "record.write"], crossRole: [] });
assert.deepEqual(record.credentials, {});
for (const action of ACTIONS) {
assert.equal(classify(record, action), ["message.send", "record.write"].includes(action) ? "within" : "gated", action);
}
for (const action of ["task.create", "task.assign", "role.launch"]) assert.equal(classify(record, action), "gated", action);
});
test("the assistant talks to the user and the chief of staff only, and no other shipped role is limited", () => {
const role = loadRole(REPO_ROLES, "assistant");
assert.deepEqual(role.messaging, { sendTo: ["human", "chief-of-staff"], receiveFrom: ["human", "chief-of-staff"] });
assert.ok(Object.isFrozen(role.messaging.sendTo));
// Limiting another role is a policy change of its own; the shipped chief of staff and PM stay open.
for (const name of ["chief-of-staff", "pm", "cto", "coder", "reviewer", "researcher"]) {
assert.equal(loadRole(REPO_ROLES, name).messaging, null, name);
}
});
test("a business grant through limits does not widen the role", () => {
const record = resolved(loadFixture((doc) => {
doc.roles[INSTANCE].vars = { "limits.authority": ["message.send", "task.create", "role.launch"] };
}));
assert.deepEqual(record.limits.authority, { withinRole: ["message.send"], crossRole: [] });
assert.equal(record.launch, null);
});
test("a launch block whose by is the assistant is refused", () => {
refuses(
() => loadFixture((doc) => {
doc.launch = { ...doc.launch, by: INSTANCE, instances: ["coder"] };
}),
/launch\.by names assistant, whose role assistant doesn't hold role\.launch within-role/,
);
});
test("the instance takes no credential references and no tracker", () => {
refuses(
() => loadFixture((doc) => {
doc.roles[INSTANCE].credentials = { gitea: { file: "/x/token", rotateBy: "2099-01-01" } };
}),
/must reference exactly the services its role definition needs \(none; got gitea\)/,
);
refuses(
() => loadFixture((doc) => {
doc.roles[INSTANCE].tracker = { bot: "bot-acme-assistant", botId: 7 };
}),
/has "tracker" but assistant uses no vikunja credential/,
);
});
// Row 133 (#1627): an arbiter messages the raiser when it closes a decision, so a role limited by
// messaging lists can't be one. The message names the kind, the instance and the definition.
test("an arbiter whose role definition declares messaging lists is refused, for each kind", () => {
for (const kind of ["delivery", "technical"]) {
refuses(
() => loadFixture((doc) => { doc.arbiters[kind] = INSTANCE; }),
new RegExp(`arbiters\\.${kind} names assistant, whose role definition assistant declares messaging lists`),
);
// An instance name that isn't the definition's: both are named.
refuses(
() => loadFixture((doc) => {
doc.roles.aide = { definition: "assistant" };
doc.arbiters[kind] = "aide";
}),
new RegExp(`arbiters\\.${kind} names aide, whose role definition assistant declares messaging lists`),
);
}
});
test("an arbiter without messaging lists is accepted beside the limited assistant", () => {
assert.deepEqual(loadFixture().business.arbiters, { delivery: "pm", technical: "cto" });
const { business } = loadFixture((doc) => { doc.arbiters.delivery = "chief"; });
assert.deepEqual(business.arbiters, { delivery: "chief", technical: "cto" });
});
test("the contract passes work to the chief of staff and directs no one", () => {
const text = readFileSync(join(REPO_ROLES, "assistant.md"), "utf8");
assert.match(text, /chief of staff/);
assert.match(text, /don't direct\s+other agents/);
assert.match(text, /create or assign\s+tasks/);
assert.match(text, /launch\s+sessions/);
assert.match(text, /exact instance key in the generated `Configured coordinator`\s+line under `This session`/);
assert.match(text, /Use that key as send_message\.to for requests\s+and rulings/);
assert.doesNotMatch(text, /addressed to the `chief-of-staff` role/);
// Round 1 F1: rulings go to the chief of staff, never to the role they concern (REQ-ASSIST-1, REQ-ASSIST-2).
assert.match(text, /Relay each ruling the user gives verbatim to the chief\s+of\s+staff,\s+naming\s+whose\s+ruling\s+it\s+is\s+and\s+which\s+role\s+it\s+concerns/);
assert.match(text, /Don't\s+send\s+it\s+to\s+that\s+role\s+yourself/);
assert.doesNotMatch(text, /to\s+the\s+role\s+it\s+concerns/);
// F2: the prose matches the record.write grant (write_record), and spec/ stays human-only.
assert.doesNotMatch(text, /You have no write tool/);
assert.match(text, /write_record commits a project document/);
assert.match(text, /never writes `spec\/`/);
});
// Row 213 (C2-08, Case 2 design 4): companion and research hold it too, for the broker's web tools.
test("REQ-HARN-2: the assistant, companion and research hold the web grant, and no other role file does", () => {
const names = readdirSync(REPO_ROLES).filter((f) => f.endsWith(".json") && f !== "conductor-policy.json").map((f) => f.slice(0, -5));
const granted = names.filter((name) => loadRole(REPO_ROLES, name).tools.includes("web"));
assert.deepEqual(granted, ["assistant", "companion", "research"]);
for (const name of ["coder", "reviewer", "chief-of-staff", "pm", "cto", "researcher"]) {
assert.ok(!loadRole(REPO_ROLES, name).tools.includes("web"), name);
}
});