Compare commits

...

3 Commits
main ... pr-566

Author SHA1 Message Date
Peter Steinberger
7a5e3662a3 fix(web): show all WhatsApp shared contacts 2026-01-10 00:16:54 +00:00
Peter Steinberger
d3d6d80f4a fix(agents): require raw for gateway config.apply (#566) (thanks @sircrumpet) 2026-01-10 00:16:17 +00:00
Eugene (via Claudius)
a51948a528 fix(tools): flatten gateway tool schema for Vertex AI compatibility
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.
2026-01-10 00:16:17 +00:00
5 changed files with 80 additions and 40 deletions

View File

@ -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

View File

@ -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<Record<string, unknown>>;
};
const conditional = parameters.allOf?.find(
(entry) => "if" in entry && "then" in entry,
) as
| { if?: Record<string, unknown>; then?: Record<string, unknown> }
| undefined;
expect(conditional).toBeDefined();
const thenRequired = conditional?.then?.required as string[] | undefined;
expect(thenRequired ?? []).toContain("raw");
const action = (
conditional?.if?.properties as Record<string, unknown> | undefined
)?.action as { const?: unknown; enum?: unknown[] } | undefined;
const values = new Set<string>();
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");

View File

@ -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<GatewayAction>({
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;

View File

@ -141,7 +141,7 @@ describe("web inbound helpers", () => {
},
} as unknown as import("@whiskeysockets/baileys").proto.IMessage);
expect(body).toBe(
"<contacts: Alice, +15555550101, Bob, +15555550102, Charlie, +15555550103 (+1 more) +1 more>",
"<contacts: Alice, +15555550101, Bob, +15555550102, Charlie, +15555550103 (+1 more), Dana, +15555550105>",
);
});

View File

@ -801,9 +801,9 @@ function formatContactsPlaceholder(labels: string[], total: number): string {
const suffix = total === 1 ? "contact" : "contacts";
return `<contacts: ${total} ${suffix}>`;
}
const { shown, remaining } = summarizeList(cleaned, total, 3);
const remaining = Math.max(total - cleaned.length, 0);
const suffix = remaining > 0 ? ` +${remaining} more` : "";
return `<contacts: ${shown.join(", ")}${suffix}>`;
return `<contacts: ${cleaned.join(", ")}${suffix}>`;
}
function formatContactLabel(