* fix(mistral): correct model id typo in usesReasoningEffort helper
* chore: re-trigger CI
* test(mistral): cover medium reasoning effort
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The markdown link regex character class [^)\s>]+ stopped at the first
closing paren, truncating URLs like wikipedia.org/wiki/URL_(disambiguation)
at the opening (. Replace with a balanced-parens pattern that supports
one level of parenthetical groups inside the URL path.
Add trimUnbalancedTrailingParens for bare URLs in prose where a trailing
) belongs to surrounding punctuation rather than the URL itself.
* fix(tools-manager): replace spawnSync extraction with safe extractArchive API
Replace the synchronous spawnSync-based archive extraction (unzip/tar)
with the safe extractArchive from @openclaw/fs-safe, which enforces:
- maxArchiveBytes: 100 MB (prevents oversized compressed input)
- maxExtractedBytes: 500 MB (prevents decompression bomb OOM)
- maxEntries: 1000 (prevents zip bomb from exhausting inodes)
- timeoutMs: 60,000 (prevents hung extraction)
- Path traversal and symlink/hardlink protection (built-in)
Removes 5 functions (formatSpawnFailure, runExtractionCommand,
extractTarGzArchive, getWindowsTarCommand, extractZipArchive) and their
associated imports, replacing them with a single async wrapper around
the existing @openclaw/fs-safe infrastructure (re-exported from
src/infra/archive.ts).
Net: -83 lines, + security boundaries across all platforms.
* fix(tools-manager): add download-stream byte cap and regression tests
Add a maxBytes parameter to downloadFile that checks Content-Length
before reading the body and enforces a streaming byte cap during
transfer, so oversized archives are rejected before hitting disk.
Extract MAX_ARCHIVE_BYTES as a module-level constant shared between
downloadFile and extractArchiveSafe, ensuring both gates use the same
100 MB limit.
Add two regression tests:
- rejects downloads with Content-Length exceeding the archive byte cap
- accepts downloads with Content-Length under the archive byte cap
Ref. https://github.com/openclaw/openclaw/pull/98988
* fix(tools-manager): replace PassThrough with Transform stream limiter
- Replace PassThrough data listener with Transform that rejects overflow
chunks *before* they are forwarded to the file pipeline, preventing the
offending chunk from landing on disk.
- Add completion guard and cleanup of partial downloads on failure so
downloadTool does not leave a partial archive on disk.
- Add real behavior proof: test output and live fd/rg download and
extraction through the safe extractArchive code path.
🦞 diamond lobster: L2 evidence (real function calls + real objects)
Ref. https://github.com/openclaw/openclaw/pull/98988
* fix(tools-manager): replace PassThrough with Transform stream limiter
Uses a Transform to reject overflow chunks *before* they are forwarded
to the file pipeline (a PassThrough data listener acts *after* emission
and cannot prevent the offending chunk from landing on disk).
Wraps the pipeline in a completion-guarded block so that partial
downloads are removed when the transfer fails mid-way.
Ref. https://github.com/openclaw/openclaw/pull/98988
* fix(agents): harden helper archive downloads
* fix(agents): preserve archive extraction cause
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(infra): include update timeout in managed-service handoff parent exit wait
The handoff parent exit grace was hard-coded to 60s regardless of
the overall update timeout. When the gateway is draining active
tasks, 60s is often not enough, causing
managed-service-handoff-parent-timeout.
Use the existing timeoutMs parameter as a floor for the parent
exit wait so callers can extend it for long-running drains.
Refs #99666
* fix(infra): add shutdown reserve to managed-service handoff parent exit wait
* fix(infra): align update handoff with restart drain
Co-authored-by: 徐闻涵0668001344 <xu.wenhan1@xydigit.com>
* fix(infra): arm restart with managed handoff
* fix(infra): use live restart drain for auto-update
* fix(infra): force process exit for managed updates
* fix(infra): normalize managed handoff restart delay
* test(infra): cover handoff drain argument
* fix(gateway): upgrade accepted restarts for managed updates
* fix(gateway): bound managed update restart upgrades
* test(gateway): stabilize managed update handoff proof
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): prefer current attempt terminal state
Co-authored-by: weiqinl <liu.weiqin@xydigit.com>
* test(agents): model current attempt terminal state
---------
Co-authored-by: weiqinl <liu.weiqin@xydigit.com>
* fix(agents): keep background exec exit notifications UTF-16 safe at the snippet cut
Background exec exit notifications truncate the command's tail output with a
raw String.slice, which can split a UTF-16 surrogate pair when an emoji lands
at the cut. Use the existing truncateUtf16Safe helper so the message delivered
to the channel never ends with a dangling surrogate half.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agents): protect exec notify tail boundaries
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agents): protect node exec follow-up tails
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(agents): type node exec follow-up assertions
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agents): keep bounded exec output Unicode-safe
* docs(changelog): note exec output Unicode safety
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout
runQmdSearchViaMcporter parsed mcporter subprocess stdout with JSON.parse
outside the runMcporter try/catch (qmd-manager.ts:2722). A non-JSON stdout
(daemon warning bleeding onto stdout, output truncated by maxOutputChars, CLI
killed early, or flag mismatch) threw a raw SyntaxError that propagated
uncaught out of runQmdSearchViaMcporter, surfacing in agent logs as a
context-free SyntaxError with no hint of the actual mcporter failure.
Wrap JSON.parse in try/catch and throw a typed Error carrying a stdout snippet
(matching the guard pattern already used in parseListedCollections in this
file, and the recent matrix #97973 / sms #97999 / signal #98073 /
telegram-ingress #98372 JSON.parse guard series).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(memory-core): preserve cause in qmd mcporter JSON.parse guard
Add { cause: err } to the re-thrown Error to satisfy the preserve-caught-error
lint rule; the original SyntaxError is now chained, improving diagnosability.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(memory-core): redact raw stdout from qmd mcporter error (security-boundary)
ClawSweeper flagged that the previous error message exposed raw mcporter
stdout (first 200 chars) before session visibility filtering, which could
leak sensitive content. Drop the stdout preview from the thrown message;
keep the original SyntaxError as `cause` for diagnostics so the parse-failure
reason is still reachable without surfacing unfiltered subprocess output.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(memory-core): keep qmd mcporter error message generic (no raw stdout leak)
The SyntaxError thrown by JSON.parse embeds a snippet of the raw input in its
message (e.g. Unexpected token '<', then the raw bytes). Including that
SyntaxError message in the thrown Error would surface unfiltered mcporter
stdout before session visibility filtering. Drop the parse-error message from
the thrown Error; keep the original SyntaxError as cause for developer
diagnostics only.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(memory-core): keep qmd mcporter cause generic (no raw stdout leak via formatErrorMessage)
formatErrorMessage walks the .cause chain into the user-visible path.
Keeping the JSON.parse SyntaxError on .cause leaked its embedded raw
stdout snippet through formatErrorMessage even with a generic message.
Give the cause a generic message too; the raw snippet no longer reaches
the user.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(build): fall back to tsx for build TypeScript scripts
* fix(build): use tsx across TypeScript build scripts
---------
Co-authored-by: Steffen Moeller <moeller@debian.org>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
Adds the middle tier between capture-off and autonomous capture: when
autonomy is disabled, detected durable-instruction signals record a
one-shot pendingSkillSuggestion on the session entry (signal-hash
fingerprint prevents transcript-history replay), and the next
non-heartbeat turn atomically consumes it and injects one bounded
user-role line offering to save the skill. The agent offers, the user
decides; skill_workshop approval flow unchanged; no new config.
Gateways that crash-loop under systemd/launchd previously flapped forever
with no persisted state and no supervisor signal. The gateway now records
every boot outcome in the shared state DB (gateway_boot_lifecycle); three
unclean boots within five minutes trip a breaker that boots the gateway in
safe mode: full control plane available, channel/provider auto-start
suppressed at the channel-manager seam (startup, config hot-reload,
secrets.reload) with manual channels.start override, one stability bundle
per trip. The breaker re-evaluates each boot and logs recovery when the
window drains. Slow shutdowns record forced_stop and never count as
crashes; /readyz stays ready and reports suppressed channels; the health
monitor treats suppressed accounts as expected-stopped. Fatal invalid-config
errors now exit 78 (EX_CONFIG) on both the startup and unhandled-rejection
paths, engaging the systemd unit's pre-existing RestartPreventExitStatus=78
so supervisors stop relaunching until the config is fixed.
The batch output file download path creates a readline interface over a
Readable.fromWeb() response body stream. If JSON.parse throws on a malformed
JSONL line, the for-await loop exits via exception but the readline interface
and underlying Readable stream were never explicitly closed or destroyed,
leaving the HTTP response body stream dangling.
Extract the stream reading into , a testable helper
that wraps the iteration in a try-finally so both reader.close() and
inputStream.destroy() are always called, matching the pattern established in
#98493 for the same class of leak.
* [AI] fix(agents): detect legacy openai-codex provider in model-not-found hint
In buildMissingProviderModelRegistrationHint, add an early-return check
for the legacy openai-codex alias (via normalizeProviderId). Instead
of suggesting a models.providers[] config entry that the config
validator rejects without baseUrl, the hint now points operators to
run openclaw doctor --fix for migration or check provider auth.
Fixes#100066
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agents): cover legacy Codex provider config hint
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
* feat(skills): diagnose skill_workshop hidden by tool policy (#87570)
Workshop can be enabled and auto-capturing while tools.profile hides the
skill_workshop tool; every inspection surface looked healthy. plugins
inspect and openclaw doctor now name the excluding policy layer (global/
agent/provider profile, allowlist, denylist) and the exact alsoAllow
grant to add, via a shared resolveSkillWorkshopToolPolicyAvailability
helper that /learn's guard now reuses instead of composing policy
itself. Diagnosis only; no policy behavior change.
* ci: retrigger