fix: validate session tool numeric params

This commit is contained in:
Peter Steinberger
2026-05-28 19:31:55 -04:00
parent 82cb02a4fd
commit 4c49ca75d9
5 changed files with 41 additions and 27 deletions

View File

@@ -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");

View File

@@ -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",
);
});
});

View File

@@ -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<unknown> }>({
method: "chat.history",

View File

@@ -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();
});
});

View File

@@ -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");