* 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 #95103/#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.
Exa search success responses were read via an unbounded `await
response.json()`, so a misbehaving or hostile endpoint could stream an
arbitrarily large body into memory before parsing. Read the success
body through the shared bounded reader (16 MiB cap, the same limit other
bundled providers use) and cancel the stream on overflow. This mirrors
the error-body bound already in place and the #95103/#95108 response
-limit campaign on the success-JSON side.
AI-assisted.
* fix(parallel): bound successful web-search JSON response reads
The Parallel web_search provider parsed its /v1/search success body with an
unbounded await res.json(). The body comes from an external web-search
upstream, so a hostile or malfunctioning endpoint streaming an unbounded JSON
payload could force the runtime to buffer the whole response before parsing,
creating memory pressure or a hang on the provider path.
Read the success body through the shared readProviderJsonResponse helper with a
16 MiB cap (matching the provider JSON cap from #95218); on overflow the stream
is cancelled and a bounded error is thrown. The error-body path was already
bounded (readResponseTextLimited, 8 KiB). Symmetric follow-up to the
#95103/#95108 response-limit campaign.
* docs(parallel): drop upstream PR ref from response-cap comment
Replace the PR-specific '#95218' annotation with a neutral description of
the shared provider JSON cap so the comment stays accurate independent of
upstream PR numbering.
* fix(duckduckgo): decode & last in decodeHtmlEntities to avoid double-decoding
decodeHtmlEntities decoded & FIRST, so result text that literally contains
an entity (e.g. a page title 'How to escape < in HTML', which DuckDuckGo
returns double-encoded as '&lt;') was re-decoded into markup: '&lt;'
became '<' instead of the literal '<', corrupting the titles, snippets, and
URLs the web-search tool returns to the model.
Reorder so & is decoded last, matching the established convention elsewhere
in the codebase (msteams/inbound.ts, openai-transport-stream.ts,
launchd-plist.ts, doctor-session-snapshots.ts all decode & last).
Behavior-preserving for all singly-encoded input.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(duckduckgo): decode html entities in one pass
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
stripHtmlFromTeamsMessage decoded & FIRST, so literal entity text the
user typed (which Microsoft Graph returns double-encoded, e.g. &lt;) got
re-decoded into markup: "The token is &lt;APIKEY&gt;" became
"The token is <APIKEY>" instead of the correct "The token is <APIKEY>".
Reorder so & is decoded last, mirroring the documented ordering in
decodeHtmlEntities (inbound.ts), whose comment already states it 'must be last
to prevent double-decoding (e.g. &lt; -> < not <)'. Behavior-preserving
for all singly-encoded input; the existing entity test is unchanged.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PROTECTED_GLOSSARY exists to preserve short technical terms that generic
filtering would discard, but every glossary match still flowed through
normalizeConceptToken's per-script minimum-length gate. The 2-char latin
entries "kv" and "s3" were therefore never emitted as concept tags despite
being on the protect-list. Thread a fromGlossary flag so glossary matches
bypass only that length check; all other gates still apply.
Because that bypass lets short entries through, a bare substring match would
also surface them from inside longer words ("kv" in "mkv", "s3" in "css3").
Match ONLY the short entries (those below their script's min length) as
delimiter-bounded whole tokens; longer entries keep substring containment, so
the shipped behavior of "backup" tagging inside "backups" is preserved. CJK
entries (no word delimiters) always use substring matching. Positive
(standalone kv/s3) and negative (mkv/css3 substrings) regression tests cover
both directions, and the short-term-promotion stable-tags assertion gains "s3".
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /api/v1/models/load success path read the response with an unbounded
await response.json(), so a misbehaving or compromised LM Studio server
could stream an arbitrarily large JSON body that is fully buffered into
memory before any size check. Read it through the shared byte-capped
readProviderJsonResponse helper instead (16 MiB provider-JSON cap, cancels
the stream on overflow, wraps malformed JSON), matching the discovery path
and the already-bounded error body.
Migrate the model fetch/load test mocks to real Response objects (the
bounded readers need a real body stream) and add a regression test that
streams an oversized success body and asserts a bounded error plus stream
cancellation.
Label: security
truncateSlackText sliced by UTF-16 code unit ('trimmed.slice(0, max - 1)'), so an
emoji or other astral character straddling the limit was cut in half, leaving a
lone high surrogate before the ellipsis — e.g. truncateSlackText('abc😀def', 5)
returned 'abc\uD83D…' instead of 'abc…'. That invalid half-character is sent in
live Slack payloads (message text and Block Kit section/button/header/option
labels, which truncate at limits as small as 75).
Use the repo's canonical sliceUtf16Safe (already re-exported from
plugin-sdk/text-utility-runtime, the module slack code imports from) so a
straddling pair is dropped whole. Behavior is byte-identical for all-BMP input.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(wiki): discover nested source files in QUERY_DIRS
Two functions in the memory-wiki extension — listWikiMarkdownFiles
(wiki_get runtime lookup) and collectMarkdownFiles (wiki compile
indexing) — used fs.readdir without { recursive: true }. Nested
source files (e.g. sources/audi/car.md) were silently invisible to
both wiki_get and wiki compile.
Add recursive: true and adjust path construction using
entry.parentPath so nested .md files in all QUERY_DIRS are
discovered while preserving the index.md exclusion and backward
compatibility with flat vaults.
* fix(wiki): remove entry.path fallback, only parentPath is typed on Dirent
* fix(wiki): add recursive scan to status.ts and add nested-file regression tests
* fix(wiki): use toSorted instead of sort to pass lint
* style(memory-wiki): format recursive discovery fix
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
* fix(msteams): use valid PascalCase Adaptive Card enums for the welcome heading
The welcome card heading TextBlock used weight "bolder" and size "medium"
(lowercase). Adaptive Card TextWeight/TextSize enums are case-sensitive
PascalCase ("Bolder"/"Medium"); Teams falls back to Default for unrecognized
values, so the "Hi! I'm <bot>." greeting rendered unstyled. Use the correct
casing, matching the sibling polls/presentation cards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(msteams): use valid PascalCase Adaptive Card enums for the welcome heading
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* chore(release): close out 2026.6.10 on main
* chore(release): align native app metadata for 2026.6.10
* chore(release): sync Android 2026.6.10 notes
* docs(changelog): preserve 2026.6.9 history
* docs(changelog): preserve 2026.6.9 history