* fix(whatsapp): wrap JSON.parse with try-catch in auth store and test helpers
Add defensive try-catch around JSON.parse calls in WhatsApp extension
to prevent crashes from corrupted state files.
- restoreCredsFromBackupIfNeeded: wrap creds.json/backup validation
JSON.parse with try-catch; corrupted creds.json now properly falls
through to backup restoration instead of skipping it entirely
- updateLastRouteMock: wrap JSON.parse with try-catch, initialize
empty store on corrupted file
* test(whatsapp): add regression test for malformed creds.json longer than one byte
- Add a focused regression test for the exact case ClawSweeper
flagged: readWebCredsJsonRawSync returns non-null content for
files with stat.size > 1, so malformed JSON like "{x" (2 bytes)
reaches JSON.parse — the inner try-catch now catches the parse
failure and falls through to backup restoration
- Without this patch, JSON.parse("{x") throws to the outer catch
and restoreCredsFromBackupIfNeeded returns false, skipping backup
🦞 diamond lobster: L2 evidence (real function call + real filesystem objects)
Ref. https://github.com/openclaw/openclaw/pull/99070
* fix(whatsapp): restore malformed creds from backup
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
* docs(changelog): defer credential recovery entry to aggregate
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: LeonidasLux <LeonidasLux@users.noreply.github.com>
(cherry picked from commit 42464de58d)
* fix(whatsapp): thread authDir through command authorization and owner bypass for LID JID resolution
WhatsApp group commands (/new, /stop) can be ignored when Baileys reports the sender as a LID JID (@lid) instead of a phone JID (@s.whatsapp.net). The resolveWhatsAppCommandAuthorized() and isOwnerSender() functions called getSelfIdentity/getSenderIdentity without passing authDir, so the LID-to-phone reverse mapping could not happen.
Fix: thread account.authDir through both command authorization and group owner-bypass identity resolution paths so that LID JIDs are properly resolved to phone E.164 identities before owner/allowlist checks.
* fix(whatsapp): replace deprecated top-level fields with admission overrides in LID JID test
(cherry picked from commit 85f7834852)
* fix(update-check): bound npm registry JSON response read to prevent OOM
- Replace unbounded res.json() with readProviderJsonResponse capped at 16 MiB
- Update test mock to use real Response instead of bare object
- Verified against live npm registry: openclaw update status --json returns latestVersion: 2026.6.11
* fix(update-check): add 4 boundary tests for bounded npm registry read
- Tests: oversized response >16 MiB, near-boundary success
- Tests: malformed JSON, non-200 status code
- All pass with the readProviderJsonResponse bounded reader
* chore: add real behavior proof script for bounded JSON reader
Adds a standalone proof script that starts a local HTTP server and
drives the bounded reader against it with real HTTP requests, showing:
- Normal responses (~1 MB) parse correctly
- Oversized responses (>20 MB) are rejected with a descriptive error
- Malformed JSON is caught
- RSS does not spike on oversized responses
Ref. https://github.com/openclaw/openclaw/pull/98508
* chore: remove PR-specific proof script before merge
The proof script was accepted by ClawSweeper (proof: sufficient,
rating: diamond lobster), but it duplicates repository logic and
fails the scripts lint lane. Removing it per review feedback.
The real behavior proof remains in the PR body.
Ref. https://github.com/openclaw/openclaw/pull/98508
(cherry picked from commit 733de866eb)
* fix(infra): guard delivery queue inflate against corrupted entry_json
* fix(infra): use toSorted in delivery queue test
(cherry picked from commit 302524bbd0)
When no chat DM pairing channels are configured, `openclaw pairing list`
(no channel argument) threw `Channel required ... (expected one of: )`
with an empty enum that reads like a bug. Users who hit this are usually
trying to approve a TUI/device or scope-upgrade request, which lives under
`openclaw devices`, not `openclaw pairing` (which only handles chat DM
pairing).
- Guard the channel hint in help text and errors so an empty channel list
no longer renders a bare `()` / `(expected one of: )`.
- When no pairing channels exist, redirect to `openclaw devices list` /
`openclaw devices approve` instead of failing opaquely.
AI-assisted (Claude Code).
(cherry picked from commit 44ec7580e2)
The Google Meet OAuth login binds a fixed localhost:8085 callback listener
and let listener failures propagate, so meet auth login aborted entirely when
port 8085 was already in use. The sibling Gemini CLI OAuth path already
recovers by switching to the manual copy/paste flow on EADDRINUSE/listen
errors; bring Google Meet to parity by catching listener errors and reusing
the existing manual-paste handler instead of failing the login.
(cherry picked from commit 38ab207591)
estimateMessageTokenPressure special-cased toolResult, tool, and
assistant roles and otherwise read record.content. bashExecution stores
its payload in command/output and branchSummary/compactionSummary store
theirs in summary, so record.content was undefined and those turns
scored as bare boundary overhead. The provider request expands them via
convertToLlm into full user text, so bash-heavy sessions skipped the
preflight overflow gate and submitted oversized prompts.
Estimate each affected role from the exact text convertToLlm renders:
bashExecutionToText for bash turns (zero for excludeFromContext records,
which convertToLlm drops), and the summary prefix/suffix plus summary
text for the two summary roles.
(cherry picked from commit 24626e5266)
When a Feishu channel has top-level appId/appSecret using SecretRef
format and sub-accounts with their own inline appSecret, the secrets
resolver marks the top-level SecretRef as inactive because
isBaseFieldActiveForChannelSurface only checks whether any explicit
account inherits the field.
Feishu account listing always creates an implicit default account from
top-level credentials. The fix detects this implicit default account
and keeps the top-level appSecret active accordingly, without changing
the shared channel secret helper semantics for other channels.
Fixes#96929
Signed-off-by: 赵旺0668001248 <0668001248@duomai.com>
(cherry picked from commit 4e7992cc9b)
A drained extraction batch is spliced off the queue before the extractor
runs. On a non-terminal failure the batch was never restored, so those
items were silently lost and never retried. Restore the batch to the
front of the queue (original order) on non-terminal errors and rethrow so
the caller still logs; terminal model/auth errors keep the existing
cooldown drop/stop behavior. Also ensure the single-slot debounce
schedules a drain from the overflow branch, so a queue left full by a
restored batch still gets retried instead of being stuck.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit be0c40bad2)
Wrap the slack outbound sanitizeText hook with sanitizeAssistantVisibleText so assistant internal tool-trace scaffolding is stripped before delivery, matching the sibling channel fixes under #90684 (Telegram #95774, Google Chat #95084, IRC #97214).
(cherry picked from commit cd6d0f9b00)
Wrap the matrix outbound sanitizeText hook with sanitizeAssistantVisibleText so assistant internal tool-trace scaffolding is stripped before delivery, matching the sibling channel fixes under #90684 (Telegram #95774, Google Chat #95084, IRC #97214).
(cherry picked from commit 25490d4c42)
* fix(irc): sanitize internal tool-trace lines from outbound text
* fix(irc): sanitize internal tool-trace lines from outbound text
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
(cherry picked from commit ddedf13190)
postJson reads the Anthropic OAuth token endpoint response body with
an unbounded await response.text(). A compromised or hijacked OAuth
endpoint can stream an arbitrarily large body and force the runtime
to buffer the entire payload before parsing — an OOM/DoS vector.
Replace with readResponseWithLimit at 16 MiB cap + TextDecoder decode
to match the sibling bounded-read pattern (provider-http-errors.ts:308).
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit b1fae752f8)
* fix(doctor): merge colliding model-ref map keys instead of dropping
When two retired model-ref keys in a models map upgrade to the same
current ref, rewriteModelRefMapKeys dropped the second entry silently
and still logged a success line, so doctor --fix lost one entry's
params/streaming/alias/agentRuntime tuning while falsely reporting it
as upgraded.
Mirror the sibling provider-models-array merge path (#90047): deep-merge
the colliding entries preserving disjoint fields, keep the existing
value deterministically on a true field conflict, and push a merge or
collision change-log line instead of dropping.
* fix(doctor): block prototype keys when merging colliding model-ref map entries
mergeModelRefMapEntries copied arbitrary incoming config fields into a
plain object, so a colliding model entry carrying a __proto__ field could
mutate the merged entry prototype during legacy config migration. Skip
isBlockedObjectKey fields before assigning or recursing, matching the
shared config merge guard. Disjoint-field merging is unchanged.
* fix(doctor): preserve canonical model migration values
* fix(doctor): filter blocked keys on both merge sides and report retired source key
* fix(doctor): preserve colliding retired model settings
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
(cherry picked from commit 82871fe21b)
When doctor --fix runs legacy OAuth profile id repair, it picks the
single non-legacy OAuth profile as the migration target. If that target
profile already exists as a separate user-configured account, the repair
unconditionally overwrites it, destroying the account's config
(displayName, email) and collapsing two distinct accounts into one.
Add a guard: if the destination profile id already exists in
cfg.auth.profiles, skip the repair entirely to preserve both accounts.
Also pass cfg to resolveAuthProfileMetadata so config-set displayName
is preserved when the repair does proceed.
Fixes#97522
(cherry picked from commit ee72fa7f5f)
serializeJsonlLine returned JSON.stringify(entry) without guarding the
undefined return case. When the root value is undefined, a function, or a
symbol, JSON.stringify returns undefined, which serializeJsonlEntry's template
literal coerced to the literal string "undefined" and wrote to disk. That is
not valid JSON, so parseJsonlEntries silently skipped the line — a fail-silent
loss of a transcript entry with no error log.
Throw a TypeError instead so the caller fixes the bad value before it reaches
the file. Circular references and BigInt already throw via JSON.stringify and
are unaffected. Null, primitives, and plain objects are unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
(cherry picked from commit feba78fc8f)
Align inner compaction retry and compaction-safeguard provider fallback
with the #90908 signal.aborted pattern so undici disconnect AbortErrors
retry or fall back to LLM instead of producing degraded placeholder
summaries. Caller abort remains terminal.
Co-authored-by: NIO <nocodet@mail.com>
(cherry picked from commit 245c18da5a)
* fix(tlon): bound error response body reads to prevent OOM
Replace bare response.text() on non-ok paths with readResponseTextLimited
capped at 16 KiB so a hostile or misconfigured Urbit ship cannot force the
gateway to buffer an arbitrary-size error body into process memory.
Affected paths:
- pokeUrbitChannel (channel-ops.ts)
- channel.runtime.ts poke path
- sendSubscription (sse-client.ts)
* fix(tlon): fix lint issues in error-body-boundary test
- Remove unused beforeEach import
- Wrap if/else bodies in braces (curly)
- Use block body for Promise executors (no-promise-executor-return)
* fix(types): resolve pre-existing TS test type errors
- Fix TS2493 tuple type errors in server-cron-notifications and
server-cron tests by adding explicit type annotations on mock.calls
- Fix TS2322 in anthropic.test.ts by adding as const to resource
content block type
* chore: trigger CI
(cherry picked from commit 8abd5d4071)
* fix(discord): bound happy-path API response reads to prevent OOM
Replace the unbounded res.text() call in requestDiscord's success path with
readResponseTextLimited capped at 4 MiB. Discord channel message lists and
attachment payloads can accumulate to large sizes; without a cap the process
can exhaust available memory. The error path already used readResponseTextLimited
with DISCORD_API_ERROR_BODY_LIMIT_BYTES — this applies the same guard to the
happy path using a separate DISCORD_API_RESPONSE_BODY_LIMIT_BYTES constant
sized appropriately for valid API payloads.
* test(discord): upgrade to real HTTP server proof for bound requestDiscord
* fix(discord): remove unnecessary type assertion in bound test
(cherry picked from commit fca15641db)
* fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM
Nextcloud Talk talks to self-hosted servers whose HTTP responses are not
trusted to be small. The send and reaction paths buffered three external
bodies without any byte cap:
- success JSON via await response.json()
- send error text via await response.text()
- reaction error text via await response.text()
A hostile or misbehaving Nextcloud endpoint could stream an unbounded body
(no content-length) into memory, pressuring or hanging the plugin/provider
path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error
bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow.
The 'message sent but receipt JSON unreadable -> unknown' fallback is
preserved (an over-limit body now also routes through the existing catch).
This is the symmetric counterpart to the #95103/#95108 response-limit
campaign, reusing the shared @openclaw/media-core helpers (newly re-exported
from plugin-sdk/response-limit-runtime for plugin consumers).
* fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface)
Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime
pushed the public plugin-SDK export count past its surface budget, failing
plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the
Nextcloud Talk send/reaction error bodies through the already-public
readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the
bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is
unchanged for callers; no new plugin-SDK surface is introduced.
Success JSON still reads through readResponseWithLimit (16 MiB cap). The
committed bounded-response-reads Vitest suite continues to prove the caps
hold against 17 MiB streamed bodies with no content-length.
* fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON
The send success receipt parsed JSON by hand via readResponseWithLimit + a
local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)),
duplicating the shared provider-http helper that the sibling room-info.ts and
bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing
shared helpers locally.
Swap the hand-rolled block for the one-stop
readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which
reads through the same bounded reader and throws on overflow/malformed JSON, so
the outer try/catch still keeps the "unknown" receipt and behavior is
equivalent. The error path keeps readResponseTextLimited (text, not JSON).
(cherry picked from commit d577cb2fe9)
* fix(googlechat): replace unbounded response.json() with readProviderJsonResponse
Replace the local readGoogleChatJsonResponse and
readGoogleChatCertsResponse wrappers with the existing SDK helper
readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the
Google Chat API JSON responses are bounded at 16 MiB, matching the
non-streaming cap already used by 15+ other extensions.
What changed:
- extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now
delegates to readProviderJsonResponse. Removed the local try/catch
wrapper.
- extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now
delegates to readProviderJsonResponse. Error message preserved.
Removed the local try/catch wrapper.
This PR applies the same pattern as Alix-007's #96042, #96038 (lmstudio,
provider JSON reads). No SDK promotion needed — readProviderJsonResponse
is already available in openclaw/plugin-sdk/provider-http.
* fix(googlechat): add inline bounded-read regression tests
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(googlechat): remove unused variable flagged by oxlint
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(googlechat): bound api error body reads
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
(cherry picked from commit 4f3d81b918)
When a channel stop times out (e.g. during a Telegram API outage),
the channel enters recoveryStopTimedOut state. The health monitor's
subsequent start call would set restartPending and return without
actually starting the channel.
If the stuck stop never completes, the channel stays in limbo forever
with the health monitor retrying every cycle but never recovering.
Fix: when the health monitor retries recovery (recoveryStartRequested
already set), clean up the stuck task state and allow the channel to
start normally.
Closes#94008
(cherry picked from commit 8b4be2fdd4)
When a required-mode batch send fails mid-batch after an earlier payload
already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup
called failDelivery. failDelivery only bumps retryCount/lastError; it does
not advance recoveryState, so the entry stayed in send_attempt_started (set
earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart).
On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started
and calls reconcileUnknownQueuedDelivery. When adapter reconciliation
misreports not_sent (the message was actually sent, per the outbound send
ok / messageId evidence), the entry is replayed and the user receives a
duplicate.
Fix: when the error carries send evidence (OutboundDeliveryError with
sentBeforeError === true and platformSendStarted === true), call
markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the
entry to unknown_after_send, which drain already routes through
reconcileUnknownQueuedDelivery, preserving the entry for adapter
reconciliation rather than leaving it in send_attempt_started for replay.
When there is no send evidence (sentBeforeError === false), failDelivery
remains correct: nothing reached the channel, so retrying is safe.
This is a third duplicate path distinct from #89812 (mirror best-effort)
and #92274 (subagent-announce-delivery retry); it is the outbound/deliver
wrapper catch, which neither prior fix covers.
Tests:
- regression: two payloads, first succeeds, second throws; asserts
markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not.
- guard: no send evidence; failDelivery still called.
(cherry picked from commit 71422a9a5a)
Summary:
- The PR preserves `failure_alert_disabled === 0` as the enabled-with-defaults failure-alert state and adds focused codec roundtrip tests.
- PR surface: Source +2, Tests +54. Total +56 across 2 files.
- Reproducibility: yes. At source level, current main encodes `failureAlert: {}` with `failure_alert_disabled = 0`, then decodes it as `undefined` when all explicit alert option columns are null.
Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head bd9b2a1798.
- Required merge gates passed before the squash merge.
Prepared head SHA: bd9b2a1798
Review: https://github.com/openclaw/openclaw/pull/96615#issuecomment-4794949533
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Approved-by: takhoffman
(cherry picked from commit a21144d8a6)