feat: expose bounded empty PDF artifacts - #105
Conversation
3b5f662 to
a58f04f
Compare
|
Thanks for this. It's a well-built PR and the writeup made it easy to check. We put it through build verification, a security review, and an API design pass. Build is green. 685 lib tests, 39 CLI tests, clippy with The security properties you claimed all hold. We traced each one:
Two things need to change before we can take it. 1.
|
|
Thanks for the thorough review. I applied the requested changes in commit Implemented:
Validation completed:
No OCR or second fetch path was introduced. |
|
Re-ran the full audit against VerifiedBuild is green: fmt, clippy with The byte-leakage surface survived re-review after you added
Blocking: the server surface isn't opt-in
pdf_artifact_max_bytes: Some(webclaw_fetch::DEFAULT_PDF_ARTIFACT_MAX_BYTES),and For a self-hoster upgrading, a scanned-PDF URL that returned a 502 now returns 200 with An Blocking: the MCP branch sits too early in the chain
Cloud escalation stops running. The block returns at The configured proxy is dropped. The branch builds a fresh client: let config = webclaw_fetch::FetchConfig {
browser, headers, pdf_artifact_max_bytes,
..Default::default()
};
Both go away if the artifact check moves after client selection and after the escalation decision instead of short-circuiting ahead of them. MCP needs a lower ceiling
Low single-digit MiB would fit the transport. Returning the sha256 and byte length while refusing the payload is also defensible on this surface. Three corrections
SemverThis is a breaking release rather than a patch.
Once the server opt-in and the MCP branch position are sorted, I'm happy to take this. The library half is in good shape. |
|
Thanks @0xMassi for the detailed and constructive review! All feedback items have been addressed in commit
|
|
Re-ran the audit against The envelope unification is the nicest part of this round. All three surfaces now serialize Blocker 1: fixed
Blocker 3: fixed
One wording problem. Blocker 2: the proxy half is fixed, the escalation half is notRepositioning after client selection is correct, and adding proxy handling to The problem is that the branch re-implements escalation instead of reusing Cause 1: the empty header map. let dummy_headers = webclaw_fetch::HeaderMap::new();
webclaw_fetch::cloud::is_bot_protected(html, &dummy_headers)
Cause 2: This is the bigger one. So an agent that sets The fix that closes several of these at onceCall Double fetch. When escalation does trigger today,
Smaller items
Rebase needed, and that one is on usThe PR now shows One blocker and a rebase, not three blockers. Everything else above is small. If you would rather split it, the |
|
Small heads-up to make the rebase smaller. The So once #111 lands, you can drop those two hunks from Keep the Nothing to do until you rebase. Flagging it now so the conflict is not a surprise. |
Why this change was needed: Ships the MCP stdio handshake fix (0xMassi#108, closing 0xMassi#107) to users. The reporter and anyone else on a client that probes before `initialize` cannot connect at all until there is a tagged release: `npx @webclaw/mcp` installs a prebuilt binary pinned to the latest tag, so a fix sitting on main is not reachable by them. Cut as a patch on its own rather than waiting for the PDF artifact work in 0xMassi#105, which adds `#[non_exhaustive]` to a public enum and is therefore a breaking release. Holding an external user's connectivity fix behind that would serve nobody. What changed: - [workspace.package] version 0.6.19 -> 0.6.20 (all 7 crates inherit it) - Cargo.lock regenerated - CHANGELOG: the Unreleased entry becomes [0.6.20] - 2026-08-16 No code changes; this is the version bump only. Problem solved: Tagging v0.6.20 publishes the binaries and republishes @webclaw/mcp pinned to the new tag, so affected clients connect without a client-side shim.
Why this change was needed: 0xMassi#106 was about a claim, not a comment. The repo said in five places that Auto mode "catches scanned PDFs". 0xMassi#111 corrected two of them, and closed the issue. The remaining three included the most user-visible copy: the PdfModeArg::Auto variant doc, which clap renders in `webclaw --help` under Possible values. The claim is wrong in a way that has already cost us. EmptyPdf fires only when the normalized text has zero non-whitespace characters, so a scanned page carrying a page number or footer stamp returns Ok with unusable text. An outside contributor built 0xMassi#104/0xMassi#105 on the wording this sweep removes. What changed: - crates/webclaw-pdf/src/lib.rs: the PdfMode::Auto variant doc. - crates/webclaw-cli/src/main.rs: the PdfModeArg::Auto variant doc, which is the one that reaches users through --help. - examples/README.md: the --pdf-mode auto example comment. Verified: grep for "catches scanned" / "scanned/image-only" / "Scanned PDFs return" across the repo now returns nothing, and `webclaw --help` prints "- auto: Error if the PDF yields no non-whitespace text". Problem solved: No surface still tells a reader that Auto detects scanned documents. Behaviour is untouched; every changed line is a comment. Refs: 0xMassi#106
- Opt-in server surface: Added optional `pdf_artifact_max_bytes` to `ScrapeRequest`, removed hardcoded server-level artifact limit, and retained 502 error on empty PDFs by default. - MCP client selection & escalation: Repositioned artifact handling after client selection in MCP server to preserve `WEBCLAW_PROXY` and `WEBCLAW_PROXY_FILE`; preserved cloud escalation for HTML responses. - MCP ceiling: Clamped MCP artifact max limit to 5 MiB (`MAX_MCP_PDF_ARTIFACT_BYTES`) to protect LLM context windows while keeping 50 MiB for crate/CLI/server. - Canonical JSON envelope: Added `PdfArtifactEnvelope` / `PdfArtifactRefEnvelope` in webclaw-fetch and unified serialization across CLI, MCP, and server. Added `Deserialize` for roundtrip support. - Semver safety: Marked `FetchExtractOutcome` and `PdfArtifactReason` with `#[non_exhaustive]`. - Updated CHANGELOG.md and tests across workspace.
- MCP extraction escalation: delegate Extracted outcome in MCP server to cloud::smart_fetch with include_raw_html: false, preserving authentic headers and full HTML body for bot/JS detection. - Non-exhaustive match arms: replaced panicking unreachable! arms in CLI, MCP, and server scrape route with safe error returns. - HTTP status mapping: added ApiError::PayloadTooLarge (413) for FetchError::PdfArtifactTooLarge so caller limits do not return 502 Bad Gateway. - Removed unused DEFAULT_PDF_ARTIFACT_MAX_BYTES and clarified rustdoc on FetchConfig::pdf_artifact_max_bytes. - Clarified MCP scrape parameter rustdoc in tools.rs regarding 5 MiB base64 wire payload (~7 MB characters, ~1.7M tokens). - Rebased onto upstream/main (0.6.21) with clean CHANGELOG and lib.rs merge.
59122af to
df1e641
Compare
|
Rebased onto
All workspace tests, clippy lints, format checks, and WASM checks pass cleanly. |
|
Ran the merge gate against Verified fixed:
One thing to fix before this goes in, and it comes from the blocker fix rather than from anything left over. A PDF that has text now comes back as PDF source
Built a text-bearing PDF to confirm. The result the code computed and discarded contained the document's text. What the caller receives is ~127 words of markdown that reads This is not a regression against The same line costs a second fetchDiscarding It is 2x bandwidth, 2x latency and 2x request rate against the target, which matters for rate limits. Worst-case wall clock doubles too: Worth noting the CLI and OSS server surfaces added by this PR both consume Both are the same ~15 lines
Gating on content-type instead does not work here: Smaller things, none blocking
Everything else is ready. Fix the |
…and tighten error handling
|
Thanks for the thorough review! All items have been addressed in 1. Preserved
|
|
Round 5 fixes both round-4 defects, and I proved it by execution rather than inspection. Built three binaries (main,
The Thanks also for One regression to fix, and it is outside this feature.
|
|
Heads-up so you only have to push once.
None of that touches the code you changed. The conflict is the changelog section only. So a rebase plus the one item from the round-5 review, in a single push:
Sorry for the churn. Three rebases across five rounds is more than a contribution should cost, and two of them were caused by things happening on our side rather than by anything in your PR. |
Summary
No OCR or second fetch path is introduced.
Validation
RUSTFLAGS="--cfg reqwest_unstable" cargo test --workspace --libRUSTFLAGS="--cfg reqwest_unstable" cargo test -p webclaw-cliRUSTFLAGS="--cfg reqwest_unstable" cargo clippy --all -- -D warningscargo fmt --check --allRUSTFLAGS="--cfg reqwest_unstable" cargo doc --no-deps --workspacegit diff --checkCloses #104