import type { Plugin } from "@opencode/plugin"; import { getInternalBridgeConfig } from "./internalBridge.js"; export async function hostQuestionBridge(operation: string, body: Record, signal?: AbortSignal): Promise { const { baseUrl, internalToken } = getInternalBridgeConfig(); const response = await fetch(`${baseUrl}/internal/chat-interactions/${operation}`, { method: "POST", headers: { "content-type": "application/json", "x-agent-internal-token": internalToken }, body: JSON.stringify(body), signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10000)]) : AbortSignal.timeout(10000), }); if (!response.ok) throw new Error("Interaction gate rejected the operation. Finish existing tools before asking, then wait for the user."); return response.json(); } export async function registerHostQuestionPolicy(ctx: Parameters[0]) { await ctx.session.hook("context", async (event) => { const policy = await hostQuestionBridge("policy", { session_id: event.sessionID }) as { enabled: boolean; waiting: boolean }; if (!policy.enabled) delete event.tools.ask_user; else { delete event.tools.question; if (policy.waiting) for (const name of Object.keys(event.tools)) delete event.tools[name]; } }); // Wrap native, MCP and custom executors, including calls via Code Mode. await ctx.tool.transform((editor) => { for (const definition of editor.list()) editor.update(definition.id, (draft) => { const execute = draft.execute; draft.execute = async (input, context) => { const identity = { session_id: context.sessionID, call_id: context.id, tool: definition.id }; await hostQuestionBridge("enter", identity, context.signal); try { return await execute(input, context); } finally { await hostQuestionBridge("leave", identity).catch(() => undefined); } }; }); }); }