Files
clawdbot/src/agents/tools/gateway-tool.ts
2026-01-20 07:43:00 +00:00

189 lines
7.2 KiB
TypeScript

import crypto from "node:crypto";
import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import {
formatDoctorNonInteractiveHint,
type RestartSentinelPayload,
writeRestartSentinel,
} from "../../infra/restart-sentinel.js";
import { stringEnum } from "../schema/typebox.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;
// 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: stringEnum(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()),
baseHash: Type.Optional(Type.String()),
// config.apply, update.run
sessionKey: Type.Optional(Type.String()),
note: Type.Optional(Type.String()),
restartDelayMs: Type.Optional(Type.Number()),
});
// NOTE: We intentionally avoid top-level `allOf`/`anyOf`/`oneOf` conditionals here:
// - OpenAI rejects tool schemas that include these keywords at the *top-level*.
// - Claude/Vertex has other JSON Schema quirks.
// Conditional requirements (like `raw` for config.apply) are enforced at runtime.
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<string, unknown>;
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 sessionKey =
typeof params.sessionKey === "string" && params.sessionKey.trim()
? params.sessionKey.trim()
: opts?.agentSessionKey?.trim() || undefined;
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;
const note =
typeof params.note === "string" && params.note.trim() ? params.note.trim() : undefined;
const payload: RestartSentinelPayload = {
kind: "restart",
status: "ok",
ts: Date.now(),
sessionKey,
message: note ?? reason ?? null,
doctorHint: formatDoctorNonInteractiveHint(),
stats: {
mode: "gateway.restart",
reason,
},
};
try {
await writeRestartSentinel(payload);
} catch {
// ignore: sentinel is best-effort
}
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 });
let baseHash = readStringParam(params, "baseHash");
if (!baseHash) {
const snapshot = await callGatewayTool("config.get", gatewayOpts, {});
if (snapshot && typeof snapshot === "object") {
const hash = (snapshot as { hash?: unknown }).hash;
if (typeof hash === "string" && hash.trim()) {
baseHash = hash.trim();
} else {
const rawSnapshot = (snapshot as { raw?: unknown }).raw;
if (typeof rawSnapshot === "string") {
baseHash = crypto.createHash("sha256").update(rawSnapshot).digest("hex");
}
}
}
}
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,
baseHash,
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}`);
},
};
}