Skip to content

fix(github-copilot): bound usage response reads#96607

Merged
sallyom merged 1 commit into
openclaw:mainfrom
Alix-007:fix/bound-copilot-usage
Jun 25, 2026
Merged

fix(github-copilot): bound usage response reads#96607
sallyom merged 1 commit into
openclaw:mainfrom
Alix-007:fix/bound-copilot-usage

Conversation

@Alix-007

@Alix-007 Alix-007 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

fetchCopilotUsage in extensions/github-copilot/usage.ts read the success response with an
unbounded await res.json(). The shared fetch layer enforces an abort timeout but does not
bound body size, so a misbehaving or compromised api.github.com/copilot_internal/user endpoint
can stream an arbitrarily large (or Content-Length-less, never-ending) JSON body that
res.json() buffers whole into memory — an OOM / hang vector on the usage-accounting path.

Changes

Real behavior proof

  • Behavior addressed: An untrusted Copilot usage success response with no
    Content-Length and an oversized / never-ending body must not be buffered whole; the
    read must stop at the cap, cancel the stream, and throw a bounded error — while valid
    small responses still parse unchanged.
  • Real environment tested: A real local node:http server (http.createServer) bound
    to 127.0.0.1, streaming a Content-Length-less JSON body in 64 KiB chunks (advertised
    ~64 MiB, 4× the 16 MiB cap). The real exported fetchCopilotUsage was driven against
    it over a real TCP socket — the supplied fetchFn rewrites the api.github.com URL
    to the loopback server and calls the global undici fetch, so the real
    fetchCopilotUsagefetchJson → real fetch → real socket →
    readProviderJsonResponse path runs. Not an in-process Response. Node v22.22.0.
  • Exact steps: node --import tsx _proof_copilot_usage_bound.mts — start the server →
    drive the real fetchCopilotUsage against /huge and assert it rejects + the server
    observed the socket abort + bytes-on-wire ≪ the full body → run a negative control (the
    OLD unbounded full-body read against the same /huge endpoint) → drive the real
    fetchCopilotUsage against /small (a valid usage object) and assert it still parses.
  • Evidence after fix:
    • Oversized case: fetchCopilotUsage threw exactly
      github-copilot-usage: JSON response exceeds 16777216 bytes; the server observed the
      socket aborted after 18,743,305 bytes (≪ the ~67 MiB it would have streamed),
      confirming the stream was cancelled, not drained.
    • Negative control: the OLD unbounded read pulled the full 67,108,875 bytes (> the
      16 MiB cap), proving the cap is load-bearing — without it the body keeps accumulating
      past 16 MiB.
    • Small case: fetchCopilotUsage parsed the valid usage JSON intact
      (plan=individual, Premium used 27%, Chat used 9%).
  • What was not tested: Did not hit the live api.github.com endpoint (would not
    reproduce 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

[proof] real node:http server on http://127.0.0.1:40987, cap=16777216 bytes, would-stream≈67108864 bytes

PASS  oversized body: real fetchCopilotUsage throws the bounded cap error :: msg="github-copilot-usage: JSON response exceeds 16777216 bytes"
PASS  stream cancelled: server observed the socket abort (body not drained) :: socketAborted=true
PASS  bytes on wire stayed near the cap, not the full ~64 MiB body :: bytesSent=18743305 (< 67108864)
PASS  negative control: unbounded read buffers PAST the 16 MiB cap (cap is load-bearing) :: buffered=67108875 bytes (> 16777216)
PASS  happy path: small valid usage JSON still parsed by real fetchCopilotUsage :: plan=individual Premium.used=27 Chat.used=9

[proof] ALL PASS (pass=5 fail=0)

Regression suite + checks (worktree on upstream/main):

  • node scripts/run-vitest.mjs extensions/github-copilot/models.test.ts --run29/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, clean
  • tsgo -p tsconfig.extensions.json → exit 0, no errors

Label: security

AI-assisted.

@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 11:21 PM ET / 03:21 UTC.

Summary
The PR routes GitHub Copilot usage success-body parsing through readProviderJsonResponse and adds an oversized-stream cancellation regression test.

PR surface: Source +4, Tests +41. Total +45 across 2 files.

Reproducibility: yes. Current main clearly calls res.json() on successful Copilot usage responses, and the PR body plus regression test provide a high-confidence oversized streaming reproduction path, though I did not rerun it in this read-only review.

Review metrics: 1 noteworthy metric.

  • Copilot usage response cap: 1 added: shared 16 MiB success-body cap. This is the compatibility boundary maintainers should notice before replacing the unbounded Copilot usage res.json() read.

Root-cause cluster
Relationship: canonical
Canonical: #96607
Summary: This PR is the narrow canonical item for bounding the GitHub Copilot usage success response read; related response-limit work is adjacent or broader overlap, not a replacement for this exact path.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] A legitimate Copilot usage success response larger than the shared 16 MiB provider JSON cap would now surface as a usage error instead of being parsed, so maintainers should consciously accept that compatibility boundary.

