/** * Curator's tools. * * Every tool is a proxy to the loopback bridge in `curator/agent_api.py`. The * schemas and descriptions are fetched from the backend at `/tools` rather than * declared here, because `registerTool` accepts a plain JSON Schema object and a * second copy in TypeScript is a copy that drifts. When contracts.py changes, * this file needs no edit. * * What this file adds on top of the shared bridge helper: * * - It refuses to start without a bridge URL and token. A silent start would * produce an agent with no tools that answers from memory instead -- which * looks like a working system and is the failure mode hardest to notice. * - It installs the guard, so no built-in tool can be reached even if a future * pi version changes which tools are on by default. * * Nothing here decides whether a write happens. `propose_write` posts a proposal * and relays the verdict; the decision is in `service.ACTION_RISK`. * * `./_shared/` is vendored by scripts/deploy-scenario.sh from shared/extensions/ * so the deployed tree is self-contained. It is not edited in place -- the * deploy script overwrites it, and pi-diff.sh reports drift against the repo. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { fetchBridgeSpecs, installGuard, registerBridgeTools, } from "./_shared/pi-guard-base.ts"; export default async function activate(pi: ExtensionAPI): Promise { const baseUrl = process.env.CURATOR_BRIDGE_URL; const token = process.env.CURATOR_BRIDGE_TOKEN; if (!baseUrl || !token) { // Fail loudly. An agent that silently loses its tools still answers, just // from the model's memory of what a media library might contain. throw new Error( "curator-tools: CURATOR_BRIDGE_URL and CURATOR_BRIDGE_TOKEN are required. " + "Without them the agent would have no way to see the library and would " + "answer from memory.", ); } const bridge = { baseUrl, token, timeoutMs: 45_000 }; const specs = await fetchBridgeSpecs(bridge); if (specs.length === 0) { throw new Error("curator-tools: the bridge served an empty tool list"); } // Deny every built-in tool. --no-builtin-tools is set on the command line too; // this is the second lock, because the flag is a launch argument while this is // enforced per call. The allow-list is derived from what the backend actually // serves, so a tool cannot be advertised and then blocked. installGuard(pi, { scenario: "curator", allowedTools: specs.map((spec) => spec.name), onBlocked: (name: string) => console.error(`curator-tools: blocked built-in tool ${name}`), }); registerBridgeTools(pi, bridge, specs); }