From 20d4773f14817e1d5c694c49c71ad28429d59751 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 00:21:15 +0000 Subject: [PATCH] fix(agents): require raw for gateway config.apply (#566) (thanks @sircrumpet) --- CHANGELOG.md | 1 + src/agents/pi-tools.test.ts | 31 ++++++++++++ src/agents/tools/gateway-tool.ts | 82 ++++++++++++++++++-------------- 3 files changed, 77 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7645f0e30..71253cc4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ - Agents: scrub unsupported JSON Schema keywords from tool schemas for Cloud Code Assist API compatibility. (#567) — thanks @erikpr1994 - Agents: sanitize Cloud Code Assist tool call IDs and detect format/quota errors for failover. (#544) — thanks @jeffersonwarrior - Agents: simplify session tool schemas for Gemini compatibility. (#599) — thanks @mcinteerj +- Agents: require `raw` for gateway `config.apply` tool calls while keeping schema 2020-12 compatible. (#566) — thanks @sircrumpet - Agents: add `session_status` agent tool for `/status`-equivalent status (incl. usage/cost) + per-session model overrides. — thanks @steipete - Auto-reply: preserve block reply ordering with timeout fallback for streaming. (#503) — thanks @joshp123 - Auto-reply: block reply ordering fix (duplicate PR superseded by #503). (#483) — thanks @AbhisekBasu1 diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 108896468..8f34136af 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -31,6 +31,37 @@ describe("createClawdbotCodingTools", () => { expect(parameters.required ?? []).toContain("action"); }); + it("requires raw for gateway config.apply tool calls", () => { + const tools = createClawdbotCodingTools(); + const gateway = tools.find((tool) => tool.name === "gateway"); + expect(gateway).toBeDefined(); + + const parameters = gateway?.parameters as { + allOf?: Array>; + }; + const conditional = parameters.allOf?.find( + (entry) => "if" in entry && "then" in entry, + ) as + | { if?: Record; then?: Record } + | undefined; + + expect(conditional).toBeDefined(); + const thenRequired = conditional?.then?.required as string[] | undefined; + expect(thenRequired ?? []).toContain("raw"); + + const action = ( + conditional?.if?.properties as Record | undefined + )?.action as { const?: unknown; enum?: unknown[] } | undefined; + const values = new Set(); + if (typeof action?.const === "string") values.add(action.const); + if (Array.isArray(action?.enum)) { + for (const value of action.enum) { + if (typeof value === "string") values.add(value); + } + } + expect(values.has("config.apply")).toBe(true); + }); + it("flattens anyOf-of-literals to enum for provider compatibility", () => { const tools = createClawdbotCodingTools(); const browser = tools.find((tool) => tool.name === "browser"); diff --git a/src/agents/tools/gateway-tool.ts b/src/agents/tools/gateway-tool.ts index 83d0571c5..60b7da983 100644 --- a/src/agents/tools/gateway-tool.ts +++ b/src/agents/tools/gateway-tool.ts @@ -5,44 +5,52 @@ import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool } from "./gateway.js"; -const GatewayToolSchema = Type.Union([ - Type.Object({ - action: Type.Literal("restart"), - delayMs: Type.Optional(Type.Number()), - reason: Type.Optional(Type.String()), +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], }), - Type.Object({ - action: Type.Literal("config.get"), - gatewayUrl: Type.Optional(Type.String()), - gatewayToken: Type.Optional(Type.String()), - timeoutMs: Type.Optional(Type.Number()), - }), - Type.Object({ - action: Type.Literal("config.schema"), - gatewayUrl: Type.Optional(Type.String()), - gatewayToken: Type.Optional(Type.String()), - timeoutMs: Type.Optional(Type.Number()), - }), - Type.Object({ - action: Type.Literal("config.apply"), - raw: Type.String(), - sessionKey: Type.Optional(Type.String()), - note: Type.Optional(Type.String()), - restartDelayMs: Type.Optional(Type.Number()), - gatewayUrl: Type.Optional(Type.String()), - gatewayToken: Type.Optional(Type.String()), - timeoutMs: Type.Optional(Type.Number()), - }), - Type.Object({ - action: Type.Literal("update.run"), - sessionKey: Type.Optional(Type.String()), - note: Type.Optional(Type.String()), - restartDelayMs: Type.Optional(Type.Number()), - timeoutMs: Type.Optional(Type.Number()), - gatewayUrl: Type.Optional(Type.String()), - gatewayToken: Type.Optional(Type.String()), - }), -]); + // 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;