Commits move as v2 bundles over bus-host/git.sock (decision 135). The bus
host keeps one bare working repository per business and project, seeds main
from the project root under a per-repository lock, fetches a pushed bundle
with fsck into a temp ref, requires history shared with main, and moves the
task's work ref only as a fast-forward. Host and runner git run with no
system or global config, hooks at /dev/null and local transports only. The
socket binds only for roles holding git.push.working or review.verdict.
The broker records a push as the action.allowed for git.push.working with
its commit, takes review.request for the latest push and one review.verdict
per request, and refuses task.close on a pushed task without an approve
verdict for the latest push. recordEvent reserves both shapes, so agents
can't forge either.
Reviewed by Dewey (28196) and Sage (28197), both approve. Gate on 7a729aef:
14 node suites and 9 scripts green.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
163 lines
9.5 KiB
JavaScript
163 lines
9.5 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { BundleError, buildBundle, sessionModel, SOURCES } from "../src/bundle.mjs";
|
|
import { resolvedFor, scratch } from "./helpers.mjs";
|
|
|
|
const sha = (p) => createHash("sha256").update(readFileSync(p)).digest("hex");
|
|
const mode = (p) => statSync(p).mode & 0o777;
|
|
|
|
function build(t, resolved, extra = {}) {
|
|
const dir = scratch(t);
|
|
const workspace = join(dir, "ws");
|
|
mkdirSync(workspace);
|
|
const bundle = buildBundle({
|
|
dir: join(dir, "bundle"),
|
|
resolved,
|
|
contract: "# Contract\n\nDo the work.\n",
|
|
tracker: true,
|
|
credentials: [{ service: "gitea", date: "2099-01-01", state: "valid" }],
|
|
session: { run: "run-1", launchedBy: "pm", workspace, toolSocket: join(dir, "tools.sock"), turnMarker: join(dir, "turn.done") },
|
|
piVersion: "0.85.1",
|
|
claudeVersion: "2.1.0 (Claude Code)",
|
|
...extra,
|
|
});
|
|
return { dir, workspace, ...bundle };
|
|
}
|
|
|
|
test("sessionModel: agent vars win, then the system's execution settings", (t) => {
|
|
assert.deepEqual(sessionModel(resolvedFor(t, "pm")), { harness: "pi", provider: "zai", model: "claude-opus-5-5", thinking: null });
|
|
assert.deepEqual(sessionModel(resolvedFor(t, "coder")), { harness: "pi", provider: "zai", model: "glm-5.3-flash", thinking: null });
|
|
// The test system's adapter is "mock", which isn't a session harness.
|
|
assert.throws(() => sessionModel(resolvedFor(t, "reviewer")), (e) => e instanceof BundleError && /harness "mock"/.test(e.message));
|
|
});
|
|
|
|
test("a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory", (t) => {
|
|
const b = build(t, resolvedFor(t, "pm"));
|
|
assert.equal(mode(join(b.dir, "bundle")), 0o700);
|
|
for (const f of Object.values(b.files)) assert.equal(mode(f), 0o600, f);
|
|
assert.deepEqual(Object.keys(b.files).sort(), ["manifest", "policy", "prompt", "tools"]);
|
|
const prompt = readFileSync(b.files.prompt, "utf8");
|
|
assert.match(prompt, /^# Contract\n\nDo the work\.\n\n## This session\n/);
|
|
assert.match(prompt, /Role instance: pm \(definition pm\)\. Run: run-1\./);
|
|
assert.match(prompt, /Bus tools: send_message, launch, /);
|
|
// Row 90 (#1584): PM fault 10 (#228 c91).
|
|
assert.match(prompt, /^- Only the human stops a run, with `mosaic stop`\. If you think a run should stop, raise a role\.revoke decision with blocking false/m);
|
|
assert.deepEqual(JSON.parse(readFileSync(b.files.policy, "utf8")), {
|
|
harness: "pi",
|
|
workspace: b.workspace,
|
|
tools: [...b.manifest.builtins],
|
|
typed: b.tools.map((x) => x.name),
|
|
network: "api-only",
|
|
needs: ["gitea", "vikunja"],
|
|
credentials: [{ service: "gitea", date: "2099-01-01", state: "valid" }],
|
|
});
|
|
const m = b.manifest;
|
|
assert.equal(m.harness, "pi");
|
|
assert.equal(m.model, "claude-opus-5-5");
|
|
assert.equal(m.piVersion, "0.85.1");
|
|
assert.equal(m.claudeVersion, null);
|
|
assert.equal(m.gateTimeout, null);
|
|
assert.deepEqual(m.skills, []);
|
|
assert.equal(m.reliesOn.length, 5);
|
|
assert.ok(m.typed.includes("launch"));
|
|
assert.deepEqual(Object.keys(m.sources).sort(), ["gate", "piExtension", "tools"]);
|
|
assert.equal(m.sources.piExtension.sha256, sha(SOURCES.piExtension));
|
|
for (const [name, digest] of Object.entries(m.files)) assert.equal(digest, sha(join(b.dir, "bundle", name)), name);
|
|
assert.deepEqual(JSON.parse(readFileSync(b.files.manifest, "utf8")), JSON.parse(JSON.stringify(m)));
|
|
});
|
|
|
|
test("a claude-code bundle adds the wrapped gate hook and the MCP config", (t) => {
|
|
const r = resolvedFor(t, "coder", (d) => (d.roles.coder.vars = { harness: "claude-code", model: "claude-sonnet-5-5" }));
|
|
const b = build(t, r);
|
|
assert.deepEqual(Object.keys(b.files).sort(), ["manifest", "mcp", "policy", "prompt", "settings", "tools"]);
|
|
const settings = JSON.parse(readFileSync(b.files.settings, "utf8"));
|
|
const hook = settings.hooks.PreToolUse[0];
|
|
assert.equal(hook.matcher, "*");
|
|
assert.equal(hook.hooks[0].timeout, 20);
|
|
assert.equal(hook.hooks[0].command, `timeout -k 2 10 '${process.execPath}' '${SOURCES.claudeGate}' '${b.files.policy}' || exit 2`);
|
|
const mcp = JSON.parse(readFileSync(b.files.mcp, "utf8"));
|
|
assert.deepEqual(mcp, { mcpServers: { mosaic: { type: "stdio", command: process.execPath, args: [SOURCES.mcpServer, b.files.tools, join(b.dir, "tools.sock")] } } });
|
|
const m = b.manifest;
|
|
assert.equal(m.piVersion, null);
|
|
assert.equal(m.claudeVersion, "2.1.0 (Claude Code)");
|
|
assert.deepEqual(m.gateTimeout, { n: 10, k: 2, hook: 20 });
|
|
assert.ok(m.builtins.includes("Bash"));
|
|
assert.ok(m.typed.every((n) => n.startsWith("mcp__mosaic__")));
|
|
assert.deepEqual(Object.keys(m.sources).sort(), ["claudeGate", "gate", "mcpServer", "tools"]);
|
|
});
|
|
|
|
// Row 89 (#1583): the prompt names the built-ins as the session's harness names them.
|
|
test("the prompt's built-in tools line is the harness's own names, as the manifest lists them", (t) => {
|
|
const line = (b) => readFileSync(b.files.prompt, "utf8").split("\n").find((l) => l.startsWith("- Built-in tools: "));
|
|
const pi = build(t, resolvedFor(t, "pm"));
|
|
assert.equal(line(pi), "- Built-in tools: read, write, edit, grep, find, ls.");
|
|
const claude = build(t, resolvedFor(t, "pm", (d) => (d.roles.pm.vars = { ...d.roles.pm.vars, harness: "claude-code", model: "claude-opus-5-5" })));
|
|
assert.equal(line(claude), "- Built-in tools: Read, Write, Edit, Grep, Glob.");
|
|
for (const b of [pi, claude]) assert.equal(line(b), `- Built-in tools: ${b.manifest.builtins.join(", ")}.`);
|
|
for (const harness of ["pi", "claude-code"]) {
|
|
const none = build(t, resolvedFor(t, "pm", (d) => (d.roles.pm.vars = { ...d.roles.pm.vars, harness, model: "claude-opus-5-5", "limits.tools": [] })));
|
|
assert.equal(line(none), "- Built-in tools: none.", harness);
|
|
}
|
|
});
|
|
|
|
// Row 91 (#1585): the prompt names each project clone, and why a kept one wasn't updated.
|
|
test("the prompt names the project clones in the workspace, and none when there are none", (t) => {
|
|
const line = (b) => readFileSync(b.files.prompt, "utf8").split("\n").filter((l) => l.startsWith("- Project clones: "));
|
|
const plain = build(t, resolvedFor(t, "pm"));
|
|
assert.deepEqual(line(plain), []);
|
|
const dir = scratch(t);
|
|
const session = (checkouts) => ({ run: "run-1", launchedBy: "pm", workspace: join(dir, "ws"), toolSocket: join(dir, "tools.sock"), turnMarker: join(dir, "turn.done"), checkouts });
|
|
const two = build(t, resolvedFor(t, "pm"), { session: session([
|
|
{ project: "stack", path: join(dir, "ws", "stack"), outcome: "updated", branch: "main" },
|
|
{ project: "site", path: join(dir, "ws", "site"), outcome: "kept", reason: "dirty" },
|
|
]) });
|
|
assert.deepEqual(line(two), [`- Project clones: stack at ${join(dir, "ws", "stack")}, on branch main; site at ${join(dir, "ws", "site")}, not updated at this launch (dirty). Each is a git clone of the project. The host fetches from its origin at each launch; the origin is outside this sandbox.`]);
|
|
const prompt = readFileSync(two.files.prompt, "utf8");
|
|
assert.ok(prompt.indexOf("- Workspace: ") < prompt.indexOf("- Project clones: "));
|
|
assert.deepEqual(line(build(t, resolvedFor(t, "pm"), { session: session([]) })), []);
|
|
});
|
|
|
|
// Row 93 (#1587): the review loop's prompt lines, for the roles with its tools.
|
|
test("the prompt explains the review loop to a role with push_working, and reviews to one with fetch_review", (t) => {
|
|
const lines = (b) => readFileSync(b.files.prompt, "utf8").split("\n").filter((l) => /^- (Review loop|Reviews): /.test(l));
|
|
const dir = scratch(t);
|
|
const session = { run: "run-1", launchedBy: "pm", workspace: join(dir, "ws"), toolSocket: join(dir, "tools.sock"), turnMarker: join(dir, "turn.done"), checkouts: [{ project: "stack", path: join(dir, "ws", "stack"), outcome: "cloned", branch: "main" }] };
|
|
const coder = lines(build(t, resolvedFor(t, "coder"), { session }));
|
|
assert.equal(coder.length, 1);
|
|
assert.match(coder[0], /^- Review loop: commit on a branch of your clone, push_working it, then request_review/);
|
|
assert.match(coder[0], /closes only on an approve verdict for its latest push/);
|
|
const reviewer = lines(build(t, resolvedFor(t, "reviewer", (d) => (d.roles.reviewer.vars = { harness: "pi" })), { session }));
|
|
assert.equal(reviewer.length, 1);
|
|
assert.match(reviewer[0], /^- Reviews: a REVIEW-REQUEST names a request id\. fetch_review it/);
|
|
assert.deepEqual(lines(build(t, resolvedFor(t, "pm"), { session })), []);
|
|
// Without a clone neither git tool exists, so neither line is there.
|
|
assert.deepEqual(lines(build(t, resolvedFor(t, "coder"))), []);
|
|
});
|
|
|
|
test("a bundle is written once: an existing file refuses", (t) => {
|
|
const dir = scratch(t);
|
|
mkdirSync(join(dir, "bundle"));
|
|
writeFileSync(join(dir, "bundle", "prompt.md"), "old");
|
|
assert.throws(() => buildBundle({
|
|
dir: join(dir, "bundle"),
|
|
resolved: resolvedFor(t, "pm"),
|
|
contract: "c",
|
|
session: { run: "r", launchedBy: "pm", workspace: dir, toolSocket: join(dir, "s"), turnMarker: join(dir, "m") },
|
|
}), /EEXIST/);
|
|
assert.equal(readFileSync(join(dir, "bundle", "prompt.md"), "utf8"), "old");
|
|
});
|
|
|
|
test("a path with a single quote can't go into the hook command", (t) => {
|
|
const r = resolvedFor(t, "coder", (d) => (d.roles.coder.vars = { harness: "claude-code", model: "claude-sonnet-5-5" }));
|
|
const dir = scratch(t);
|
|
assert.throws(() => buildBundle({
|
|
dir: join(dir, "it's"),
|
|
resolved: r,
|
|
contract: "c",
|
|
session: { run: "r", launchedBy: "pm", workspace: dir, toolSocket: join(dir, "s"), turnMarker: join(dir, "m") },
|
|
}), (e) => e instanceof BundleError && /single quote/.test(e.message));
|
|
});
|