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