Compare commits

...
8 Commits
Author SHA1 Message Date
jason.woltje 1e65ff3f8b fix(org): applyPack requires the secret check on every write (starter packs SP3 security P2-1)
A board, workflow or pack write without assertClean now refuses
definition-secret-check-required, and the scan covers every key and
string in the plan before the writer reads any of it.
2026-11-12 15:30:07 -06:00
jason.woltje 9cfb5f64fe test(org): a definition a workflow names can still be removed (starter packs SP3 review P2) 2026-11-12 15:30:07 -06:00
jason.woltje 2af12340ae feat(org): packs, board and workflows keys and the applyPack writer op (starter packs SP3)
The organization file gains packs, board and workflows, and each code
project gains board and workflows. The reader checks their shapes and keeps
them on the loaded organization and on projects[id]. A workflow step's roles
are checked for shape only at load; applyPack resolves them at write time
(org definition, V2 template or human). An inline proposed hire keeps its
full role entry beside its {inline, sha256} pin, bounded as a definition
entry is.

applyPack(id, plan, {revision, actor, assertClean}) writes definitions,
board, workflows, project boards and workflows and one packs entry in one
commit. It keeps setDefinition's definition rules, refuses an agent writing
a definition with human-required, and refuses any other plan key.
2026-11-12 15:30:07 -06:00
jason.woltje c0a60ed3ae docs(plans): starter packs fix round 3, system types removed outright 2026-11-12 15:18:16 -06:00
jason.woltje dd878cefcf docs(plans): starter packs security and re-check fixes
Saved templates: owner-scoped path, file checks before read, provenance
reduced to id and version, agent verbs refuse origin saved. Pack, board
and workflow writes stay owner only until row 193. Board and workflow
verbs, writer path, read paths and owner lists now sit in named slices.
SP5 matches organization.set (decision-required). SP9 red test is red
today. Kind removal moves to a follow-up row; SP10 is the conversion.
2026-11-12 14:57:58 -06:00
jason.woltje 2404729232 docs(plans): starter packs slices ready for one go
Each slice names its owned files, the exact KINDS, org file key,
VERB_RIGHTS and CONSOLE_VERBS change it makes, acceptance and a test
that fails before it. No slice edits roles/ or contracts/. Open
questions say which slice each answer changes.
2026-11-12 14:42:39 -06:00
jason.woltje 2912482cd0 docs(plans): starter packs review round
Design review (8 findings) and security review (6 findings) fixed:
expected flagged lines per proposed hire, applyPack writer op, loader-set
origin, organization.change holders for board and workflow writes,
filtered-view board labels, workflow prompt guidance (inert, no runner),
system-type retired for one release before removal, installed packs
classified as executable PL1 code, saved-template placeholders and
free-text preview, pinned proposals, and removal tombstones.
2026-11-12 14:39:50 -06:00
jason.woltje bf65ea9802 docs(plans): starter packs design; system-type toolsets design superseded
Design only, per Jason's 2026-11-12 starter-packs ruling. Inventory of
existing building blocks, pack format, apply and re-apply, customizing
from inside, saved templates, system-type retirement, five shipped
packs, build slices and three open questions.
2026-11-12 14:25:04 -06:00
5 changed files with 2056 additions and 6 deletions

No files matched your search

