diff --git a/src/agents/tools/pdf-tool.test.ts b/src/agents/tools/pdf-tool.test.ts index 813cbb2e81a..0ce36b21301 100644 --- a/src/agents/tools/pdf-tool.test.ts +++ b/src/agents/tools/pdf-tool.test.ts @@ -287,6 +287,42 @@ describe("createPdfTool", () => { }); }); + it("rejects invalid maxBytesMb before loading PDFs", async () => { + await withConfiguredPdfTool(async (tool) => { + const loadSpy = vi.spyOn(webMedia, "loadWebMediaRaw"); + + await expect( + tool.execute("t1", { + prompt: "test", + pdf: "/tmp/doc.pdf", + maxBytesMb: 0, + }), + ).rejects.toThrow("maxBytesMb must be greater than 0"); + expect(loadSpy).not.toHaveBeenCalled(); + }); + }); + + it("passes validated maxBytesMb to PDF loading", async () => { + await withTempPdfAgentDir(async (agentDir) => { + const { loadSpy } = await stubPdfToolInfra(agentDir, { + provider: "anthropic", + input: ["text", "document"], + }); + vi.spyOn(pdfNativeProviders, "anthropicAnalyzePdf").mockResolvedValue("native summary"); + const cfg = withPdfModel(ANTHROPIC_PDF_MODEL); + const tool = requirePdfTool((await loadCreatePdfTool())({ config: cfg, agentDir })); + + await tool.execute("t1", { + prompt: "summarize", + pdf: "/tmp/doc.pdf", + maxBytesMb: "0.5", + }); + + const [, loadOptions] = firstMockCall(loadSpy, "loadWebMediaRaw"); + expectFields(loadOptions, { maxBytes: 524_288 }); + }); + }); + it("respects fsPolicy.workspaceOnly for non-sandbox pdf paths", async () => { await withTempPdfAgentDir(async (agentDir) => { const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pdf-ws-")); @@ -686,5 +722,9 @@ describe("createPdfTool", () => { expect(props).toHaveProperty("password"); expect(props).toHaveProperty("model"); expect(props).toHaveProperty("maxBytesMb"); + expect(PdfToolSchema.properties.maxBytesMb).toMatchObject({ + type: "number", + exclusiveMinimum: 0, + }); }); }); diff --git a/src/agents/tools/pdf-tool.ts b/src/agents/tools/pdf-tool.ts index d379c1d585d..7b7bff1f75d 100644 --- a/src/agents/tools/pdf-tool.ts +++ b/src/agents/tools/pdf-tool.ts @@ -14,7 +14,8 @@ import { } from "../../shared/string-coerce.js"; import { resolveUserPath } from "../../utils.js"; import type { AuthProfileStore } from "../auth-profiles/types.js"; -import { ToolInputError } from "./common.js"; +import { optionalFiniteNumberSchema } from "../schema/typebox.js"; +import { readFiniteNumberParam, ToolInputError } from "./common.js"; import { coerceImageModelConfig, type ImageModelConfig } from "./image-tool.helpers.js"; import { applyImageModelConfigDefaults, @@ -73,7 +74,7 @@ export const PdfToolSchema = Type.Object({ ), password: Type.Optional(Type.String({ description: "Password for encrypted PDFs." })), model: Type.Optional(Type.String()), - maxBytesMb: Type.Optional(Type.Number()), + maxBytesMb: optionalFiniteNumberSchema({ exclusiveMinimum: 0 }), }); // --------------------------------------------------------------------------- @@ -354,11 +355,12 @@ export function createPdfTool(options?: { record, DEFAULT_PROMPT, ); - const maxBytesMbRaw = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined; const maxBytesMb = - typeof maxBytesMbRaw === "number" && Number.isFinite(maxBytesMbRaw) && maxBytesMbRaw > 0 - ? maxBytesMbRaw - : configuredMaxBytesMb; + readFiniteNumberParam(record, "maxBytesMb", { + min: 0, + minExclusive: true, + message: "maxBytesMb must be greater than 0", + }) ?? configuredMaxBytesMb; const maxBytes = Math.floor(maxBytesMb * 1024 * 1024); // Parse page range