Skip to content

fix(speech): bound TTS/STT voice-list and transcription JSON responses#96496

Merged
sallyom merged 1 commit into
openclaw:mainfrom
hugenshen:fix/bound-speech-json-responses
Jun 25, 2026
Merged

fix(speech): bound TTS/STT voice-list and transcription JSON responses#96496
sallyom merged 1 commit into
openclaw:mainfrom
hugenshen:fix/bound-speech-json-responses

Conversation

@hugenshen

Copy link
Copy Markdown
Contributor

What Problem This Solves

Six TTS/STT providers parse their success responses — voice-list catalog calls
and audio transcription results — with an unbounded await response.json().
response.json() reads the entire body into memory before parsing, with no
byte ceiling and regardless of (or in the absence of) a Content-Length header.
These providers are external, untrusted sources: a misbehaving, compromised, or
hostile endpoint (including a self-hosted baseUrl override) can stream an
arbitrarily large or never-ending JSON body and force the gateway/plugin to
buffer it all, causing memory pressure or a hang on the TTS/STT code path.

The affected call sites are:

  • extensions/azure-speech/tts.ts — Azure Speech voice-list catalog
  • extensions/microsoft/speech-provider.ts — Microsoft Azure speech voice-list catalog
  • extensions/elevenlabs/speech-provider.ts — ElevenLabs voice-list catalog
  • extensions/minimax/tts.ts — MiniMax TTS synthesis response
  • extensions/xai/stt.ts — xAI audio transcription result
  • extensions/openrouter/media-understanding-provider.ts — OpenRouter STT transcription result

This change closes all six unbounded surfaces, making this the TTS/STT companion
to the #95103 / #95108 / #95218 response-limit campaign.

Changes

  • Each of the six TTS/STT providers now reads its success JSON body through the
    shared bounded reader readProviderJsonResponse (from
    openclaw/plugin-sdk/provider-http), which enforces a 16 MiB cap and
    calls the shared readResponseWithLimit helper internally.
  • On overflow the helper cancels the underlying stream and throws a bounded
    error (<label>: JSON response exceeds <N> bytes); existing
    malformed-JSON and HTTP error paths are preserved for backward compatibility.
  • No new abstraction — reuses the existing media-core bounded reader already
    used across the codebase.
  • extensions/openrouter/media-understanding-provider.test.ts, which declares
    a closed vi.mock("openclaw/plugin-sdk/provider-http") factory, receives a
    pass-through mock for readProviderJsonResponse so existing transcription
    fixture tests remain valid; extensions/xai/stt.test.ts uses
    importOriginal spread and requires no change.

Real behavior proof

  • Behavior addressed: An untrusted TTS/STT JSON 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 node:http server (createServer) bound
    to 127.0.0.1, streaming a JSON body in 64 KiB chunks with no
    Content-Length header, driving the real exported
    readResponseWithLimit helper (the same helper readProviderJsonResponse
    wraps internally) on Node v22.22.0.
  • Exact steps:
    1. Boot the server; GET /huge streams an unbounded JSON object (~24 MiB) with
      no Content-Length; GET /small returns one valid response.
    2. Drive readResponseWithLimit(response, 1 MiB, { onOverflow: ... }) against
      /huge and assert it rejects + the server socket is closed early.
    3. Negative control: run the OLD path await response.json() against the
      same /huge body and measure how many bytes the server pushed.
    4. Drive readResponseWithLimit(response, 1 MiB) against /small and assert
      the result is parsed intact.
  • Evidence after fix:
    • Bounded case: threw JSON response exceeds 1048576 bytes; server socket
      closed early (stream cancelled); only 1,064,178 bytes reached the
      wire — near the 1 MiB cap, not the 24 MiB body.
    • Negative control: the unbounded response.json() read pulled the full
      25,165,824 bytes (>23x the bounded read), proving the cap is
      load-bearing.
    • Small case: parsed into the expected result intact, no truncation.
  • Observed result: ALL PROOF ASSERTIONS PASSED. Plus the in-repo Vitest
    suite for the two providers with closed mocks passes — 2/2 files,
    12/12 tests
    — including existing transcription fixture tests. oxlint is
    clean on changed files.
  • What was not tested: Live TTS/STT endpoints were not hit (no keys / would
    not reproduce a hostile oversized body). The 64-bit memory-exhaustion end
    state was not driven to OOM — the proof instead measures bytes-on-wire and
    early socket close, which is the load-bearing signal.

Evidence

[case 1] oversized TTS/STT JSON, cap=1024 KiB, NO content-length
  ok: readResponseWithLimit rejected on oversized body — true
  ok: bounded error message present (got: JSON response exceeds 1048576 bytes) — true
  ok: bytes on wire stayed near the cap, not the full body (sent 1064178) — true

[negative control] OLD unbounded `await response.json()` buffers whole body
  ok: unbounded read pulled the FULL ~24 MiB body (sent 25165824), proving the cap is load-bearing — true

[case 3] normal small JSON still parses unchanged
  ok: small body parsed into result — true
  ok: result content intact (valid small body not truncated) — true

ALL PROOF ASSERTIONS PASSED
 Test Files  2 passed (2)
      Tests  12 passed (12)
   Start at  00:23:51
   Duration  1.00s

Label: security

AI-assisted.

… reads

Route success JSON reads through readProviderJsonResponse (16 MiB cap) in
azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and
openrouter/media-understanding to prevent OOM from oversized or hostile
endpoint responses. Mirrors the response-limit campaign already applied to
other provider paths.

AI-assisted.

Co-authored-by: Cursor <[email protected]>
@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 12:38 PM ET / 16:38 UTC.

