// 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: {} }; }, }); } }