Skip to content

fix(ollama): bound model-discovery JSON response reads#96027

Merged
sallyom merged 2 commits into
openclaw:mainfrom
Alix-007:fix/bound-ollama-model-discovery
Jun 24, 2026
Merged

fix(ollama): bound model-discovery JSON response reads#96027
sallyom merged 2 commits into
openclaw:mainfrom
Alix-007:fix/bound-ollama-model-discovery

Conversation

@Alix-007

Copy link
Copy Markdown
Contributor

What Problem This Solves

extensions/ollama/src/provider-models.ts discovers Ollama models by calling /api/tags
(fetchOllamaModels) and /api/show (queryOllamaModelShowInfo), and parsed both responses with
an unbounded await response.json(). The Ollama base URL is user-supplied and can point at a
remote/cloud endpoint (https://ollama.com, a self-hosted box, or a host reachable via SSRF), so
these discovery responses are untrusted input. A hostile or buggy server can return a
Content-Length-less JSON body that streams forever (or many MiB), and response.json() buffers the
entire payload before parsing — driving the provider model-discovery path into memory pressure / OOM
or hanging the discovery call. This is not a cosmetic cleanup: the read sits on the provider
onboarding/refresh path that runs against an attacker-influenceable endpoint.

Changes

  • Add a OLLAMA_DISCOVERY_JSON_MAX_BYTES cap (16 MiB, aligned with the merged provider-JSON
    bound-stream PRs fix(agents): bound provider JSON response reads #95218 / fix(agents): bound OpenRouter model-scan catalog success body #95418 / fix(agents): bound OpenRouter model catalog response reads #95420) and a small readOllamaDiscoveryJson<T>(response, label)
    helper that reads the success body via readResponseWithLimit from @openclaw/media-core
    (re-exported through openclaw/plugin-sdk/response-limit-runtime, the same path style this module
    already uses for ssrf-runtime), cancelling the stream on overflow, then JSON.parses the decoded
    bytes. No new abstraction — it reuses the shared bounded reader.
  • Route both /api/tags (fetchOllamaModels) and /api/show (queryOllamaModelShowInfo) through
    the bounded reader instead of await response.json().
  • Fail-soft semantics preserved: the overflow throw is caught by the existing discovery error
    handling, so a capped endpoint degrades gracefully — /api/tags returns
    { reachable: false, models: [] } and /api/show returns {}. No OOM, no hang.

Real behavior proof

  • Behavior addressed: Unbounded await response.json() on the two Ollama discovery reads lets an
    untrusted endpoint stream an unbounded JSON body into memory.
  • Real environment tested: A real local node:http server (createServer) bound to
    127.0.0.1, returning a Content-Length-less body that streams 64 KiB chunks until the socket is
    torn down (would total ~64 MiB, 4× the 16 MiB cap). The real exported fetchOllamaModels and
    queryOllamaModelShowInfo were driven against it (through the real fetchWithSsrFGuard dispatcher,
    with the loopback allowed by the existing buildOllamaBaseUrlSsrFPolicy).
  • Exact steps: node --import tsx proof.mts — start server → drive the real functions →
    read back fail-soft results + the server's observed bytes-sent / socket-abort counters → run a
    negative control (raw unbounded read of the same endpoint) → flip the server to small valid bodies
    and re-drive the real functions.
  • Evidence after fix: readResponseWithLimit threw ... exceeds 16777216 bytes; the server
    observed the socket aborted after ~17–19 MiB (≪ the ~64 MiB it would have sent), confirming the
    stream was cancelled, not drained. fetchOllamaModels returned { reachable: false, models: [] }
    and queryOllamaModelShowInfo returned {} (fail-soft intact).
  • Observed result (all 8 checks PASS):
    • readResponseWithLimit throws bounded error on unbounded body → threw, msg exceeds 16777216 bytes
    • stream cancelled: server saw socket abort → aborted=true, bytesSent=19333120 (< 33554432)
    • fetchOllamaModels fail-soft on overflow{ reachable:false, models:[] }
    • queryOllamaModelShowInfo fail-soft on overflow{}
    • show stream cancelled → aborted=true, bytesSent=17825792
    • negative control: unbounded read buffers PAST the 16 MiB cap → buffered 16799524 bytes (> cap)
      — proves the cap is load-bearing (without it the body keeps accumulating past 16 MiB).
    • happy path: small /api/tags still parsedreachable:true, 2 models
    • happy path: small /api/show still parsedcontextWindow=65536
  • What was not tested: Did not exercise a live external Ollama Cloud endpoint (ollama.com); the
    untrusted-body behavior is fully reproduced with a local streaming server, which is the same
    transport path. The proof script is not committed.

Evidence

[proof] local server on http://127.0.0.1:46829, cap=16777216 bytes, would-stream≈67108864 bytes

PASS  readResponseWithLimit throws bounded error on unbounded body :: threw=true msg="bounded: exceeds 16777216 bytes"
PASS  stream cancelled: server saw socket abort before sending the full ~64MiB :: aborted=true bytesSent=19333120 (<33554432)
PASS  fetchOllamaModels fail-soft on overflow (reachable:false, models:[]) :: {"reachable":false,"models":[]}
PASS  queryOllamaModelShowInfo fail-soft on overflow ({}) :: {}
PASS  show stream cancelled: server saw socket abort, bytes ≪ full body :: aborted=true bytesSent=17825792
PASS  negative control: unbounded read buffers PAST the 16MiB cap (cap is load-bearing) :: buffered=16799524 bytes (>16777216)
PASS  happy path: small /api/tags still parsed (2 models) :: {"reachable":true,"models":[{"name":"llama3:8b"},{"name":"qwen3:4b"}]}
PASS  happy path: small /api/show still parsed (contextWindow=65536) :: contextWindow=65536

[proof] ALL PASS

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

  • node scripts/run-vitest.mjs extensions/ollama/src/provider-models.test.ts --run16/16 passed
  • oxlint extensions/ollama/src/provider-models.ts → exit 0, clean
  • tsgo -p tsconfig.extensions.json → no errors in extensions/ollama (only pre-existing,
    unrelated extensions/qa-lab crabline module-resolution failures present on untouched main)

This is the symmetric counterpart to the #95103 / #95108 response-limit campaign, applying the same
@openclaw/media-core bounded reader to the two Ollama provider-discovery JSON reads.

Label: security

AI-assisted.

@Alix-007

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

@clawsweeper

clawsweeper Bot commented Jun 23, 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.

Alix-007 added 2 commits June 23, 2026 21:10
The /api/tags and /api/show discovery reads in extensions/ollama/src/provider-models.ts
parsed their HTTP responses with an unbounded await response.json(). Ollama base URLs
are user-supplied and can point at remote/cloud endpoints, so a hostile or buggy server
(or one reachable via SSRF) could stream an unbounded or never-ending JSON body and drive
model discovery into OOM.

Route both reads through the shared @openclaw/media-core byte-bounded reader
(readResponseWithLimit, re-exported via openclaw/plugin-sdk/response-limit-runtime) under
a single 16 MiB cap before JSON.parse, cancelling the stream on overflow. Overflow throws a
bounded error that the existing fail-soft handlers swallow, so a capped endpoint degrades
gracefully: /api/tags returns { reachable: false, models: [] } and /api/show returns {}.

Symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign.

AI-assisted.
Replace the local readOllamaDiscoveryJson helper with the shared
readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which
already enforces the 16 MiB cap, cancels the stream on overflow, and wraps
malformed JSON with the caller label. The /api/tags and /api/show discovery
reads now go through it directly while keeping the existing fail-soft
handlers ({ reachable: false, models: [] } and {}).

Add a focused regression test: when a discovery stream exceeds the JSON byte
cap, fetchOllamaModels returns { reachable: false, models: [] },
queryOllamaModelShowInfo returns {}, and the bounded reader cancels the body
mid-flight so less than the full advertised stream is read.
@Alix-007
Alix-007 force-pushed the fix/bound-ollama-model-discovery branch from fc1804c to 8876908 Compare June 23, 2026 13:10
@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 23, 2026, 4:51 PM ET / 20:51 UTC.

Summary
The PR replaces Ollama /api/tags and /api/show response.json() reads with readProviderJsonResponse and adds a regression test for oversized discovery streams failing soft and canceling early.

PR surface: Source +4, Tests +55. Total +59 across 2 files.

Reproducibility: yes. Current main source shows direct response.json() on both Ollama discovery success bodies, and the PR body gives a concrete local streaming-server proof path against the real exported functions.

Review metrics: 1 noteworthy metric.

  • Discovery reads bounded: 2 changed. Both model listing and model show metadata success-body reads now use the shared bounded JSON path, which is the full model-discovery surface claimed by the PR.

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 Ollama endpoint with a /api/tags or /api/show JSON body over the shared 16 MiB provider cap would now fail soft during discovery instead of being parsed, so maintainers need to accept that compatibility tradeoff.

Maintainer options:

  1. Accept Shared JSON Cap (recommended)
    Merge with the existing 16 MiB provider JSON limit if maintainers agree that oversized Ollama discovery responses should fail soft rather than risk unbounded memory growth.
  2. Use An Ollama-Specific Cap
    If maintainers expect legitimate Ollama catalogs or show payloads above 16 MiB, adjust the patch to pass an explicit cap and document the rationale in the test or code.

Next step before merge

  • No automated repair is needed; the remaining maintainer action is to accept the shared 16 MiB compatibility tradeoff and merge when normal gates are satisfied.

Security
Cleared: The diff hardens an existing untrusted provider response path and does not add dependencies, workflows, secrets handling, or new code execution surfaces.

Review details

Best possible solution:

Land the bounded-reader change if maintainers accept the shared provider JSON cap for Ollama discovery; otherwise add a narrowly justified Ollama-specific cap override with matching overflow and happy-path tests.

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

Yes. Current main source shows direct response.json() on both Ollama discovery success bodies, and the PR body gives a concrete local streaming-server proof path against the real exported functions.

Is this the best way to solve the issue?

Yes. Reusing readProviderJsonResponse through the public plugin SDK subpath is the narrowest maintainable fix; a local Ollama-only reader would duplicate the shared provider HTTP contract.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add P2: This is a normal-priority security hardening for an optional provider discovery path with limited blast radius.
  • add merge-risk: 🚨 compatibility: The new shared 16 MiB cap can make exceptionally large existing Ollama discovery responses fail soft during setup or catalog refresh.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal output from a real local HTTP streaming server driving the exported Ollama functions and showing fail-soft results plus early stream cancellation.
  • add rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes terminal output from a real local HTTP streaming server driving the exported Ollama functions and showing fail-soft results plus early stream cancellation.

Label justifications:

  • P2: This is a normal-priority security hardening for an optional provider discovery path with limited blast radius.
  • merge-risk: 🚨 compatibility: The new shared 16 MiB cap can make exceptionally large existing Ollama discovery responses fail soft during setup or catalog refresh.
  • 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 output from a real local HTTP streaming server driving the exported Ollama functions and showing fail-soft results plus early stream cancellation.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal output from a real local HTTP streaming server driving the exported Ollama functions and showing fail-soft results plus early stream cancellation.
Evidence reviewed

PR surface:

Source +4, Tests +55. Total +59 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 7 3 +4
Tests 1 55 0 +55
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 62 3 +59

What I checked:

  • Repository policy read: The root and scoped extension AGENTS.md files were read; the review applied the plugin boundary and compatibility guidance for extension code using openclaw/plugin-sdk/* and provider runtime changes. (AGENTS.md:1, cc981f8a7303)
  • Current main still has the unbounded reads: On current main, queryOllamaModelShowInfo and fetchOllamaModels both parse successful provider discovery bodies with direct await response.json(). (extensions/ollama/src/provider-models.ts:149, cc981f8a7303)
  • PR changes the claimed discovery reads: The PR imports readProviderJsonResponse from the public plugin SDK provider HTTP subpath and routes both /api/show and /api/tags success bodies through it. (extensions/ollama/src/provider-models.ts:147, 8876908d1c11)
  • Shared helper enforces the JSON cap: readProviderJsonResponse uses readResponseWithLimit with the shared 16 MiB default and wraps malformed JSON with the caller label. (src/agents/provider-http-errors.ts:297, cc981f8a7303)
  • Bounded reader cancels on overflow: readResponseWithLimit stops when a chunk would exceed the byte cap, cancels the stream, and throws the overflow error. (packages/media-core/src/read-response-with-limit.ts:96, cc981f8a7303)
  • Runtime entry points use this discovery path: Ollama setup and runtime catalog paths call fetchOllamaModels, enrichOllamaModelsWithContext, and queryOllamaModelShowInfo, so the patch covers onboarding, non-interactive setup, and dynamic catalog discovery. (extensions/ollama/src/setup.ts:552, cc981f8a7303)

Likely related people:

  • steipete: GitHub commit history for extensions/ollama/src/provider-models.ts shows repeated recent Ollama discovery and model metadata changes, including GLM-5.2 cloud discovery and live catalog metadata preservation. (role: recent Ollama provider contributor; confidence: high; commits: 11484f8a1483, af79cd6a9d35, 69daef8246f1; files: extensions/ollama/src/provider-models.ts)
  • Alix-007: Prior merged work introduced readProviderJsonResponse, the exact shared helper reused by this PR, and related bounded provider JSON hardening in the same campaign. (role: adjacent helper author; confidence: high; commits: 2592f8a51a4e, 8876908d1c11; files: src/agents/provider-http-errors.ts, extensions/ollama/src/provider-models.ts)
  • vincentkoc: Git history shows recent work on provider HTTP response body limits and sibling stream-bound hardening, including binary response reads and gateway pricing catalog streams. (role: shared response-limit area contributor; confidence: medium; commits: 79691d485847, e802fb8a9fbb, b073d7cc11dc; files: src/agents/provider-http-errors.ts, src/gateway/model-pricing-cache.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: 🦞 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. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 23, 2026

sallyom commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Merge-ready from my review.

This follows the same response-body cap pattern as #96042 and the sibling Exa/Parallel/Ollama PRs: replace unbounded success JSON reads from untrusted provider discovery endpoints with the existing bounded reader, preserve normal small-response behavior, and add focused overflow/cancel coverage.

I ran local autoreview in the PR worktree against origin/main: clean, no accepted/actionable findings. I also reviewed upgrade/compatibility risk and found no blocking concerns: no config, persisted state, migration, plugin SDK, or Ollama API compatibility blocker. The bounded-reader errors stay inside the existing fail-soft discovery behavior.

Best-fix verdict: this is the right narrow fix because it reuses the existing bounded provider JSON helper instead of adding a broader provider refactor.

@sallyom
sallyom merged commit d1c2934 into openclaw:main Jun 24, 2026
121 of 124 checks passed
Alix-007 added a commit to Alix-007/openclaw that referenced this pull request Jun 25, 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.
Alix-007 added a commit to Alix-007/openclaw that referenced this pull request Jun 25, 2026
The batch status read (fetchVoyageBatchStatus) parsed its response with an
unbounded await res.json(), and the batch error-file read (readVoyageBatchError)
buffered the whole body via await res.text(). On top of that, the non-OK
(4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did
await res.text() before throwing, and the non-OK output-file branch in
runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and
reachable via SSRF, so a misbehaving or hostile endpoint could stream an
unbounded body into memory on any of these paths before parsing.

Route the status JSON through the shared readProviderJsonResponse, the error
file through readResponseWithLimit, and now the non-OK diagnostic body through
readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the
stream on overflow before decode/parse. assertVoyageResponseOk preserves its
original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies
and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK
output-file branch now reuses it instead of a duplicate unbounded read. The
existing error-file fail-soft handling (formatUnavailableBatchError) is
preserved, so a capped endpoint degrades gracefully. The submit path already
bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched.

Symmetric counterpart to the openclaw#96027/openclaw#96038 response-limit campaign.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 25, 2026
* fix(ollama): bound model-discovery JSON response reads

The /api/tags and /api/show discovery reads in extensions/ollama/src/provider-models.ts
parsed their HTTP responses with an unbounded await response.json(). Ollama base URLs
are user-supplied and can point at remote/cloud endpoints, so a hostile or buggy server
(or one reachable via SSRF) could stream an unbounded or never-ending JSON body and drive
model discovery into OOM.

Route both reads through the shared @openclaw/media-core byte-bounded reader
(readResponseWithLimit, re-exported via openclaw/plugin-sdk/response-limit-runtime) under
a single 16 MiB cap before JSON.parse, cancelling the stream on overflow. Overflow throws a
bounded error that the existing fail-soft handlers swallow, so a capped endpoint degrades
gracefully: /api/tags returns { reachable: false, models: [] } and /api/show returns {}.

Symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign.

AI-assisted.

* fix(ollama): reuse shared bounded JSON reader for model discovery

Replace the local readOllamaDiscoveryJson helper with the shared
readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which
already enforces the 16 MiB cap, cancels the stream on overflow, and wraps
malformed JSON with the caller label. The /api/tags and /api/show discovery
reads now go through it directly while keeping the existing fail-soft
handlers ({ reachable: false, models: [] } and {}).

Add a focused regression test: when a discovery stream exceeds the JSON byte
cap, fetchOllamaModels returns { reachable: false, models: [] },
queryOllamaModelShowInfo returns {}, and the bounded reader cancels the body
mid-flight so less than the full advertised stream is read.
sallyom pushed a commit that referenced this pull request Jun 25, 2026
#96608)

The batch status read (fetchVoyageBatchStatus) parsed its response with an
unbounded await res.json(), and the batch error-file read (readVoyageBatchError)
buffered the whole body via await res.text(). On top of that, the non-OK
(4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did
await res.text() before throwing, and the non-OK output-file branch in
runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and
reachable via SSRF, so a misbehaving or hostile endpoint could stream an
unbounded body into memory on any of these paths before parsing.

Route the status JSON through the shared readProviderJsonResponse, the error
file through readResponseWithLimit, and now the non-OK diagnostic body through
readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the
stream on overflow before decode/parse. assertVoyageResponseOk preserves its
original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies
and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK
output-file branch now reuses it instead of a duplicate unbounded read. The
existing error-file fail-soft handling (formatUnavailableBatchError) is
preserved, so a capped endpoint degrades gracefully. The submit path already
bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched.

Symmetric counterpart to the #96027/#96038 response-limit campaign.
sallyom pushed a commit that referenced this pull request Jun 25, 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 #96027 /
#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.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 26, 2026
openclaw#96608)

The batch status read (fetchVoyageBatchStatus) parsed its response with an
unbounded await res.json(), and the batch error-file read (readVoyageBatchError)
buffered the whole body via await res.text(). On top of that, the non-OK
(4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did
await res.text() before throwing, and the non-OK output-file branch in
runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and
reachable via SSRF, so a misbehaving or hostile endpoint could stream an
unbounded body into memory on any of these paths before parsing.

Route the status JSON through the shared readProviderJsonResponse, the error
file through readResponseWithLimit, and now the non-OK diagnostic body through
readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the
stream on overflow before decode/parse. assertVoyageResponseOk preserves its
original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies
and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK
output-file branch now reuses it instead of a duplicate unbounded read. The
existing error-file fail-soft handling (formatUnavailableBatchError) is
preserved, so a capped endpoint degrades gracefully. The submit path already
bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched.

Symmetric counterpart to the openclaw#96027/openclaw#96038 response-limit campaign.
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
* fix(ollama): bound model-discovery JSON response reads

The /api/tags and /api/show discovery reads in extensions/ollama/src/provider-models.ts
parsed their HTTP responses with an unbounded await response.json(). Ollama base URLs
are user-supplied and can point at remote/cloud endpoints, so a hostile or buggy server
(or one reachable via SSRF) could stream an unbounded or never-ending JSON body and drive
model discovery into OOM.

Route both reads through the shared @openclaw/media-core byte-bounded reader
(readResponseWithLimit, re-exported via openclaw/plugin-sdk/response-limit-runtime) under
a single 16 MiB cap before JSON.parse, cancelling the stream on overflow. Overflow throws a
bounded error that the existing fail-soft handlers swallow, so a capped endpoint degrades
gracefully: /api/tags returns { reachable: false, models: [] } and /api/show returns {}.

Symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign.

AI-assisted.

* fix(ollama): reuse shared bounded JSON reader for model discovery

Replace the local readOllamaDiscoveryJson helper with the shared
readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which
already enforces the 16 MiB cap, cancels the stream on overflow, and wraps
malformed JSON with the caller label. The /api/tags and /api/show discovery
reads now go through it directly while keeping the existing fail-soft
handlers ({ reachable: false, models: [] } and {}).

Add a focused regression test: when a discovery stream exceeds the JSON byte
cap, fetchOllamaModels returns { reachable: false, models: [] },
queryOllamaModelShowInfo returns {}, and the bounded reader cancels the body
mid-flight so less than the full advertised stream is read.
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
openclaw#96608)

The batch status read (fetchVoyageBatchStatus) parsed its response with an
unbounded await res.json(), and the batch error-file read (readVoyageBatchError)
buffered the whole body via await res.text(). On top of that, the non-OK
(4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did
await res.text() before throwing, and the non-OK output-file branch in
runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and
reachable via SSRF, so a misbehaving or hostile endpoint could stream an
unbounded body into memory on any of these paths before parsing.

Route the status JSON through the shared readProviderJsonResponse, the error
file through readResponseWithLimit, and now the non-OK diagnostic body through
readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the
stream on overflow before decode/parse. assertVoyageResponseOk preserves its
original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies
and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK
output-file branch now reuses it instead of a duplicate unbounded read. The
existing error-file fail-soft handling (formatUnavailableBatchError) is
preserved, so a capped endpoint degrades gracefully. The submit path already
bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched.

Symmetric counterpart to the openclaw#96027/openclaw#96038 response-limit campaign.
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: ollama 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: 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