From 4c49ca75d9f4064e935a95028b2240cc0d4f2e0f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 19:31:55 -0400 Subject: [PATCH] fix: validate session tool numeric params --- src/agents/openclaw-tools.sessions.test.ts | 10 +++---- .../tools/sessions-history-tool.test.ts | 8 +++++ src/agents/tools/sessions-history-tool.ts | 9 ++---- src/agents/tools/sessions-list-tool.test.ts | 12 ++++++++ src/agents/tools/sessions-list-tool.ts | 29 +++++++++---------- 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 062c5d42a5b7..43ffd0ee78d7 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -247,7 +247,7 @@ describe("sessions tools", () => { }); }); - it("uses number (not integer) in tool schemas for Gemini compatibility", () => { + it("uses integer schemas for session count and window parameters", () => { const tools = createOpenClawTools(); const byName = (name: string) => { const tool = tools.find((candidate) => candidate.name === name); @@ -275,10 +275,10 @@ describe("sessions tools", () => { return value; }; - expect(schemaProp("sessions_history", "limit").type).toBe("number"); - expect(schemaProp("sessions_list", "limit").type).toBe("number"); - expect(schemaProp("sessions_list", "activeMinutes").type).toBe("number"); - expect(schemaProp("sessions_list", "messageLimit").type).toBe("number"); + expect(schemaProp("sessions_history", "limit").type).toBe("integer"); + expect(schemaProp("sessions_list", "limit").type).toBe("integer"); + expect(schemaProp("sessions_list", "activeMinutes").type).toBe("integer"); + expect(schemaProp("sessions_list", "messageLimit").type).toBe("integer"); expect(schemaProp("sessions_list", "label").type).toBe("string"); expect(schemaProp("sessions_list", "agentId").type).toBe("string"); expect(schemaProp("sessions_list", "search").type).toBe("string"); diff --git a/src/agents/tools/sessions-history-tool.test.ts b/src/agents/tools/sessions-history-tool.test.ts index d3fb8514bb68..c808e4f152ac 100644 --- a/src/agents/tools/sessions-history-tool.test.ts +++ b/src/agents/tools/sessions-history-tool.test.ts @@ -83,4 +83,12 @@ describe("sessions_history redaction", () => { expect(serialized).toContain("intern"); expect((result.details as { contentRedacted?: unknown }).contentRedacted).toBe(true); }); + + it.each([0, 1.5])("rejects invalid limit value %s", async (limit) => { + const tool = createHistoryToolWithMessage("hello"); + + await expect(tool.execute("call-1", { sessionKey: "main", limit })).rejects.toThrow( + "limit must be a positive integer", + ); + }); }); diff --git a/src/agents/tools/sessions-history-tool.ts b/src/agents/tools/sessions-history-tool.ts index 2474488e8186..ee784739ca4e 100644 --- a/src/agents/tools/sessions-history-tool.ts +++ b/src/agents/tools/sessions-history-tool.ts @@ -12,7 +12,7 @@ import { SESSIONS_HISTORY_TOOL_DISPLAY_SUMMARY, } from "../tool-description-presets.js"; import type { AnyAgentTool } from "./common.js"; -import { jsonResult, readStringParam } from "./common.js"; +import { jsonResult, readPositiveIntegerParam, readStringParam } from "./common.js"; import { createSessionVisibilityGuard, createAgentToAgentPolicy, @@ -25,7 +25,7 @@ import { const SessionsHistoryToolSchema = Type.Object({ sessionKey: Type.String(), - limit: Type.Optional(Type.Number({ minimum: 1 })), + limit: Type.Optional(Type.Integer({ minimum: 1 })), includeTools: Type.Optional(Type.Boolean()), }); @@ -247,10 +247,7 @@ export function createSessionsHistoryTool(opts?: { }); } - const limit = - typeof params.limit === "number" && Number.isFinite(params.limit) - ? Math.max(1, Math.floor(params.limit)) - : undefined; + const limit = readPositiveIntegerParam(params, "limit"); const includeTools = Boolean(params.includeTools); const result = await gatewayCall<{ messages: Array }>({ method: "chat.history", diff --git a/src/agents/tools/sessions-list-tool.test.ts b/src/agents/tools/sessions-list-tool.test.ts index adef2421e6ed..0560944772db 100644 --- a/src/agents/tools/sessions-list-tool.test.ts +++ b/src/agents/tools/sessions-list-tool.test.ts @@ -193,4 +193,16 @@ describe("sessions-list-tool", () => { expect(session?.elevatedLevel).toBe("on"); expect(session?.responseUsage).toBe("full"); }); + + it.each([ + [{ limit: 1.5 }, "limit must be a positive integer"], + [{ activeMinutes: 0 }, "activeMinutes must be a positive integer"], + [{ messageLimit: 1.5 }, "messageLimit must be a non-negative integer"], + [{ messageLimit: -1 }, "messageLimit must be a non-negative integer"], + ])("rejects invalid numeric parameter %o", async (params, message) => { + const tool = createSessionsListTool({ config: {} as never }); + + await expect(tool.execute("call-4", params)).rejects.toThrow(message); + expect(mocks.gatewayCall).not.toHaveBeenCalled(); + }); }); diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 67c5f057f363..b40c1f36ed67 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -21,7 +21,13 @@ import { SESSIONS_LIST_TOOL_DISPLAY_SUMMARY, } from "../tool-description-presets.js"; import type { AnyAgentTool } from "./common.js"; -import { jsonResult, readStringArrayParam, readStringParam } from "./common.js"; +import { + jsonResult, + readNonNegativeIntegerParam, + readPositiveIntegerParam, + readStringArrayParam, + readStringParam, +} from "./common.js"; import { createAgentToAgentPolicy, createSessionVisibilityRowChecker, @@ -38,9 +44,9 @@ import { const SessionsListToolSchema = Type.Object({ kinds: Type.Optional(Type.Array(Type.String())), - limit: Type.Optional(Type.Number({ minimum: 1 })), - activeMinutes: Type.Optional(Type.Number({ minimum: 1 })), - messageLimit: Type.Optional(Type.Number({ minimum: 0 })), + limit: Type.Optional(Type.Integer({ minimum: 1 })), + activeMinutes: Type.Optional(Type.Integer({ minimum: 1 })), + messageLimit: Type.Optional(Type.Integer({ minimum: 0 })), label: Type.Optional(Type.String({ minLength: 1 })), agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })), search: Type.Optional(Type.String({ minLength: 1 })), @@ -97,18 +103,9 @@ export function createSessionsListTool(opts?: { ); const allowedKinds = allowedKindsList.length ? new Set(allowedKindsList) : undefined; - const limit = - typeof params.limit === "number" && Number.isFinite(params.limit) - ? Math.max(1, Math.floor(params.limit)) - : undefined; - const activeMinutes = - typeof params.activeMinutes === "number" && Number.isFinite(params.activeMinutes) - ? Math.max(1, Math.floor(params.activeMinutes)) - : undefined; - const messageLimitRaw = - typeof params.messageLimit === "number" && Number.isFinite(params.messageLimit) - ? Math.max(0, Math.floor(params.messageLimit)) - : 0; + const limit = readPositiveIntegerParam(params, "limit"); + const activeMinutes = readPositiveIntegerParam(params, "activeMinutes"); + const messageLimitRaw = readNonNegativeIntegerParam(params, "messageLimit") ?? 0; const messageLimit = Math.min(messageLimitRaw, 20); const label = readStringParam(params, "label"); const agentId = readStringParam(params, "agentId");