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]>
50 lines
2.0 KiB
JavaScript
50 lines
2.0 KiB
JavaScript
// Pi extension for a managed session, loaded with `-e` by adapters/pi.
|
|
// Three jobs:
|
|
// - register the session's typed tools, which call the runner's tool socket;
|
|
// - gate every tool call with gate.mjs (S0 lines 1 and 2: a block or a
|
|
// throw stops the tool);
|
|
// - write the turn marker on agent_end, so the runner can tell a finished
|
|
// turn from a pi that exited 0 mid-turn (S0 line 4).
|
|
// Missing or unreadable configuration throws here, which fails pi's start
|
|
// (S0 line 3): nothing is defaulted.
|
|
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { decide } from "./gate.mjs";
|
|
import { callTool } from "./tools.mjs";
|
|
|
|
function need(name) {
|
|
const value = process.env[name];
|
|
if (typeof value !== "string" || !value) throw new Error(`mosaic extension: ${name} is not set`);
|
|
return value;
|
|
}
|
|
|
|
export default function (pi) {
|
|
const policy = JSON.parse(readFileSync(need("MOSAIC_POLICY_FILE"), "utf8"));
|
|
const tools = JSON.parse(readFileSync(need("MOSAIC_TOOLS_FILE"), "utf8"));
|
|
const socket = need("MOSAIC_TOOL_SOCKET");
|
|
const marker = need("MOSAIC_TURN_MARKER");
|
|
if (policy.harness !== "pi" || !Array.isArray(policy.tools) || !Array.isArray(policy.typed)) throw new Error("mosaic extension: policy file is not a pi policy");
|
|
|
|
pi.on("tool_call", async (event) => {
|
|
const verdict = decide(policy, event.toolName, event.input);
|
|
return verdict.allow ? undefined : { block: true, reason: verdict.reason };
|
|
});
|
|
pi.on("agent_end", async () => {
|
|
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
});
|
|
|
|
for (const t of tools) {
|
|
pi.registerTool({
|
|
name: t.name,
|
|
label: t.name,
|
|
description: t.description,
|
|
parameters: t.parameters,
|
|
async execute(_toolCallId, params) {
|
|
// A refusal throws, which pi reports to the model as a tool error.
|
|
const result = await callTool(socket, t.name, params ?? {});
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: {} };
|
|
},
|
|
});
|
|
}
|
|
}
|