@@ -1,5 +1,7 @@
# System-type toolsets at launch
Superseded 2026-11-12 by `docs/plans/2026-11-12_starter-packs-design.md` under Jason's starter-packs ruling; do not build this design.
Status: design only. Nothing here is built and no `roles/` or contract file is
changed. Prepared 2026-11-10 against `refactor` `8630b8dfc` for phase P2 of
#1873 (unified workspace). PRD requirement: REQ-SYS-2, "An agent working in a
File diff suppressed because it is too large. Load diff
@@ -11,9 +11,9 @@ import { checkModels, checkLimits, checkAccount } from "./launch.mjs";
import { checkVars } from "./vars.mjs";
import { parseCredentialRef } from "./credentials.mjs";
import { loadRole } from "./role.mjs";
import { checkDefinitions, checkDefinitionName, orgDefinitionName, ORG_PREFIX } from "./definitions.mjs";
import { checkDefinition, checkDefinitions, checkDefinitionName, orgDefinitionName, ORG_PREFIX } from "./definitions.mjs";
import { END_PROTECTED, parseRepoRemote } from "./vocabulary.mjs";
import { checkPlugins, checkUsageSettings } from "../../plugins/src/kinds.mjs";
import { checkPlugins, checkUsageSettings, NAME as KIND_NAME } from "../../plugins/src/kinds.mjs";
import { hasModule } from "../../plugins/src/loader.mjs";
import { readPluginLock } from "../../plugins/src/pinned.mjs";
import { checkMapping } from "../../library/src/mapping.mjs";
@@ -29,7 +29,148 @@ const BOT_NAME = /^bot-[a-z0-9][a-z0-9._-]{0,60}$/;
// How long a thread's session stays up with no turn before the launcher stops
// it, when launch.threadIdleSeconds is absent (REQ-THREAD-2, row 121).
export const THREAD_IDLE_SECONDS = 900;
const TOP_KEYS = ["businessVersion", "id", "human", "arbiters", "projects", "vars", "tracker", "roles", "launch", "plugins", "hosts", "retired", "replyLimits", "usage", "appearance", "definitions", "autonomy", "copyBindings"];
const TOP_KEYS = ["businessVersion", "id", "human", "arbiters", "projects", "vars", "tracker", "roles", "launch", "plugins", "hosts", "retired", "replyLimits", "usage", "appearance", "definitions", "autonomy", "copyBindings", "packs", "board", "workflows"];
// Starter packs SP3 (design dd878cefc sections 2, 3 and 8): the board style, the workflows and the
// record of each pack applied. The task tracker's states are fixed (REQ-TASK-1); a board names
// each once, with a label and an order. These are the bus store's states (broker.mjs STORE_STATES).
const STORE_STATES = Object.freeze(["todo", "in-progress", "in-review", "blocked", "done"]);
const BOARD_LAYOUTS = Object.freeze(["kanban", "list"]);
const PACK_ORIGINS = Object.freeze(["shipped", "installed", "saved"]);
const PACK_KEYS = Object.freeze(["id", "version", "sha256", "origin", "scope", "project", "at", "by", "added", "proposedHires"]);
const HEX64 = /^[0-9a-f]{64}$/;
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
// A workflow, step role or pack name: the plug-in kinds' NAME, at most 64 characters (ID_PATTERN's
// length, so a step role can be read as a template or definition name at write time).
const isKindName = (value) => typeof value === "string" && value.length <= 64 && KIND_NAME.test(value);
// What a pack apply wrote, as item paths (design section 3, re-apply).
const ADDED_PATH = /^(definitions\.[a-z0-9][a-z0-9._-]{0,63}|board|workflows\.[a-z][a-z0-9-]{0,63}|projects\.[a-z0-9][a-z0-9._-]{0,63}\.(board|workflows\.[a-z][a-z0-9-]{0,63}))$/;
// A proposed hire's pin (design section 3): {template, sha256} for a template reference,
// {definition, rev} for a definition the apply wrote, {inline, sha256} for a pack definition it did
// not write (inline names it; sha256 is the canonical hash of the full entry stored beside the pin).
const PIN_SHAPES = Object.freeze([["template", "sha256"], ["definition", "rev"], ["inline", "sha256"]]);
// board: {columns: [{state, label}] x5, layout}. Returns a copy.
export function checkBoard(board, where) {
requireObject(board, where);
rejectUnknownKeys(board, ["columns", "layout"], where);
if (!Array.isArray(board.columns) || board.columns.length !== STORE_STATES.length) refuse(`${where}.columns must list the ${STORE_STATES.length} task states`);
const seen = new Set();
const columns = board.columns.map((column, i) => {
const at = `${where}.columns[${i}]`;
requireObject(column, at);
rejectUnknownKeys(column, ["state", "label"], at);
if (!STORE_STATES.includes(column.state)) refuse(`${at}.state must be one of ${STORE_STATES.join(", ")}`);
if (seen.has(column.state)) refuse(`${where}.columns names ${column.state} twice`);
seen.add(column.state);
requireString(column.label, `${at}.label`, { max: 32 });
return { state: column.state, label: column.label };
});
if (!BOARD_LAYOUTS.includes(board.layout)) refuse(`${where}.layout must be ${BOARD_LAYOUTS.join(" or ")}`);
return { columns, layout: board.layout };
}
// workflows: {<name>: {title?, description?, steps: [{title, state, roles}]}}. The load checks each
// step role's shape only: a role that no longer resolves (a definition removed, a template changed)
// must not fail the whole organization. applyPack resolves the roles of the workflows it writes.
export function checkWorkflows(workflows, where) {
requireObject(workflows, where);
const out = {};
Object.entries(workflows).forEach(([name, workflow], n) => {
if (!isKindName(name)) refuse(`${where} key ${n + 1} must be a workflow name matching ${KIND_NAME}, at most 64 characters`);
const at = `${where}.${name}`;
requireObject(workflow, at);
rejectUnknownKeys(workflow, ["title", "description", "steps"], at);
if (Object.hasOwn(workflow, "title")) requireString(workflow.title, `${at}.title`, { max: 120 });
if (Object.hasOwn(workflow, "description")) requireString(workflow.description, `${at}.description`, { max: 1000 });
if (!Array.isArray(workflow.steps) || workflow.steps.length < 1 || workflow.steps.length > 20) refuse(`${at}.steps must list 1 to 20 steps`);
const steps = workflow.steps.map((step, i) => {
const s = `${at}.steps[${i}]`;
requireObject(step, s);
rejectUnknownKeys(step, ["title", "state", "roles"], s);
requireString(step.title, `${s}.title`, { max: 120 });
if (!STORE_STATES.includes(step.state)) refuse(`${s}.state must be one of ${STORE_STATES.join(", ")}`);
if (!Array.isArray(step.roles) || step.roles.length < 1 || step.roles.length > 8) refuse(`${s}.roles must list 1 to 8 roles`);
const roles = requireDistinctList(step.roles, `${s}.roles`, (role) => {
if (!isKindName(role)) refuse(`${s}.roles entries must be role names matching ${KIND_NAME}, or human`);
});
return { title: step.title, state: step.state, roles };
});
out[name] = {
...(Object.hasOwn(workflow, "title") ? { title: workflow.title } : {}),
...(Object.hasOwn(workflow, "description") ? { description: workflow.description } : {}), steps,
};
});
return out;
}
function checkPin(pin, where) {
requireObject(pin, where);
const keys = Object.keys(pin);
const shape = PIN_SHAPES.find((s) => s.length === keys.length && s.every((k) => keys.includes(k)));
if (!shape) refuse(`${where} must be {template, sha256}, {definition, rev} or {inline}`);
if (shape[0] === "template") {
requireId(pin.template, `${where}.template`);
if (typeof pin.sha256 !== "string" || !HEX64.test(pin.sha256)) refuse(`${where}.sha256 must be 64 lowercase hex characters`);
} else if (shape[0] === "definition") {
checkDefinitionName(pin.definition, `${where}.definition`);
requirePositiveInt(pin.rev, `${where}.rev`);
} else {
checkDefinitionName(pin.inline, `${where}.inline`);
if (typeof pin.sha256 !== "string" || !HEX64.test(pin.sha256)) refuse(`${where}.sha256 must be 64 lowercase hex characters`);
}
return { ...pin };
}
// packs: the record of each pack applied, oldest first. Each entry is history: its project is not
// checked against today's projects, and its proposals are pins, not hires.
export function checkPacks(packs, where) {
if (!Array.isArray(packs)) refuse(`${where} must be a list`);
return packs.map((entry, i) => {
const at = `${where}[${i}]`;
requireObject(entry, at);
rejectUnknownKeys(entry, PACK_KEYS, at);
for (const key of PACK_KEYS) if (!Object.hasOwn(entry, key)) refuse(`${at} requires "${key}"`);
if (!isKindName(entry.id)) refuse(`${at}.id must match ${KIND_NAME}, at most 64 characters`);
if (typeof entry.version !== "string" || entry.version.length > 64 || !SEMVER.test(entry.version)) refuse(`${at}.version must be a semver version`);
if (typeof entry.sha256 !== "string" || !HEX64.test(entry.sha256)) refuse(`${at}.sha256 must be 64 lowercase hex characters`);
if (!PACK_ORIGINS.includes(entry.origin)) refuse(`${at}.origin must be one of ${PACK_ORIGINS.join(", ")}`);
if (entry.scope === "org") { if (entry.project !== null) refuse(`${at}.project must be null for an org pack`); }
else if (entry.scope === "project") requireId(entry.project, `${at}.project`);
else refuse(`${at}.scope must be org or project`);
if (typeof entry.at !== "string" || Number.isNaN(Date.parse(entry.at)) || new Date(entry.at).toISOString() !== entry.at) refuse(`${at}.at must be an ISO timestamp`);
requireId(entry.by, `${at}.by`);
const added = requireDistinctList(entry.added, `${at}.added`, (path) => {
if (typeof path !== "string" || !ADDED_PATH.test(path)) refuse(`${at}.added entries must be item paths`);
});
if (!Array.isArray(entry.proposedHires) || entry.proposedHires.length > 8) refuse(`${at}.proposedHires must list at most 8 hires`);
const names = new Set();
const proposedHires = entry.proposedHires.map((hire, j) => {
const h = `${at}.proposedHires[${j}]`;
requireObject(hire, h);
rejectUnknownKeys(hire, ["name", "reason", "pin", "entry"], h);
requireId(hire.name, `${h}.name`);
if (names.has(hire.name)) refuse(`${at}.proposedHires names ${hire.name} twice`);
names.add(hire.name);
if (Object.hasOwn(hire, "reason")) requireString(hire.reason, `${h}.reason`, { max: 500 });
if (!Object.hasOwn(hire, "pin")) refuse(`${h} requires "pin"`);
const pin = checkPin(hire.pin, `${h}.pin`);
// An inline proposal keeps the full role entry, so the raise can read it back and check its
// hash; no other pin carries one. It is bounded as a definition entry is (keys, then
// checkComposition), with no rev of its own required. The hash is not checked here: a stale
// proposal is the raise's refusal, not a reason the organization fails to load.
if (Object.hasOwn(pin, "inline") !== Object.hasOwn(hire, "entry")) refuse(`${h} carries an entry exactly when its pin is inline`);
let entry;
if (Object.hasOwn(hire, "entry")) {
requireObject(hire.entry, `${h}.entry`);
checkDefinition(pin.inline, Object.hasOwn(hire.entry, "rev") ? hire.entry : { ...hire.entry, rev: 1 }, { where: `${h}.entry` });
entry = structuredClone(hire.entry);
}
return { name: hire.name, ...(Object.hasOwn(hire, "reason") ? { reason: hire.reason } : {}), pin, ...(entry === undefined ? {} : { entry }) };
});
return { id: entry.id, version: entry.version, sha256: entry.sha256, origin: entry.origin, scope: entry.scope, project: entry.project, at: entry.at, by: entry.by, added, proposedHires };
});
}
// The config directory: next to the system config, $MOSAIC_CONFIG or
// ~/.config/mosaic-dev/config.json, the same rule mosaic-config.mjs uses.
@@ -303,7 +444,7 @@ export function validateOrganizationDocument(document, file, { rolesDir, dataRoo
projects[id] = checkMapping(entry, `${file} projects.${id}`, { fail: (code, message) => refuse(`${code}: ${message}`) });
continue;
}
rejectUnknownKeys(entry, ["root", "repo", "remote", "git", "agents"], `${file} projects.${id}`);
rejectUnknownKeys(entry, ["root", "repo", "remote", "git", "agents", "board", "workflows"], `${file} projects.${id}`);
// #1873 A1: the project's own agent entries. Checked so a bad entry refuses the whole file, but
// not copied below: nothing a bus loads sees them; only the Console's agent-levels read does.
if (Object.hasOwn(entry, "agents")) checkProjectAgents(entry.agents, { rolesDir, where: `projects.${id}.agents`, definitions:document.definitions });
@@ -327,7 +468,9 @@ export function validateOrganizationDocument(document, file, { rolesDir, dataRoo
}
projects[id] = { root: entry.root, ...(Object.hasOwn(entry, "repo") ? { repo: entry.repo } : {}),
...(Object.hasOwn(entry, "remote") ? { remote: entry.remote } : {}),
...(Object.hasOwn(entry, "git") ? { git: { ...entry.git } } : {}) };
...(Object.hasOwn(entry, "git") ? { git: { ...entry.git } } : {}),
...(Object.hasOwn(entry, "board") ? { board: checkBoard(entry.board, `${file} projects.${id}.board`) } : {}),
...(Object.hasOwn(entry, "workflows") ? { workflows: checkWorkflows(entry.workflows, `${file} projects.${id}.workflows`) } : {}) };
}
for (const [instance, role] of Object.entries(roles)) {
for (const project of role.publish ?? []) {
@@ -375,11 +518,15 @@ export function validateOrganizationDocument(document, file, { rolesDir, dataRoo
}
const appearance = checkAppearance(document.appearance, `${file} appearance`);
const retired = document.retired === undefined ? undefined : checkRetired(document.retired, roles, file);
const board = document.board === undefined ? undefined : checkBoard(document.board, `${file} board`);
const workflows = document.workflows === undefined ? undefined : checkWorkflows(document.workflows, `${file} workflows`);
const packs = document.packs === undefined ? undefined : checkPacks(document.packs, `${file} packs`);
return deepFreeze({
businessVersion: 1, id: document.id, file, human, arbiters, projects, vars, tracker, roles, launch, plugins, definitions,
...(autonomy === undefined ? {} : { autonomy }), ...(retired ? { retired } : {}), ...(usage === undefined ? {} : { usage }), ...(appearance === undefined ? {} : { appearance }),
...(copyBindings === undefined ? {} : { copyBindings }),
...(board === undefined ? {} : { board }), ...(workflows === undefined ? {} : { workflows }), ...(packs === undefined ? {} : { packs }),
});
}
+194 -1
View File
@@ -441,6 +441,199 @@ export function organizationWriter({ dir, rolesDir, dataRoot = null, barrier = (
return { ...out, rev: logged.rev };
}
// Starter packs SP3 (design dd878cefc sections 3 and 8): the one writer of `board`, `workflows`,
// each code project's `board` and `workflows`, and `packs`, and a pack's way to add definitions.
// Plan keys, and nothing else (invalid-request):
// definitions {name: entry without rev}: adds only, at rev 1; needs a packs entry
// board a board, or null to remove it
// workflows {name: workflow, or null to remove it}, merged by name
// projects.<p>.board as board, for code project p
// projects.<p>.workflows as workflows, for code project p
// packs the one entry to append, without at, by and added: this writer stamps them
// A plan with a packs entry only adds: it overwrites and removes nothing, an org pack writes no
// project key, and a project pack writes only its own project's keys. A board or workflow write is
// a plan holding that key alone. One commit: a refusal or a fault leaves all of it or none.
// setDefinition's rules hold for each definition: an agent or the tester never writes one
// (human-required), a name an instance uses refuses definition-in-use, the credential store's
// check is required and checkDefinition runs it with checkComposition. An agent never applies a
// saved template either. The rest is organization.change's, with no decision (Jason, 11-12).
// assertClean, the credential store's check, is required on every call and runs on every key and
// string in the plan before anything else reads it: workflow text reaches agents' prompts and a
// pack's inline proposals are role entries. Each workflow written must name
// roles that resolve now: an org definition (after this plan), a V2 template, or human. The load
// checks only their shape, so a later removal never fails the file.
const PLAN_TOP = ["definitions", "board", "workflows", "packs"];
const PLAN_PROJECT = /^projects\.([a-z0-9][a-z0-9._-]{0,63})\.(board|workflows)$/;
const PLAN_PACK_KEYS = ["id", "version", "sha256", "origin", "scope", "project", "proposedHires"];
const PLAN_NAME = /^[a-z][a-z0-9-]{0,63}$/;
const planObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && [Object.prototype, null].includes(Object.getPrototypeOf(v));
function applyPack(id, plan, { revision, actor, assertClean, detail }) {
if (typeof assertClean !== "function") fail("definition-secret-check-required", "a pack, board or workflow write needs the credential store's secret check; write it through the bus");
if (!planObject(plan) || Object.keys(plan).length === 0) fail("invalid-request", "a pack plan is an object naming at least one item");
const scan = (value) => {
if (typeof value === "string") assertClean(value);
else if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) { assertClean(k); scan(v); }
};
scan(plan);
const projectKeys = [];
Object.keys(plan).forEach((key, i) => {
const match = PLAN_PROJECT.exec(key);
if (match) projectKeys.push({ key, project: match[1], item: match[2] });
else if (!PLAN_TOP.includes(key)) fail("invalid-request", `plan key ${i + 1} is not one a pack writes (definitions, board, workflows, projects.<p>.board, projects.<p>.workflows, packs)`);
});
const pack = plan.packs;
if (pack !== undefined) {
if (!planObject(pack)) fail("invalid-request", "plan packs must be one entry");
for (const key of Object.keys(pack)) {
if (["at", "by", "added"].includes(key)) fail("invalid-request", `plan packs carries ${key}, which the writer stamps`);
if (!PLAN_PACK_KEYS.includes(key)) fail("invalid-request", "plan packs carries an unknown key");
}
if (pack.scope === "project") {
if (Object.hasOwn(plan, "board") || Object.hasOwn(plan, "workflows")) fail("invalid-request", "a project pack writes only its project's board and workflows");
if (projectKeys.some((k) => k.project !== pack.project)) fail("invalid-request", "a project pack writes only its own project's keys");
} else if (projectKeys.length) fail("invalid-request", "an org pack writes no project key");
} else if (Object.hasOwn(plan, "definitions")) {
fail("invalid-request", "definitions arrive only with a packs entry; write one definition through setDefinition");
}
const at = new Date().toISOString();
const logged = { ...detail, verb: pack ? "pack.apply" : "organization.layout", pack: pack ? pack.id : null, added: null };
const build = (old) => {
const agents = Object.entries(old.roles ?? {}).flatMap(([n, role]) => [n, `${role?.definition}.${n}`]);
const agent = actor === "tester" || agents.includes(actor) || (old.retired ?? []).includes(actor);
if (agent && (Object.hasOwn(plan, "definitions") || pack?.origin === "saved")) fail("human-required");
const next = structuredClone(old);
const added = [];
const written = [];
if (Object.hasOwn(plan, "definitions")) {
if (!planObject(plan.definitions) || Object.keys(plan.definitions).length === 0) fail("invalid-request", "plan definitions must name at least one definition");
const definitions = { ...(old.definitions ?? {}) };
for (const [name, entry] of Object.entries(plan.definitions)) {
checkDefinitionName(name, "plan definition name");
if (Object.hasOwn(definitions, name)) {
const users = instancesNaming(old, name);
if (users.length) fail("definition-in-use", `definition ${name} is named by ${users.join(", ")}; a change to a hired agent's role is a change decision`);
fail("definition-exists", `definitions already declares ${name}; a pack apply overwrites nothing`);
}
if (!planObject(entry)) fail("invalid-request", `plan definition ${name} must be an object`);
if (Object.hasOwn(entry, "rev")) fail("definition-rev-conflict", `a pack definition carries no rev; apply writes ${name} at rev 1`);
const value = { ...structuredClone(entry), rev: 1 };
checkDefinition(name, value, { assertClean });
definitions[name] = value;
added.push(`definitions.${name}`);
}
next.definitions = definitions;
}
const setBoard = (target, value, path) => {
if (value === null) {
if (pack) fail("invalid-request", "a pack apply removes nothing");
if (!Object.hasOwn(target, "board")) fail("invalid-request", `${path} is not set`);
delete target.board;
return;
}
if (pack && Object.hasOwn(target, "board")) fail("invalid-request", `${path} is already set; a pack apply overwrites nothing`);
target.board = structuredClone(value);
added.push(path);
};
const setWorkflows = (target, value, path) => {
if (!planObject(value) || Object.keys(value).length === 0) fail("invalid-request", `${path} must name at least one workflow`);
const workflows = { ...(target.workflows ?? {}) };
for (const [name, workflow] of Object.entries(value)) {
if (!PLAN_NAME.test(name)) fail("invalid-request", `${path} keys must be workflow names matching ${PLAN_NAME}`);
const exists = Object.hasOwn(workflows, name);
if (workflow === null) {
if (pack) fail("invalid-request", "a pack apply removes nothing");
if (!exists) fail("invalid-request", `${path}.${name} is not set`);
delete workflows[name];
continue;
}
if (pack && exists) fail("invalid-request", `${path}.${name} is already set; a pack apply overwrites nothing`);
workflows[name] = structuredClone(workflow);
added.push(`${path}.${name}`);
written.push({ path: `${path}.${name}`, workflow: workflows[name] });
}
if (Object.keys(workflows).length) target.workflows = workflows;
else delete target.workflows;
};
if (Object.hasOwn(plan, "board")) setBoard(next, plan.board, "board");
if (Object.hasOwn(plan, "workflows")) setWorkflows(next, plan.workflows, "workflows");
for (const { key, project, item } of projectKeys) {
const entry = Object.hasOwn(next.projects ?? {}, project) ? next.projects[project] : null;
if (!planObject(entry) || entry.kind === "library") fail("invalid-request", `projects declares no code project ${project}`);
if (item === "board") setBoard(entry, plan[key], `projects.${project}.board`);
else setWorkflows(entry, plan[key], `projects.${project}.workflows`);
}
// Each role a written workflow names resolves now. A role that isn't a name is left to the
// file check, which refuses it; only a name reaches readTemplate.
const templates = new Map();
const template = (name) => {
if (!templates.has(name)) {
try { readTemplate(rolesDir, name); templates.set(name, true); } catch (error) {
if (error?.name !== "BusinessError") throw error;
templates.set(name, false);
}
}
return templates.get(name);
};
for (const { path, workflow } of written) {
(Array.isArray(workflow?.steps) ? workflow.steps : []).forEach((step, i) => {
for (const role of Array.isArray(step?.roles) ? step.roles : []) {
if (role === "human" || typeof role !== "string" || !PLAN_NAME.test(role)) continue;
if (Object.hasOwn(next.definitions ?? {}, role) || template(role)) continue;
fail("invalid-request", `${path} step ${i + 1} names ${role}, which is not an org definition, a V2 template or human`);
}
});
}
if (pack) {
// A {definition, rev} pin names a definition this apply wrote; an {inline, sha256} pin is the
// canonical hash of the entry stored beside it (design section 3). The file check bounds the
// entry's shape; this is the write's own check that the pin means what it says.
for (const hire of Array.isArray(pack.proposedHires) ? pack.proposedHires : []) {
const pin = hire?.pin;
if (planObject(pin) && Object.hasOwn(pin, "definition") && !(added.includes(`definitions.${pin.definition}`) && pin.rev === 1)) {
fail("invalid-request", "a {definition, rev} proposal pins a definition this apply writes, at rev 1");
}
if (planObject(pin) && Object.hasOwn(pin, "inline") && planObject(hire.entry) && pin.sha256 !== sha256(canonicalJson(hire.entry))) {
fail("invalid-request", "an {inline, sha256} proposal's sha256 is the canonical hash of its entry");
}
}
next.packs = [...(old.packs ?? []), {
id: pack.id, version: pack.version, sha256: pack.sha256, origin: pack.origin, scope: pack.scope, project: pack.project,
at, by: actor, added, proposedHires: structuredClone(pack.proposedHires),
}];
}
logged.added = added;
return { next, added };
};
let result;
const guard = (old, document) => {
if (canonicalJson(document) !== canonicalJson(build(old).next)) fail("invalid-request", "the document is not the plan's");
// The key fence itself, whatever build does: only the plan's keys change, definitions and
// packs only grow, and a project changes only in a planned board or workflows.
for (const key of new Set([...Object.keys(old), ...Object.keys(document)])) {
if (key === "projects" || PLAN_TOP.includes(key)) continue;
if (canonicalJson(old[key] ?? null) !== canonicalJson(document[key] ?? null)) fail("invalid-request", "a pack plan changes only definitions, board, workflows, packs and project boards and workflows");
}
for (const [name, entry] of Object.entries(old.definitions ?? {})) {
if (canonicalJson(entry) !== canonicalJson(document.definitions?.[name] ?? null)) fail("organization-definitions-only", "a pack apply changes no existing definition");
}
const oldPacks = old.packs ?? [];
if (canonicalJson(oldPacks) !== canonicalJson((document.packs ?? []).slice(0, oldPacks.length))) fail("invalid-request", "a pack apply keeps every earlier packs entry");
const planned = new Set(projectKeys.map((k) => `${k.project}.${k.item}`));
const projects = (doc) => doc?.projects ?? {};
for (const project of new Set([...Object.keys(projects(old)), ...Object.keys(projects(document))])) {
const a = projects(old)[project] ?? null;
const b = projects(document)[project] ?? null;
const rest = (e) => (planObject(e) ? Object.fromEntries(Object.entries(e).filter(([k]) => k !== "board" && k !== "workflows")) : e);
if (canonicalJson(rest(a)) !== canonicalJson(rest(b))) fail("organization-projects-protected");
for (const item of ["board", "workflows"]) {
if (canonicalJson(a?.[item] ?? null) !== canonicalJson(b?.[item] ?? null) && !planned.has(`${project}.${item}`)) fail("organization-projects-protected");
}
}
};
const out = commit(id, (old) => { result = build(old); return result.next; }, { revision, actor, guard, pathsOnly: true, detail: logged });
return { ...out, added: result.added };
}
// Lifecycle Task 2: the only writer that adds a project mapping, and only the exact bootstrap
// addition: one new key under the managed product layout, a receipt naming the request and plan, and
// no other change. It compares both the document it built and the bytes it will publish. A stale
@@ -779,5 +972,5 @@ export function organizationWriter({ dir, rolesDir, dataRoot = null, barrier = (
});
}
return { logFile, read, readRaw, write, addProject, setProjectRemote, setProjectAgent, setDefinition, applyHireDecision, reverseHireDecision, applyCopyBindingDecision, stageCopyGrant, commitCopyGrant, hold, holdAsync, findAddition, record, recover: id => withLock(id, () => recover(id)) };
return { logFile, read, readRaw, write, addProject, setProjectRemote, setProjectAgent, setDefinition, applyPack, applyHireDecision, reverseHireDecision, applyCopyBindingDecision, stageCopyGrant, commitCopyGrant, hold, holdAsync, findAddition, record, recover: id => withLock(id, () => recover(id)) };
}
@@ -0,0 +1,425 @@
// Starter packs SP3 (design dd878cefc sections 3 and 8, fix round 3 c0a60ed3a): the organization file's packs, board and
// workflows keys, each project's board and workflows, and the writer's applyPack operation.
// Fixtures live in a temp directory; nothing reads the real config.
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 { validateBusinessDocument } from "../src/organization-config.mjs";
import { businessDoc, rolesCopy, tmp, writeJson } from "./helpers.mjs";
import { canonicalJson, sha256 } from "../src/util.mjs";
const SECRET = "sk-test-NOT-A-SECRET-1234";
const assertClean = (value) => { if (String(value).includes(SECRET)) throw new Error("credential-leak"); };
const HASH = "a".repeat(64);
const PLAIN = { role: { tools: ["read"], network: "none", authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: "Read and report.\n" };
// An inline proposal: a pack definition the apply did not write, kept whole beside its pin.
const WRITER = { ...PLAIN, instructions: "Write the docs.\n" };
const WRITER_HASH = sha256(canonicalJson(WRITER));
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 LIST_BOARD = { ...structuredClone(BOARD), layout: "list" };
const WORKFLOWS = {
change: {
title: "Change", description: "One change from task to merge.",
steps: [
{ title: "Write the task", state: "todo", roles: ["pm"] },
{ title: "Implement", state: "in-progress", roles: ["coder"] },
{ title: "Approve the merge", state: "done", roles: ["human"] },
],
},
};
const PACK = {
id: "coding", version: "1.0.0", sha256: HASH, origin: "shipped", scope: "org", project: null,
at: "2026-11-12T20:00:00.000Z", by: "jason", added: ["definitions.scout", "board", "workflows.change"],
proposedHires: [
{ name: "coder-1", reason: "writes the code", pin: { template: "coder", sha256: HASH } },
{ name: "scout-1", pin: { definition: "scout", rev: 1 } },
{ name: "writer-1", pin: { inline: "writer", sha256: WRITER_HASH }, entry: WRITER },
],
};
function fixture(t, edit = (doc) => doc, barrier) {
const root = tmp();
t.after(() => rmSync(root, { recursive: true, force: true }));
const dir = join(root, "config");
const rolesDir = rolesCopy(root);
const document = edit(businessDoc(root));
const file = writeJson(join(dir, "businesses/acme.json"), document);
return { root, dir, file, rolesDir, document, writer: organizationWriter({ dir, rolesDir, barrier }) };
}
const rev = (f) => f.writer.read("acme").revision;
const doc = (f) => JSON.parse(readFileSync(f.file, "utf8"));
const bytes = (f) => readFileSync(f.file, "utf8");
const load = (f, document) => validateBusinessDocument(document, f.file, { rolesDir: f.rolesDir });
const without = (object, ...keys) => Object.fromEntries(Object.entries(object).filter(([k]) => !keys.includes(k)));
// An undefined value in `extra` drops that key from the plan.
const packPlan = (extra = {}) => Object.fromEntries(Object.entries({
definitions: { scout: PLAIN }, board: BOARD, workflows: WORKFLOWS,
packs: without({ ...PACK, proposedHires: PACK.proposedHires.slice(0, 2) }, "at", "by", "added"), ...extra,
}).filter(([, value]) => value !== undefined));
test("SP3 red before: a file with a board key loads, and the loaded organization keeps it", (t) => {
const f = fixture(t);
const loaded = load(f, { ...f.document, board: BOARD });
assert.deepEqual(loaded.board, BOARD);
});
test("SP3 valid packs, board and workflows load, and the organization and each project keep them", (t) => {
const f = fixture(t);
const document = {
...f.document, board: BOARD, workflows: WORKFLOWS, packs: [PACK, { ...PACK, origin: "saved", scope: "project", project: "stack", added: ["projects.stack.board", "projects.stack.workflows.change"], proposedHires: [] }],
projects: { stack: { ...f.document.projects.stack, board: LIST_BOARD, workflows: WORKFLOWS } },
};
const loaded = load(f, document);
assert.deepEqual(loaded.board, BOARD);
assert.deepEqual(loaded.workflows, WORKFLOWS);
assert.deepEqual(loaded.packs, document.packs);
assert.deepEqual(loaded.projects.stack.board, LIST_BOARD);
assert.deepEqual(loaded.projects.stack.workflows, WORKFLOWS);
assert.ok(Object.isFrozen(loaded.projects.stack.board));
// Title and description are optional; the absent keys stay absent.
const bare = load(f, { ...f.document, workflows: { quick: { steps: [{ title: "Do it", state: "done", roles: ["human"] }] } } });
assert.deepEqual(bare.workflows, { quick: { steps: [{ title: "Do it", state: "done", roles: ["human"] }] } });
// A file without the keys loads as it did, with none of them on the loaded object.
const plain = load(f, f.document);
for (const key of ["board", "workflows", "packs"]) assert.ok(!Object.hasOwn(plain, key), key);
assert.ok(!Object.hasOwn(plain.projects.stack, "board"));
});
test("SP3 a workflow naming a role that does not exist still loads (the load checks shape only)", (t) => {
const f = fixture(t);
const ghost = { change: { steps: [{ title: "Haunt", state: "todo", roles: ["ghost-role", "no-such-template"] }] } };
assert.deepEqual(load(f, { ...f.document, workflows: ghost }).workflows, ghost);
assert.deepEqual(load(f, { ...f.document, projects: { stack: { ...f.document.projects.stack, workflows: ghost } } }).projects.stack.workflows, ghost);
});
test("SP3 a definition a workflow names can still be removed, and the file then still loads", (t) => {
// Design c0a60ed3a SP3 acceptance: "an org file whose step names a definition removed since loads,
// and removing that definition succeeds".
const f = fixture(t);
const scouting = { scouting: { steps: [{ title: "Scout", state: "todo", roles: ["scout", "human"] }] } };
const packs = { ...packPlan().packs, proposedHires: [] };
f.writer.applyPack("acme", { definitions: { scout: PLAIN }, workflows: scouting, packs }, { revision: rev(f), actor: "jason", assertClean });
f.writer.applyPack("acme", { "projects.stack.workflows": scouting }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f).definitions.scout, { ...PLAIN, rev: 1 });
f.writer.setDefinition("acme", "scout", () => null, { revision: rev(f), actor: "jason", assertClean });
const after = doc(f);
assert.equal(after.definitions?.scout, undefined);
assert.deepEqual(after.workflows, scouting);
assert.deepEqual(after.projects.stack.workflows, scouting);
const loaded = load(f, after);
assert.deepEqual(loaded.workflows, scouting);
assert.deepEqual(loaded.projects.stack.workflows, scouting);
// The writer reads it back too; only a new write of that workflow resolves its roles again.
assert.equal(f.writer.read("acme").revision, rev(f));
assert.throws(() => f.writer.applyPack("acme", { workflows: { again: scouting.scouting } }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
});
test("SP3 each invalid shape refuses", (t) => {
const f = fixture(t);
const col = (i, patch) => ({ ...BOARD, columns: BOARD.columns.map((c, j) => (j === i ? { ...c, ...patch } : c)) });
const step = (patch) => ({ change: { ...WORKFLOWS.change, steps: [{ ...WORKFLOWS.change.steps[0], ...patch }] } });
const pack = (patch) => [{ ...PACK, ...patch }];
const steps = (n) => Array.from({ length: n }, () => ({ title: "Step", state: "todo", roles: ["human"] }));
const cases = {
"board not an object": { board: [] },
"board unknown key": { board: { ...BOARD, colour: "red" } },
"board missing layout": { board: without(BOARD, "layout") },
"board four columns": { board: { ...BOARD, columns: BOARD.columns.slice(0, 4) } },
"board six columns": { board: { ...BOARD, columns: [...BOARD.columns, BOARD.columns[0]] } },
"board duplicate state": { board: col(1, { state: "todo" }) },
"board unknown state": { board: col(0, { state: "backlog" }) },
"board column unknown key": { board: col(0, { colour: "red" }) },
"board label empty": { board: col(0, { label: "" }) },
"board label 33 characters": { board: col(0, { label: "x".repeat(33) }) },
"board layout grid": { board: { ...BOARD, layout: "grid" } },
"workflows not an object": { workflows: [] },
"workflow name not NAME": { workflows: { Change_1: WORKFLOWS.change } },
"workflow unknown key": { workflows: { change: { ...WORKFLOWS.change, owner: "pm" } } },
"workflow no steps": { workflows: { change: { ...WORKFLOWS.change, steps: [] } } },
"workflow 21 steps": { workflows: { change: { steps: steps(21) } } },
"workflow title 121 characters": { workflows: { change: { ...WORKFLOWS.change, title: "x".repeat(121) } } },
"workflow description 1001 characters": { workflows: { change: { ...WORKFLOWS.change, description: "x".repeat(1001) } } },
"step unknown key": { workflows: step({ id: "plan" }) },
"step missing roles": { workflows: { change: { steps: [{ title: "Step", state: "todo" }] } } },
"step title 121 characters": { workflows: step({ title: "x".repeat(121) }) },
"step unknown state": { workflows: step({ state: "doing" }) },
"step roles empty": { workflows: step({ roles: [] }) },
"step nine roles": { workflows: step({ roles: ["a", "b", "c", "d", "e", "f", "g", "h", "i"] }) },
"step role not NAME": { workflows: step({ roles: ["Coder_1"] }) },
"step duplicate role": { workflows: step({ roles: ["pm", "pm"] }) },
"packs not a list": { packs: {} },
"pack unknown key": { packs: pack({ hires: [] }) },
"pack missing key": { packs: [without(PACK, "sha256")] },
"pack origin unknown": { packs: pack({ origin: "borrowed" }) },
"pack scope root": { packs: pack({ scope: "root" }) },
"org pack with project": { packs: pack({ project: "stack" }) },
"project pack without project": { packs: pack({ scope: "project", project: null }) },
"pack id not NAME": { packs: pack({ id: "Coding" }) },
"pack version not semver": { packs: pack({ version: "v1" }) },
"pack sha256 not hex": { packs: pack({ sha256: "A".repeat(64) }) },
"pack at not a timestamp": { packs: pack({ at: "yesterday" }) },
"pack by not an id": { packs: pack({ by: "Not An Id" }) },
"pack added bad path": { packs: pack({ added: ["plugins.theme"] }) },
"pack nine proposed hires": { packs: pack({ proposedHires: Array.from({ length: 9 }, (_, i) => ({ name: `coder-${i}`, pin: { template: "coder", sha256: HASH } })) }) },
"proposed hire two pins": { packs: pack({ proposedHires: [{ name: "coder-1", pin: { template: "coder", sha256: HASH, inline: "writer" } }] }) },
"proposed hire no pin": { packs: pack({ proposedHires: [{ name: "coder-1" }] }) },
"proposed hire unknown key": { packs: pack({ proposedHires: [{ name: "coder-1", level: "org", pin: { template: "coder", sha256: HASH } }] }) },
"proposed hire duplicate name": { packs: pack({ proposedHires: [{ name: "c", pin: { template: "coder", sha256: HASH } }, { name: "c", pin: { template: "coder", sha256: HASH } }] }) },
"inline pin old hash-only shape": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: HASH }, entry: WRITER }] }) },
"inline pin without entry": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH } }] }) },
"inline pin name not an id": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "Writer One", sha256: WRITER_HASH }, entry: WRITER }] }) },
"inline pin sha256 not hex": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: "x" }, entry: WRITER }] }) },
"template pin with entry": { packs: pack({ proposedHires: [{ name: "w", pin: { template: "coder", sha256: HASH }, entry: WRITER }] }) },
"inline entry not an object": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: "Write the docs." }] }) },
"inline entry unknown key": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: { ...WRITER, level: "org" } }] }) },
"inline entry unknown tool": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: { ...WRITER, role: { ...WRITER.role, tools: ["teleport"] } } }] }) },
"inline entry without role": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: without(WRITER, "role") }] }) },
"inline entry rev zero": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: { ...WRITER, rev: 0 } }] }) },
"proposed hire rev zero": { packs: pack({ proposedHires: [{ name: "c", pin: { definition: "scout", rev: 0 } }] }) },
};
for (const [name, patch] of Object.entries(cases)) {
assert.throws(() => load(f, { ...f.document, ...patch }), { name: "BusinessError" }, name);
}
// The project keys refuse the same shapes, and other project keys still refuse.
const project = (patch) => ({ ...f.document, projects: { stack: { ...f.document.projects.stack, ...patch } } });
for (const [name, patch] of Object.entries({
"project board layout": { board: { ...BOARD, layout: "grid" } },
"project workflow 0 steps": { workflows: { change: { steps: [] } } },
"project unknown key": { packs: [] },
})) assert.throws(() => load(f, project(patch)), { name: "BusinessError" }, name);
});
test("SP3 applyPack writes definitions, board, workflows and the packs entry in one commit", (t) => {
const f = fixture(t);
const before = doc(f);
const out = f.writer.applyPack("acme", packPlan(), { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(out.added, ["definitions.scout", "board", "workflows.change"]);
assert.equal(out.revision, rev(f));
const after = doc(f);
assert.deepEqual(after.definitions.scout, { ...PLAIN, rev: 1 });
assert.deepEqual(after.board, BOARD);
assert.deepEqual(after.workflows, WORKFLOWS);
assert.equal(after.packs.length, 1);
const entry = after.packs[0];
assert.deepEqual(without(entry, "at"), { ...without(PACK, "at"), by: "jason", added: out.added, proposedHires: PACK.proposedHires.slice(0, 2) });
assert.match(entry.at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
// Nothing else changed.
assert.deepEqual(without(after, "definitions", "board", "workflows", "packs"), without(before, "definitions", "board", "workflows", "packs"));
// One prepared and one committed receipt, and the receipt logs paths, not values.
const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l));
assert.deepEqual(log.map((e) => e.state), ["prepared", "committed"]);
assert.ok(!JSON.stringify(log).includes("Backlog"));
// A second pack appends; the first entry stays.
const second = { packs: { ...packPlan().packs, id: "review", proposedHires: [] }, workflows: { review: WORKFLOWS.change } };
f.writer.applyPack("acme", second, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f).packs.map((p) => p.id), ["coding", "review"]);
assert.deepEqual(doc(f).packs[0], entry);
assert.deepEqual(doc(f).packs[1].added, ["workflows.review"]);
});
test("SP3 applyPack writes everything or nothing", (t) => {
// A plan item after the definitions is bad: the valid definition is not written either.
const f = fixture(t);
const original = bytes(f);
const bad = packPlan({ workflows: { change: { steps: [{ title: "Haunt", state: "todo", roles: ["ghost-role"] }] } } });
assert.throws(() => f.writer.applyPack("acme", bad, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
assert.equal(bytes(f), original);
const badBoard = packPlan({ board: { ...BOARD, layout: "grid" } });
assert.throws(() => f.writer.applyPack("acme", badBoard, { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" });
assert.equal(bytes(f), original);
// A fault after the candidate is prepared, with every item checked, leaves the file unchanged.
const crash = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir, barrier: (stage) => { if (stage === "prepared") throw new Error("fixture crash"); } });
assert.throws(() => crash.applyPack("acme", packPlan(), { revision: rev(f), actor: "jason", assertClean }), /fixture crash/);
assert.equal(bytes(f), original);
f.writer.recover("acme");
assert.equal(bytes(f), original);
assert.equal(JSON.parse(readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").at(-1)).state, "aborted");
// The same plan then lands whole.
f.writer.applyPack("acme", packPlan(), { revision: rev(f), actor: "jason", assertClean });
assert.ok(doc(f).definitions.scout && doc(f).board && doc(f).workflows && doc(f).packs);
});
test("SP3 an agent actor with a definition in the plan refuses human-required; board and workflow writes pass", (t) => {
const f = fixture(t);
const original = bytes(f);
for (const actor of ["coder", "chief-of-staff.chief", "tester"]) {
assert.throws(() => f.writer.applyPack("acme", packPlan(), { revision: rev(f), actor, assertClean }), { code: "human-required" }, actor);
}
// A saved template carries invented roles; an agent never applies one.
const saved = { packs: { ...packPlan().packs, origin: "saved", proposedHires: [] }, board: BOARD };
assert.throws(() => f.writer.applyPack("acme", saved, { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), { code: "human-required" });
assert.equal(bytes(f), original);
// The non-definition half of a pack, and a board write, need no human (Jason's answer 2).
const half = { packs: { ...packPlan().packs, proposedHires: [{ name: "coder-1", pin: { template: "coder", sha256: HASH } }] }, workflows: WORKFLOWS };
f.writer.applyPack("acme", half, { revision: rev(f), actor: "chief-of-staff.chief", assertClean });
assert.equal(doc(f).packs[0].by, "chief-of-staff.chief");
f.writer.applyPack("acme", { board: BOARD }, { revision: rev(f), actor: "chief-of-staff.chief", assertClean });
assert.deepEqual(doc(f).board, BOARD);
});
test("SP3 a plan holding any other key refuses", (t) => {
const f = fixture(t);
const original = bytes(f);
for (const [name, plan] of Object.entries({
plugins: { plugins: { theme: ["dark"] } },
roles: { roles: { pm: { definition: "pm" } } },
limits: { limits: { total: 3 } },
credentials: { credentials: { gitea: { file: "/x" } } },
launch: { launch: {} },
copyBindings: { copyBindings: {} },
autonomy: { autonomy: "near" },
"projects whole": { projects: { stack: { root: "/x" } } },
"projects root": { "projects.stack.root": "/x" },
"projects agents": { "projects.stack.agents": {} },
"board beside plugins": { board: BOARD, plugins: {} },
"dotted workflow item": { "workflows.change": WORKFLOWS.change },
"empty plan": {},
"not an object": [],
})) {
assert.throws(() => f.writer.applyPack("acme", plan, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" }, name);
}
assert.equal(bytes(f), original);
});
test("SP3 a board-only plan changes only board, and a project board only that project's board", (t) => {
const f = fixture(t);
const before = doc(f);
f.writer.applyPack("acme", { board: BOARD }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(without(doc(f), "board"), before);
assert.deepEqual(doc(f).board, BOARD);
// board.set replaces a board, and null removes it.
f.writer.applyPack("acme", { board: LIST_BOARD }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f).board, LIST_BOARD);
f.writer.applyPack("acme", { board: null }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f), before);
// A project's board.
f.writer.applyPack("acme", { "projects.stack.board": BOARD }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f).projects.stack, { ...before.projects.stack, board: BOARD });
assert.deepEqual(without(doc(f), "projects"), without(before, "projects"));
assert.deepEqual(f.writer.read("acme").document.projects.stack.board, BOARD);
// Workflows merge by name; null removes one; a project's workflows likewise.
f.writer.applyPack("acme", { workflows: WORKFLOWS }, { revision: rev(f), actor: "jason", assertClean });
f.writer.applyPack("acme", { workflows: { review: WORKFLOWS.change } }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(Object.keys(doc(f).workflows), ["change", "review"]);
f.writer.applyPack("acme", { workflows: { change: null } }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(Object.keys(doc(f).workflows), ["review"]);
f.writer.applyPack("acme", { workflows: { review: null } }, { revision: rev(f), actor: "jason", assertClean });
assert.ok(!Object.hasOwn(doc(f), "workflows"));
f.writer.applyPack("acme", { "projects.stack.workflows": WORKFLOWS }, { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f).projects.stack.workflows, WORKFLOWS);
// Removing what isn't there refuses.
assert.throws(() => f.writer.applyPack("acme", { board: null }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
assert.throws(() => f.writer.applyPack("acme", { workflows: { nope: null } }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
});
test("SP3 a workflow in the plan must name an org definition, a V2 template or human", (t) => {
const f = fixture(t);
const original = bytes(f);
const flow = (roles) => ({ workflows: { change: { steps: [{ title: "Step", state: "todo", roles }] } } });
for (const roles of [["ghost-role"], ["pm", "ghost-role"], ["conductor-policy"]]) {
assert.throws(() => f.writer.applyPack("acme", flow(roles), { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" }, roles.join());
assert.throws(() => f.writer.applyPack("acme", { "projects.stack.workflows": flow(roles).workflows }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
}
assert.equal(bytes(f), original);
// A template, human, and a definition the same plan adds.
f.writer.applyPack("acme", flow(["pm", "human"]), { revision: rev(f), actor: "jason", assertClean });
f.writer.applyPack("acme", packPlan({ board: undefined, workflows: { scouting: { steps: [{ title: "Scout", state: "todo", roles: ["scout"] }] } } }), { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(Object.keys(doc(f).workflows), ["change", "scouting"]);
});
test("SP3 applyPack keeps setDefinition's definition rules", (t) => {
const f = fixture(t, (d) => ({ ...d, definitions: { scout: { ...PLAIN, rev: 1 }, spare: { ...PLAIN, rev: 2 } }, roles: { ...d.roles, looker: { definition: "org:scout" } } }));
const original = bytes(f);
const plan = (definitions) => packPlan({ definitions, board: undefined, workflows: undefined });
assert.throws(() => f.writer.applyPack("acme", plan({ scout: PLAIN }), { revision: rev(f), actor: "jason", assertClean }), { code: "definition-in-use" });
assert.throws(() => f.writer.applyPack("acme", plan({ spare: PLAIN }), { revision: rev(f), actor: "jason", assertClean }), { code: "definition-exists" });
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: { ...PLAIN, rev: 1 } }), { revision: rev(f), actor: "jason", assertClean }), { code: "definition-rev-conflict" });
// The credential store's check is required on every apply, not only one that writes definitions.
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: PLAIN }), { revision: rev(f), actor: "jason" }), { code: "definition-secret-check-required" });
for (const bare of [{ board: BOARD }, { workflows: WORKFLOWS }, { "projects.stack.board": BOARD }, { packs: { ...packPlan().packs, proposedHires: [] }, workflows: WORKFLOWS }]) {
assert.throws(() => f.writer.applyPack("acme", bare, { revision: rev(f), actor: "jason" }), { code: "definition-secret-check-required" }, Object.keys(bare).join());
}
// A secret in workflow text, a plan key or an inline proposal's entry refuses before anything is written.
const leak = { ...WORKFLOWS.change, description: SECRET };
assert.throws(() => f.writer.applyPack("acme", { workflows: { change: leak } }, { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
assert.throws(() => f.writer.applyPack("acme", { [SECRET]: BOARD }, { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: { ...PLAIN, instructions: `token ${SECRET}\n` } }), { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: { ...PLAIN, role: { ...PLAIN.role, tools: ["no-such-tool"] } } }), { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" });
// checkComposition runs: a hired composition never holds role.hire.
const hiring = { ...PLAIN, role: { ...PLAIN.role, authority: { withinRole: [], crossRole: ["role.hire"] } } };
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: hiring }), { revision: rev(f), actor: "jason", assertClean }));
// A definition needs a packs entry, and a {definition, rev} pin names a definition this apply writes at rev 1.
assert.throws(() => f.writer.applyPack("acme", { definitions: { fresh: PLAIN } }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
const pin = (p) => ({ ...plan({ fresh: PLAIN }), packs: { ...packPlan().packs, proposedHires: [{ name: "x-1", pin: p }] } });
assert.throws(() => f.writer.applyPack("acme", pin({ definition: "spare", rev: 2 }), { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
assert.throws(() => f.writer.applyPack("acme", pin({ definition: "fresh", rev: 2 }), { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
// A held secret in a board label or workflow text refuses when the caller passes the check.
assert.throws(() => f.writer.applyPack("acme", { board: { ...BOARD, columns: [{ state: "todo", label: SECRET }, ...BOARD.columns.slice(1)] } }, { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
assert.equal(bytes(f), original);
f.writer.applyPack("acme", pin({ definition: "fresh", rev: 1 }), { revision: rev(f), actor: "jason", assertClean });
assert.deepEqual(doc(f).definitions.fresh, { ...PLAIN, rev: 1 });
});
test("SP3 a pack apply only adds, and its scope fences the project keys", (t) => {
const f = fixture(t, (d) => ({ ...d, board: BOARD, workflows: WORKFLOWS }));
const original = bytes(f);
// No definitions in these plans, so no {definition, rev} pin: each refusal below is the one named.
const packs = { ...packPlan().packs, proposedHires: PACK.proposedHires.slice(0, 1) };
const go = (plan) => f.writer.applyPack("acme", plan, { revision: rev(f), actor: "jason", assertClean });
// Nothing is overwritten or removed by an apply.
assert.throws(() => go({ packs, board: LIST_BOARD }), { code: "invalid-request" });
assert.throws(() => go({ packs, workflows: { change: WORKFLOWS.change } }), { code: "invalid-request" });
assert.throws(() => go({ packs, workflows: { other: null } }), { code: "invalid-request" });
// The writer stamps at, by and added.
for (const key of ["at", "by", "added"]) assert.throws(() => go({ packs: { ...packs, [key]: PACK[key] }, workflows: { other: WORKFLOWS.change } }), { code: "invalid-request" }, key);
// An org pack writes no project key; a project pack writes only its project's keys.
assert.throws(() => go({ packs, "projects.stack.board": BOARD }), { code: "invalid-request" });
const projectPack = { ...packs, scope: "project", project: "stack" };
assert.throws(() => go({ packs: projectPack, workflows: { other: WORKFLOWS.change } }), { code: "invalid-request" });
assert.throws(() => go({ packs: { ...projectPack, project: "nowhere" }, "projects.nowhere.board": BOARD }), { code: "invalid-request" });
assert.throws(() => go({ "projects.nowhere.board": BOARD }), { code: "invalid-request" });
assert.equal(bytes(f), original);
go({ packs: projectPack, "projects.stack.board": BOARD, "projects.stack.workflows": WORKFLOWS });
assert.deepEqual(doc(f).packs[0].added, ["projects.stack.board", "projects.stack.workflows.change"]);
assert.deepEqual(doc(f).projects.stack.board, BOARD);
});
test("SP3 the generic writer still refuses project board changes", (t) => {
const f = fixture(t);
const next = structuredClone(f.document);
next.projects.stack.board = BOARD;
assert.throws(() => f.writer.write("acme", next, { revision: rev(f), actor: "jason" }), { code: "organization-projects-protected" });
});
test("SP3 an inline proposal keeps its full role entry beside the pin, and the pin is the entry's hash", (t) => {
const f = fixture(t);
const inline = PACK.proposedHires[2];
// The loaded organization keeps the entry, so the raise can read it back.
const loaded = load(f, { ...f.document, packs: [{ ...PACK, proposedHires: [inline] }] });
assert.deepEqual(loaded.packs[0].proposedHires[0], inline);
// A recorded hash that no longer matches still loads: staleness is the raise's refusal.
const stale = { ...inline, pin: { ...inline.pin, sha256: HASH } };
assert.deepEqual(load(f, { ...f.document, packs: [{ ...PACK, proposedHires: [stale] }] }).packs[0].proposedHires[0], stale);
// An apply records the entry, and refuses a pin that is not the entry's canonical hash.
const original = bytes(f);
const plan = (hire) => ({ packs: { ...packPlan().packs, proposedHires: [hire] }, workflows: WORKFLOWS });
assert.throws(() => f.writer.applyPack("acme", plan(stale), { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), { code: "invalid-request" });
assert.throws(() => f.writer.applyPack("acme", plan({ ...inline, entry: { ...WRITER, instructions: "Something else.\n" } }), { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), { code: "invalid-request" });
// A held secret in the entry refuses when the caller passes the check.
const leaky = { ...WRITER, instructions: SECRET };
assert.throws(() => f.writer.applyPack("acme", plan({ ...inline, pin: { ...inline.pin, sha256: sha256(canonicalJson(leaky)) }, entry: leaky }), { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), /credential-leak/);
assert.equal(bytes(f), original);
// An agent apply writes no definition, so its hires are template or inline pins.
f.writer.applyPack("acme", plan(inline), { revision: rev(f), actor: "chief-of-staff.chief", assertClean });
assert.deepEqual(doc(f).packs[0].proposedHires, [inline]);
assert.equal(doc(f).definitions?.writer, undefined);
assert.deepEqual(load(f, doc(f)).packs[0].proposedHires, [inline]);
});