fix(google): bound Veo video operation response reads#96605
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 11:21 PM ET / 03:21 UTC. Summary PR surface: Source +9, Tests +60. Total +69 across 2 files. Reproducibility: yes. Current main has an unbounded 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. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the bounded read in the Google plugin after maintainers accept or tune the 16 MiB Veo operation-metadata ceiling. Do we have a high-confidence way to reproduce the issue? Yes. Current main has an unbounded Is this the best way to solve the issue? Yes, with a maintainer cap decision remaining. Reusing the existing plugin SDK bounded reader inside the Google plugin helper is the narrowest owner-boundary fix; the broader Google JSON sweep remains separate at #96324. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 643410c1f3c0. Label changesLabel justifications:
Evidence reviewedPR surface: Source +9, Tests +60. Total +69 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. |
1aae5ff to
e9f508f
Compare
|
Maintainer local review: merge-ready provided green CI, reviewed as a batch:
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. |
What Problem This Solves
The Google Veo video REST operation read in
extensions/google/video-generation-provider.ts(
requestGoogleVideoJson) buffers the entire external response via an unboundedawait response.text()before checkingresponse.ok. The Veo create/poll response is untrustedprovider content: a hostile or misconfigured endpoint can stream an arbitrarily large,
Content-Length-less body that is fully read into memory before parsing — a memory-pressure / hang(OOM) vector on the video-generation path.
Scope is the video-generation operation JSON only. This is distinct from #96324, which covers
Google embedding/image/media/oauth/speech and does not touch
video-generation-provider.ts.Changes
readResponseWithLimit(re-exported via
openclaw/plugin-sdk/response-limit-runtime, from@openclaw/media-core)instead of
await response.text(), capped at a newGOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES(16 MiB, aligned with merged provider bound-stream 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). The result is
TextDecoder().decoded before the existingresponse.ok/ HTTP-detail /JSON.parseflow.(
Google video operation response exceeds 16777216 bytes). Theresponse.okerror-detail pathand the success
JSON.parseshape are otherwise unchanged.requestGoogleVideoJson(generateGoogleVideoViaRestcalls it forthe
:predictLongRunningcreate POST and for eachGET <operation name>poll), so the singlebounded read covers both verbs.
media-corebounded reader already used across the codebase.Real behavior proof
await response.text()on the Veo REST operation read letan untrusted endpoint stream an unbounded,
Content-Length-less JSON body fully into memory. Theread must instead stop at the 16 MiB cap, cancel the stream, and throw a bounded error — while
small valid operation JSON still parses unchanged.
node:httpserver (createServer) bound to127.0.0.1, streaming a JSON operation body in 64 KiB chunks with noContent-Lengthheader(would total ~64 MiB, 4× the 16 MiB cap), reached over the real global
fetch+ real TCPsocket (not an in-process
ReadableStreamfixture, not a mockedfetch). The proof drives theexact post-fix
requestGoogleVideoJsonread body —readResponseWithLimit(response, GOOGLE_VIDEO_OPERATION_RESPONSE_MAX_BYTES, { onOverflow })→TextDecoder().decode→response.okbranch →JSON.parse— against that real socket, for both the create POST and thepoll GET verbs that share the helper.
readResponseWithLimitis the real@openclaw/media-corefunction the changed line calls. Node v22.22.0.
node --import tsx proof.mts— start the real server → drive the shared read pathagainst
POST /create-hugeandGET /poll-huge(oversized, no Content-Length) and assert eachthrows the bounded error + the server observed the socket aborted with bytes-on-wire ≪ the full
~64 MiB → run a negative control (the OLD unbounded read of the same body, measuring it buffers
past the cap) → drive the shared read path against small valid create/poll operation JSON and
assert they still parse.
readResponseWithLimitthrewGoogle video operation response exceeds 16777216 bytes; the server observed the socket abortedafter ~17–18 MiB (≪ the ~64 MiB it would have sent), confirming the stream was cancelled, not
drained. Small create/poll operation JSON parsed back into intact operation objects
(
name/done, and the polleddone:trueresult with one generated sample).create POST shared path: overflow throws bounded error→ threw, msgexceeds 16777216 bytescreate POST shared path: stream cancelled near 16MiB→ aborted=true, bytesSent=18284544 (< 33554432)poll GET shared path: overflow throws bounded error→ threw, msgexceeds 16777216 bytespoll GET shared path: stream cancelled near 16MiB→ aborted=true, bytesSent=17825792 (< 33554432)negative control: OLD unbounded read buffers PAST the 16MiB cap→ buffered 16799559 bytes (> cap)— proves the cap is load-bearing (without it the body keeps accumulating past 16 MiB).
happy path: small create operation JSON still parsed→name=operations/veo-abc done=falsehappy path: small poll operation JSON still parsed→done=true samples=1generativelanguage.googleapis.comVeo endpoint(no key / 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 did not drive the read
to actual OOM — it measures bytes-on-wire + early socket abort, which is the load-bearing signal.
The proof script is not committed.
Evidence
Real
node:httpterminal proof (node --import tsx proof.mts):In-repo Vitest suite for this provider (
node scripts/run-vitest.mjs extensions/google/video-generation-provider.test.ts --run) — includes a streaming-fixtureregression test asserting the oversized read stops before consuming all chunks and cancels the
stream:
oxlint extensions/google/video-generation-provider.ts→ exit 0, clean.This is the Google Veo video-operation counterpart to the #95103 / #95108 response-limit campaign,
applying the same
@openclaw/media-corebounded reader to the remaining unbounded provider read.Label: security
AI-assisted.