Skip to content

Commit fc8b57e

Browse files
committed
fix: validate gateway rpc timeouts
1 parent 92a405b commit fc8b57e

6 files changed

Lines changed: 32 additions & 6 deletions

File tree

src/agents/tools/gateway-tool.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,12 @@ describe("gateway tool restart continuation", () => {
129129
properties?: {
130130
delayMs?: { minimum?: number; type?: string };
131131
restartDelayMs?: { minimum?: number; type?: string };
132+
timeoutMs?: { minimum?: number; type?: string };
132133
};
133134
};
134135
expect(parameters.properties?.delayMs).toMatchObject({ type: "integer", minimum: 0 });
135136
expect(parameters.properties?.restartDelayMs).toMatchObject({ type: "integer", minimum: 0 });
137+
expect(parameters.properties?.timeoutMs).toMatchObject({ type: "integer", minimum: 1 });
136138
});
137139

138140
it("instructs agents to use continuationMessage when a restart still needs a reply", async () => {

src/agents/tools/gateway-tool.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
1717
import { collectEnabledInsecureOrDangerousFlags } from "../../security/dangerous-config-flags.js";
1818
import { isRecord as isPlainObject } from "../../shared/record-coerce.js";
1919
import { normalizeOptionalString, readStringValue } from "../../shared/string-coerce.js";
20-
import { optionalNonNegativeIntegerSchema, stringEnum } from "../schema/typebox.js";
20+
import {
21+
optionalNonNegativeIntegerSchema,
22+
optionalPositiveIntegerSchema,
23+
stringEnum,
24+
} from "../schema/typebox.js";
2125
import {
2226
type AnyAgentTool,
2327
jsonResult,
@@ -348,7 +352,7 @@ const GatewayToolSchema = Type.Object({
348352
// config.get, config.schema.lookup, config.apply, update.run
349353
gatewayUrl: Type.Optional(Type.String()),
350354
gatewayToken: Type.Optional(Type.String()),
351-
timeoutMs: Type.Optional(Type.Number()),
355+
timeoutMs: optionalPositiveIntegerSchema(),
352356
// config.schema.lookup
353357
path: Type.Optional(Type.String()),
354358
// config.apply, config.patch

src/agents/tools/gateway.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
22
import type { CallGatewayScopedOptions } from "../../gateway/call.js";
33
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
44
import { setActivePluginRegistry } from "../../plugins/runtime.js";
5-
import { callGatewayTool, resolveGatewayOptions } from "./gateway.js";
5+
import { callGatewayTool, readGatewayCallOptions, resolveGatewayOptions } from "./gateway.js";
66

77
const mocks = vi.hoisted(() => ({
88
callGateway: vi.fn(),
@@ -68,6 +68,24 @@ describe("gateway tool defaults", () => {
6868
expect(call.scopes).toEqual(["operator.read"]);
6969
});
7070

71+
it("rejects invalid gateway timeoutMs before RPC", async () => {
72+
expect(() => readGatewayCallOptions({ timeoutMs: -1 })).toThrow(
73+
"timeoutMs must be a positive integer",
74+
);
75+
expect(() => readGatewayCallOptions({ timeoutMs: 1.5 })).toThrow(
76+
"timeoutMs must be a positive integer",
77+
);
78+
expect(mocks.callGateway).not.toHaveBeenCalled();
79+
});
80+
81+
it("accepts string gateway timeoutMs through the shared numeric reader", async () => {
82+
mocks.callGateway.mockResolvedValueOnce({ ok: true });
83+
84+
await callGatewayTool("health", readGatewayCallOptions({ timeoutMs: "5000" }), {});
85+
86+
expect(capturedGatewayCall().timeoutMs).toBe(5000);
87+
});
88+
7189
it("uses OPENCLAW_GATEWAY_TOKEN for allowlisted local overrides", () => {
7290
process.env.OPENCLAW_GATEWAY_TOKEN = "env-token";
7391
const opts = resolveGatewayOptions({ gatewayUrl: "ws://127.0.0.1:18789" });

src/agents/tools/gateway.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
normalizeLowercaseStringOrEmpty,
1414
normalizeOptionalString,
1515
} from "../../shared/string-coerce.js";
16-
import { readStringParam } from "./common.js";
16+
import { readPositiveIntegerParam, readStringParam } from "./common.js";
1717

1818
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
1919

@@ -29,7 +29,7 @@ export function readGatewayCallOptions(params: Record<string, unknown>): Gateway
2929
return {
3030
gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }),
3131
gatewayToken: readStringParam(params, "gatewayToken", { trim: false }),
32-
timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : undefined,
32+
timeoutMs: readPositiveIntegerParam(params, "timeoutMs"),
3333
};
3434
}
3535

src/agents/tools/nodes-tool.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,13 @@ describe("createNodesTool screen_record duration guardrails", () => {
178178
const tool = createNodesTool();
179179
const schema = tool.parameters as {
180180
properties?: {
181+
timeoutMs?: { minimum?: number; type?: string };
181182
maxAgeMs?: { minimum?: number; type?: string };
182183
locationTimeoutMs?: { minimum?: number; type?: string };
183184
invokeTimeoutMs?: { minimum?: number; type?: string };
184185
};
185186
};
187+
expect(schema.properties?.timeoutMs).toMatchObject({ type: "integer", minimum: 1 });
186188
expect(schema.properties?.maxAgeMs).toMatchObject({ type: "integer", minimum: 0 });
187189
expect(schema.properties?.locationTimeoutMs).toMatchObject({ type: "integer", minimum: 1 });
188190
expect(schema.properties?.invokeTimeoutMs).toMatchObject({ type: "integer", minimum: 1 });

src/agents/tools/nodes-tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const NodesToolSchema = Type.Object({
8484
action: stringEnum(NODES_TOOL_ACTIONS),
8585
gatewayUrl: Type.Optional(Type.String()),
8686
gatewayToken: Type.Optional(Type.String()),
87-
timeoutMs: Type.Optional(Type.Number()),
87+
timeoutMs: optionalPositiveIntegerSchema(),
8888
node: Type.Optional(Type.String()),
8989
requestId: Type.Optional(Type.String()),
9090
// notify

0 commit comments

Comments
 (0)