fix: validate pdf byte cap

This commit is contained in:
Peter Steinberger
2026-05-28 19:59:42 -04:00
parent f77a2687b6
commit a92eb02ec3
2 changed files with 48 additions and 6 deletions

View File

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

View File

@@ -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