🐛 fix(sdk): web 抓取/搜索域九项修复——span OOM、解压预算、reader 零联网、全链路可中断 - #428
Conversation
Huge rowspan/colspan attributes were parseInt'd without an upper bound and the rectangular grid materialized rowSpan×colSpan slots synchronously, so a single hostile table could OOM or hang the fetch pipeline. Clamp each span to 1000, skip normalization entirely when the expanded grid would exceed a 250k-cell budget, and compute the grid width with a loop instead of spread so wide rows cannot overflow the call stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
unzipSync trusted the central directory's originalSize to preallocate output buffers and budget the filter, so an archive that lied about its entry sizes made CPU spin inflating far past the intended ceiling. Switch to streaming Unzip/UnzipInflate (+UnzipPassThrough for stored entries): every entry is measured by actual ondata bytes against the 64MB budget, entries cap at 100, and hitting the ceiling stops pushing compressed input so remaining entries are never inflated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under HTTP(S)_PROXY the curl fallback capped output at a hard-coded 4MB execFile maxBuffer while the fetch budget allows 50MB, so large proxied responses failed where direct fetch succeeded; and request bodies rode on argv, which Windows caps around 32K chars. Replace promisified execFile with a spawn runner that streams output up to 50MB + slack, caps stderr, and feeds bodies to curl over stdin via --data-binary @-. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
trafilatura -u and lynx -dump made the external binaries fetch context.url directly: a redirect from an allowlisted host could carry them to an internal address with no network-guard re-check, bypassing the DNS-level sandbox that the guarded fetch path enforces. Both readers now parse a temp file holding the HTML already obtained through the guarded fetch; they never touch the network. A commandRunner seam on ReaderContext keeps the flow testable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
downloadAndLocalizeImages took no signal and no count cap, so an interrupted WebFetch kept sequentially downloading every image on the page. Accept an optional AbortSignal (checked per image and forwarded to loadBinary), cap downloads at 50 per page, and pass the tool's abortSignal from the WebFetch call site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ment WebSearchTool.call never referenced the caller's abort signal: the eight-provider serial fallback and the result-enrichment page fetches ran to completion regardless of cancellation. Providers now merge the user signal with their fixed per-provider timeout (AbortSignal.any when available), the fallback loop checks the signal between attempts, and fetchPageContent/enrichResultsWithContent bail out on abort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scraper handlers gated on hostname.includes(domain), so lookalike hosts (news.ycombinator.com.evil.io and friends) were accepted as trusted sites; semantic-scholar matched the domain anywhere in the full URL. Switch every domain gate in the scrapers directory to an exact hostname comparison (www. stripped) with explicit host allowlists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A direct image response up to 20MB was base64-inlined straight into model context with no size gate. Introduce a 5MB inline threshold: larger images are written under the asset dir (hash-named, lume-file reference returned) when one is available, or degrade to a text notice otherwise. sniffExt is exported from the image pipeline for reuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every redirect hop and user-agent retry inside a fetch started a fresh full timeoutMs budget, so after reader probing the configured timeout was never a total-duration ceiling. runWebFetch now records one deadline: each request receives whatever time remains, speculative probes (alternates/.md/content-negotiation/llms.txt) are skipped once less than 2s remains, and llms.txt candidate enumeration is capped at six for deep paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The proxied transport glued the response body onto stdout with only a newline separating it from the --write-out status sentinel, so the byte-exact result depended on how each curl build interleaves stream writes: Linux CI saw stray trailing newlines (one extra byte on GET, two on POST) where Windows happened to emit none, and text-decoding the shared stream would corrupt binary bodies. Write the body with --output to a temp file (headers to a second file in manual-redirect mode) and let stdout carry only the status sentinel; the body is now byte-exact on every platform. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review九项逐项核对 + 攻击性验证完毕。八项半真实落地且实现质量高于修复批次平均水准,但 #303 的修复不合格:预算作用域错误,可被多表组合平凡绕过,OOM 依旧可达——需修复后才能合。 [P0] #303 的 25 万格预算是 per-table 的,多张合法小表绕过后 OOM 依旧可达
其余发现(不阻塞)[P2] #343 [P2] 新测试盲区集中在错误路径,两条关键 throw 路径零覆盖 [P3] #339 落盘方案后 [P3] [P3] #372 oversized 图片落盘失败静默降级( [P3] #342 中断在途的图片被标记为 [P3] #371 精确匹配收窄掉了 [P3] guanlan provider 未接 abortSignal(已披露)+ 值得肯定
CI 6/6 全绿(连长期红的 Computer Use 也过了)、typecheck 绿。结论:修掉 #303 的 document 级预算并补绕过测试后可合,其余 findings 均可 follow-up。 |
概述
修复 sdk 全量 review 提出的 9 个 issue(web 抓取/搜索域),每个 commit 独立可跑绿。
--data-binary @-规避 Windows argv 上限验证
tsc --noEmit零错误Notes
Closes #303, closes #339, closes #340, closes #341, closes #342, closes #343, closes #371, closes #372, closes #373
🤖 Generated with Claude Code