fix: validate firecrawl numeric options

This commit is contained in:
Peter Steinberger
2026-05-28 18:18:29 -04:00
parent ff21b4e731
commit 11ef608685
5 changed files with 74 additions and 10 deletions
@@ -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 =
@@ -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<string, unknown>) => {
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");
@@ -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" },
+16
View File
@@ -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", () => {
+5
View File
@@ -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;
}