Maintainer options:

  1. Accept the Copilot usage cap (recommended)
    Keep the shared 16 MiB provider JSON cap for this plugin-specific usage response because normal quota payloads should be small and the cap removes unbounded buffering.
  2. Tune the cap before merge
    If maintainers know legitimate Copilot usage payloads can exceed 16 MiB, adjust the cap or reader choice and refresh proof before landing.
  3. Fold into broader response hardening
    Pause only if maintainers prefer one coordinated multi-plugin response-bound change over this narrow proof-positive Copilot fix.

Next step before merge

  • No automated repair is needed; maintainers should accept or tune the 16 MiB Copilot usage response cap before relying on normal exact-head merge gates.

Security
Cleared: No new security or supply-chain concern was found; the diff narrows an unbounded response-read availability exposure without changing dependencies, workflows, secrets, permissions, or package metadata.

Review details

Best 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 res.json() on successful Copilot usage responses, and the PR body plus regression test provide a high-confidence oversized streaming reproduction path, though I did not rerun it in this read-only review.

Is this the best way to solve the issue?

Yes. Reusing readProviderJsonResponse through openclaw/plugin-sdk/provider-http is the clean plugin-boundary fix; the remaining maintainer decision is whether the shared 16 MiB cap is the right compatibility boundary.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 643410c1f3c0.

Label changes

Label justifications:

  • P2: This is normal-priority security and availability hardening for one provider usage path with limited blast radius and no active outage evidence.
  • merge-risk: 🚨 compatibility: The PR deliberately changes oversized successful Copilot usage JSON from unbounded parsing to fail-closed behavior at the shared 16 MiB provider JSON cap.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes terminal proof from a real local HTTP server over TCP, covering oversized cancellation, a negative unbounded-read control, and a small valid response.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal proof from a real local HTTP server over TCP, covering oversized cancellation, a negative unbounded-read control, and a small valid response.
Evidence reviewed

PR surface:

Source +4, Tests +41. Total +45 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 5 1 +4
Tests 1 41 0 +41
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 46 1 +45

What I checked:

Likely related people:

  • Shakker: git blame attributes the current fetchCopilotUsage implementation and unbounded success-body read to commit 9d82906f792b325c04b66e7d33d9c9241506ab7a. (role: introduced current behavior; confidence: high; commits: 9d82906f792b; files: extensions/github-copilot/usage.ts, extensions/github-copilot/models.test.ts)
  • Alix-007: Related merged response-limit PRs from this contributor introduced or applied the same bounded JSON response pattern that this PR reuses. (role: adjacent hardening contributor; confidence: high; commits: 2592f8a51a4e, d1c2934d0d17, 605aede38c10; files: src/agents/provider-http-errors.ts, extensions/ollama/src/provider-models.ts, extensions/exa/src/exa-web-search-provider.runtime.ts)
  • joshavant: Recent current-main history includes provider response-read hardening in the same shared helper family used by this PR. (role: recent shared-helper contributor; confidence: medium; commits: 0a14444924e3; files: src/agents/provider-http-errors.ts, src/plugin-sdk/provider-http.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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 keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jun 25, 2026
@Alix-007

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

@clawsweeper

clawsweeper Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

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.
@Alix-007
Alix-007 force-pushed the fix/bound-copilot-usage branch from b7368d1 to af6a483 Compare June 25, 2026 02:28
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 25, 2026
@sallyom

sallyom commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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.

@sallyom sallyom self-assigned this Jun 25, 2026
@sallyom
sallyom merged commit 646e54a into openclaw:main Jun 25, 2026
125 of 135 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 26, 2026
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.
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
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.
hugenshen added a commit to hugenshen/openclaw that referenced this pull request Jul 7, 2026
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.
hugenshen added a commit to hugenshen/openclaw that referenced this pull request Jul 7, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extensions: github-copilot merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants