fix(github-copilot): bound usage response reads#96607
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 11:21 PM ET / 03:21 UTC. Summary PR surface: Source +4, Tests +41. Total +45 across 2 files. Reproducibility: yes. Current main clearly calls Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this scoped Copilot usage bounded-reader fix after maintainer acceptance of the 16 MiB cap, keeping the broader multi-plugin response-bound sweep separate. Do we have a high-confidence way to reproduce the issue? Yes. Current main clearly calls Is this the best way to solve the issue? Yes. Reusing AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 643410c1f3c0. Label changesLabel justifications:
Evidence reviewedPR surface: Source +4, Tests +41. Total +45 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 review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the openclaw#96027 / openclaw#96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
b7368d1 to
af6a483
Compare
|
Maintainer local review: merge-ready, reviewed as a batch:
Current CI red check is unrelated to this PR change Autoreview is clean, the 16 MiB cap is acceptable for these provider JSON/status/error/usage bodies, and the changes follow the established shared bounded-reader pattern. This is the best narrow fix for these call sites: no new SDK/API/config surface, no upgrade/backward-compatibility concern beyond the intended oversized-body failure mode, and no expected breaking behavior for normal provider responses. |
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the openclaw#96027 / openclaw#96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the openclaw#96027 / openclaw#96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
The fal music generation provider in extensions/fal/music-generation-provider.ts parsed its HTTP success response with an unbounded await response.json(). A hostile or buggy fal.ai endpoint — the base URL is user-configurable via models.providers.fal.baseUrl (resolved by resolveFalHttpRequestConfig in http-config.ts, defaulting to https://fal.run) — could stream an arbitrarily large JSON body into memory before parsing, forcing the music generation handler into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap (PROVIDER_JSON_RESPONSE_MAX_BYTES), cancels the stream on overflow, and wraps malformed JSON with the caller label. The change is minimal — a single .json() call replaced by readProviderJsonResponse<unknown> — and preserves the existing downstream logic unchanged. Update the test suite: replace fake { json: async () => ... } response stubs with proper Response objects carrying JSON body streams via a new jsonBodyResponse() helper, so the real readProviderJsonResponse implementation actually exercises the byte-bounded reader under test. Preserve the real readProviderJsonResponse in the provider-http mock (via importOriginal) so the bounded reader streams and cancels oversized bodies. Add a focused regression test: when the music generation stream exceeds the JSON byte cap (32 MiB body, double the 16 MiB cap), generateMusic rejects with a "fal-music-generation: JSON response exceeds" error and the reader cancels the body mid-flight (ReadableStream.cancel fires, bytesPulled < 32 MiB). Existing parse/HTTP-error cases keep passing. Symmetric counterpart to the openclaw#96027/openclaw#96038/openclaw#96042/openclaw#96606/openclaw#96607 response-limit campaign.
The fal music generation provider in extensions/fal/music-generation-provider.ts parsed its HTTP success response with an unbounded await response.json(). A hostile or buggy fal.ai endpoint — the base URL is user-configurable via models.providers.fal.baseUrl (resolved by resolveFalHttpRequestConfig in http-config.ts, defaulting to https://fal.run) — could stream an arbitrarily large JSON body into memory before parsing, forcing the music generation handler into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap (PROVIDER_JSON_RESPONSE_MAX_BYTES), cancels the stream on overflow, and wraps malformed JSON with the caller label. The change is minimal — a single .json() call replaced by readProviderJsonResponse<unknown> — and preserves the existing downstream logic unchanged. Update the test suite: replace fake { json: async () => ... } response stubs with proper Response objects carrying JSON body streams via a new jsonBodyResponse() helper, so the real readProviderJsonResponse implementation actually exercises the byte-bounded reader under test. Preserve the real readProviderJsonResponse in the provider-http mock (via importOriginal) so the bounded reader streams and cancels oversized bodies. Add a focused regression test: when the music generation stream exceeds the JSON byte cap (32 MiB body, double the 16 MiB cap), generateMusic rejects with a "fal-music-generation: JSON response exceeds" error and the reader cancels the body mid-flight (ReadableStream.cancel fires, bytesPulled < 32 MiB). Existing parse/HTTP-error cases keep passing. Symmetric counterpart to the openclaw#96027/openclaw#96038/openclaw#96042/openclaw#96606/openclaw#96607 response-limit campaign.
What Problem This Solves
fetchCopilotUsageinextensions/github-copilot/usage.tsread the success response with anunbounded
await res.json(). The shared fetch layer enforces an abort timeout but does notbound body size, so a misbehaving or compromised
api.github.com/copilot_internal/userendpointcan stream an arbitrarily large (or
Content-Length-less, never-ending) JSON body thatres.json()buffers whole into memory — an OOM / hang vector on the usage-accounting path.Changes
readProviderJsonResponse(re-exported via
openclaw/plugin-sdk/provider-http): it reads under the shared 16 MiBPROVIDER_JSON_RESPONSE_MAX_BYTEScap, cancels the underlying stream on overflow, andthrows
github-copilot-usage: JSON response exceeds <N> bytes.response-limit PRs (fix(agents): bound provider JSON response reads #95218 / fix(ollama): bound model-discovery JSON response reads #96027 / fix(exa): bound untrusted search JSON response reads #96038), bringing GitHub Copilot usage in line with the
other providers' bounded JSON reads.
Real behavior proof
Content-Lengthand an oversized / never-ending body must not be buffered whole; theread must stop at the cap, cancel the stream, and throw a bounded error — while valid
small responses still parse unchanged.
node:httpserver (http.createServer) boundto
127.0.0.1, streaming aContent-Length-less JSON body in 64 KiB chunks (advertised~64 MiB, 4× the 16 MiB cap). The real exported
fetchCopilotUsagewas driven againstit over a real TCP socket — the supplied
fetchFnrewrites theapi.github.comURLto the loopback server and calls the global undici
fetch, so the realfetchCopilotUsage→fetchJson→ realfetch→ real socket →readProviderJsonResponsepath runs. Not an in-processResponse. Node v22.22.0.node --import tsx _proof_copilot_usage_bound.mts— start the server →drive the real
fetchCopilotUsageagainst/hugeand assert it rejects + the serverobserved the socket abort + bytes-on-wire ≪ the full body → run a negative control (the
OLD unbounded full-body read against the same
/hugeendpoint) → drive the realfetchCopilotUsageagainst/small(a valid usage object) and assert it still parses.fetchCopilotUsagethrew exactlygithub-copilot-usage: JSON response exceeds 16777216 bytes; the server observed thesocket aborted after 18,743,305 bytes (≪ the ~67 MiB it would have streamed),
confirming the stream was cancelled, not drained.
16 MiB cap), proving the cap is load-bearing — without it the body keeps accumulating
past 16 MiB.
fetchCopilotUsageparsed the valid usage JSON intact(
plan=individual, Premium used 27%, Chat used 9%).api.github.comendpoint (would notreproduce a hostile oversized body); the untrusted-body behavior is fully reproduced with
a local streaming server over the same transport. The proof script is not committed.
Evidence
Regression suite + checks (worktree on
upstream/main):node scripts/run-vitest.mjs extensions/github-copilot/models.test.ts --run→ 29/29 passed(includes a streaming-fixture regression test asserting the oversized read cancels the
stream and never pulls the full advertised body)
oxlint extensions/github-copilot/usage.ts extensions/github-copilot/models.test.ts→ exit 0, cleantsgo -p tsconfig.extensions.json→ exit 0, no errorsLabel: security
AI-assisted.