/** * What is left of the hand-rolled chat: the tool boundary. * * This file used to be the interactive agent — an SSE reader, a tool-call * assembler, a four-turn budget and the system prompt. Prime Agent does all of * that now, and the pieces that were ours have moved to where they belong: the * prompt to `agent/prompt.ts`, the session to `agent/session.ts`, the zod-to- * harness translation to `agent/tool-bridge.ts`. * * One thing did not move, because it is not the harness's job. Every tool Piggy * is handed must be a PIG application tool, and the check has to live in PIG's * own code rather than in a configuration flag whose meaning an upgrade could * change underneath us. */ import type { PiggyChatContext } from '@pig/core'; // Re-exported so the several call sites that already import the context type // from here keep working. The definition lives in @pig/core because it crosses // four process boundaries and two `.strict()` schemas. export type { PiggyChatContext }; /** * The gate that survived the harness swap. * * `noTools: 'all'` already means a session starts with no bash, no filesystem * and no code execution, and the explicit `tools` allowlist means only our names * are enabled. This is the gate behind both, and the only one written in PIG's * own code: whatever the harness's defaults become across an upgrade, a tool * that does not begin `pig_`, or whose name reads like a shell, never reaches * the model. It takes only a name, so it holds equally for a zod `AgentTool` on * its way through the bridge and for a `ToolDefinition` built directly. It is * cheap, it is greppable, and it has no reason ever to be removed. */ /** * The shapes a tool name may not have, whatever it is prefixed with. * * The prefix rule is a convention, and a convention alone is not a boundary: * the interesting mistake is not a tool called `bash`, it is one called * `pig_python_exec`, which reads like house style and passes the prefix. This * list therefore names the interpreters and the process-spawning verbs as well * as the shell, and it must stay in step with the equivalent list in * .gitea/workflows/ci.yml — CI already rejected `pig_python_exec` while this * gate, the one that runs in production, waved it through. * * Deliberately NOT here: `read`, `write`, `list` and their kin. Every PIG tool * is a read or a write of the book, `pig_get_record_by_id` is exactly that, and * a rule that fires on the words the domain is made of is a rule somebody * deletes the first time it is inconvenient. */ const FORBIDDEN_TOOL_NAME = /bash|shell|filesystem|file_read|file_write|python|ipython|notebook|subprocess|_exec\b|^pig_exec|process_run|spawn|eval/i; export function assertPigToolBoundary(tools: readonly { name: string }[]): void { for (const tool of tools) { if (!tool.name.startsWith('pig_') || FORBIDDEN_TOOL_NAME.test(tool.name)) { throw new Error(`Interactive Piggy tool '${tool.name}' is outside the PIG tool boundary.`); } } }