From 11ef608685ceeb592d076ddb24343269e9a97f9c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 18:18:29 -0400 Subject: [PATCH] fix: validate firecrawl numeric options --- .../firecrawl/src/firecrawl-scrape-tool.ts | 12 +++--- .../firecrawl/src/firecrawl-search-tool.ts | 8 ++-- .../firecrawl/src/firecrawl-tools.test.ts | 43 +++++++++++++++++++ src/agents/tools/common.params.test.ts | 16 +++++++ src/agents/tools/common.ts | 5 +++ 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/extensions/firecrawl/src/firecrawl-scrape-tool.ts b/extensions/firecrawl/src/firecrawl-scrape-tool.ts index 9efbceca4a94..7efffb5cc7c5 100644 --- a/extensions/firecrawl/src/firecrawl-scrape-tool.ts +++ b/extensions/firecrawl/src/firecrawl-scrape-tool.ts @@ -15,7 +15,7 @@ const FirecrawlScrapeToolSchema = Type.Object( description: 'Extraction mode ("markdown" or "text"). Default: markdown.', }), maxChars: Type.Optional( - Type.Number({ + Type.Integer({ description: "Maximum characters to return.", minimum: 100, }), @@ -26,7 +26,7 @@ const FirecrawlScrapeToolSchema = Type.Object( }), ), maxAgeMs: Type.Optional( - Type.Number({ + Type.Integer({ description: "Maximum Firecrawl cache age in milliseconds.", minimum: 0, }), @@ -40,7 +40,7 @@ const FirecrawlScrapeToolSchema = Type.Object( }), ), timeoutSeconds: Type.Optional( - Type.Number({ + Type.Integer({ description: "Timeout in seconds for the Firecrawl scrape request.", minimum: 1, }), @@ -60,10 +60,10 @@ export function createFirecrawlScrapeTool(api: OpenClawPluginApi) { const url = readStringParam(rawParams, "url", { required: true }); const extractMode = readStringParam(rawParams, "extractMode") === "text" ? "text" : "markdown"; - const maxChars = readNumberParam(rawParams, "maxChars", { integer: true }); - const maxAgeMs = readNumberParam(rawParams, "maxAgeMs", { integer: true }); + const maxChars = readNumberParam(rawParams, "maxChars", { positiveInteger: true }); + const maxAgeMs = readNumberParam(rawParams, "maxAgeMs", { nonNegativeInteger: true }); const timeoutSeconds = readNumberParam(rawParams, "timeoutSeconds", { - integer: true, + positiveInteger: true, }); const proxyRaw = readStringParam(rawParams, "proxy"); const proxy = diff --git a/extensions/firecrawl/src/firecrawl-search-tool.ts b/extensions/firecrawl/src/firecrawl-search-tool.ts index a3935e7b197a..a64c09f3e48f 100644 --- a/extensions/firecrawl/src/firecrawl-search-tool.ts +++ b/extensions/firecrawl/src/firecrawl-search-tool.ts @@ -12,7 +12,7 @@ const FirecrawlSearchToolSchema = Type.Object( { query: Type.String({ description: "Search query string." }), count: Type.Optional( - Type.Number({ + Type.Integer({ description: "Number of results to return (1-10).", minimum: 1, maximum: 10, @@ -34,7 +34,7 @@ const FirecrawlSearchToolSchema = Type.Object( }), ), timeoutSeconds: Type.Optional( - Type.Number({ + Type.Integer({ description: "Timeout in seconds for the Firecrawl Search request.", minimum: 1, }), @@ -52,9 +52,9 @@ export function createFirecrawlSearchTool(api: OpenClawPluginApi) { parameters: FirecrawlSearchToolSchema, execute: async (_toolCallId: string, rawParams: Record) => { const query = readStringParam(rawParams, "query", { required: true }); - const count = readNumberParam(rawParams, "count", { integer: true }); + const count = readNumberParam(rawParams, "count", { positiveInteger: true }); const timeoutSeconds = readNumberParam(rawParams, "timeoutSeconds", { - integer: true, + positiveInteger: true, }); const sources = readStringArrayParam(rawParams, "sources"); const categories = readStringArrayParam(rawParams, "categories"); diff --git a/extensions/firecrawl/src/firecrawl-tools.test.ts b/extensions/firecrawl/src/firecrawl-tools.test.ts index dc3706add685..8c75a0f5d698 100644 --- a/extensions/firecrawl/src/firecrawl-tools.test.ts +++ b/extensions/firecrawl/src/firecrawl-tools.test.ts @@ -489,6 +489,49 @@ describe("firecrawl tools", () => { }); }); + it("drops malformed numeric Firecrawl tool options", async () => { + const searchTool = createFirecrawlSearchTool({ + config: { env: "test" }, + } as never); + await searchTool.execute("call-search", { + query: "web search", + count: 6.5, + timeoutSeconds: Number.POSITIVE_INFINITY, + }); + + expect(runFirecrawlSearch).toHaveBeenLastCalledWith({ + cfg: { env: "test" }, + query: "web search", + count: undefined, + timeoutSeconds: undefined, + sources: undefined, + categories: undefined, + scrapeResults: false, + }); + + const scrapeTool = createFirecrawlScrapeTool({ + config: { env: "test" }, + } as never); + await scrapeTool.execute("call-scrape", { + url: "https://docs.openclaw.ai", + maxChars: 1500.5, + maxAgeMs: -1, + timeoutSeconds: 22.5, + }); + + expect(runFirecrawlScrape).toHaveBeenLastCalledWith({ + cfg: { env: "test" }, + url: "https://docs.openclaw.ai", + extractMode: "markdown", + maxChars: undefined, + onlyMainContent: undefined, + maxAgeMs: undefined, + proxy: undefined, + storeInCache: undefined, + timeoutSeconds: undefined, + }); + }); + it("passes text mode through and ignores invalid proxy values", async () => { const tool = createFirecrawlScrapeTool({ config: { env: "test" }, diff --git a/src/agents/tools/common.params.test.ts b/src/agents/tools/common.params.test.ts index 47fbb8c0d4c3..8a3525dc7048 100644 --- a/src/agents/tools/common.params.test.ts +++ b/src/agents/tools/common.params.test.ts @@ -74,6 +74,22 @@ describe("readNumberParam", () => { }), ).toBeUndefined(); }); + + it("accepts only nonnegative safe integers when nonNegativeInteger is true", () => { + expect(readNumberParam({ cacheAge: 0 }, "cacheAge", { nonNegativeInteger: true })).toBe(0); + expect(readNumberParam({ cacheAge: "42" }, "cacheAge", { nonNegativeInteger: true })).toBe(42); + expect( + readNumberParam({ cacheAge: "42.9" }, "cacheAge", { nonNegativeInteger: true }), + ).toBeUndefined(); + expect( + readNumberParam({ cacheAge: -1 }, "cacheAge", { nonNegativeInteger: true }), + ).toBeUndefined(); + expect( + readNumberParam({ cacheAge: Number.POSITIVE_INFINITY }, "cacheAge", { + nonNegativeInteger: true, + }), + ).toBeUndefined(); + }); }); describe("snake_case aliases", () => { diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index 932ee2c30676..22e271b7ccd6 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -163,6 +163,7 @@ export function readNumberParam( integer?: boolean; strict?: boolean; positiveInteger?: boolean; + nonNegativeInteger?: boolean; } = {}, ): number | undefined { const { @@ -171,6 +172,7 @@ export function readNumberParam( integer = false, strict = false, positiveInteger = false, + nonNegativeInteger = false, } = options; const raw = readParamRaw(params, key); let value: number | undefined; @@ -194,6 +196,9 @@ export function readNumberParam( if (positiveInteger) { return Number.isSafeInteger(value) && value > 0 ? value : undefined; } + if (nonNegativeInteger) { + return Number.isSafeInteger(value) && value >= 0 ? value : undefined; + } return integer ? Math.trunc(value) : value; }