fix(infra): bound ClawHub fetchJson and error response bodies#95226
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 22, 2026, 5:28 PM ET / 21:28 UTC. Summary PR surface: Source +22, Tests +89. Total +111 across 2 files. Reproducibility: yes. Source inspection shows current main and v2026.6.9 still use direct response.text()/response.json() on ClawHub metadata/error paths, and the PR body includes after-fix live output plus oversized-stream cancellation proof. Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the bounded ClawHub JSON/error readers after maintainers accept or tune the limits and exact-head CI is green; keep artifact-download sizing as a separate binary-download contract. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main and v2026.6.9 still use direct response.text()/response.json() on ClawHub metadata/error paths, and the PR body includes after-fix live output plus oversized-stream cancellation proof. Is this the best way to solve the issue? Yes, with maintainer cap acceptance. Centralizing ClawHub metadata JSON and diagnostics on media-core bounded readers is the narrow owner-boundary fix; binary artifact downloads are a separate contract. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 88c3bb539189. Label changesLabel justifications:
Evidence reviewedPR surface: Source +22, Tests +89. Total +111 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper re-review — Addressed both P1 blockers: (1) added a live normal-path proof (real clawhub.ai search returning 5 skills, parsed through the bounded reader) and (2) the sibling install-resolution path now also uses the bounded |
|
🦞🧹 I asked ClawSweeper to review this item again. |
7c96a81 to
eb81ab8
Compare
Dependency GuardThis PR changes dependency-related files. Maintainers should confirm these changes are intentional. Changed files:
Maintainer follow-up:
|
Dependency graph changes are blockedOpenClaw does not accept dependency graph changes through PRs unless a repository admin or security explicitly authorizes the current head SHA. Dependency updates are generated internally by maintainers so external PRs cannot change the resolved graph. Detected dependency graph changes:
Auto-scrub was not attempted because this PR changes package manifest dependency graph fields:
Dependency graph changes must be reviewed by security or handled by maintainers internally. Please remove lockfile changes manually if they are not needed. To remove lockfile changes, restore them from the target branch: git fetch origin
git checkout 'origin/main' -- 'extensions/acpx/npm-shrinkwrap.json' 'pnpm-lock.yaml'
git commit -m 'chore: remove dependency lockfile change'
git pushIf this PR intentionally needs a dependency graph change, ask a repository admin or member of The action will approve the current head SHA ( |
|
Maintainer verification complete on exact head
The final stack preserves each caller's resolved timeout through body reads and removes PR-specific production-comment lore. No remaining proof gaps. |
|
Merged via rebase.
|
|
Thanks @steipete for the detailed ClawHub verification and rebase merge. |
Summary
ClawHub is an external marketplace and therefore an untrusted source. Every
ClawHub response reader in
src/infra/clawhub.tspreviously consumed the bodywithout a byte cap, so a hostile or malfunctioning ClawHub host could exhaust
process memory by streaming an unbounded response:
fetchJson(shared by search/detail/version/artifact-resolver/verify/securitymetadata) called
await response.json()— the entire success body wasbuffered before parsing, with no upper bound.
readErrorBodycalledawait response.text()— the entire error body wasbuffered into the diagnostic/error message, with no upper bound.
fetchClawHubSkillInstallResolutioncalled a bareawait response.json()forboth the OK and the structured 403/409/410/423 install bodies — the one
remaining uncapped ClawHub JSON reader.
This is the symmetric, harder-justified counterpart to the error-stream
hardening landed in #95108 (which bounded the Anthropic Messages error stream):
here the source is a third-party marketplace, not first-party transport.
Changes
fetchJsonnow delegates to a smallparseClawHubJsonBodyhelper that readsthe success body through the existing
readResponseWithLimithelper under a16 MiB cap (
CLAWHUB_JSON_MAX_BYTES), then decodes andJSON.parses thebounded buffer. Overflow and idle-timeout cancel the stream (releasing the
socket/buffer) and throw a descriptive error; malformed JSON keeps throwing
the existing
returned malformed JSONerror.readErrorBodynow usesreadResponseTextSnippet(the same helper fix(agents): bound Anthropic error streams #95108used) under an 8 KiB byte cap and 400-char display cap, collapsing whitespace
and truncating with
…. ThestatusText/HTTP <status>fallback chain ispreserved.
fetchClawHubSkillInstallResolutionnow reuses the sameparseClawHubJsonBodyhelper instead of its own bareresponse.json(), soevery ClawHub JSON success/structured-block body is bounded by the identical
cap. This is pure reuse of the helper introduced in this PR — no new
abstraction — and closes the last uncapped ClawHub JSON path that the review
flagged.
searchClawHubSkills(asserts the cap error + stream cancellation), an oversized error body
(asserts the message is collapsed/truncated and stays far below the raw body),
and an oversized install-resolution body via
fetchClawHubSkillInstallResolution(asserts the cap error + cancellation).Out of scope (intentionally focused): the
MAX_SAFE_INTEGERdefault onreadClawHubResponseBytesfor artifact/archive downloads — those are length-checked binary fetches with their own integrity handling, not in-memory JSON
parses, and belong in a separate change.
Why this is worth merging
marketplace; an unbounded
response.json()/response.text()lets a hostile ormalfunctioning host stream an arbitrarily large body straight into process
memory. The narrowest maintainable fix is to route reads through the existing
response-limit helper, which is exactly what this does.
(
fetchClawHubSkillInstallResolution) was still uncapped; it is now boundedthrough the same helper, so there is no remaining direct
response.json()/response.text()on any ClawHub response path.How the
merge-risk: availabilityis boundedThe label flags that this changes response-body reading for live ClawHub
requests. The change is purely additive on the success path: the bounded
reader streams chunks and only diverges from prior behaviour when a body exceeds
16 MiB (JSON) / 8 KiB (error snippet). Real ClawHub responses are kilobytes
(a live
?q=calendar&limit=5search is ~2 KB), orders of magnitude under thecap, so normal traffic parses byte-for-byte as before — verified live below.
Beyond the cap, the previous behaviour was unbounded buffering (the actual
availability/OOM risk); the new behaviour is a fast, descriptive failure with
the stream cancelled. So the cap narrows an availability risk rather than adding
one.
Real behavior proof
A local tsx harness (
proof.mts) drove the real exportedsearchClawHubSkillsandfetchClawHubSkillInstallResolution(no mocks of thecode under test). For injected cases it streams an oversized body through a
ReadableStreamwhosecancel()hook is observed, so acancelled: truereadback proves the production code released the stream instead of draining it.
For the normal path it hits the live clawhub.ai marketplace over the network.
error response bodies (memory-exhaustion / availability vector).
tsx, against the patchedsrc/infra/clawhub.ts; vitest vianode scripts/run-vitest.mjs../node_modules/.bin/tsx proof.mtsnode scripts/run-vitest.mjs run --config test/vitest/vitest.infra.config.ts src/infra/clawhub.test.tssearchClawHubSkills→live
https://clawhub.ai/api/v1/search?q=calendar):HTTP 200,5 skill(s)parsed through the boundedparseClawHubJsonBody(
first.slug = calendar,displayNamepresent). Production JSON parsescorrectly after
response.json()was replaced by the bounded reader.ClawHub /api/v1/search response exceeded 16777216 bytes (17301504 bytes received),stream cancelled: true.ClawHub /api/v1/skills/weather/install response exceeded 16777216 bytes (17301504 bytes received),stream cancelled: true.Test Files 1 passed (1),Tests 55 passed | 2 skipped (57)(the 2 skips are pre-existing live-network-gated tests).
bodies on both the shared and the install-resolution JSON paths stop at the
cap and cancel the underlying stream.
unbounded
Response.arrayBuffer()drain of the same 17 MiB stream. It readsthe full
17825792bytes (~17 MiB),stream cancelled: false, and neverstops early. This confirms the cap is load-bearing: without it the stream is
fully buffered into memory, with it the reader cancels at 16 MiB.
returns small bodies; oversized behaviour is proven with injected streams that
drive the same production reader), and the out-of-scope artifact-download
default cap noted above.
AI-assisted (human-run real behavior proof included).