Summary
The PR changes six speech provider success-response JSON reads to use readProviderJsonResponse instead of direct response.json() calls, plus updates the OpenRouter provider-http test mock.

PR surface: Source +21, Tests +2. Total +23 across 7 files.

Reproducibility: yes. from source: current main directly uses unbounded response.json() in the six targeted TTS/STT success paths, and the PR replaces those reads with the existing bounded helper.

Review metrics: 1 noteworthy metric.

  • Bounded success JSON surfaces: 6 changed. All six modified runtime call sites are provider-controlled success-body reads, so the metric captures the security-hardening scope reviewers should verify.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

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

Rank-up moves:

  • none.

Next step before merge

  • No ClawSweeper repair lane is needed because the patch is narrow and has no actionable review findings; maintainer merge readiness can proceed through normal checks and review.

Security
Cleared: The diff reduces a provider-controlled response-size denial-of-service surface and does not add new dependencies, secret handling, permissions, or code-execution paths.

Review details

Best possible solution:

Land a focused provider-response hardening change that keeps the existing parsing logic but routes provider-controlled success JSON bodies through the shared bounded SDK helper.

Do we have a high-confidence way to reproduce the issue?

Yes from source: current main directly uses unbounded response.json() in the six targeted TTS/STT success paths, and the PR replaces those reads with the existing bounded helper.

Is this the best way to solve the issue?

Yes: reusing the existing public plugin SDK bounded JSON helper is the narrowest maintainable fix; provider-local readers would duplicate the already tested contract.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add P2: This is a normal-priority security hardening bugfix for external provider response handling with a small diff and limited blast radius.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes live-output proof with a real local HTTP stream, a negative control for unbounded response.json(), early socket close, small-response success, targeted Vitest results, and oxlint output.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes live-output proof with a real local HTTP stream, a negative control for unbounded response.json(), early socket close, small-response success, targeted Vitest results, and oxlint output.

Label justifications:

  • P2: This is a normal-priority security hardening bugfix for external provider response handling with a small diff and limited blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes live-output proof with a real local HTTP stream, a negative control for unbounded response.json(), early socket close, small-response success, targeted Vitest results, and oxlint output.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes live-output proof with a real local HTTP stream, a negative control for unbounded response.json(), early socket close, small-response success, targeted Vitest results, and oxlint output.
Evidence reviewed

PR surface:

Source +21, Tests +2. Total +23 across 7 files.

View PR surface stats
Area Files Added Removed Net
Source 6 37 16 +21
Tests 1 2 0 +2
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 7 39 16 +23

What I checked:

Likely related people:

  • vincentkoc: Recent blame and PR history connect Vincent Koc to the current imported speech/provider surfaces and provider ownership refactors that contain these reads. (role: recent area contributor; confidence: high; commits: bfffc77bfc86, 213198123042, de6bf58e792c; files: extensions/azure-speech/tts.ts, extensions/microsoft/speech-provider.ts, extensions/elevenlabs/speech-provider.ts)
  • Alix-007: Authored the merged bounded provider JSON helper and recent adjacent response-limit campaign PRs that this PR reuses. (role: adjacent bounded-response contributor; confidence: high; commits: 2592f8a51a4e, d1c2934d0d17, 6163b1977b4d; files: src/agents/provider-http-errors.ts, extensions/ollama/src/provider-models.ts, extensions/parallel/src/parallel-web-search-provider.runtime.ts)
  • steipete: Shortlog and file history show repeated refactors across speech/provider plugin surfaces before the current imported surface. (role: historical area contributor; confidence: medium; commits: 775b78e186c4, 90a45a49078e, d72115c9df77; files: extensions/elevenlabs/speech-provider.ts, extensions/minimax/tts.ts, extensions/openrouter/media-understanding-provider.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 proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 24, 2026
@sallyom sallyom self-assigned this Jun 25, 2026
@sallyom

sallyom commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Merge-ready from maintainer review.

This is the best narrow fix for the remaining TTS/STT voice-list and transcription success-body reads: it replaces the exact unbounded response.json() call sites with the shared bounded readProviderJsonResponse helper through the public plugin SDK boundary, without adding a new abstraction.

Local proof:

.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
git diff --check origin/main...origin/pr/96496

Focused tests passed locally for the changed provider paths: Azure Speech TTS, ElevenLabs speech provider, and the provider shard covering Microsoft, MiniMax, OpenRouter, and xAI. Autoreview is clean with no accepted/actionable findings, current CI is green, and GitHub reports the PR mergeable. I do not see a breaking-change or compatibility concern for supported TTS/STT behavior; the only changed behavior is the intended fail-fast cap for oversized/runaway successful provider JSON bodies using the existing shared provider JSON limit.

@sallyom
sallyom merged commit 66e2fcc into openclaw:main Jun 25, 2026
162 of 171 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 26, 2026
… reads (openclaw#96496)

Route success JSON reads through readProviderJsonResponse (16 MiB cap) in
azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and
openrouter/media-understanding to prevent OOM from oversized or hostile
endpoint responses. Mirrors the response-limit campaign already applied to
other provider paths.

AI-assisted.

Co-authored-by: Cursor <[email protected]>
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
… reads (openclaw#96496)

Route success JSON reads through readProviderJsonResponse (16 MiB cap) in
azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and
openrouter/media-understanding to prevent OOM from oversized or hostile
endpoint responses. Mirrors the response-limit campaign already applied to
other provider paths.

AI-assisted.

Co-authored-by: Cursor <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extensions: elevenlabs extensions: microsoft extensions: minimax extensions: openrouter extensions: xai P2 Normal backlog priority with limited blast radius. plugin: azure-speech Azure Speech plugin proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S 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