import type { Express } from "express"; import { z } from "zod"; import { isTrustedInternalBridgeRequest } from "../product/internal-bridge-auth.js"; import { getRuntimeSessionContext } from "../runtime/sessionContext.js"; import { interactionQuestionsSchema } from "../chat/interaction-request-contract.js"; import type { InteractionRequestStore } from "../sessions/interaction-request-store.js"; import type { OpencodeRuntimeAdapter } from "../runtime/opencode.js"; const identity = { session_id: z.string().min(1).max(128) }; const call = z.object({ ...identity, call_id: z.string().min(1).max(128), tool: z.string().min(1).max(200) }).strict(); /** Only trusted executors enter this gate; model input never selects an owner or turn. */ export function registerChatInteractionBridge(app: Express, options: { internalToken: string; interactions: InteractionRequestStore; runtime?: OpencodeRuntimeAdapter }) { const calls = new Map }>(); for (const operation of ["policy", "enter", "leave", "create"] as const) { app.post(`/internal/chat-interactions/${operation}`, async (req, res) => { if (!isTrustedInternalBridgeRequest(req, options.internalToken)) { res.status(403).json({ message: "forbidden" }); return; } try { const body = operation === "policy" ? z.object(identity).strict().parse(req.body) : operation === "create" ? call.extend({ questions: interactionQuestionsSchema }).parse(req.body) : call.parse(req.body); const context = getRuntimeSessionContext(body.session_id); const enabled = Boolean(context?.hostTurnId && context.hostMessageId && context.capabilities?.includes("host-question@1") && context.purpose !== "task_execution" && context.learningMode !== "review"); if (operation === "policy") { const requested = context && enabled ? await options.interactions.getForTurn(context, context.hostTurnId!) : null; res.json({ enabled, waiting: Boolean(requested) }); return; } const input = call.parse({ session_id: body.session_id, call_id: "call_id" in body ? body.call_id : "", tool: "tool" in body ? body.tool : "" }); if (!context) { res.status(403).json({ message: "No authorized host turn is bound to this session" }); return; } if (!enabled || !context) { if (input.tool === "ask_user") { res.status(403).json({ message: "Host questions are not enabled for this turn" }); return; } res.json({ allowed: true }); return; } let state = calls.get(context.sessionId); if (!state || state.turn !== context.hostTurnId) { state = { turn: context.hostTurnId!, active: new Map() }; calls.set(context.sessionId, state); } if (operation === "leave") { state.active.delete(input.call_id); res.json({ allowed: true }); return; } if (context.hostQuestionClosed) { res.status(409).json({ message: "The owning turn has been stopped" }); return; } if (operation === "create") { if (input.tool !== "ask_user" || state.active.get(input.call_id) !== "ask_user" || !("questions" in body)) { res.status(409).json({ message: "Question tool has not acquired the turn" }); return; } const previous = await options.interactions.getBlocking(context); if (previous?.delivery && options.runtime && await options.runtime.agentMessageState(context.sessionId, previous.delivery.native_message_id) === "delivered") { await options.interactions.updateDelivery(context, previous.interaction_id, { expected_revision: previous.delivery.revision, status: "delivered", response_id: previous.delivery.response_id }); } const record = await options.interactions.create(context, { message_id: context.hostMessageId!, turn_id: context.hostTurnId!, call_id: input.call_id, questions: body.questions }); res.json(record); return; } const blocking = await options.interactions.getBlocking(context); const requested = await options.interactions.getForTurn(context, context.hostTurnId!); if (requested || blocking?.status === "pending" || input.tool === "question" || [...state.active.values()].includes("ask_user") || (input.tool === "ask_user" && state.active.size > 0)) { res.status(409).json({ message: "Finish other tools before asking; after asking, wait for the user" }); return; } state.active.set(input.call_id, input.tool); res.json({ allowed: true }); } catch (error) { if (error instanceof z.ZodError) { res.status(400).json({ message: "invalid interaction bridge request" }); return; } const status = error && typeof error === "object" && "status" in error && typeof error.status === "number" ? error.status : 503; res.status(status).json({ message: "Interaction state is unavailable; no question was acknowledged" }); } }); } }