import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { loadToolCatalog, type ToolCatalog, type ToolCatalogEntry, } from "./system-prompt-config.ts"; type AnyMessage = { role?: string; content?: unknown } & Record; type AnyHandler = (event: never, ctx: never) => unknown; /** * The description a tool registers with under its catalog entry: a `description` * override replaces the upstream text, a `descriptionAppend` is joined onto * whichever base wins (override first, upstream otherwise) with a blank line. */ export function catalogDescription(description: string, entry: ToolCatalogEntry): string { if ("inherit" in entry) return description; const base = typeof entry.description === "string" ? entry.description : description; return typeof entry.descriptionAppend === "string" ? `${base.trimEnd()}\n\n${entry.descriptionAppend}` : base; } export interface PromptGuardOptions { /** * Post-process the messages a wrapped `context` handler leaves behind. * Receives the message list snapshot from before the handler ran and the * list after; returns the list the handler's result should report. */ contextFilter?: (before: AnyMessage[], after: AnyMessage[]) => AnyMessage[]; } /** * Proxy an `ExtensionAPI` for third-party extensions so they cannot bypass the * bundle's single system-prompt owner (`extensions/system-prompt.ts`): * * - `registerTool` applies the tool's `description` override and * `descriptionAppend` suffix from `config/prompt/tools.jsonc` when the * catalog carries them (lazily loaded; a catalog error keeps the upstream * description). An append composes onto the override when both are set, or * onto the upstream description when only the append is set. * - `before_agent_start` handlers keep every returned field except * `systemPrompt`, which is dropped. * - `context` handlers are post-processed through `opts.contextFilter`. * - Everything else passes through unchanged. */ export function guardExtensionApi(pi: ExtensionAPI, opts: PromptGuardOptions = {}): ExtensionAPI { let catalogCache: ToolCatalog | null | undefined; const catalog = (): ToolCatalog | null => { if (catalogCache === undefined) { try { catalogCache = loadToolCatalog(); } catch { catalogCache = null; } } return catalogCache; }; const registerTool: ExtensionAPI["registerTool"] = (tool) => { const entry = catalog()?.tools[tool.name]; const description = entry === undefined ? tool.description : catalogDescription(tool.description, entry); if (description === tool.description) { pi.registerTool(tool); } else { pi.registerTool({ ...tool, description }); } }; const on: ExtensionAPI["on"] = ((event: string, handler: AnyHandler): unknown => { let wrapped = handler; if (event === "before_agent_start") { wrapped = (async (eventPayload: unknown, ctx: unknown) => { const result = await handler(eventPayload as never, ctx as never); if (result !== null && typeof result === "object" && "systemPrompt" in result) { const rest = { ...(result as Record) }; delete rest.systemPrompt; return rest; } return result; }) as AnyHandler; } else if (event === "context" && opts.contextFilter) { const filter = opts.contextFilter; wrapped = (async (eventPayload: { messages?: AnyMessage[] }, ctx: unknown) => { const messages = Array.isArray(eventPayload.messages) ? eventPayload.messages : []; const before = messages.slice(); const result = await handler(eventPayload as never, ctx as never); const resultMessages = result !== null && typeof result === "object" && Array.isArray((result as { messages?: unknown }).messages) ? ((result as { messages: AnyMessage[] }).messages) : messages; const filtered = filter(before, resultMessages); if (filtered === resultMessages) { return result; } return { ...(result !== null && typeof result === "object" ? (result as Record) : {}), messages: filtered, }; }) as AnyHandler; } // `on` is overloaded per event name; drop to a loosely typed call and // forward whatever the real registration returns (e.g. a disposer). return (pi.on as (name: string, h: AnyHandler) => unknown)(event, wrapped); }) as ExtensionAPI["on"]; return new Proxy(pi, { get(target, prop, receiver) { if (prop === "registerTool") return registerTool; if (prop === "on") return on; const value = Reflect.get(target, prop, receiver); return typeof value === "function" ? value.bind(target) : value; }, }); }