import { Type } from "@sinclair/typebox"; import type { ClawdbotConfig } from "../../config/config.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool } from "./gateway.js"; const GATEWAY_ACTIONS = [ "restart", "config.get", "config.schema", "config.apply", "update.run", ] as const; type GatewayAction = (typeof GATEWAY_ACTIONS)[number]; // NOTE: Using a flattened object schema instead of Type.Union([Type.Object(...), ...]) // because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema. // The discriminator (action) determines which properties are relevant; runtime validates. const GatewayToolSchema = Type.Object({ action: Type.Unsafe({ type: "string", enum: [...GATEWAY_ACTIONS], }), // restart delayMs: Type.Optional(Type.Number()), reason: Type.Optional(Type.String()), // config.get, config.schema, config.apply, update.run gatewayUrl: Type.Optional(Type.String()), gatewayToken: Type.Optional(Type.String()), timeoutMs: Type.Optional(Type.Number()), // config.apply raw: Type.Optional(Type.String()), // config.apply, update.run sessionKey: Type.Optional(Type.String()), note: Type.Optional(Type.String()), restartDelayMs: Type.Optional(Type.Number()), }); // Keep top-level object schemas while enforcing conditional requirements. (GatewayToolSchema as typeof GatewayToolSchema & { allOf?: unknown[] }).allOf = [ { if: { properties: { action: { const: "config.apply" }, }, required: ["action"], }, // biome-ignore lint/suspicious/noThenProperty: JSON Schema keyword. then: { required: ["raw"] }, }, ]; export function createGatewayTool(opts?: { agentSessionKey?: string; config?: ClawdbotConfig; }): AnyAgentTool { return { label: "Gateway", name: "gateway", description: "Restart, apply config, or update the gateway in-place (SIGUSR1). Use config.apply/update.run to write config or run updates with validation and restart.", parameters: GatewayToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; const action = readStringParam(params, "action", { required: true }); if (action === "restart") { if (opts?.config?.commands?.restart !== true) { throw new Error( "Gateway restart is disabled. Set commands.restart=true to enable.", ); } const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.floor(params.delayMs) : undefined; const reason = typeof params.reason === "string" && params.reason.trim() ? params.reason.trim().slice(0, 200) : undefined; console.info( `gateway tool: restart requested (delayMs=${delayMs ?? "default"}, reason=${reason ?? "none"})`, ); const scheduled = scheduleGatewaySigusr1Restart({ delayMs, reason, }); return jsonResult(scheduled); } const gatewayUrl = typeof params.gatewayUrl === "string" && params.gatewayUrl.trim() ? params.gatewayUrl.trim() : undefined; const gatewayToken = typeof params.gatewayToken === "string" && params.gatewayToken.trim() ? params.gatewayToken.trim() : undefined; const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? Math.max(1, Math.floor(params.timeoutMs)) : undefined; const gatewayOpts = { gatewayUrl, gatewayToken, timeoutMs }; if (action === "config.get") { const result = await callGatewayTool("config.get", gatewayOpts, {}); return jsonResult({ ok: true, result }); } if (action === "config.schema") { const result = await callGatewayTool("config.schema", gatewayOpts, {}); return jsonResult({ ok: true, result }); } if (action === "config.apply") { const raw = readStringParam(params, "raw", { required: true }); const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim() ? params.sessionKey.trim() : opts?.agentSessionKey?.trim() || undefined; const note = typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined; const restartDelayMs = typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs) ? Math.floor(params.restartDelayMs) : undefined; const result = await callGatewayTool("config.apply", gatewayOpts, { raw, sessionKey, note, restartDelayMs, }); return jsonResult({ ok: true, result }); } if (action === "update.run") { const sessionKey = typeof params.sessionKey === "string" && params.sessionKey.trim() ? params.sessionKey.trim() : opts?.agentSessionKey?.trim() || undefined; const note = typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined; const restartDelayMs = typeof params.restartDelayMs === "number" && Number.isFinite(params.restartDelayMs) ? Math.floor(params.restartDelayMs) : undefined; const result = await callGatewayTool("update.run", gatewayOpts, { sessionKey, note, restartDelayMs, timeoutMs, }); return jsonResult({ ok: true, result }); } throw new Error(`Unknown action: ${action}`); }, }; }