Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.
Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.
Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
113 lines
5.5 KiB
JavaScript
113 lines
5.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, /);
|
|
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"]);
|
|
});
|
|
|
|
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));
|
|
});
|