Design section 8. The bus answers packs, pack.preview, pack.apply, board.set and workflow.set to the organization's human, and their .console forms to the owner's Console cap. The broker runs the section 3 merge, the plan digest (pack-plan-stale on a mismatch) and the held credential check. The organization writer keeps packs, board and workflows out of the generic write and refuses definition writes to every actor name an agent carries, project agents included. mosaic org pack list|preview|apply, board set and workflow set|remove go over the organization's bus as its human and refuse inside an agent shell before any file is read. The Console serves GET /api/orgs/<id>/packs, packs/preview, packs/apply, board and workflows/<name> (POST and DELETE) to the root owner's session only, writes under --send only.
85 lines
4.9 KiB
JavaScript
85 lines
4.9 KiB
JavaScript
// Starter packs SP4: the writer's fences for the pack keys. The generic write never changes packs,
|
|
// board or workflows (only applyPack does), and every actor name an agent carries, a project agent's
|
|
// included, is refused a write of definitions.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync, rmSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { organizationWriter } from "../src/organization.mjs";
|
|
import { agentActors } from "../src/definitions.mjs";
|
|
import { PACKS_MAX, WORKFLOWS_MAX, checkPacks, checkWorkflows } from "../src/organization-config.mjs";
|
|
import { businessDoc, rolesCopy, tmp, writeJson } from "./helpers.mjs";
|
|
|
|
const assertClean = () => {};
|
|
const PLAIN = { role: { tools: ["read"], network: "none", authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: "Read and report.\n" };
|
|
const BOARD = {
|
|
columns: [
|
|
{ state: "todo", label: "Backlog" }, { state: "in-progress", label: "Building" },
|
|
{ state: "in-review", label: "Review" }, { state: "blocked", label: "Blocked" }, { state: "done", label: "Merged" },
|
|
],
|
|
layout: "kanban",
|
|
};
|
|
const PACK = { id: "coding", version: "1.0.0", sha256: "a".repeat(64), origin: "shipped", scope: "org", project: null, proposedHires: [] };
|
|
const FLOW = { description: "One.", steps: [{ title: "Do it", state: "todo", roles: ["human"] }] };
|
|
|
|
function fixture(t) {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const dir = join(root, "config");
|
|
const rolesDir = rolesCopy(root);
|
|
const file = writeJson(join(dir, "businesses/acme.json"), businessDoc(root));
|
|
return { file, writer: organizationWriter({ dir, rolesDir }) };
|
|
}
|
|
const rev = (f) => f.writer.read("acme").revision;
|
|
const doc = (f) => JSON.parse(readFileSync(f.file, "utf8"));
|
|
|
|
test("SP4 the generic write refuses any change to packs, board or workflows", (t) => {
|
|
const f = fixture(t);
|
|
const before = readFileSync(f.file, "utf8");
|
|
for (const [key, value] of [["board", BOARD], ["workflows", { one: FLOW }], ["packs", []]]) {
|
|
const next = { ...doc(f), [key]: value };
|
|
assert.throws(() => f.writer.write("acme", next, { revision: rev(f), actor: "jason" }), { code: "organization-packs-protected" }, key);
|
|
}
|
|
assert.equal(readFileSync(f.file, "utf8"), before);
|
|
// Once applyPack wrote them, the generic write may carry them unchanged, but not remove them.
|
|
f.writer.applyPack("acme", { board: BOARD, workflows: { one: FLOW } }, { revision: rev(f), actor: "jason", assertClean });
|
|
const kept = doc(f);
|
|
kept.vars = { ...kept.vars, "tracker.pollSeconds": 90 };
|
|
f.writer.write("acme", kept, { revision: rev(f), actor: "jason" });
|
|
assert.equal(doc(f).vars["tracker.pollSeconds"], 90);
|
|
const { board: _board, ...dropped } = doc(f);
|
|
assert.throws(() => f.writer.write("acme", dropped, { revision: rev(f), actor: "jason" }), { code: "organization-packs-protected" });
|
|
});
|
|
|
|
test("SP4 agentActors names root instances and project agents in every form", () => {
|
|
const document = {
|
|
roles: { coder: { definition: "coder" }, chief: { definition: "chief-of-staff" } },
|
|
projects: { stack: { agents: { checker: { definition: "fact-checker" } } }, docs: {} },
|
|
};
|
|
assert.deepEqual(agentActors(document).sort(), ["checker", "checker.stack", "chief", "chief-of-staff.chief", "coder", "coder.coder", "fact-checker.checker.stack"].sort());
|
|
assert.deepEqual(agentActors(null), []);
|
|
});
|
|
|
|
test("SP4 a project agent's actor name refuses a definition write through setDefinition and applyPack", (t) => {
|
|
const f = fixture(t);
|
|
let { revision } = f.writer.read("acme");
|
|
({ revision } = f.writer.setProjectAgent("acme", { revision, project: "stack", name: "checker", entry: { definition: "fact-checker" } }, "jason"));
|
|
const before = readFileSync(f.file, "utf8");
|
|
for (const actor of ["checker", "checker.stack", "fact-checker.checker.stack"]) {
|
|
assert.throws(() => f.writer.setDefinition("acme", "scout", () => PLAIN, { revision, actor, assertClean }), { code: "human-required" }, actor);
|
|
assert.throws(() => f.writer.applyPack("acme", { definitions: { scout: PLAIN }, packs: PACK }, { revision, actor, assertClean }), { code: "human-required" }, actor);
|
|
}
|
|
assert.equal(readFileSync(f.file, "utf8"), before);
|
|
f.writer.setDefinition("acme", "scout", () => PLAIN, { revision, actor: "jason", assertClean });
|
|
assert.ok(doc(f).definitions.scout);
|
|
});
|
|
|
|
test("SP4 caps: at most 32 workflows in a scope and 256 packs entries", () => {
|
|
const flows = (n) => Object.fromEntries(Array.from({ length: n }, (_, i) => [`w${i}`, FLOW]));
|
|
assert.equal(WORKFLOWS_MAX, 32);
|
|
assert.equal(PACKS_MAX, 256);
|
|
assert.equal(Object.keys(checkWorkflows(flows(32), "workflows")).length, 32);
|
|
assert.throws(() => checkWorkflows(flows(33), "workflows"), /at most 32 workflows/);
|
|
assert.throws(() => checkPacks(Array.from({ length: 257 }, () => ({})), "packs"), /at most 256 entries/);
|
|
});
|