Claude API on Vertex AI (Cloud Code Assist / Antigravity) enforces strict
JSON Schema 2020-12 validation and rejects root-level anyOf without a
top-level type field.
TypeBox Type.Union compiles to { anyOf: [...] } which Anthropic's direct
API accepts but Vertex rejects with:
tools.11.custom.input_schema: JSON schema is invalid
This follows the same pattern used in browser-tool.ts which has the same
fix with an explanatory comment.
Flatten the schema to Type.Object with an action enum, matching how
browser tool handles this constraint.
150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
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<GatewayAction>({
|
|
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, update.run
|
|
raw: Type.Optional(Type.String()),
|
|
sessionKey: Type.Optional(Type.String()),
|
|
note: Type.Optional(Type.String()),
|
|
restartDelayMs: Type.Optional(Type.Number()),
|
|
});
|
|
|
|
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 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}`);
|
|
},
|
|
};
|
|
}
|