Compare commits

..

383 Commits

Author SHA1 Message Date
Rocuts
69c551ac48 docs(iOS): add 2026 security audit report and domain-specific findings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit 673d732dc6)
2026-03-03 14:09:32 +01:00
libokai
4ad8bb8630 fix(ios): reset pcmFormatUnavailable at start of reloadConfig for re-probe
(cherry picked from commit 31ecbbbf3d)
2026-03-03 14:09:28 +01:00
libokai
e0fd16fb61 fix(ios): auto-fallback from PCM to MP3 for ElevenLabs TTS
The default output format pcm_44100 requires an ElevenLabs Pro tier
subscription. Users on free or starter plans get a silent 403 failure
and hear no audio.

Instead of hardcoding mp3, keep pcm_44100 as the default (better
quality for Pro users) but remember the failure: when a PCM request
is rejected, set pcmFormatUnavailable and use mp3_44100_128 for all
subsequent requests in the session. The flag resets on config reload
so it re-probes after reconnection.

Also standardize the MP3 fallback format from mp3_44100 to
mp3_44100_128 for consistent bitrate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit fbc26ef9f3)
2026-03-03 14:09:28 +01:00
Rocuts
64ce1a11d4 iOS: gate env var API key behind DEBUG, tighten deep link rate limit, cap WebSocket message size
(cherry picked from commit fea884f4d0)
2026-03-03 14:09:24 +01:00
Rocuts
2d676d6460 iOS: fix data races with OSAllocatedUnfairLock in TLS probe and camera delegates
(cherry picked from commit 96a1edcdc2)
2026-03-03 14:09:16 +01:00
Rocuts
2e90bc3d7d test(iOS): update gateway tests for Keychain-backed storage
(cherry picked from commit d08960aef8)
2026-03-03 14:09:12 +01:00
Rocuts
cafb5c8e12 fix(iOS): preserve legacy data when Keychain write fails during migration
(cherry picked from commit ea47df92fa)
2026-03-03 14:09:12 +01:00
Rocuts
253aec92d1 iOS: migrate TLS fingerprints from UserDefaults to Keychain
(cherry picked from commit fe5e14657a)
2026-03-03 14:09:12 +01:00
Rocuts
78b7a72510 iOS: move gateway connection metadata to Keychain, harden diagnostics log
(cherry picked from commit 4a30f8820a)
2026-03-03 14:09:12 +01:00
Rocuts
7f682a747d iOS: harden Keychain storage with delete-then-add and accessibility attribute
(cherry picked from commit cf122ce68e)
2026-03-03 14:09:12 +01:00
OpenCils
3fe4c19305 fix(telegram): prevent duplicate messages in DM draft streaming mode (#32118)
* fix(telegram): prevent duplicate messages in DM draft streaming mode

When using sendMessageDraft for DM streaming (streaming: 'partial'),
the draft bubble auto-converts to the final message. The code was
incorrectly falling through to sendPayload() after the draft was
finalized, causing a duplicate message.

This fix checks if we're in draft preview mode with hasStreamedMessage
and skips the sendPayload call, returning "preview-finalized" directly.

Key changes:
- Use hasStreamedMessage flag instead of previewRevision comparison
- Avoids double stopDraftLane calls by returning early
- Prevents duplicate messages when final text equals last streamed text

Root cause: In lane-delivery.ts, the final message handling logic
did not properly handle the DM draft flow where sendMessageDraft
creates a transient bubble that doesn't need a separate final send.

* fix(telegram): harden DM draft finalization path

* fix(telegram): require emitted draft preview for unchanged finals

* fix(telegram): require final draft text emission before finalize

* fix: update changelog for telegram draft finalization (#32118) (thanks @OpenCils)

---------

Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
2026-03-03 17:34:46 +05:30
Altay
627813aba4 fix(heartbeat): scope exec wake dispatch to session key (#32724)
Merged via squash.

Prepared head SHA: 563fee0e65
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-03-03 14:47:40 +03:00
Ayaan Zaidi
1ded5cc9a9 fix: guard malformed Telegram replies and pass hook accountId 2026-03-03 17:01:04 +05:30
Ayaan Zaidi
5f95f46070 docs: update changelog for telegram message_sent fix (#32649) 2026-03-03 16:56:20 +05:30
Ayaan Zaidi
5b8fc68ea2 fix(telegram): include reply hook metadata 2026-03-03 16:56:20 +05:30
KimGLee
9830b7c298 fix(telegram): mark message_sent success only when delivery occurred 2026-03-03 16:56:20 +05:30
KimGLee
6d118ab815 fix(telegram): run outbound message hooks in reply delivery path 2026-03-03 16:56:20 +05:30
Nimrod Gutman
4aa548cf7d macOS: add tailscale serve discovery fallback for remote gateways (#32860)
* feat(macos): add tailscale serve gateway discovery fallback

* fix: add changelog note for tailscale serve discovery fallback (#32860) (thanks @ngutman)
2026-03-03 13:25:36 +02:00
Sid
4ffe15c6b2 fix(telegram): warn when accounts.default is missing in multi-account setup (#32544)
Merged via squash.

Prepared head SHA: 7ebc3f65b2
Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-03 03:27:19 -05:00
Gustavo Madeira Santana
2370ea5d1b agents: propagate config for embedded skill loading 2026-03-03 02:44:56 -05:00
Liu Xiaopai
ae29842158 Gateway: fix stale self version in status output (#32655)
Merged via squash.

Prepared head SHA: b9675d1f90
Co-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-03 02:41:52 -05:00
Muhammed Mukhthar CM
b1b41eb443 feat(mattermost): add native slash command support (refresh) (#32467)
Merged via squash.

Prepared head SHA: 989126574e
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm
2026-03-03 12:39:18 +05:30
Eugene
5341b5c71c Diffs: Migrate tool usage guidance from before_prompt_build to a plugin skill (#32630)
Merged via squash.

Prepared head SHA: 585697a4e1
Co-authored-by: sircrumpet <4436535+sircrumpet@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-03 01:50:59 -05:00
Henry Loenwind
997197c6c9 bug: Workaround for QMD upstream bug (#27028)
Merged via squash.

Prepared head SHA: 939f9f4574
Co-authored-by: HenryLoenwind <1485873+HenryLoenwind@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-03 01:48:43 -05:00
JT
de9031da22 fix: improve compaction summary instructions to preserve active work (#8903)
fix: improve compaction summary instructions to preserve active work

Expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context.

Co-authored-by: joetomasone <56984887+joetomasone@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
2026-03-02 22:36:19 -08:00
Henry Loenwind
75775f2fe6 chore: Updated Brave documentation (#26860)
Merged via squash.

Prepared head SHA: f8fc4bf01e
Co-authored-by: HenryLoenwind <1485873+HenryLoenwind@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-03 01:34:15 -05:00
Tak Hoffman
dbccc73d7a security(line): synthesize strict LINE auth boundary hardening
LINE auth boundary hardening synthesis for inbound webhook authn/z/authz:
- account-scoped pairing-store access
- strict DM/group allowlist boundary separation
- fail-closed webhook auth/runtime behavior
- replay and duplicate handling with in-flight continuity for concurrent redeliveries

Source PRs: #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777
Related continuity context: #21955

Co-authored-by: bmendonca3 <208517100+bmendonca3@users.noreply.github.com>
Co-authored-by: davidahmann <46606159+davidahmann@users.noreply.github.com>
Co-authored-by: harshang03 <58983401+harshang03@users.noreply.github.com>
Co-authored-by: haosenwang1018 <167664334+haosenwang1018@users.noreply.github.com>
Co-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
Co-authored-by: coygeek <65363919+coygeek@users.noreply.github.com>
Co-authored-by: lailoo <20536249+lailoo@users.noreply.github.com>
2026-03-03 00:21:15 -06:00
Peter Steinberger
fe92113472 test(e2e): isolate module mocks across harnesses 2026-03-03 05:52:14 +00:00
Peter Steinberger
1d7a287cf6 fix(telegram): debounce forwarded media-only bursts 2026-03-03 05:52:14 +00:00
Peter Steinberger
094140bdb1 test(live): harden gateway model profile probes 2026-03-03 05:52:14 +00:00
Peter Steinberger
b52c9f2575 fix(ci): handle disabled systemd units in docker doctor flow 2026-03-03 05:52:14 +00:00
Peter Steinberger
de62ccbf81 fix(test): stabilize appcast version assertion 2026-03-03 05:51:50 +00:00
Tak Hoffman
9a5bfb1fe5 fix(line): synthesize media/auth/routing webhook regressions (openclaw#32546) thanks @Takhoffman
Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Takhoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 23:47:56 -06:00
Viz
0b3bbfec06 fix(gateway+acp): thread stopReason through final event to ACP bridge (#24867)
Complete the stop reason propagation chain so ACP clients can
distinguish end_turn from max_tokens:

- server-chat.ts: emitChatFinal accepts optional stopReason param,
  includes it in the final payload, reads it from lifecycle event data
- translator.ts: read stopReason from the final payload instead of
  hardcoding end_turn

Chain: LLM API → run.ts (meta.stopReason) → agent.ts (lifecycle event)
→ server-chat.ts (final payload) → ACP translator (PromptResponse)
2026-03-03 00:40:54 -05:00
Peter Steinberger
b34530a05d docs(changelog): reattribute duplicated PR credits 2026-03-03 05:40:05 +00:00
Peter Steinberger
e1503349c3 fix: scope extension runtime deps to plugin manifests 2026-03-03 05:33:12 +00:00
Shadow
2a888c5703 ci: enable stale workflow 2026-03-02 23:21:34 -06:00
Peter Steinberger
786ff6afca chore(release): bump to 2026.3.3 and seed changelog 2026-03-03 05:12:23 +00:00
Peter Steinberger
2d67c9b2a0 fix: repair Feishu reset hook typing and stabilize secret resolver timeout 2026-03-03 05:06:08 +00:00
Viz
a9ec75fe81 fix(gateway): flush throttled delta before emitChatFinal (#24856)
* fix(gateway): flush throttled delta before emitChatFinal

The 150ms throttle in emitChatDelta can suppress the last text chunk
before emitChatFinal fires, causing streaming clients (e.g. ACP) to
receive truncated responses. The final event carries the complete text,
but clients that build responses incrementally from deltas miss the
tail end.

Flush one last unthrottled delta with the complete buffered text
immediately before sending the final event. This ensures all streaming
consumers have the full response without needing to reconcile deltas
against the final payload.

* fix(gateway): avoid duplicate delta flush when buffer unchanged

Track the text length at the time of the last broadcast. The flush in
emitChatFinal now only sends a delta if the buffer has grown since the
last broadcast, preventing duplicate sends when the final delta passed
the 150ms throttle and was already broadcast.

* fix(gateway): honor heartbeat suppression in final delta flush

* test(gateway): add final delta flush and dedupe coverage

* fix(gateway): skip final flush for silent lead fragments

* docs(changelog): note gateway final-delta flush fix credits

---------

Co-authored-by: Jonathan Taylor <visionik@pobox.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-02 23:45:46 -05:00
dongdong
0566845b71 fix(feishu): validate outbound renderMode routing with tests (#31562)
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 22:41:01 -06:00
Jealous
9083a3f2e3 fix(feishu): normalize all mentions in inbound agent context (#30252)
* fix(feishu): normalize all mentions in inbound agent context

Convert Feishu mention placeholders to explicit <at user_id="..."> tags (including bot mentions), add mention semantics hints for the model, and remove unused mentionMessageBody parsing to keep context handling consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(feishu): use replacer callback and escape only < > in normalizeMentions

Switch String.replace to a function replacer to prevent $ sequences in
display names from being interpolated as replacement patterns. Narrow
escaping to < and > only — & does not need escaping in LLM prompt tag
bodies and escaping it degrades readability (e.g. R&D → R&amp;D).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(feishu): only use open_id in normalizeMentions tag, drop user_id fallback

When a mention has no open_id, degrade to @name instead of emitting
<at user_id="uid_...">. This keeps the tag user_id space exclusively
open_id, so the bot self-reference hint (which uses botOpenId) is
always consistent with what appears in the tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(feishu): register mention strip pattern for <at> tags in channel dock

Add mentions.stripPatterns to feishuPlugin so that normalizeCommandBody
receives a slash-clean string after normalizeMentions replaces Feishu
placeholders with <at user_id="...">name</at> tags. Without this,
group slash commands like @Bot /help had their leading / obscured by
the tag prefix and no longer triggered command handlers.

Pattern mirrors the approach used by Slack (<@[^>]+>) and Discord (<@!?\d+>).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(feishu): strip bot mention in p2p to preserve DM slash commands

In p2p messages the bot mention is a pure addressing prefix; converting
it to <at user_id="..."> breaks slash commands because buildCommandContext
skips stripMentions for DMs. Extend normalizeMentions with a stripKeys
set and populate it with bot mention keys in p2p, so @Bot /help arrives
as /help. Non-bot mentions (mention-forward targets) are still normalized
to <at> tags in both p2p and group contexts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Changelog: note Feishu inbound mention normalization

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 22:40:17 -06:00
Peter Steinberger
85377a2817 chore(release): cut 2026.3.2 2026-03-03 04:35:46 +00:00
Vincent Koc
d45aa68ae8 CI: disable flaky sticky disk mount for Windows pnpm setup 2026-03-02 20:34:10 -08:00
Vincent Koc
be5de30de5 CI: start push test lanes earlier and drop check gating 2026-03-02 20:29:06 -08:00
挨踢小茶
406e7aba75 fix(feishu): guard against false-positive @mentions in multi-app groups (#30315)
* fix(feishu): guard against false-positive @mentions in multi-app groups

When multiple Feishu bot apps share a group chat, Feishu's WebSocket
event delivery remaps the open_id in mentions[] per-app. This causes
checkBotMentioned() to return true for ALL bots when only one was
actually @mentioned, making requireMention ineffective.

Add a botName guard: if the mention's open_id matches this bot but the
mention's display name differs from this bot's configured botName, treat
it as a false positive and skip.

botName is already available via account.config.botName (set during
onboarding).

Closes #24249

* fix(feishu): support @all mention in multi-bot groups

When a user sends @all (@_all in Feishu message content), treat it as
mentioning every bot so all agents respond when requireMention is true.

Feishu's @all does not populate the mentions[] array, so this needs
explicit content-level detection.

* fix(feishu): auto-fetch bot display name from API for reliable mention matching

Instead of relying on the manually configured botName (which may differ
from the actual Feishu bot display name), fetch the bot's display name
from the Feishu API at startup via probeFeishu().

This ensures checkBotMentioned() always compares against the correct
display name, even when the config botName doesn't match (e.g. config
says 'Wanda' but Feishu shows '绯红女巫').

Changes:
- monitor.ts: fetchBotOpenId → fetchBotInfo (returns both openId and name)
- monitor.ts: store botNames map, pass botName to handleFeishuMessage
- bot.ts: accept botName from params, prefer it over config fallback

* Changelog: note Feishu multi-app mention false-positive guard

---------

Co-authored-by: Teague Xiao <teaguexiao@TeaguedeMac-mini.local>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 22:27:35 -06:00
Andy Tien
cad06faafe fix: add session-memory hook support for Feishu provider (#31437)
* fix: add session-memory hook support for Feishu provider

Issue #31275: Session-memory hook not triggered when using /new command in Feishu

- Added command handler to Feishu provider
- Integrated with OpenClaw's before_reset hook system
- Ensures session memory is saved when /new or /reset commands are used

* Changelog: note Feishu session-memory hook parity

---------

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 22:19:24 -06:00
Huaqing.Hao
a5a7239182 fix(feishu): non-blocking WS ACK and preserve full streaming card content (#29616)
* fix(feishu): non-blocking ws ack and preserve streaming card full content

* fix(feishu): preserve fragmented streaming text without newline artifacts

---------

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 22:17:15 -06:00
Vincent Koc
a5a6952bf2 CI: reduce critical path for check build and windows jobs 2026-03-02 20:11:28 -08:00
Vincent Koc
d28fa50f8b CI: make node deps install optional in setup action 2026-03-02 20:11:28 -08:00
Vincent Koc
5ef04d2822 CI: speed up Windows dependency warmup 2026-03-02 20:11:12 -08:00
Peter Steinberger
bb5796265b docs(changelog): remove docs-only 2026.3.2 entries 2026-03-03 04:07:40 +00:00
Tian Wei
7c179f9288 feishu, line: pass per-group systemPrompt to inbound context (#31713)
* feishu: pass per-group systemPrompt to inbound context

The Feishu extension schema supports systemPrompt in per-group config
(channels.feishu.accounts.<id>.groups.<groupId>.systemPrompt) but the
value was never forwarded to the inbound context as GroupSystemPrompt.

This means per-group system prompts configured for Feishu had no effect,
unlike IRC, Discord, Slack, Telegram, Matrix, and other channels that
already pass this field correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* line: pass per-group systemPrompt to inbound context

Same issue as feishu: the Line config schema defines systemPrompt in
per-group config but the value was never forwarded as GroupSystemPrompt
in the inbound context payload.

Added resolveLineGroupSystemPrompt helper that mirrors the existing
resolveLineGroupConfig lookup logic (groupId > roomId > wildcard).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Changelog: note Feishu and LINE group systemPrompt propagation

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 22:07:35 -06:00
Peter Steinberger
d9d604c6ad docs: add dedicated pdf tool docs page 2026-03-03 04:07:04 +00:00
Tak Hoffman
6cdfd2eaaa fix(feishu): correct invalid scope name in permission grant URL (#32509)
* fix(feishu): correct invalid scope name in permission grant URL

The Feishu API returns error code 99991672 with an authorization URL
containing the non-existent scope `contact:contact.base:readonly`
when the `contact.user.get` endpoint is called without the correct
permission. The valid scope is `contact:user.base:readonly`.

Add a scope correction map that replaces known incorrect scope names
in the extracted grant URL before presenting it to the user/agent,
so the authorization link actually works.

Closes #31761

* chore(changelog): note feishu scope correction

---------

Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
2026-03-02 22:06:42 -06:00
Peter Steinberger
b3b4fd30c3 chore(release): update appcast for 2026.3.2-beta.1 2026-03-03 03:58:35 +00:00
Vincent Koc
a951ecdd7b CI: shard Windows tests into sixths and skip cache restore 2026-03-02 19:54:52 -08:00
Vincent Koc
c6634b4083 CI: add toggle to skip pnpm actions cache restore 2026-03-02 19:54:52 -08:00
Peter Steinberger
524fb16619 fix(gateway): skip google rate limits in live suite 2026-03-03 03:48:09 +00:00
青雲
1fdc20a24f refactor(feishu): unify Lark SDK error handling with LarkApiError (#31450)
* refactor(feishu): unify Lark SDK error handling with LarkApiError

- Add LarkApiError class with code, api, and context fields for better diagnostics
- Add ensureLarkSuccess helper to replace 9 duplicate error check patterns
- Update tool registration layer to return structured error info (code, api, context)

This improves:
- Observability: errors now include API name and request context for easier debugging
- Maintainability: single point of change for error handling logic
- Extensibility: foundation for retry strategies, error classification, etc.

Affected APIs:
- wiki.space.getNode
- bitable.app.get
- bitable.app.create
- bitable.appTableField.list
- bitable.appTableField.create
- bitable.appTableRecord.list
- bitable.appTableRecord.get
- bitable.appTableRecord.create
- bitable.appTableRecord.update

* Changelog: note Feishu bitable error handling unification

---------

Co-authored-by: echoVic <echovic@163.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 21:44:40 -06:00
Vincent Koc
925da0fe99 Delete changelog/fragments directory 2026-03-02 19:43:23 -08:00
Peter Steinberger
99ae722e57 fix(ci): complete feishu route mock typing in broadcast tests 2026-03-03 03:42:30 +00:00
Peter Steinberger
eb8a8840d6 chore(release): prepare 2026.3.2-beta.1 2026-03-03 03:38:49 +00:00
Runkun Miao
7c6f8bfe73 feat(feishu): add broadcast support for multi-agent groups (#29575)
* feat(feishu): add broadcast support for multi-agent group observation

When multiple agents share a Feishu group chat, only the @mentioned
agent receives the message. This prevents observer agents from building
session memory of group activity they weren't directly addressed in.

Adds broadcast support (reusing the same cfg.broadcast schema as
WhatsApp) so all configured agents receive every group message in their
session transcripts. Only the @mentioned agent responds on Feishu;
observer agents process silently via no-op dispatchers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): guard sequential broadcast dispatch against single-agent failure

Wrap each dispatchForAgent() call in the sequential loop with try/catch
so one agent's dispatch failure doesn't abort delivery to remaining agents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): avoid duplicate messages in broadcast observer mode and normalize agent IDs

- Skip recordPendingHistoryEntryIfEnabled for broadcast groups when not
  mentioned, since the message is dispatched directly to all agents.
  Previously the message appeared twice in the agent prompt.
- Normalize agent IDs with toLowerCase() before membership checks so
  config casing mismatches don't silently skip valid agents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): set WasMentioned per-agent and normalize broadcast IDs

- buildCtxPayloadForAgent now takes a wasMentioned parameter so active
  agents get WasMentioned=true and observers get false (P1 fix)
- Normalize broadcastAgents to lowercase at resolution time and
  lowercase activeAgentId so all comparisons and session key generation
  use canonical IDs regardless of config casing (P2 fix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): canonicalize broadcast agent IDs with normalizeAgentId

* fix(feishu): match ReplyDispatcher sync return types for noop dispatcher

The upstream ReplyDispatcher changed sendToolResult/sendBlockReply/
sendFinalReply to synchronous (returning boolean). Update the broadcast
observer noop dispatcher to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): deduplicate broadcast agent IDs after normalization

Config entries like "Main" and "main" collapse to the same canonical ID
after normalizeAgentId but were dispatched multiple times. Use Set to
deduplicate after normalization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): honor requireMention=false when selecting broadcast responder

When requireMention is false, the routed agent should be active (reply
on Feishu) even without an explicit @mention. Previously activeAgentId
was null whenever ctx.mentionedBot was false, so all agents got the
noop dispatcher and no reply was sent — silently breaking groups that
disabled mention gating.

Hoist requireMention out of the if(isGroup) block so it's accessible
in the dispatch code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): cross-account broadcast dedup to prevent duplicate dispatches

In multi-account Feishu setups, the same message event is delivered to
every bot account in a group. Without cross-account dedup, each account
independently dispatches broadcast agents, causing 2×N dispatches instead
of N (where N = number of broadcast agents).

Two changes:
1. requireMention=true + bot not mentioned: return early instead of
   falling through to broadcast. The mentioned bot's handler will
   dispatch for all agents. Non-mentioned handlers record to history.
2. Add cross-account broadcast dedup using a shared 'broadcast' namespace
   (tryRecordMessagePersistent). The first handler to reach the broadcast
   block claims the message; subsequent accounts skip. This handles the
   requireMention=false multi-account case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): strip CommandAuthorized from broadcast observer contexts

Broadcast observer agents inherited CommandAuthorized from the sender,
causing slash commands (e.g. /reset) to silently execute on every observer
session. Now only the active agent retains CommandAuthorized; observers
have it stripped before dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): use actual mention state for broadcast WasMentioned

The active broadcast agent's WasMentioned was set to true whenever
requireMention=false, even when the bot was not actually @mentioned.
Now uses ctx.mentionedBot && agentId === activeAgentId, consistent
with the single-agent path which passes ctx.mentionedBot directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(feishu): skip history buffer for broadcast accounts and log parallel failures

1. In requireMention groups with broadcast, non-mentioned accounts no
   longer buffer pending history — the mentioned handler's broadcast
   dispatch already writes turns into all agent sessions. Buffering
   caused duplicate replay via buildPendingHistoryContextFromMap.

2. Parallel broadcast dispatch now inspects Promise.allSettled results
   and logs rejected entries, matching the sequential path's per-agent
   error logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Changelog: note Feishu multi-agent broadcast dispatch

* Changelog: restore author credit for Feishu broadcast entry

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 21:38:46 -06:00
Peter Steinberger
92c4a2a29e fix(gateway): retry exec-read live tool probe 2026-03-03 03:36:55 +00:00
Peter Steinberger
70ab91500a test(ci): add changed-scope shell-injection regression 2026-03-03 03:34:51 +00:00
Peter Steinberger
f175a5d6d3 fix(ci): avoid shell interpolation in changed-scope git diff 2026-03-03 03:34:46 +00:00
xbsheng
02d26ced98 docs(feishu): Feishu docs – add verificationToken and align zh-CN with EN (openclaw#31555) thanks @xbsheng
Verified:
- pnpm build
- pnpm test:macmini
- pnpm check (blocked locally by pre-existing mainline lint issue in src/scripts/ci-changed-scope.test.ts unrelated to this PR)

Co-authored-by: xbsheng <56357338+xbsheng@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 21:33:41 -06:00
Vincent Koc
99a48aad08 CI: increase checks-windows test shards to 4 2026-03-02 19:32:46 -08:00
Vincent Koc
8b80848ae9 CI: increase checks-windows test shards to 3 2026-03-02 19:31:27 -08:00
Vincent Koc
153a4f55db CI: reduce pre-test Windows setup latency 2026-03-02 19:30:29 -08:00
Vincent Koc
578a7a82be CI: add exact-key mode for pnpm cache restore 2026-03-02 19:30:29 -08:00
Sid
e6f34b25aa fix(feishu): preserve block streaming text when final payload is missing (#30663)
* fix(feishu): preserve block streaming text when final payload is missing

When Feishu card streaming receives block payloads without matching final/partial
callbacks, keep block text in stream state so onIdle close still publishes the
reply instead of an empty message. Add a regression test for block-only streaming.

Closes #30628

* Feishu: preserve streaming block fallback when final text is missing

---------

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 21:26:21 -06:00
Peter Steinberger
17bb87f432 fix(venice): retry model discovery on transient fetch failures 2026-03-03 03:21:00 +00:00
joshavant
85a320de54 docs(changelog): add SecretRef note for #29580 2026-03-02 21:19:17 -06:00
Peter Steinberger
46b62c53f0 fix(ci): restore scope-test require import and sync host policy 2026-03-03 03:18:45 +00:00
Peter Steinberger
ca1b50908f chore(gitignore): ignore android kotlin cache 2026-03-03 03:13:23 +00:00
Vincent Koc
05aa16c040 CI: allow blacksmith 32 vCPU Windows runner in actionlint 2026-03-02 19:13:14 -08:00
Vincent Koc
2c6616b830 CI: gate Windows checks by windows-relevant scope (#32456)
* CI: add windows scope output for changed-scope

* Test: cover windows scope gating in changed-scope

* CI: gate checks-windows by windows scope

* Docs: update CI windows scope and runner label

* CI: move checks-windows to 32 vCPU runner

* Docs: align CI windows runner with workflow
2026-03-02 19:10:58 -08:00
Peter Steinberger
80efcb75c7 style(swift): apply lint and format cleanup 2026-03-03 03:07:55 +00:00
Peter Steinberger
ba50dfaae3 refactor(macos): simplify pairing alert and host helper paths 2026-03-03 03:07:54 +00:00
Peter Steinberger
04a8f97c57 fix(swift): align async helper callsites across iOS and macOS 2026-03-03 03:07:54 +00:00
Peter Steinberger
5cba9a6bab test: load ci changed-scope script via esm import 2026-03-03 03:06:22 +00:00
Peter Steinberger
da6e6fb900 test: fix strict runtime mock types in channel tests 2026-03-03 03:06:22 +00:00
Peter Steinberger
805de8537c fix(telegram): move unchanged command-sync log to verbose 2026-03-03 03:05:39 +00:00
Peter Steinberger
f7f0caa5c7 fix(ci): tighten type signatures in gateway params validation 2026-03-03 03:04:13 +00:00
Peter Steinberger
7fd4328854 fix(e2e): include shared tool display resource in onboard docker build 2026-03-03 03:02:27 +00:00
Peter Steinberger
7bad42910b docs: reorder unreleased changelog by user interest 2026-03-03 03:00:37 +00:00
Vincent Koc
f2c37e543e CI: optimize Windows lane by splitting bundle and dropping duplicate lanes 2026-03-02 18:58:43 -08:00
Josh Avant
806803b7ef feat(secrets): expand SecretRef coverage across user-supplied credentials (#29580)
* feat(secrets): expand secret target coverage and gateway tooling

* docs(secrets): align gateway and CLI secret docs

* chore(protocol): regenerate swift gateway models for secrets methods

* fix(config): restore talk apiKey fallback and stabilize runner test

* ci(windows): reduce test worker count for shard stability

* ci(windows): raise node heap for test shard stability

* test(feishu): make proxy env precedence assertion windows-safe

* fix(gateway): resolve auth password SecretInput refs for clients

* fix(gateway): resolve remote SecretInput credentials for clients

* fix(secrets): skip inactive refs in command snapshot assignments

* fix(secrets): scope gateway.remote refs to effective auth surfaces

* fix(secrets): ignore memory defaults when enabled agents disable search

* fix(secrets): honor Google Chat serviceAccountRef inheritance

* fix(secrets): address tsgo errors in command and gateway collectors

* fix(secrets): avoid auth-store load in providers-only configure

* fix(gateway): defer local password ref resolution by precedence

* fix(secrets): gate telegram webhook secret refs by webhook mode

* fix(secrets): gate slack signing secret refs to http mode

* fix(secrets): skip telegram botToken refs when tokenFile is set

* fix(secrets): gate discord pluralkit refs by enabled flag

* fix(secrets): gate discord voice tts refs by voice enabled

* test(secrets): make runtime fixture modes explicit

* fix(cli): resolve local qr password secret refs

* fix(cli): fail when gateway leaves command refs unresolved

* fix(gateway): fail when local password SecretRef is unresolved

* fix(gateway): fail when required remote SecretRefs are unresolved

* fix(gateway): resolve local password refs only when password can win

* fix(cli): skip local password SecretRef resolution on qr token override

* test(gateway): cast SecretRef fixtures to OpenClawConfig

* test(secrets): activate mode-gated targets in runtime coverage fixture

* fix(cron): support SecretInput webhook tokens safely

* fix(bluebubbles): support SecretInput passwords across config paths

* fix(msteams): make appPassword SecretInput-safe in onboarding/token paths

* fix(bluebubbles): align SecretInput schema helper typing

* fix(cli): clarify secrets.resolve version-skew errors

* refactor(secrets): return structured inactive paths from secrets.resolve

* refactor(gateway): type onboarding secret writes as SecretInput

* chore(protocol): regenerate swift models for secrets.resolve

* feat(secrets): expand extension credential secretref support

* fix(secrets): gate web-search refs by active provider

* fix(onboarding): detect SecretRef credentials in extension status

* fix(onboarding): allow keeping existing ref in secret prompt

* fix(onboarding): resolve gateway password SecretRefs for probe and tui

* fix(onboarding): honor secret-input-mode for local gateway auth

* fix(acp): resolve gateway SecretInput credentials

* fix(secrets): gate gateway.remote refs to remote surfaces

* test(secrets): cover pattern matching and inactive array refs

* docs(secrets): clarify secrets.resolve and remote active surfaces

* fix(bluebubbles): keep existing SecretRef during onboarding

* fix(tests): resolve CI type errors in new SecretRef coverage

* fix(extensions): replace raw fetch with SSRF-guarded fetch

* test(secrets): mark gateway remote targets active in runtime coverage

* test(infra): normalize home-prefix expectation across platforms

* fix(cli): only resolve local qr password refs in password mode

* test(cli): cover local qr token mode with unresolved password ref

* docs(cli): clarify local qr password ref resolution behavior

* refactor(extensions): reuse sdk SecretInput helpers

* fix(wizard): resolve onboarding env-template secrets before plaintext

* fix(cli): surface secrets.resolve diagnostics in memory and qr

* test(secrets): repair post-rebase runtime and fixtures

* fix(gateway): skip remote password ref resolution when token wins

* fix(secrets): treat tailscale remote gateway refs as active

* fix(gateway): allow remote password fallback when token ref is unresolved

* fix(gateway): ignore stale local password refs for none and trusted-proxy

* fix(gateway): skip remote secret ref resolution on local call paths

* test(cli): cover qr remote tailscale secret ref resolution

* fix(secrets): align gateway password active-surface with auth inference

* fix(cli): resolve inferred local gateway password refs in qr

* fix(gateway): prefer resolvable remote password over token ref pre-resolution

* test(gateway): cover none and trusted-proxy stale password refs

* docs(secrets): sync qr and gateway active-surface behavior

* fix: restore stability blockers from pre-release audit

* Secrets: fix collector/runtime precedence contradictions

* docs: align secrets and web credential docs

* fix(rebase): resolve integration regressions after main rebase

* fix(node-host): resolve gateway secret refs for auth

* fix(secrets): harden secretinput runtime readers

* gateway: skip inactive auth secretref resolution

* cli: avoid gateway preflight for inactive secret refs

* extensions: allow unresolved refs in onboarding status

* tests: fix qr-cli module mock hoist ordering

* Security: align audit checks with SecretInput resolution

* Gateway: resolve local-mode remote fallback secret refs

* Node host: avoid resolving inactive password secret refs

* Secrets runtime: mark Slack appToken inactive for HTTP mode

* secrets: keep inactive gateway remote refs non-blocking

* cli: include agent memory secret targets in runtime resolution

* docs(secrets): sync docs with active-surface and web search behavior

* fix(secrets): keep telegram top-level token refs active for blank account tokens

* fix(daemon): resolve gateway password secret refs for probe auth

* fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled

* fix(secrets): align token inheritance and exec timeout defaults

* docs(secrets): clarify active-surface notes in cli docs

* cli: require secrets.resolve gateway capability

* gateway: log auth secret surface diagnostics

* secrets: remove dead provider resolver module

* fix(secrets): restore gateway auth precedence and fallback resolution

* fix(tests): align plugin runtime mock typings

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-03-03 02:58:20 +00:00
Peter Steinberger
f212351aed refactor(telegram): dedupe monitor retry test helpers 2026-03-03 02:53:14 +00:00
Peter Steinberger
6408b7f81c refactor(agents): dedupe steer restart test replacement flow 2026-03-03 02:53:14 +00:00
Peter Steinberger
1538813096 refactor(agents): dedupe ollama provider test scaffolding 2026-03-03 02:53:14 +00:00
Peter Steinberger
55c128ddc2 refactor(memory): dedupe readonly recovery test scenarios 2026-03-03 02:53:14 +00:00
Peter Steinberger
3ff0cf262d refactor(infra): dedupe update startup test setup 2026-03-03 02:53:14 +00:00
Peter Steinberger
a50dd0bb06 refactor(infra): dedupe ssrf fetch guard test fixtures 2026-03-03 02:53:13 +00:00
Peter Steinberger
8b4cdbb21d refactor(infra): dedupe exec approval allowlist evaluation flow 2026-03-03 02:53:13 +00:00
Peter Steinberger
b8181e5944 refactor(gateway): dedupe agents server-method handlers 2026-03-03 02:53:13 +00:00
Peter Steinberger
7a8232187b refactor(config): dedupe session store save error handling 2026-03-03 02:53:13 +00:00
Peter Steinberger
1a0036283d refactor(security): dedupe telegram allowlist validation loops 2026-03-03 02:53:13 +00:00
Peter Steinberger
4fb6da2b32 refactor(tests): dedupe canvas host server setup 2026-03-03 02:53:13 +00:00
Peter Steinberger
4a59d0ad98 refactor(tests): dedupe session store route fixtures 2026-03-03 02:53:13 +00:00
Peter Steinberger
d068fc9f9d refactor(tests): dedupe agent handler test scaffolding 2026-03-03 02:53:13 +00:00
Peter Steinberger
369646a513 refactor(tests): dedupe openresponses http fixtures 2026-03-03 02:53:13 +00:00
Peter Steinberger
3460aa4dee refactor(browser): dedupe playwright interaction helpers 2026-03-03 02:53:13 +00:00
Peter Steinberger
e290f4ca41 refactor(config): dedupe repeated zod schema shapes 2026-03-03 02:53:13 +00:00
Peter Steinberger
884ca65dc7 refactor(acp): dedupe runtime option command plumbing 2026-03-03 02:53:13 +00:00
Peter Steinberger
1a52d943ed refactor(tests): dedupe model compat assertions 2026-03-03 02:53:13 +00:00
Peter Steinberger
7897ffb72f refactor(memory): dedupe openai batch fetch flows 2026-03-03 02:53:13 +00:00
Peter Steinberger
5c18ba6f65 refactor(tests): dedupe gateway chat history fixtures 2026-03-03 02:53:13 +00:00
Peter Steinberger
25a2fe2bea refactor(tests): dedupe control ui auth pairing fixtures 2026-03-03 02:53:13 +00:00
Peter Steinberger
fa4ff5f3d2 refactor(acp): extract install hint resolver 2026-03-03 02:51:24 +00:00
Peter Steinberger
ac318be405 refactor(voice-call): unify runtime cleanup lifecycle 2026-03-03 02:51:17 +00:00
Peter Steinberger
c85bd2646a refactor(cli): extract plugin install plan helper 2026-03-03 02:51:11 +00:00
Peter Steinberger
6472e03412 refactor(agents): share failover error matchers 2026-03-03 02:51:00 +00:00
Vincent Koc
24fd6c8278 CI: use Blacksmith docker builder in sandbox smoke 2026-03-02 18:48:18 -08:00
Vincent Koc
5cffbbda32 CI: use Blacksmith docker builder in install smoke 2026-03-02 18:48:18 -08:00
Vincent Koc
85d17fd429 CI: migrate docker release build cache to Blacksmith 2026-03-02 18:48:18 -08:00
Vincent Koc
96d56a9721 CI: enable sticky-disk pnpm cache on Linux CI jobs 2026-03-02 18:48:18 -08:00
Vincent Koc
ffd3ad032a CI: add sticky-disk mode to pnpm cache action 2026-03-02 18:48:18 -08:00
Vincent Koc
8a463af823 CI: add sticky-disk toggle to setup node action 2026-03-02 18:48:18 -08:00
Peter Steinberger
6bf1abf603 ci: use valid Blacksmith Windows runner label 2026-03-03 02:47:06 +00:00
Josh Lehman
3a8133d587 fix(scripts/pr): SSH-first prhead remote with GraphQL fallback for fork PRs (#32126)
Co-authored-by: Shakker <shakkerdroid@gmail.com>
2026-03-03 02:46:01 +00:00
Peter Steinberger
8ac924c769 refactor(security): centralize audit execution context 2026-03-03 02:42:43 +00:00
Peter Steinberger
2d033d2aa8 refactor(agents): split tool-result char estimator 2026-03-03 02:42:43 +00:00
Peter Steinberger
1ec9673cc5 refactor(telegram): split lane preview target helpers 2026-03-03 02:42:43 +00:00
Peter Steinberger
fdb0bf804f refactor(test): dedupe telegram draft-stream fixtures 2026-03-03 02:42:43 +00:00
Peter Steinberger
40f2e2b8a6 ci: scale Windows CI runner and test workers 2026-03-03 02:42:32 +00:00
Ayaan Zaidi
87977d7a19 fix: unblock build type errors 2026-03-03 08:11:51 +05:30
Peter Steinberger
9f691099db fix(voice-call): harden webhook lifecycle cleanup and retries (#32395) (thanks @scoootscooob) 2026-03-03 02:39:50 +00:00
scoootscooob
e707c97ca6 fix(voice-call): prevent EADDRINUSE by guarding webhook server lifecycle
Three issues caused the port to remain bound after partial failures:

1. VoiceCallWebhookServer.start() had no idempotency guard — calling it
   while the server was already listening would create a second server on
   the same port.

2. createVoiceCallRuntime() did not clean up the webhook server if a step
   after webhookServer.start() failed (e.g. manager.initialize). The
   server kept the port bound while the runtime promise rejected.

3. ensureRuntime() cached the rejected promise forever, so subsequent
   calls would re-throw the same error without ever retrying. Combined
   with (2), the port stayed orphaned until gateway restart.

Fixes #32387

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:39:50 +00:00
Peter Steinberger
0750fc2de1 test: consolidate extension runtime mocks and split bluebubbles webhook auth suite 2026-03-03 02:37:23 +00:00
Peter Steinberger
59567a8c5d ci: move changed-scope logic into tested script 2026-03-03 02:37:23 +00:00
Peter Steinberger
8ee357fc76 refactor: extract session init helpers 2026-03-03 02:37:23 +00:00
Peter Steinberger
9702d94196 refactor: split plugin runtime type contracts 2026-03-03 02:37:23 +00:00
AI南柯(KingMo)
30ab9b2068 fix(agents): recognize connection errors as retryable timeout failures (#31697)
* fix(agents): recognize connection errors as retryable timeout failures

## Problem

When a model endpoint becomes unreachable (e.g., local proxy down,
relay server offline), the failover system fails to switch to the
next candidate model. Errors like "Connection error." are not
classified as retryable, causing the session to hang on a broken
endpoint instead of falling back to healthy alternatives.

## Root Cause

Connection/network errors are not recognized by the current failover
classifier:
- Text patterns like "Connection error.", "fetch failed", "network error"
- Error codes like ECONNREFUSED, ENOTFOUND, EAI_AGAIN (in message text)

While `failover-error.ts` handles these as error codes (err.code),
it misses them when they appear as plain text in error messages.

## Solution

Extend timeout error patterns to include connection/network failures:

**In `errors.ts` (ERROR_PATTERNS.timeout):**
- Text: "connection error", "network error", "fetch failed", etc.
- Regex: /\beconn(?:refused|reset|aborted)\b/i, /\benotfound\b/i, /\beai_again\b/i

**In `failover-error.ts` (TIMEOUT_HINT_RE):**
- Same patterns for non-assistant error paths

## Testing

Added test cases covering:
- "Connection error."
- "fetch failed"
- "network error: ECONNREFUSED"
- "ENOTFOUND" / "EAI_AGAIN" in message text

## Impact

- **Compatibility:** High - only expands retryable error detection
- **Behavior:** Connection failures now trigger automatic fallback
- **Risk:** Low - changes are additive and well-tested

* style: fix code formatting for test file
2026-03-03 02:37:23 +00:00
riftzen-bit
5e1a2ea019 chore: remove unreachable "LINUX" from resolvePlatform return type
Address review feedback: since resolvePlatform() no longer returns
"LINUX", remove it from the union type to prevent future confusion.
2026-03-03 02:36:01 +00:00
riftzen-bit
008e4804a6 fix(gemini-cli-auth): use PLATFORM_UNSPECIFIED for Linux in loadCodeAssist
Google's loadCodeAssist API rejects "LINUX" as an invalid Platform enum
value, causing OAuth setup to fail with 400 Bad Request on Linux systems.

The pi-ai runtime already uses "PLATFORM_UNSPECIFIED" for this field.
This aligns the extension's discoverProject() with that approach by
returning "PLATFORM_UNSPECIFIED" for Linux (and other non-Windows/macOS
platforms) instead of "LINUX".

Also fixes the original resolvePlatform() which incorrectly fell through
to "MACOS" as default instead of explicitly checking for "darwin".
2026-03-03 02:36:01 +00:00
AaronWander
4c32411bee fix(exec): suggest increasing timeout on timeouts 2026-03-03 02:35:10 +00:00
Gustavo Madeira Santana
91cdb703bd Agents: add context metadata warmup retry backoff 2026-03-02 21:34:55 -05:00
john
04ac688dff fix(acp): use publishable acpx install hint 2026-03-03 02:34:07 +00:00
苏敏童0668001043
b29e913efe fix(docker): correct awk quoting in Docker GPG fingerprint check (#32153) 2026-03-03 02:32:46 +00:00
Peter Steinberger
895abc5a64 perf(security): allow audit snapshot and summary cache reuse 2026-03-03 02:32:13 +00:00
Peter Steinberger
62582fc088 perf(agents): cache per-pass context char estimates 2026-03-03 02:32:13 +00:00
Peter Steinberger
57336203d5 test(telegram): move preview-finalization cases to lane unit tests 2026-03-03 02:32:13 +00:00
Peter Steinberger
1929151103 refactor(telegram): extract sequential key module 2026-03-03 02:32:13 +00:00
Peter Steinberger
6ab9e00e17 fix: resolve pi-tools typing regressions 2026-03-03 02:27:59 +00:00
Peter Steinberger
2380c1b5fd refactor(ui): dedupe inline code wrap rules 2026-03-03 02:19:34 +00:00
Peter Steinberger
493b560dfd refactor(runtime): unify node version guard parsing 2026-03-03 02:19:34 +00:00
Peter Steinberger
1dd77e4106 refactor(slack): extract socket reconnect policy helpers 2026-03-03 02:19:34 +00:00
Peter Steinberger
4d52dfe85b refactor(sessions): add explicit merge activity policies 2026-03-03 02:19:34 +00:00
Peter Steinberger
d380ed710d refactor(agents): split pi-tools param and host-edit wrappers 2026-03-03 02:19:34 +00:00
Peter Steinberger
03755f8463 test(telegram): dedupe streaming cases and tighten sequential key checks 2026-03-03 02:14:15 +00:00
Peter Steinberger
7fdbf1202e test(security): reduce audit fixture setup overhead 2026-03-03 02:14:15 +00:00
Peter Steinberger
70db52de71 test(agents): centralize AgentMessage fixtures and remove unsafe casts 2026-03-03 02:14:15 +00:00
Gustavo Madeira Santana
15a0455d04 CLI: unify routed config positional parsing 2026-03-02 21:11:53 -05:00
Peter Steinberger
d3c637d193 fix: recover host edit success after post-write upstream throw (#32383) (thanks @polooooo) 2026-03-03 02:06:59 +00:00
倪汉杰0668001185
0fb3f188b2 fix(agents): only recover edit when oldText no longer in file (review feedback) 2026-03-03 02:06:59 +00:00
倪汉杰0668001185
bf6aa7ca67 fix(agents): treat host edit tool as success when file contains newText after upstream throw (fixes #32333) 2026-03-03 02:06:59 +00:00
Peter Steinberger
0fd77c9856 refactor: modularize plugin runtime and test hooks 2026-03-03 02:06:58 +00:00
Peter Steinberger
f77f1d3800 fix: preserve inline code copy fidelity in web ui (#32346) (thanks @hclsys) 2026-03-03 02:05:45 +00:00
HCL
7c90ef7c52 fix(webui): prevent inline code from breaking mid-token on copy/paste
The parent `.chat-text` applies `overflow-wrap: anywhere; word-break: break-word;`
which forces long tokens (UUIDs, hashes) inside inline `<code>` to break across
visual lines. When copied, the browser injects spaces at those break points,
corrupting the pasted value.

Override with `overflow-wrap: normal; word-break: keep-all;` on inline `<code>`
selectors so tokens stay intact.

Fixes #32230

Signed-off-by: HCL <chenglunhu@gmail.com>
2026-03-03 02:05:37 +00:00
Peter Steinberger
7dadd5027b fix: enforce node v22.12+ preflight for installer and runtime (#32356) (thanks @jasonhargrove) 2026-03-03 02:03:45 +00:00
Jason Hargrove
f8ed48293c fix(cli): align Node 22.12 preflight checks and clean runtime guard output
Tighten installer/runtime consistency so users on Node 22.0-22.11 are blocked before install/runtime drift, with cleaner CLI guidance.

- Enforce Node >=22.12 in scripts/install.sh preflight checks
- Align installer messages to the same 22.12+ runtime floor
- Replace openclaw.mjs thrown version error with stderr+exit to avoid noisy stack traces
2026-03-03 02:03:45 +00:00
Jason Hargrove
96a38d5aa4 fix(cli): fail fast on unsupported Node versions in install and runtime paths
Surface a clear Node 22.12+ requirement before npm/install bootstrap work so users avoid misleading downstream errors.

- Add installer shell preflight to block active Node <22 and suggest NVM recovery commands
- Add openclaw.mjs runtime preflight for npm/npx usage with explicit Node version guidance
- Keep messaging actionable for both NVM and non-NVM environments
2026-03-03 02:03:45 +00:00
Peter Steinberger
c7ec237089 fix: fail fast on non-recoverable slack auth errors (#32377) (thanks @scoootscooob) 2026-03-03 01:59:47 +00:00
scoootscooob
1ae82be55a fix(slack): fail fast on non-recoverable auth errors instead of retry loop
When a Slack bot is removed from a workspace while still configured in
OpenClaw, the gateway enters an infinite retry loop on account_inactive
or invalid_auth errors, making the entire gateway unresponsive.

Add isNonRecoverableSlackAuthError() to detect permanent credential
failures (account_inactive, invalid_auth, token_revoked, etc.) and
throw immediately instead of retrying.  This mirrors how the Telegram
provider already distinguishes recoverable network errors from fatal
auth errors via isRecoverableTelegramNetworkError().

The check is applied in both the startup catch block and the disconnect
reconnect path so stale credentials always fail fast with a clear error
message.

Closes #32366

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:59:47 +00:00
Peter Steinberger
fd782d811e fix: preserve idle reset timestamp on inbound metadata writes (#32379) (thanks @romeodiaz) 2026-03-03 01:57:53 +00:00
romeodiaz
a467517b2b fix(sessions): preserve idle reset timestamp on inbound metadata 2026-03-03 01:57:53 +00:00
nico-hoff
3eec79bd6c feat(memory): add Ollama embedding provider (#26349)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: ac41386543
Co-authored-by: nico-hoff <43175972+nico-hoff@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-02 20:56:40 -05:00
Peter Steinberger
4ba5937ef9 refactor(tests): dedupe tools invoke http request helpers 2026-03-03 01:54:28 +00:00
Peter Steinberger
6fc3f504d6 refactor(tests): dedupe media transcript echo config setup 2026-03-03 01:54:28 +00:00
Peter Steinberger
b17687b775 refactor(tests): dedupe security fix scenario helpers 2026-03-03 01:54:27 +00:00
Peter Steinberger
eca242b971 refactor(tests): dedupe manifest registry link fixture setup 2026-03-03 01:54:27 +00:00
Peter Steinberger
4494844d17 refactor(tests): dedupe discord monitor e2e fixtures 2026-03-03 01:54:27 +00:00
Peter Steinberger
5193189953 refactor(tests): dedupe cron store migration setup 2026-03-03 01:54:27 +00:00
Peter Steinberger
fbb88d5063 refactor(tests): dedupe isolated agent cron turn assertions 2026-03-03 01:54:27 +00:00
Peter Steinberger
c0715db3c8 fix: add session hook context regression tests (#26394) (thanks @tempeste) 2026-03-03 01:48:46 +00:00
tempeste
20c15ccc63 Plugins: add sessionKey to session lifecycle hooks 2026-03-03 01:48:46 +00:00
Peter Steinberger
16fd604219 fix(security): pin tlon api source and secure hold music url 2026-03-03 01:45:24 +00:00
Peter Steinberger
61f29830bc fix(test): resolve upstream typing drift in feishu and cron suites 2026-03-03 01:44:21 +00:00
Peter Steinberger
47736e3432 refactor(test): extract cron issue-regression harness and frozen-time helper 2026-03-03 01:44:21 +00:00
Peter Steinberger
39520ad21b test(agents): tighten pi message typing and dedupe malformed tool-call cases 2026-03-03 01:44:21 +00:00
Sk Akram
bd8c3230e8 fix: force supportsDeveloperRole=false for non-native OpenAI endpoints (#29479)
Merged via squash.

Prepared head SHA: 1416c584ac
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-02 20:43:49 -05:00
Peter Steinberger
ebbb572639 fix: add requestHeartbeatNow runtime coverage (#19464) (thanks @AustinEral) 2026-03-03 01:40:31 +00:00
Austin Eral
3b9877dee7 fix: add requestHeartbeatNow to bluebubbles test mock 2026-03-03 01:40:31 +00:00
Austin Eral
40e5c6a18d feat(plugins): expose requestHeartbeatNow on plugin runtime
Add requestHeartbeatNow to PluginRuntime.system so extensions can
trigger an immediate heartbeat wake without importing internal modules.

This enables extensions to inject a system event and wake the agent
in one step — useful for inbound message handlers that use the
heartbeat model (e.g. agent-to-agent DMs via Nostr).

Changes:
- src/plugins/runtime/types.ts: add RequestHeartbeatNow type alias
  and requestHeartbeatNow to PluginRuntime.system
- src/plugins/runtime/index.ts: import and wire requestHeartbeatNow
  into createPluginRuntime()
2026-03-03 01:40:31 +00:00
David Rudduck
11e1363d2d feat(hooks): add trigger and channelId to plugin hook agent context (#28623)
* feat(hooks): add trigger and channelId to plugin hook agent context

Adds `trigger` and `channelId` fields to `PluginHookAgentContext` so
plugins can determine what initiated the agent run and which channel
it originated from, without session-key parsing or Redis bridging.

trigger values: "user", "heartbeat", "cron", "memory"
channelId values: "telegram", "discord", "whatsapp", etc.

Both fields are threaded through run.ts and attempt.ts hookCtx so all
hook phases receive them (before_model_resolve, before_prompt_build,
before_agent_start, llm_input, llm_output, agent_end).

channelId falls back from messageChannel to messageProvider when the
former is not set. followup-runner passes originatingChannel so queued
followup runs also carry channel context.

* docs(changelog): note hook context parity fix for #28623

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-02 17:39:20 -08:00
Peter Steinberger
ee646dae82 fix: add runtime.events regression tests (#16044) (thanks @scifantastic) 2026-03-03 01:37:56 +00:00
SciFantastic
85f01cd9eb Fix styles 2026-03-03 01:37:56 +00:00
SciFantastic
bab5d994bc docs: expand JSDoc for onSessionTranscriptUpdate params and return 2026-03-03 01:37:56 +00:00
SciFantastic
2365c6c86a docs: add JSDoc to onSessionTranscriptUpdate 2026-03-03 01:37:56 +00:00
SciFantastic
53ada1e9b9 fix: add missing events property to bluebubbles PluginRuntime mock 2026-03-03 01:37:56 +00:00
SciFantastic
b91a22a3fb style: fix indentation in transcript-events 2026-03-03 01:37:56 +00:00
SciFantastic
2aab6dff76 fix: wrap transcript event listeners in try/catch to prevent throw propagation 2026-03-03 01:37:56 +00:00
SciFantastic
980388fcf0 plugin-sdk: expose onAgentEvent + onSessionTranscriptUpdate via PluginRuntime.events 2026-03-03 01:37:56 +00:00
Peter Steinberger
3e6451f2d8 refactor(feishu): expose default-account selection source 2026-03-03 01:37:39 +00:00
Peter Steinberger
2f6718b8e7 refactor(gateway): extract channel health policy and timing aliases 2026-03-03 01:37:39 +00:00
Peter Steinberger
b5350bf46f refactor(outbound): unify channel selection and action input normalization 2026-03-03 01:37:39 +00:00
Peter Steinberger
0f5f20ee6b refactor(tests): dedupe cron delivered status assertions 2026-03-03 01:37:12 +00:00
Peter Steinberger
6b6af1a64f refactor(tests): dedupe web fetch and embedded tool hook fixtures 2026-03-03 01:37:12 +00:00
Peter Steinberger
c1b37f29f0 refactor(tests): dedupe browser and telegram tool test fixtures 2026-03-03 01:37:12 +00:00
Peter Steinberger
a3b674cc98 refactor(tests): dedupe agent lock and loop detection fixtures 2026-03-03 01:37:12 +00:00
Brian Mendonca
cdc1ef85e8 Feishu: cache failing probes (#29970)
* Feishu: cache failing probes

* Changelog: add Feishu probe failure backoff note

---------

Co-authored-by: bmendonca3 <208517100+bmendonca3@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 19:37:07 -06:00
Peter Steinberger
1ca69c8fd7 fix: add channelRuntime regression coverage (#25462) (thanks @guxiaobo) 2026-03-03 01:34:50 +00:00
Gu XiaoBo
469cd5b464 feat(plugin-sdk): Add channelRuntime support for external channel plugins
## Overview

This PR enables external channel plugins (loaded via Plugin SDK) to access
advanced runtime features like AI response dispatching, which were previously
only available to built-in channels.

## Changes

### src/gateway/server-channels.ts
- Import PluginRuntime type
- Add optional channelRuntime parameter to ChannelManagerOptions
- Pass channelRuntime to channel startAccount calls via conditional spread
- Ensures backward compatibility (field is optional)

### src/gateway/server.impl.ts
- Import createPluginRuntime from plugins/runtime
- Create and pass channelRuntime to channel manager

### src/channels/plugins/types.adapters.ts
- Import PluginRuntime type
- Add comprehensive documentation for channelRuntime field
- Document available features, use cases, and examples
- Improve type safety (use imported PluginRuntime type vs inline import)

## Benefits

External channel plugins can now:
- Generate AI-powered responses using dispatchReplyWithBufferedBlockDispatcher
- Access routing, text processing, and session management utilities
- Use command authorization and group policy resolution
- Maintain feature parity with built-in channels

## Backward Compatibility

- channelRuntime field is optional in ChannelGatewayContext
- Conditional spread ensures it's only passed when explicitly provided
- Existing channels without channelRuntime support continue to work unchanged
- No breaking changes to channel plugin API

## Testing

- Email channel plugin successfully uses channelRuntime for AI responses
- All existing built-in channels (slack, discord, telegram, etc.) work unchanged
- Gateway loads and runs without errors when channelRuntime is provided
2026-03-03 01:34:50 +00:00
Peter Steinberger
666073ee46 test: fix tsgo baseline test compatibility 2026-03-03 01:24:20 +00:00
Vincent Koc
747902a26a fix(hooks): propagate run/tool IDs for tool hook correlation (#32360)
* Plugin SDK: add run and tool call fields to tool hooks

* Agents: propagate runId and toolCallId in before_tool_call

* Agents: thread runId through tool wrapper context

* Runner: pass runId into tool hook context

* Compaction: pass runId into tool hook context

* Agents: scope after_tool_call start data by run

* Tests: cover run and tool IDs in before_tool_call hooks

* Tests: add run-scoped after_tool_call collision coverage

* Hooks: scope adjusted tool params by run

* Tests: cover run-scoped adjusted param collisions

* Hooks: preserve active tool start metadata until end

* Changelog: add tool-hook correlation note
2026-03-02 17:23:08 -08:00
Peter Steinberger
61adcea68e fix(test): tighten tool result typing in context pruning tests 2026-03-03 01:18:29 +00:00
Peter Steinberger
5ee6ca13b7 docs(changelog): add landed notes for #32336 and #32364 2026-03-03 01:18:05 +00:00
Peter Steinberger
71cd337137 fix(gateway): harden message action channel fallback and startup grace
Take the safe, tested subset from #32367:\n- per-channel startup connect grace in health monitor\n- tool-context channel-provider fallback for message actions\n\nCo-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
2026-03-03 01:17:27 +00:00
Peter Steinberger
4d04e1a41f fix(test): harden discord lifecycle status sink typing 2026-03-03 01:15:16 +00:00
Peter Steinberger
67e3eb85d7 refactor(tests): dedupe browser and config cli test setup 2026-03-03 01:15:09 +00:00
Peter Steinberger
1b4062defd refactor(tests): dedupe pi embedded test harness 2026-03-03 01:15:09 +00:00
Peter Steinberger
3e4dd84511 fix: webchat gfm table rendering and overflow (#32365) (thanks @BlueBirdBack) 2026-03-03 01:14:30 +00:00
Ash (Bug Lab)
5084621f43 fix(ui): ensure GFM tables render in WebChat markdown (#20410)
- Pass gfm:true + breaks:true explicitly to marked.parse() so table
  support is guaranteed even if global setOptions() is bypassed or
  reset by a future refactor (defense-in-depth)
- Add display:block + overflow-x:auto to .chat-text table so wide
  multi-column tables scroll horizontally instead of being clipped
  by the parent overflow-x:hidden chat container
- Add regression tests for GFM table rendering in markdown.test.ts
2026-03-03 01:14:30 +00:00
Peter Steinberger
346d3590fb fix(discord): harden voice ffmpeg path and opus fast-path 2026-03-03 01:14:15 +00:00
Peter Steinberger
687ef2e00f refactor(media): add shared ffmpeg helpers 2026-03-03 01:14:14 +00:00
Peter Steinberger
1187464041 fix: feishu default account outbound resolution (#32253) (thanks @bmendonca3) 2026-03-03 01:13:18 +00:00
bmendonca3
4e4a100038 Feishu: honor configured default account 2026-03-03 01:13:18 +00:00
Peter Steinberger
ddd71bc9f6 fix: guard gemini schema null properties (#32332) (thanks @webdevtodayjason) 2026-03-03 01:12:06 +00:00
webdevtodayjason
1a7a18d0bc fix(agents): guard gemini tool schema properties against null 2026-03-03 01:12:06 +00:00
Peter Steinberger
4e4d94cd38 fix(test): satisfy auth profile secret ref typing in runtime tests 2026-03-03 01:12:01 +00:00
Peter Steinberger
f0640b0100 fix(test): align gateway and session spawn hook typings 2026-03-03 01:12:01 +00:00
dongdong
46df7e2421 fix(feishu): skip typing indicator keepalive re-adds to prevent notification spam (#31580)
* fix(feishu): skip typing indicator keepalive re-adds to prevent notification spam

The typing keepalive loop calls addTypingIndicator() every 3 seconds,
which creates a new messageReaction.create API call each time. Feishu
treats each re-add as a new reaction event and fires a push notification,
causing users to receive repeated notifications while waiting for a
response.

Unlike Telegram/Discord where typing status expires after a few seconds,
Feishu reactions persist until explicitly removed. Skip the keepalive
re-add when a reaction already exists (reactionId is set) since there
is no need to refresh it.

Closes #28660

* Changelog: note Feishu typing keepalive suppression

---------

Co-authored-by: yuxh1996 <yuxh1996@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 19:11:47 -06:00
Peter Steinberger
42626648d7 docs(models): clarify moonshot thinking and failover stop-reason errors 2026-03-03 01:11:29 +00:00
Mitch McAlister
17b40c4a59 fix: guard isConnected check against already-aborted signal
When abortSignal is already aborted at lifecycle start, onAbort() fires
synchronously and pushes connected: false. Without a lifecycleStopping
guard, the subsequent gateway.isConnected check could push a spurious
connected: true, contradicting the shutdown.

Adds !lifecycleStopping to the isConnected guard and a test verifying
no connected: true is emitted when the signal is pre-aborted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:10:56 +00:00
Mitch McAlister
d9119f0791 fix(discord): push connected status when gateway is already connected at lifecycle start
When the Discord gateway completes its READY handshake before
`runDiscordGatewayLifecycle` registers its debug event listener, the
initial "WebSocket connection opened" event is missed. This leaves
`connected` as undefined in the channel runtime, causing the health
monitor to treat the channel as "stuck" and restart it every check
cycle.

Check `gateway.isConnected` immediately after registering the debug
listener and push the initial connected status if the gateway is
already connected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:10:56 +00:00
HCL
586f057c24 fix(cron): let resolveOutboundTarget handle missing delivery target fallback
The cron delivery path short-circuits with an error when `toCandidate` is
falsy (line 151), before reaching `resolveOutboundTarget()` which provides
the `plugin.config.resolveDefaultTo()` fallback. The direct send path in
`targets.ts` already uses this fallback correctly.

Remove the early `!toCandidate` exit so that `resolveOutboundTarget()`
can attempt the plugin-provided default. Guard the WhatsApp allowFrom
override against falsy `toCandidate` to maintain existing behavior when
a target IS resolved.

Fixes #32355

Signed-off-by: HCL <chenglunhu@gmail.com>
2026-03-03 01:09:47 +00:00
Peter Steinberger
90d8b40808 perf(test): simplify plugin install fixture archive loading 2026-03-03 01:09:07 +00:00
Peter Steinberger
d7bafae387 perf(test): trim fixture and serialization overhead in integration suites 2026-03-03 01:09:07 +00:00
Peter Steinberger
588fbd5b68 perf(test): reduce temp fixture churn in guardrail-heavy suites 2026-03-03 01:09:07 +00:00
Peter Steinberger
ef920f2f39 refactor(channels): dedupe monitor message test flows 2026-03-03 01:06:00 +00:00
Peter Steinberger
57e1534df8 refactor(tests): consolidate repeated setup helpers 2026-03-03 01:06:00 +00:00
Peter Steinberger
a48a3dbdda refactor(tests): dedupe tool, projector, and delivery fixtures 2026-03-03 01:06:00 +00:00
Peter Steinberger
c3d5159121 refactor(hooks): dedupe install parameter wiring 2026-03-03 01:06:00 +00:00
Peter Steinberger
1bd20dbdb6 fix(failover): treat stop reason error as timeout 2026-03-03 01:05:24 +00:00
Peter Steinberger
a2fdc3415f fix(failover): handle unhandled stop reason error 2026-03-03 01:05:24 +00:00
Peter Steinberger
ced267c5cb fix(moonshot): apply native thinking payload compatibility 2026-03-03 01:05:24 +00:00
Peter Steinberger
287606e445 feat(acp): add kimi harness support surfaces 2026-03-03 01:05:24 +00:00
Gustavo Madeira Santana
f26853f14c CLI: dedupe config validate errors and expose allowed values 2026-03-02 20:05:12 -05:00
AytuncYildizli
a44843507f fix(slack): restore persistent per-channel session routing (#32320)
Top-level channel messages were creating isolated per-message sessions because roomThreadId fell through to threadContext.messageTs whenever replyToMode was not off.

Introduced in #10686, every new channel message got its own session key (agent:...🧵<messageTs>), breaking conversation continuity.

Fix: only derive thread-specific session keys for actual thread replies. Top-level channel messages stay on the per-channel session key regardless of replyToMode.

Fixes #32285
2026-03-03 01:00:49 +00:00
scoootscooob
de09ca149f fix(telegram): use retry logic for sticker getFile calls (#32349)
The sticker code path called ctx.getFile() directly without retry,
unlike the non-sticker media path which uses resolveTelegramFileWithRetry
(3 attempts with jitter). This made sticker downloads vulnerable to
transient Telegram API failures, particularly in group topics where
file availability can be delayed.

Refs #32326

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:00:31 +00:00
hcl
503d395780 fix(memoryFlush): guard transcript-size forced flush against repeated runs (#32358)
The `forceFlushTranscriptBytes` path (introduced in d729ab21) bypasses the
`memoryFlushCompactionCount` guard that prevents repeated flushes within the
same compaction cycle. Once the session transcript exceeds 2 MB, memory flush
fires on every single message — even when token count is well under the
compaction threshold.

Extract `hasAlreadyFlushedForCurrentCompaction()` from the inline guard in
`shouldRunMemoryFlush` and apply it to both the token-based and the
transcript-size trigger paths.

Fixes #32317

Signed-off-by: HCL <chenglunhu@gmail.com>
2026-03-03 01:00:18 +00:00
Shawn
924d9e34ef fix(discord): resample audio to 48kHz for voice messages (#32298)
Fixes #32293: Discord voice message plays at ~0.5x speed with 24kHz TTS source

When TTS providers (like mlx-audio Qwen3-TTS) output audioHz,
Discord voice at 24k messages play at half speed because Discord expects 48kHz.

This fix adds explicit sample rate conversion to 48kHz in the ensureOggOpus
function, ensuring voice messages always play at correct speed regardless
of the input audio's sample rate.

Co-authored-by: Kevin Shenghui <shenghuikevin@gmail.com>
2026-03-03 01:00:04 +00:00
Peter Steinberger
f3e6578e6c fix(test): tighten websocket and runner fixture typing 2026-03-03 00:55:01 +00:00
Peter Steinberger
e930517154 fix(ci): resolve docs lint and test typing regressions 2026-03-03 00:55:01 +00:00
Peter Steinberger
47083460ea refactor: unify inbound debounce policy and split gateway/models helpers 2026-03-03 00:54:33 +00:00
Peter Steinberger
7de4204e57 docs(acp): document sandbox limitation 2026-03-03 00:52:09 +00:00
Peter Steinberger
36dfd462a8 feat(acp): enable dispatch by default 2026-03-03 00:47:35 +00:00
Peter Steinberger
6649c22471 fix(agents): harden openai ws tool call id handling 2026-03-03 00:43:48 +00:00
Peter Steinberger
596621919c chore(test): add vitest hotspot reporter script 2026-03-03 00:43:01 +00:00
Peter Steinberger
9657ded2e1 test(perf): trim slack, hook, and plugin-validation test overhead 2026-03-03 00:43:01 +00:00
Peter Steinberger
282b107e99 test(perf): speed up cron, memory, and secrets hotspots 2026-03-03 00:43:01 +00:00
Peter Steinberger
86090b0ff2 docs(models): refresh minimax kimi glm provider docs 2026-03-03 00:40:15 +00:00
Peter Steinberger
77ecef1fde feat(models): support minimax highspeed across onboarding 2026-03-03 00:40:15 +00:00
ademczuk
53fd7f8163 fix(test): resolve Feishu hoisted mock export syntax error (#32128)
- Remove vi.hoisted() wrapper from exported mock in shared module
  (Vitest cannot export hoisted variables)
- Inline vi.hoisted + vi.mock in startup test so Vitest's per-file
  hoisting registers mocks before production imports

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:34:16 +00:00
Peter Steinberger
1b5ac8b0b1 feat(cli): add configurable banner tagline mode 2026-03-03 00:31:51 +00:00
Peter Steinberger
f6233cfa5c fix: dedupe restart sentinel reason output (#32083) (thanks @velamints2) 2026-03-03 00:30:34 +00:00
velamints2
61be533ad4 fix(restart): deduplicate reason line in restart sentinel message
When gateway.restart is triggered with a reason but no separate note,
the payload sets both message and stats.reason to the same text.
formatRestartSentinelMessage() then emits both the message line and a
redundant 'Reason: <same text>' line, doubling the restart reason in
the notification delivered to the agent session.

Skip the 'Reason:' line when stats.reason matches the already-emitted
message text. Add regression tests for both duplicate and distinct
reason scenarios.
2026-03-03 00:30:34 +00:00
Peter Steinberger
d76ddd61ec fix(discord): add missing accountId to reaction routing params 2026-03-03 00:29:20 +00:00
Peter Steinberger
82101b152a test(voice-call): split call manager tests by scenario 2026-03-03 00:29:20 +00:00
Peter Steinberger
439a7732f4 refactor(voice-call): split webhook server and tailscale helpers 2026-03-03 00:29:20 +00:00
Peter Steinberger
a96b3b406a refactor(voice-call): extract twilio twiml policy and status mapping 2026-03-03 00:29:20 +00:00
Peter Steinberger
68e982ec80 fix: stabilize internal hooks singleton registry (#32292) (thanks @Drickon) 2026-03-03 00:27:10 +00:00
Eric Lytle
d0a3743abd refactor: use ??= operator for cleaner globalThis singleton init
Addresses greptile review: collapses the if-guard + assignment into
a single ??= expression so TypeScript can narrow the type without
a non-null assertion.
2026-03-03 00:27:10 +00:00
Eric Lytle
0d8beeb4e5 fix(hooks): use globalThis singleton for handler registry to survive bundle splitting
Without this fix, the bundler can emit multiple copies of internal-hooks
into separate chunks. registerInternalHook writes to one Map instance
while triggerInternalHook reads from another — resulting in hooks that
silently fire with zero handlers regardless of how many were registered.

Reproduce: load a hook via hooks.external.entries (loader reads one chunk),
then send a message:transcribed event (get-reply imports a different chunk).
The handler list is empty; the hook never runs.

Fix: use globalThis.__openclaw_internal_hook_handlers__ as a shared
singleton. All module copies check for and reuse the same Map, ensuring
registrations are always visible to triggers.
2026-03-03 00:27:10 +00:00
Peter Steinberger
1e8afa16f0 fix: apply config env vars before model discovery (#32295) (thanks @hsiaoa) 2026-03-03 00:25:24 +00:00
hsiaoa
65dc3ee76c models-config: apply config env vars before implicit provider discovery 2026-03-03 00:25:24 +00:00
Hunter Miller
f4682742d9 feat: update tlon channel/plugin to be more fully featured (#21208)
* feat(tlon): sync with openclaw-tlon master

- Add tlon CLI tool registration with binary lookup
- Add approval, media, settings, foreigns, story, upload modules
- Add http-api wrapper for Urbit connection patching
- Update types for defaultAuthorizedShips support
- Fix type compatibility with core plugin SDK
- Stub uploadFile (API not yet available in @tloncorp/api-beta)
- Remove incompatible test files (security, sse-client, upload)

* chore(tlon): remove dead code

Remove unused Urbit channel client files:
- channel-client.ts
- channel-ops.ts
- context.ts

These were not imported anywhere in the extension.

* feat(tlon): add image upload support via @tloncorp/api

- Import configureClient and uploadFile from @tloncorp/api
- Implement uploadImageFromUrl using uploadFile
- Configure API client before media uploads
- Update dependency to github:tloncorp/api-beta#main

* fix(tlon): restore SSRF protection with event ack tracking

- Restore context.ts and channel-ops.ts for SSRF support
- Restore sse-client.ts with urbitFetch for SSRF-protected requests
- Add event ack tracking from openclaw-tlon (acks every 20 events)
- Pass ssrfPolicy through authenticate() and UrbitSSEClient
- Fixes security regression from sync with openclaw-tlon

* fix(tlon): restore buildTlonAccountFields for allowPrivateNetwork

The inlined payload building was missing allowPrivateNetwork field,
which would prevent the setting from being persisted to config.

* fix(tlon): restore SSRF protection in probeAccount

- Restore channel-client.ts for UrbitChannelClient
- Use UrbitChannelClient with ssrfPolicy in probeAccount
- Ensures account probe respects allowPrivateNetwork setting

* feat(tlon): add ownerShip to setup flow

ownerShip should always be set as it controls who receives
approval requests and can approve/deny actions.

* chore(tlon): remove unused http-api.ts

After restoring SSRF protection, probeAccount uses UrbitChannelClient
instead of @urbit/http-api. The http-api.ts wrapper is no longer needed.

* refactor(tlon): simplify probeAccount to direct /~/name request

No channel needed - just authenticate and GET /~/name.
Removes UrbitChannelClient, keeping only UrbitSSEClient for monitor.

* chore(tlon): add logging for event acks

* chore(tlon): lower ack threshold to 5 for testing

* fix(tlon): address security review issues

- Fix SSRF in upload.ts: use urbitFetch with SSRF protection
- Fix SSRF in media.ts: use urbitFetch with SSRF protection
- Add command whitelist to tlon tool to prevent command injection
- Add getDefaultSsrFPolicy() helper for uploads/downloads

* fix(tlon): restore auth retry and add reauth on SSE reconnect

- Add authenticateWithRetry() helper with exponential backoff (restores lost logic from #39)
- Add onReconnect callback to re-authenticate when SSE stream reconnects
- Add UrbitSSEClient.updateCookie() method for proper cookie normalization on reauth

* fix(tlon): add infinite reconnect with reset after max attempts

Instead of giving up after maxReconnectAttempts, wait 10 seconds then
reset the counter and keep trying. This ensures the monitor never
permanently disconnects due to temporary network issues.

* test(tlon): restore security, sse-client, and upload tests

- security.test.ts: DM allowlist, group invite, bot mention detection, ship normalization
- sse-client.test.ts: subscription handling, cookie updates, reconnection params
- upload.test.ts: image upload with SSRF protection, error handling

* fix(tlon): restore DM partner ship extraction for proper routing

- Add extractDmPartnerShip() to extract partner from 'whom' field
- Use partner ship for routing (more reliable than essay.author)
- Explicitly ignore bot's own outbound DM events
- Log mismatch between author and partner for debugging

* chore(tlon): restore ack threshold to 20

* chore(tlon): sync slash commands support from upstream

- Add stripBotMention for proper CommandBody parsing
- Add command authorization logic for owner-only slash commands
- Add CommandAuthorized and CommandSource to context payload

* fix(tlon): resolve TypeScript errors in tests and monitor

- Store validated account url/code before closure to fix type narrowing
- Fix test type annotations for mode rules
- Add proper Response type cast in sse-client mock
- Use optional chaining for init properties

* docs(tlon): update docs for new config options and capabilities

- Document ownerShip for approval system
- Document autoAcceptDmInvites and autoAcceptGroupInvites
- Update status to reflect rich text and image support
- Add bundled skill section
- Update notes with formatting and image details
- Fix pnpm-lock.yaml conflict

* docs(tlon): fix dmAllowlist description and improve allowPrivateNetwork docs

- Correct dmAllowlist: empty means no DMs allowed (not allow all)
- Promote allowPrivateNetwork to its own section with examples
- Add warning about SSRF protection implications

* docs(tlon): clarify ownerShip is auto-authorized everywhere

- Add ownerShip to minimal config example (recommended)
- Document that owner is automatically allowed for DMs and channels
- No need to add owner to dmAllowlist or defaultAuthorizedShips

* docs(tlon): add capabilities table, troubleshooting, and config reference

Align with Matrix docs format:
- Capabilities table for quick feature reference
- Troubleshooting section with common failures
- Configuration reference with all options

* docs(tlon): fix reactions status and expand bundled skill section

- Reactions ARE supported via bundled skill (not missing)
- Add link to skill GitHub repo
- List skill capabilities: contacts, channels, groups, DMs, reactions, settings

* fix(tlon): use crypto.randomUUID instead of Math.random for channel ID

Fixes security test failure - Math.random is flagged as weak randomness.

* docs: fix markdown lint - add blank line before </Step>

* fix: address PR review issues for tlon plugin

- upload.ts: Use fetchWithSsrFGuard directly instead of urbitFetch to
  preserve full URL path when fetching external images; add release() call
- media.ts: Same fix - use fetchWithSsrFGuard for external media downloads;
  add release() call to clean up resources
- channel.ts: Use urbitFetch for poke API to maintain consistent SSRF
  protection (DNS pinning + redirect handling)
- upload.test.ts: Update mocks to use fetchWithSsrFGuard instead of urbitFetch

Addresses blocking issues from jalehman's review:
1. Fixed incorrect URL being fetched (validateUrbitBaseUrl was stripping path)
2. Fixed missing release() calls that could leak resources
3. Restored guarded fetch semantics for poke operations

* docs: add tlon changelog fragment

* style: format tlon monitor

* fix: align tlon lockfile and sse id generation

* docs: fix onboarding markdown list spacing

---------

Co-authored-by: Josh Lehman <josh@martian.engineering>
2026-03-02 16:23:42 -08:00
Peter Steinberger
d37ad9d866 test(perf): slim ios team-id harness and add perf budget guard 2026-03-03 00:20:46 +00:00
Peter Steinberger
4b3d9f4fb2 test(perf): trim fixture churn in install and cron suites 2026-03-03 00:20:46 +00:00
Peter Steinberger
6bf84ac28c perf(runtime): reduce hot-path config and routing overhead 2026-03-03 00:20:46 +00:00
Glucksberg
051b380d38 fix(hooks): return 200 instead of 202 for webhook responses (#28204)
* fix(hooks): return 200 instead of 202 for webhook responses (#22036)

* docs(webhook): document 200 status for hooks agent

* chore(changelog): add webhook ack note openclaw#28204 thanks @Glucksberg

---------

Co-authored-by: Shakker <shakkerdroid@gmail.com>
2026-03-03 00:19:31 +00:00
Hershey Goldberger
dee7cda1ec feat(voice-call): add call-waiting queue for inbound Twilio calls 2026-03-03 00:17:21 +00:00
Peter Steinberger
8824565c2a chore(cli): refresh tagline set 2026-03-03 00:17:18 +00:00
Peter Steinberger
d7dda4dd1a refactor: dedupe channel outbound and monitor tests 2026-03-03 00:15:15 +00:00
Peter Steinberger
6a42d09129 refactor: dedupe gateway config and infra flows 2026-03-03 00:15:14 +00:00
Peter Steinberger
fd3ca8a34c refactor: dedupe agent and browser cli helpers 2026-03-03 00:15:00 +00:00
Peter Steinberger
fe14be2352 Merge pull request #4325: fix(voice-call) verify stale calls with provider 2026-03-03 00:14:37 +00:00
Peter Steinberger
e870cee542 fix: restore control-ui basePath webhook passthrough (#32311) (thanks @ademczuk) 2026-03-03 00:11:13 +00:00
ademczuk
3e9c8721fb fix(gateway): let non-GET requests fall through controlUi routing when basePath is set
When controlUiBasePath is set, classifyControlUiRequest returned
method-not-allowed (405) for all non-GET/HEAD requests under basePath,
blocking plugin webhook handlers (BlueBubbles, Mattermost, etc.) from
receiving POST requests. This is a 2026.3.1 regression.

Return not-control-ui instead, matching the empty-basePath behavior, so
requests fall through to plugin HTTP handlers. Remove the now-dead
method-not-allowed type variant, handler branch, and utility function.

Closes #31983
Closes #32275

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:11:13 +00:00
Peter Steinberger
11c397ef46 docs: strengthen prompt injection warning for weaker models 2026-03-03 00:06:39 +00:00
Peter Steinberger
4bfbf2dfff test(refactor): dedupe secret resolver posix fixtures and add registry cache regression 2026-03-03 00:05:39 +00:00
Peter Steinberger
1d0a4d1be2 refactor(runtime): harden channel-registry cache invalidation and split outbound delivery flow 2026-03-03 00:05:39 +00:00
Shakker
d6491d8d71 fix: narrow webhook event provider call id typing 2026-03-03 00:05:03 +00:00
Peter Steinberger
6b85ec3022 docs: tighten subscription guidance and update MiniMax M2.5 refs 2026-03-03 00:02:37 +00:00
Peter Steinberger
3e1ec5ad8b fix: land Twilio signature port-variant verification (#25140) (thanks @drvoss) 2026-03-03 00:02:03 +00:00
drvoss
c5ddba52d7 fix(voice-call): retry Twilio signature verification without port in URL
Twilio signs webhook requests using the URL without the port component,
even when the publicUrl config includes a non-standard port. Add a fallback
that strips the port from the verification URL when initial validation fails,
matching the behavior of Twilio's official helper library.

Closes #6334
2026-03-03 00:02:03 +00:00
Peter Steinberger
381bb867ac fix: land external Twilio outbound-api webhook calls (#31181) (thanks @scoootscooob) 2026-03-02 23:56:41 +00:00
scoootscooob
24dcd68f42 fix: rename createInboundCall → createWebhookCall, preserve event direction
Address Greptile review: externally-initiated outbound-api calls were
stored with hardcoded direction: "inbound". Now createWebhookCall accepts
a direction parameter so the CallRecord accurately reflects the event's
actual direction. Also skip inboundGreeting for outbound calls and add a
test asserting inbound direction is preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:56:41 +00:00
scoootscooob
a1b4a0066b fix(voice-call): accept externally-initiated Twilio outbound-api calls
Fixes #30900 — Calls initiated directly via the Twilio REST API
(Direction=outbound-api) were rejected as "unknown call" because
processEvent only auto-registered calls with direction=inbound.
External outbound-api calls now get registered in the CallManager
so the media stream is accepted. Inbound policy checks still only
apply to true inbound calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:56:41 +00:00
Peter Steinberger
a5b81d1c13 test(perf): remove long exec-delay sleep in secret resolver tests 2026-03-02 23:56:30 +00:00
Peter Steinberger
d3dc4e54f7 perf(runtime): trim hot-path allocations and cache channel plugin lookups 2026-03-02 23:56:30 +00:00
Peter Steinberger
dba47f349f fix: land Twilio inbound greeting for answered calls (#29121) (thanks @xinhuagu) 2026-03-02 23:54:54 +00:00
Xinhua Gu
fe4c627432 fix(voice-call): speak inbound greeting for twilio answered calls 2026-03-02 23:54:54 +00:00
Peter Steinberger
b8b8a5f314 fix(security): enforce explicit ingress owner context 2026-03-02 23:50:36 +00:00
Peter Steinberger
ea3b7dfde5 fix(channels): normalize MIME kind parsing and reaction fallbacks 2026-03-02 23:48:11 +00:00
Peter Steinberger
32ecd6f579 refactor(auto-reply,telegram,config): extract guard and forum helpers 2026-03-02 23:48:11 +00:00
Peter Steinberger
dc825e59f5 refactor: unify system.run approval cwd revalidation 2026-03-02 23:46:54 +00:00
Peter Steinberger
500d7cb107 fix: revalidate approval cwd before system.run execution 2026-03-02 23:42:10 +00:00
Brian Mendonca
1234cc4c31 Feishu: reply to topic roots (#29968)
* Feishu: reply to topic roots

* Changelog: note Feishu topic-root reply targeting

---------

Co-authored-by: bmendonca3 <208517100+bmendonca3@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:41:36 -06:00
Peter Steinberger
abec8a4f0a test: preserve windows backup-rotation compose coverage (#32286) (thanks @jalehman) 2026-03-02 23:38:17 +00:00
Josh Lehman
41bdf2df41 test: skip chmod-dependent backup rotation tests on Windows
chmod is a no-op on Windows — file permissions always report 0o666
regardless of what was set, so asserting 0o600 will never pass.
2026-03-02 23:38:17 +00:00
Peter Steinberger
c20ee11348 fix: harden fs-safe write boundary checks 2026-03-02 23:36:23 +00:00
ningding97
4d19dc8671 test(cron): assert embedded model on last call to avoid bun ordering flake
Bun runs can trigger multiple embedded agent invocations in a single cron
turn (e.g. retries/fallbacks), making assertions against call[0] flaky.
Assert against the last invocation instead.
2026-03-02 23:36:13 +00:00
Peter Steinberger
73e08ed7b0 test: expand reminder guard fail-closed coverage (#32255) (thanks @scoootscooob) 2026-03-02 23:35:14 +00:00
scoootscooob
5868344ade fix(reminder): do not suppress note when sessionKey is unavailable
Address Greptile review: when sessionKey is undefined the fallback
matched any enabled cron job, which could silently suppress the guard
note due to jobs from unrelated sessions.  Return false instead so the
note always appears when session scoping is not possible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:35:14 +00:00
scoootscooob
abb0252a1a fix(reply): suppress unscheduled-reminder note when session already has active cron
Before appending the "I did not schedule a reminder" guard note, check the
cron store for enabled jobs matching the current session key.  This prevents
false positives when the agent references an existing cron created in a
prior turn (e.g. "I'll ping you when it's done" while a monitoring cron is
already running).

The check only fires on the rare path where the text matches commitment
patterns AND no cron was added in the current turn, so the added I/O is
negligible.

Closes #32228

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:35:14 +00:00
Mark L
55f04636f3 fix(feishu): suppress stale missing-scope grant notices (openclaw#31870) thanks @liuxiaopai-ai
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on unrelated baseline lint in src/browser/chrome.ts)

Co-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:34:11 -06:00
YolenSong
f22fc17c78 feat(feishu): prefer thread_id for topic session routing (openclaw#29788) thanks @songyaolun
Verified:
- pnpm test -- extensions/feishu/src/bot.test.ts extensions/feishu/src/reply-dispatcher.test.ts
- pnpm build

Co-authored-by: songyaolun <26423459+songyaolun@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:33:08 -06:00
Peter Steinberger
28c88e9fa1 fix: harden telegram forum-service mention guard typing (#32262) (thanks @scoootscooob) 2026-03-02 23:32:53 +00:00
scoootscooob
58ad617e64 fix: detect forum service messages by field presence, not text absence
Stickers, voice notes, and captionless photos from the bot also lack
text and caption fields, so the previous check incorrectly classified
them as system messages and suppressed implicitMention.

Switch to checking for Telegram's forum_topic_* / general_forum_topic_*
service-message fields which only appear on actual service messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:32:53 +00:00
scoootscooob
dc2aa1e21d fix(telegram): also check caption for bot media replies
Address Greptile review feedback: bot media messages (photo/video) use
caption instead of text, so they would be incorrectly classified as
system messages.  Add !caption guard to the system message check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:32:53 +00:00
scoootscooob
8fdd1d2f05 fix(telegram): exclude forum topic system messages from implicitMention
When a Telegram Forum topic is created by the bot, Telegram generates a
system message with from.id=botId and empty text.  Every subsequent user
message in that topic has reply_to_message pointing to this system
message, causing the implicitMention check to fire and bypassing
requireMention for every single message.

Add a guard that recognises system messages (is_bot=true with no text)
and excludes them from implicit mention detection, so that only genuine
replies to bot messages trigger the bypass.

Closes #32256

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:32:53 +00:00
Peter Steinberger
bb60687b89 refactor(nodes): dedupe camera payload and node resolve helpers 2026-03-02 23:32:41 +00:00
Peter Steinberger
a282b459b9 fix(ci): remove unused chrome ws type import 2026-03-02 23:31:42 +00:00
Peter Steinberger
de77a36579 test: harden MIME normalization regression coverage (#32280) (thanks @Lucenx9) 2026-03-02 23:31:19 +00:00
Lucenx9
79e114a82f test(media): ensure WhatsApp scope rule is exercised in MIME regression 2026-03-02 23:31:19 +00:00
Lucenx9
7c7c22d66f test(media): use direct chatType in WhatsApp MIME regression case 2026-03-02 23:31:19 +00:00
Lucenx9
ec688d809f fix(media): normalize MIME kind detection for audio transcription 2026-03-02 23:31:19 +00:00
Sid
481da215b9 fix(feishu): persist dedup cache across gateway restarts via warmup (openclaw#31605) thanks @Sid-Qin
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini (fails on unrelated baseline test: src/config/config.legacy-config-detection.rejects-routing-allowfrom.test.ts)

Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:30:40 -06:00
Peter Steinberger
132794fe74 feat(security): audit workspace skill symlink escapes 2026-03-02 23:28:54 +00:00
Peter Steinberger
d4ec0ed3c7 docs(security): clarify trusted-local hardening-only cases 2026-03-02 23:28:54 +00:00
Peter Steinberger
2e0f5b73d1 fix(ci): stabilize cross-platform config test assertions 2026-03-02 23:28:24 +00:00
不做了睡大觉
66397c2855 fix(feishu): restore private chat pairing replies in Lark/Feishu (openclaw#31403) thanks @stakeswky
Verified:
- pnpm test -- extensions/feishu/src/bot.test.ts
- pnpm build

Co-authored-by: stakeswky <64798754+stakeswky@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:27:39 -06:00
Tak Hoffman
e2483a5381 Browser: fix ws RawData type import for dts build 2026-03-02 17:24:34 -06:00
Peter Steinberger
c703aa0fe9 fix(agents): align sandboxed ACP prompt guidance 2026-03-02 23:24:02 +00:00
Peter Steinberger
3bf19d6f40 fix(security): fail-close node camera URL downloads 2026-03-02 23:23:39 +00:00
Peter Steinberger
7365aefa19 fix(ci): resolve chrome websocket raw-data typing 2026-03-02 23:18:06 +00:00
Peter Steinberger
7066d5e192 refactor: extract shared sandbox and gateway plumbing 2026-03-02 23:16:47 +00:00
Sid
350d041eaf fix(feishu): serialize message handling per chat to prevent skipped messages (openclaw#31807) thanks @Sid-Qin
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on unrelated pre-existing TypeScript error in src/browser/chrome.ts)

Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:14:00 -06:00
Onur Solmaz
e05bcccde8 chore(pi): change wording in landpr slash prompt, prefer squash (#32279)
* chore(pi): remove rebase flow from landpr prompt

* chore(pi): prefer squash wording in landpr prompt
2026-03-03 00:13:11 +01:00
Vincent Koc
0954b6bf5f fix(hooks): propagate ephemeral sessionId through embedded tool contexts (#32273)
* fix(plugins): expose ephemeral sessionId in tool contexts for per-conversation isolation

The plugin tool context (`OpenClawPluginToolContext`) and tool hook
context (`PluginHookToolContext`) only provided `sessionKey`, which
is a durable channel identifier that survives /new and /reset.
Plugins like mem0 that need per-conversation isolation (e.g. mapping
Mem0 `run_id`) had no way to distinguish between conversations,
causing session-scoped memories to persist unbounded across resets.

Add `sessionId` (ephemeral UUID regenerated on /new and /reset) to:
- `OpenClawPluginToolContext` (factory context for plugin tools)
- `PluginHookToolContext` (before_tool_call / after_tool_call hooks)
- Internal `HookContext` for tool call wrappers

Thread the value from the run attempt through createOpenClawCodingTools
→ createOpenClawTools → resolvePluginTools and through the tool hook
wrapper.

Closes #31253

Made-with: Cursor

* fix(agents): propagate embedded sessionId through tool hook context

* test(hooks): cover sessionId in embedded tool hook contexts

* docs(changelog): add sessionId hook context follow-up note

* test(hooks): avoid toolCallId collision in after_tool_call e2e

---------

Co-authored-by: SidQin-cyber <sidqin0410@gmail.com>
2026-03-02 15:11:51 -08:00
Berton
3b3e47e15d Feishu: wire inbound message debounce (openclaw#31548) thanks @bertonhan
Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check (fails on unrelated pre-existing lint in ui/src/ui/views/agents-utils.ts and src/pairing/pairing-store.ts)
- pnpm test:macmini (previous run passed before rebase)

Co-authored-by: bertonhan <60309291+bertonhan@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:10:47 -06:00
Vincent Koc
8f3eb0f7b4 fix(browser): use CDP command probe for cdpReady health (#31421)
* fix(browser): validate cdp command channel health

* test(browser): cover stale cdp command channel readiness

* changelog: note cdp command-channel readiness check

* browser(cdp): detach ws message listener on health-probe cleanup
2026-03-02 15:10:28 -08:00
Peter Steinberger
0e16749f00 ci: fix lint and audit regressions on main 2026-03-02 23:08:23 +00:00
Peter Steinberger
7eda632324 refactor: split slack/discord/session maintenance helpers 2026-03-02 23:07:20 +00:00
不做了睡大觉
3043e68dfa fix(feishu): support Lark private chats as direct messages (openclaw#31400) thanks @stakeswky
Verified:
- pnpm test -- extensions/feishu/src/bot.checkBotMentioned.test.ts
- pnpm build
- pnpm check (blocked by unrelated baseline lint errors in untouched files)
- pnpm test:macmini (not run after pnpm check blocked)

Co-authored-by: stakeswky <64798754+stakeswky@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 17:04:42 -06:00
Peter Steinberger
36c6b63ea6 style(telegram): apply formatter to draft-stream warning 2026-03-02 23:04:13 +00:00
Peter Steinberger
fc1787fd4b feat(telegram): default streaming preview to partial 2026-03-02 23:04:12 +00:00
Peter Steinberger
2287d1ec13 test: micro-optimize slow suites and CLI command setup 2026-03-02 23:00:49 +00:00
Peter Steinberger
ba5ae5b4f1 perf(routing): cache route and mention regex resolution 2026-03-02 23:00:49 +00:00
Altay
a81704e622 fix(skills): scope skill-command APIs to respect agent allowlists (#32155)
* refactor(skills): use explicit skill-command scope APIs

* test(skills): cover scoped listing and telegram allowlist

* fix(skills): add mergeSkillFilters edge-case tests and simplify dead code

Cover unrestricted-co-tenant and empty-allowlist merge paths in
skill-commands tests. Remove dead ternary in bot-handlers pagination.
Add clarifying comments on undefined vs [] filter semantics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(skills): collapse scope functions into single listSkillCommandsForAgents

Replace listSkillCommandsForAgentIds, listSkillCommandsForAllAgents, and
the deprecated listSkillCommandsForAgents with a single function that
accepts optional agentIds and falls back to all agents when omitted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(skills): harden realpathSync race and add missing test coverage

- Wrap fs.realpathSync in try-catch to gracefully skip workspaces that
  disappear between existsSync and realpathSync (TOCTOU race).
- Log verbose diagnostics for missing/unresolvable workspace paths.
- Add test for overlapping allowlists deduplication on shared workspaces.
- Add test for graceful skip of missing workspaces.
- Add test for pagination callback without agent suffix (default agent).
- Clean up temp directories in skill-commands tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(telegram): warn when nativeSkillsEnabled but no agent route is bound

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use runtime.log instead of nonexistent runtime.warn

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:00:05 +03:00
Peter Steinberger
02eeb08e04 fix: enforce sandbox workspace mount mode (#32227) (thanks @guanyu-zhang) 2026-03-02 22:59:11 +00:00
Evan
7cbcbbc642 fix(sandbox): same fix for browser.ts - make /workspace bind mount read-only when workspaceAccess is not rw
The identical buggy logic from docker.ts also exists in browser.ts.
Applying the same fix here.
2026-03-02 22:58:09 +00:00
Evan
903e4dff35 fix(sandbox): make /workspace bind mount read-only when workspaceAccess is not rw
This ensures that when workspaceAccess is set to 'ro' or 'none', the
sandbox workspace (/workspace inside the container) is mounted as
read-only, matching the documented behavior.

Previously, the condition was:
  workspaceAccess === 'ro' && workspaceDir === agentWorkspaceDir

This was always false in 'ro' mode because workspaceDir equals
sandboxWorkspaceDir, not agentWorkspaceDir.

Now the logic is simplified:
  - 'rw': /workspace is writable
  - 'ro': /workspace is read-only
  - 'none': /workspace is read-only
2026-03-02 22:58:09 +00:00
12
905c3357eb fix(feishu): encode non-ASCII filenames in file uploads (openclaw#31328) thanks @Kay-051
Verified:
- pnpm test extensions/feishu/src/media.test.ts

Co-authored-by: Kay-051 <210470990+Kay-051@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 16:56:57 -06:00
dunamismax
f431f20c48 fix(followup): pass currentChannelId to queued message agent runs
The followup runner (which processes queued messages) was calling
runEmbeddedPiAgent without currentChannelId or currentThreadTs.
This meant the message tool's toolContext had no channel routing
info, causing reactions (and other target-inferred actions) to
fail with 'Action react requires a target' on queued messages.

Pass originatingTo as currentChannelId so the message tool can
infer the reaction target from context, matching the behavior
of the initial (non-queued) agent run.
2026-03-02 22:53:04 +00:00
dunamismax
d9fdec12ab fix(signal): fall back to toolContext.currentMessageId for reactions
Signal reactions required an explicit messageId parameter, unlike
Telegram which already fell back to toolContext.currentMessageId.
This made agent-initiated reactions fail on Signal because the
inbound message ID was available in tool context but never used.

- Destructure toolContext in Signal action handler
- Fall back to toolContext.currentMessageId when messageId omitted
- Update reaction schema descriptions (not Telegram-specific)
- Add tests for fallback and missing-messageId rejection

Closes #17651
2026-03-02 22:53:04 +00:00
Peter Steinberger
f25be781c4 fix: honor chat completions message-channel header (#30462) (thanks @bmendonca3) 2026-03-02 22:51:32 +00:00
bmendonca3
0d8f14fed3 gateway: cover default message-channel fallback 2026-03-02 22:51:32 +00:00
bmendonca3
842a79cf99 Gateway: honor message-channel header for chat completions 2026-03-02 22:51:32 +00:00
Peter Steinberger
caae34cbaf refactor: unify message hook mapping and async dispatch 2026-03-02 22:51:28 +00:00
Mark L
fa47f74c0f Feishu: normalize group announce targets to chat ids (openclaw#31546) thanks @liuxiaopai-ai
Verified:
- pnpm build
- pnpm check (fails on unrelated existing main-branch lint violations in ui/src/ui/views/agents-utils.ts and src/pairing/pairing-store.ts)
- pnpm test:macmini

Co-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 16:50:55 -06:00
Bob
ac11f0af73 Security: enforce ACP sandbox inheritance for sessions_spawn (#32254)
* Security: enforce ACP sandbox inheritance in sessions_spawn

* fix: add changelog attribution for ACP sandbox inheritance (#32254) (thanks @dutifulbob)

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-02 23:50:38 +01:00
Peter Steinberger
a78ec81ae6 fix: align exec no-output timeout defaults (#32235) (thanks @bmendonca3) 2026-03-02 22:47:03 +00:00
bmendonca3
be578b43d3 secrets: default exec no-output timeout to timeoutMs 2026-03-02 22:47:03 +00:00
Peter Steinberger
0b5d8e5b47 fix: harden discord audio preflight mention detection (#32136) (thanks @jnMetaCode) 2026-03-02 22:45:41 +00:00
jiangnan
b9b47f5002 fix(discord): use correct content_type property for audio attachment detection
The preflight audio transcription detection used camelCase `contentType`
but Discord's APIAttachment type uses snake_case `content_type`. This
caused `hasAudioAttachment` to always be false, preventing voice message
transcription from triggering in guild channels where mention detection
requires audio preflight.

Fixes #30034

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:45:41 +00:00
Peter Steinberger
319b7c68a1 fix: preserve inline-status newlines (#32224) (thanks @scoootscooob) 2026-03-02 22:43:10 +00:00
scoootscooob
6200e242b2 fix(auto-reply): preserve newlines in stripInlineStatus and extractInlineSimpleCommand
The /\s+/g whitespace normalizer collapsed newlines along with spaces/tabs,
destroying paragraph structure in multi-line messages before they reached
the LLM. Use /[^\S\n]+/g to only collapse horizontal whitespace while
preserving line breaks.

Closes #32216

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:43:10 +00:00
Mark L
5b5ccb0769 fix(ui): avoid toSorted in cron suggestions (#31775)
* Control UI: avoid toSorted in cron suggestions

* Control UI: make sortLocaleStrings legacy-safe

* fix(ui): use sort fallback in locale string helper

* fix(ui): remove toSorted from locale helper

* fix(ui): remove toSorted from locale helper

* fix(ui): remove toSorted from locale helper

* fix(ui): remove toSorted from locale helper

* fix(ui): remove toSorted from locale helper

* fix(ui): avoid sort in locale helper for browser compatibility

* ui: avoid unnecessary assertions in locale sort

* changelog: credit browser-compat cron fix PR

* fix(ui): use native locale sort in compatibility helper

* ui: use compat merge-sort for locale strings

* style: format locale sort helper

* style: fix oxfmt ordering in agents utils

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-03-02 14:41:01 -08:00
ademczuk
0743463b88 fix(webchat): suppress NO_REPLY token in chat transcript rendering (#32183)
* fix(types): resolve pre-existing TS errors in agent-components and pairing-store

- agent-components.ts: normalizeDiscordAllowList returns {allowAll, ids, names},
  not an array — use ids.values().next().value instead of [0] indexing
- pairing-store.ts: add non-null assertions for stat after cache-miss guard
  (resolveAllowFromReadCacheOrMissing returns early when stat is null)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webchat): suppress NO_REPLY token in chat transcript rendering

Filter assistant NO_REPLY-only entries from chat.history responses at
the gateway API boundary and add client-side defense-in-depth guards in
the UI chat controller so internal silent tokens never render as visible
chat bubbles.

Two-layer fix:
1. Gateway: extractAssistantTextForSilentCheck + isSilentReplyText
   filter in sanitizeChatHistoryMessages (entry.text takes precedence
   over entry.content to avoid dropping messages with real text)
2. UI: isAssistantSilentReply + isSilentReplyStream guards on all 5
   message insertion points in handleChatEvent and loadChatHistory

Fixes #32015

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webchat): align isAssistantSilentReply text/content precedence with gateway

* webchat: tighten NO_REPLY transcript and delta filtering

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-03-02 16:39:08 -06:00
Peter Steinberger
48155729fc fix: document Homebrew stable node path resolution (#32185) (thanks @scoootscooob) 2026-03-02 22:37:09 +00:00
scoootscooob
163f5184b3 fix(daemon): handle versioned node@XX Homebrew formulas in Cellar resolution
Address review feedback: versioned Homebrew formulas (node@22, node@20)
use keg-only paths where the stable symlink is at <prefix>/opt/<formula>/bin/node,
not <prefix>/bin/node. Updated resolveStableNodePath to:

1. Try <prefix>/opt/<formula>/bin/node first (works for both default + versioned)
2. Fall back to <prefix>/bin/node for the default "node" formula
3. Return the original Cellar path if neither stable path exists

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:37:09 +00:00
scoootscooob
8950c59581 fix(daemon): resolve Homebrew Cellar path to stable symlink for gateway install
When `openclaw gateway install` runs under Homebrew Node, `process.execPath`
resolves to the versioned Cellar path (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node).
This path breaks when Homebrew upgrades Node, silently killing the gateway daemon.

Resolve Cellar paths to the stable Homebrew symlink (/opt/homebrew/bin/node)
which Homebrew updates automatically during upgrades.

Closes #32182

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:37:09 +00:00
Peter Steinberger
29dde80c3e fix: harden message hook session/group context and add integration coverage (#9859) (thanks @Drickon) 2026-03-02 22:34:43 +00:00
Eric Lytle
b5102ba4f9 fix(hooks): add isGroup and groupId to message:sent context
Adds group context fields to MessageSentHookContext so hooks can
correlate sent events with received events for the same conversation.

Previously, message:received included isGroup/groupId but message:sent
did not, forcing hooks to use mismatched identifiers (e.g. groupId vs
numeric chat ID) when tracking conversations.

Fields are derived from MsgContext in dispatch-from-config and threaded
through route-reply and deliver via the mirror parameter.

Addresses feedback from matskevich (production user, 550+ events)
reported on PR #6797.
2026-03-02 22:34:43 +00:00
Eric Lytle
7ad6a04058 fix(hooks): resolve type/lint errors from CI check failure
Arrow function passed to registerInternalHook was implicitly returning
the number from Array.push(), which is not assignable to void | Promise<void>.
Use block body to discard the return value.
2026-03-02 22:34:43 +00:00
Eric Lytle
e0b8b80067 feat(hooks): add message:transcribed and message:preprocessed internal hooks
Adds two new internal hook events that fire after media/link processing:

- message:transcribed: fires when audio has been transcribed, providing
  the transcript text alongside the original body and media metadata.
  Useful for logging, analytics, or routing based on spoken content.

- message:preprocessed: fires for every message after all media + link
  understanding completes. Gives hooks access to the fully enriched body
  (transcripts, image descriptions, link summaries) before the agent sees it.

Both hooks are added in get-reply.ts, after applyMediaUnderstanding and
applyLinkUnderstanding. message:received and message:sent are already
in upstream (f07bb8e8) and are not duplicated here.

Typed contexts (MessageTranscribedHookContext, MessagePreprocessedHookContext)
and type guards (isMessageTranscribedEvent, isMessagePreprocessedEvent) added
to internal-hooks.ts alongside the existing received/sent types.

Test coverage in src/hooks/message-hooks.test.ts.
2026-03-02 22:34:43 +00:00
Vincent Koc
44183c6eb1 fix(hooks): consolidate after_tool_call context + single-fire behavior (#32201)
* fix(hooks): deduplicate after_tool_call hook in embedded runs

(cherry picked from commit c129a1a74b)

* fix(hooks): propagate sessionKey in after_tool_call context

The after_tool_call hook in handleToolExecutionEnd was passing
`sessionKey: undefined` in the ToolContext, even though the value is
available on ctx.params. This broke plugins that need session context
in after_tool_call handlers (e.g., for per-session audit trails or
security logging).

- Add `sessionKey` to the `ToolHandlerParams` Pick type
- Pass `ctx.params.sessionKey` through to the hook context
- Add test assertion to prevent regression

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit b7117384fc)

* fix(hooks): thread agentId through to after_tool_call hook context

Follow-up to #30511 — the after_tool_call hook context was passing
`agentId: undefined` because SubscribeEmbeddedPiSessionParams did not
carry the agent identity. This threads sessionAgentId (resolved in
attempt.ts) through the session params into the tool handler context,
giving plugins accurate agent-scoped context for both before_tool_call
and after_tool_call hooks.

Changes:
- Add `agentId?: string` to SubscribeEmbeddedPiSessionParams
- Add "agentId" to ToolHandlerParams Pick type
- Pass `agentId: sessionAgentId` at the subscribeEmbeddedPiSession()
  call site in attempt.ts
- Wire ctx.params.agentId into the after_tool_call hook context
- Update tests to assert agentId propagation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
(cherry picked from commit aad01edd3e)

* changelog: credit after_tool_call hook contributors

* Update CHANGELOG.md

* agents: preserve adjusted params until tool end

* agents: emit after_tool_call with adjusted args

* tests: cover adjusted after_tool_call params

* tests: align adapter after_tool_call expectation

---------

Co-authored-by: jbeno <jim@jimbeno.net>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 14:33:37 -08:00
Peter Steinberger
f9cbcfca0d refactor: modularize slack/config/cron/daemon internals 2026-03-02 22:30:21 +00:00
Peter Steinberger
5d3032b293 fix: align gateway and zalouser typing imports 2026-03-02 22:29:18 +00:00
Peter Steinberger
11adaa15a8 test: isolate high-variance suites in parallel scheduler 2026-03-02 22:29:13 +00:00
Peter Steinberger
3cb851be90 test: micro-optimize heavy gateway/browser/telegram suites 2026-03-02 22:29:04 +00:00
Peter Steinberger
1fa2488db1 fix: wire telegram disableAudioPreflight config validation and precedence tests (#23067) (thanks @yangnim21029) 2026-03-02 22:26:52 +00:00
gemini-3-flash
d3cb85eaf5 feat(telegram): add disableAudioPreflight config for groups and topics 2026-03-02 22:26:52 +00:00
Peter Steinberger
d89c25d69e fix: support parakeet-mlx output-dir transcript parsing (#9177) (thanks @mac-110) 2026-03-02 22:22:17 +00:00
Alessandro Rodi
f257818ea5 fix(sandbox): prevent Windows PATH from poisoning docker exec (#13873)
* fix(sandbox): prevent Windows PATH from poisoning docker exec shell lookup

On Windows hosts, `buildDockerExecArgs` passes the host PATH env var
(containing Windows paths like `C:\Windows\System32`) to `docker exec -e
PATH=...`. Docker uses this PATH to resolve the executable argument
(`sh`), which fails because Windows paths don't exist in the Linux
container — producing `exec: "sh": executable file not found in $PATH`.

Two changes:
- Skip PATH in the `-e` env loop (it's already handled separately via
  OPENCLAW_PREPEND_PATH + shell export)
- Use absolute `/bin/sh` instead of bare `sh` to eliminate PATH
  dependency entirely

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: add braces around continue to satisfy linter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): update assertion to match /bin/sh in buildDockerExecArgs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:17:33 -06:00
magos-minor
350ac0d824 fix(daemon): default NODE_USE_SYSTEM_CA=1 on macOS 2026-03-02 22:17:14 +00:00
Peter Steinberger
19fafed11d refactor(zalouser): extract policy and message helpers 2026-03-02 22:16:47 +00:00
garnetlyx
ffa7c13c9b fix(voice-call): verify call status with provider before loading stale calls
On gateway restart, persisted non-terminal calls are now verified with
the provider (Twilio/Plivo/Telnyx) before being restored to memory.
This prevents phantom calls from blocking the concurrent call limit.

- Add getCallStatus() to VoiceCallProvider interface
- Implement for all providers with SSRF-guarded fetch
- Transient errors (5xx, network) keep the call with timer fallback
- 404/known-terminal statuses drop the call
- Restart max-duration timers for restored answered calls
- Skip calls older than maxDurationSeconds or without providerCallId
2026-03-01 22:13:24 -08:00
1099 changed files with 63419 additions and 20314 deletions

View File

@@ -51,6 +51,10 @@ vendor/
# Keep the rest of apps/ and vendor/ excluded to avoid a large build context.
!apps/shared/
!apps/shared/OpenClawKit/
!apps/shared/OpenClawKit/Sources/
!apps/shared/OpenClawKit/Sources/OpenClawKit/
!apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/
!apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json
!apps/shared/OpenClawKit/Tools/
!apps/shared/OpenClawKit/Tools/CanvasA2UI/
!apps/shared/OpenClawKit/Tools/CanvasA2UI/**

View File

@@ -8,6 +8,7 @@ self-hosted-runner:
- blacksmith-8vcpu-windows-2025
- blacksmith-16vcpu-ubuntu-2404
- blacksmith-16vcpu-windows-2025
- blacksmith-32vcpu-windows-2025
- blacksmith-16vcpu-ubuntu-2404-arm
# Ignore patterns for known issues

View File

@@ -1,7 +1,7 @@
name: Setup Node environment
description: >
Initialize submodules with retry, install Node 22, pnpm, optionally Bun,
and run pnpm install. Requires actions/checkout to run first.
and optionally run pnpm install. Requires actions/checkout to run first.
inputs:
node-version:
description: Node.js version to install.
@@ -15,6 +15,14 @@ inputs:
description: Whether to install Bun alongside Node.
required: false
default: "true"
use-sticky-disk:
description: Use Blacksmith sticky disks for pnpm store caching.
required: false
default: "false"
install-deps:
description: Whether to run pnpm install after environment setup.
required: false
default: "true"
frozen-lockfile:
description: Whether to use --frozen-lockfile for install.
required: false
@@ -40,13 +48,14 @@ runs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
check-latest: true
check-latest: false
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: ${{ inputs.pnpm-version }}
cache-key-suffix: "node22"
use-sticky-disk: ${{ inputs.use-sticky-disk }}
- name: Setup Bun
if: inputs.install-bun == 'true'
@@ -63,10 +72,12 @@ runs:
if command -v bun &>/dev/null; then bun -v; fi
- name: Capture node path
if: inputs.install-deps == 'true'
shell: bash
run: echo "NODE_BIN=$(dirname "$(node -p "process.execPath")")" >> "$GITHUB_ENV"
- name: Install dependencies
if: inputs.install-deps == 'true'
shell: bash
env:
CI: "true"

View File

@@ -9,6 +9,18 @@ inputs:
description: Suffix appended to the cache key.
required: false
default: "node22"
use-sticky-disk:
description: Use Blacksmith sticky disks instead of actions/cache for pnpm store.
required: false
default: "false"
use-restore-keys:
description: Whether to use restore-keys fallback for actions/cache.
required: false
default: "true"
use-actions-cache:
description: Whether to restore/save pnpm store with actions/cache.
required: false
default: "true"
runs:
using: composite
steps:
@@ -38,7 +50,22 @@ runs:
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
- name: Mount pnpm store sticky disk
if: inputs.use-sticky-disk == 'true'
uses: useblacksmith/stickydisk@v1
with:
key: ${{ github.repository }}-pnpm-store-${{ runner.os }}-${{ inputs.cache-key-suffix }}
path: ${{ steps.pnpm-store.outputs.path }}
- name: Restore pnpm store cache (exact key only)
if: inputs.use-actions-cache == 'true' && inputs.use-sticky-disk != 'true' && inputs.use-restore-keys != 'true'
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ inputs.cache-key-suffix }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Restore pnpm store cache (with fallback keys)
if: inputs.use-actions-cache == 'true' && inputs.use-sticky-disk != 'true' && inputs.use-restore-keys == 'true'
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}

View File

@@ -32,12 +32,13 @@ jobs:
# Push to main keeps broad coverage.
changed-scope:
needs: [docs-scope]
if: needs.docs-scope.outputs.docs_only != 'true'
if: github.event_name == 'pull_request' && needs.docs-scope.outputs.docs_only != 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
outputs:
run_node: ${{ steps.scope.outputs.run_node }}
run_macos: ${{ steps.scope.outputs.run_macos }}
run_android: ${{ steps.scope.outputs.run_android }}
run_windows: ${{ steps.scope.outputs.run_windows }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -57,75 +58,11 @@ jobs:
BASE="${{ github.event.pull_request.base.sha }}"
fi
CHANGED="$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN")"
if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then
# Fail-safe: run broad checks if detection fails.
echo "run_node=true" >> "$GITHUB_OUTPUT"
echo "run_macos=true" >> "$GITHUB_OUTPUT"
echo "run_android=true" >> "$GITHUB_OUTPUT"
exit 0
fi
run_node=false
run_macos=false
run_android=false
has_non_docs=false
has_non_native_non_docs=false
while IFS= read -r path; do
[ -z "$path" ] && continue
case "$path" in
docs/*|*.md|*.mdx)
continue
;;
*)
has_non_docs=true
;;
esac
case "$path" in
# Generated protocol models are already covered by protocol:check and
# should not force the full native macOS lane.
apps/macos/Sources/OpenClawProtocol/*|apps/shared/OpenClawKit/Sources/OpenClawProtocol/*)
;;
apps/macos/*|apps/ios/*|apps/shared/*|Swabble/*)
run_macos=true
;;
esac
case "$path" in
apps/android/*|apps/shared/*)
run_android=true
;;
esac
case "$path" in
src/*|test/*|extensions/*|packages/*|scripts/*|ui/*|.github/*|openclaw.mjs|package.json|pnpm-lock.yaml|pnpm-workspace.yaml|tsconfig*.json|vitest*.ts|tsdown.config.ts|.oxlintrc.json|.oxfmtrc.jsonc)
run_node=true
;;
esac
case "$path" in
apps/android/*|apps/ios/*|apps/macos/*|apps/shared/*|Swabble/*|appcast.xml)
;;
*)
has_non_native_non_docs=true
;;
esac
done <<< "$CHANGED"
# If there are non-doc files outside native app trees, keep Node checks enabled.
if [ "$run_node" = false ] && [ "$has_non_docs" = true ] && [ "$has_non_native_non_docs" = true ]; then
run_node=true
fi
echo "run_node=${run_node}" >> "$GITHUB_OUTPUT"
echo "run_macos=${run_macos}" >> "$GITHUB_OUTPUT"
echo "run_android=${run_android}" >> "$GITHUB_OUTPUT"
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
# Build dist once for Node-relevant changes and share it with downstream jobs.
build-artifacts:
needs: [docs-scope, changed-scope, check]
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
@@ -138,6 +75,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Build dist
run: pnpm build
@@ -164,6 +102,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Download dist artifact
uses: actions/download-artifact@v4
@@ -175,7 +114,7 @@ jobs:
run: pnpm release:check
checks:
needs: [docs-scope, changed-scope, check]
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
strategy:
@@ -207,6 +146,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "${{ matrix.runtime == 'bun' }}"
use-sticky-disk: "true"
- name: Configure Node test resources
if: (github.event_name != 'push' || matrix.runtime != 'bun') && matrix.task == 'test' && matrix.runtime == 'node'
@@ -223,8 +163,8 @@ jobs:
# Types, lint, and format check.
check:
name: "check"
needs: [docs-scope]
if: needs.docs-scope.outputs.docs_only != 'true'
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- name: Checkout
@@ -236,6 +176,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Check types and lint and oxfmt
run: pnpm check
@@ -275,6 +216,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Run ${{ matrix.tool }} dead-code scan
run: ${{ matrix.command }}
@@ -300,6 +242,7 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "true"
- name: Check docs
run: pnpm check:docs
@@ -342,6 +285,8 @@ jobs:
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
install-deps: "false"
- name: Setup Python
uses: actions/setup-python@v5
@@ -385,15 +330,15 @@ jobs:
run: pre-commit run --all-files pnpm-audit-prod
checks-windows:
needs: [docs-scope, changed-scope, build-artifacts, check]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true')
runs-on: blacksmith-16vcpu-windows-2025
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_windows == 'true')
runs-on: blacksmith-32vcpu-windows-2025
timeout-minutes: 45
env:
NODE_OPTIONS: --max-old-space-size=4096
# Keep total concurrency predictable on the 16 vCPU runner:
# `scripts/test-parallel.mjs` runs some vitest suites in parallel processes.
OPENCLAW_TEST_WORKERS: 2
NODE_OPTIONS: --max-old-space-size=6144
# Keep total concurrency predictable on the 32 vCPU runner.
# Windows shard 2 has shown intermittent instability at 2 workers.
OPENCLAW_TEST_WORKERS: 1
defaults:
run:
shell: bash
@@ -401,26 +346,36 @@ jobs:
fail-fast: false
matrix:
include:
- runtime: node
task: lint
shard_index: 0
shard_count: 1
command: pnpm lint
- runtime: node
task: test
shard_index: 1
shard_count: 2
command: pnpm canvas:a2ui:bundle && pnpm test
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 2
shard_count: 2
command: pnpm canvas:a2ui:bundle && pnpm test
shard_count: 6
command: pnpm test
- runtime: node
task: protocol
shard_index: 0
shard_count: 1
command: pnpm protocol:check
task: test
shard_index: 3
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 4
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 5
shard_count: 6
command: pnpm test
- runtime: node
task: test
shard_index: 6
shard_count: 6
command: pnpm test
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -446,31 +401,22 @@ jobs:
Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)"
}
- name: Download dist artifact (lint lane)
if: matrix.task == 'lint'
uses: actions/download-artifact@v4
with:
name: dist-build
path: dist/
- name: Verify dist artifact (lint lane)
if: matrix.task == 'lint'
run: |
set -euo pipefail
test -s dist/index.js
test -s dist/plugin-sdk/index.js
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
check-latest: true
check-latest: false
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "10.23.0"
cache-key-suffix: "node22"
# Sticky disk mount currently retries/fails on every shard and adds ~50s
# before install while still yielding zero pnpm store reuse.
use-sticky-disk: "false"
use-restore-keys: "false"
use-actions-cache: "false"
- name: Runtime versions
run: |
@@ -489,7 +435,7 @@ jobs:
which node
node -v
pnpm -v
pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
- name: Configure test shard (Windows)
if: matrix.task == 'test'
@@ -497,6 +443,10 @@ jobs:
echo "OPENCLAW_TEST_SHARDS=${{ matrix.shard_count }}" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=${{ matrix.shard_index }}" >> "$GITHUB_ENV"
- name: Build A2UI bundle (Windows)
if: matrix.task == 'test'
run: pnpm canvas:a2ui:bundle
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
run: ${{ matrix.command }}
@@ -738,7 +688,7 @@ jobs:
PY
android:
needs: [docs-scope, changed-scope, check]
needs: [docs-scope, changed-scope]
if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true')
runs-on: blacksmith-16vcpu-ubuntu-2404
strategy:

View File

@@ -34,8 +34,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -92,14 +92,12 @@ jobs:
- name: Build and push amd64 image
id: build
uses: docker/build-push-action@v6
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: linux/amd64
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:amd64,mode=max
provenance: false
push: true
@@ -115,8 +113,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -173,14 +171,12 @@ jobs:
- name: Build and push arm64 image
id: build
uses: docker/build-push-action@v6
uses: useblacksmith/build-push-action@v2
with:
context: .
platforms: linux/arm64
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache:arm64,mode=max
provenance: false
push: true

View File

@@ -44,10 +44,14 @@ jobs:
with:
pnpm-version: "10.23.0"
cache-key-suffix: "node22"
use-sticky-disk: "true"
- name: Install pnpm deps (minimal)
run: pnpm install --ignore-scripts --frozen-lockfile
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Run root Dockerfile CLI smoke
run: |
docker build -t openclaw-dockerfile-smoke:local -f Dockerfile .

View File

@@ -26,6 +26,9 @@ jobs:
with:
submodules: false
- name: Set up Docker Builder
uses: useblacksmith/setup-docker-builder@v1
- name: Build minimal sandbox base (USER sandbox)
shell: bash
run: |

155
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,155 @@
name: Stale
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
permissions: {}
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
continue-on-error: true
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token-fallback
if: steps.app-token.outcome == 'failure'
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Mark stale issues and pull requests
uses: actions/stale@v9
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 7
days-before-issue-close: 5
days-before-pr-stale: 5
days-before-pr-close: 3
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
exempt-pr-labels: maintainer,no-stale
operations-per-run: 10000
exempt-all-assignees: true
remove-stale-when-updated: true
stale-issue-message: |
This issue has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
stale-pr-message: |
This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
lock-closed-issues:
permissions:
issues: write
runs-on: blacksmith-16vcpu-ubuntu-2404
steps:
- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
id: app-token
with:
app-id: "2729701"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Lock closed issues after 48h of no comments
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const lockAfterHours = 48;
const lockAfterMs = lockAfterHours * 60 * 60 * 1000;
const perPage = 100;
const cutoffMs = Date.now() - lockAfterMs;
const { owner, repo } = context.repo;
let locked = 0;
let inspected = 0;
let page = 1;
while (true) {
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: "closed",
sort: "updated",
direction: "desc",
per_page: perPage,
page,
});
if (issues.length === 0) {
break;
}
for (const issue of issues) {
if (issue.pull_request) {
continue;
}
if (issue.locked) {
continue;
}
if (!issue.closed_at) {
continue;
}
inspected += 1;
const closedAtMs = Date.parse(issue.closed_at);
if (!Number.isFinite(closedAtMs)) {
continue;
}
if (closedAtMs > cutoffMs) {
continue;
}
let lastCommentMs = 0;
if (issue.comments > 0) {
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: issue.number,
per_page: 1,
page: 1,
sort: "created",
direction: "desc",
});
if (comments.length > 0) {
lastCommentMs = Date.parse(comments[0].created_at);
}
}
const lastActivityMs = Math.max(closedAtMs, lastCommentMs || 0);
if (lastActivityMs > cutoffMs) {
continue;
}
await github.rest.issues.lock({
owner,
repo,
issue_number: issue.number,
lock_reason: "resolved",
});
locked += 1;
}
page += 1;
}
core.info(`Inspected ${inspected} closed issues; locked ${locked}.`);

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ mise.toml
apps/android/.gradle/
apps/android/app/build/
apps/android/.cxx/
apps/android/.kotlin/
# Bun build artifacts
*.bun-build

View File

@@ -9,7 +9,7 @@ Input
- If ambiguous: ask.
Do (end-to-end)
Goal: PR must end in GitHub state = MERGED (never CLOSED). Use `gh pr merge` with `--rebase` or `--squash`.
Goal: PR must end in GitHub state = MERGED (never CLOSED). Prefer `gh pr merge --squash`; use `--rebase` only when preserving commit history is required.
1. Assign PR to self:
- `gh pr edit <PR> --add-assignee @me`
@@ -37,8 +37,8 @@ Goal: PR must end in GitHub state = MERGED (never CLOSED). Use `gh pr merge` wit
- Implement fixes + add/adjust tests
- Update `CHANGELOG.md` and mention `#<PR>` + `@$contrib`
9. Decide merge strategy:
- Rebase if we want to preserve commit history
- Squash if we want a single clean commit
- Squash (preferred): use when we want a single clean commit
- Rebase: use only when we explicitly want to preserve commit history
- If unclear, ask
10. Full gate (BEFORE commit):
- `pnpm lint && pnpm build && pnpm test`
@@ -54,8 +54,8 @@ Goal: PR must end in GitHub state = MERGED (never CLOSED). Use `gh pr merge` wit
```
13. Merge PR (must show MERGED on GitHub):
- Rebase: `gh pr merge <PR> --rebase`
- Squash: `gh pr merge <PR> --squash`
- Squash (preferred): `gh pr merge <PR> --squash`
- Rebase (history-preserving fallback): `gh pr merge <PR> --rebase`
- Never `gh pr close` (closing is wrong)
14. Sync main:
- `git checkout main`

View File

@@ -2,136 +2,242 @@
Docs: https://docs.openclaw.ai
## 2026.3.2 (Unreleased)
## 2026.3.3
### Changes
- Outbound adapters/plugins: add shared `sendPayload` support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.
- Media understanding/audio echo: add optional `tools.media.audio.echoTranscript` + `echoFormat` to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.
- Plugin runtime/STT: add `api.runtime.stt.transcribeAudioFile(...)` so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.
- Sessions/Attachments: add inline file attachment support for `sessions_spawn` (subagent runtime only) with base64/utf8 encoding, transcript content redaction, lifecycle cleanup, and configurable limits via `tools.sessions_spawn.attachments`. (#16761) Thanks @napetrov.
- Tools/PDF analysis: add a first-class `pdf` tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (`agents.defaults.pdfModel`, `pdfMaxBytesMb`, `pdfMaxPages`), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.
- Zalo Personal plugin (`@openclaw/zalouser`): rebuilt channel runtime to use native `zca-js` integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.
- Telegram/DM streaming: use `sendMessageDraft` for private preview streaming, keep reasoning/answer preview lanes separated in DM reasoning-stream mode. (#31824) Thanks @obviyus.
- CLI/Config validation: add `openclaw config validate` (with `--json`) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.
- Tools/Diffs: add PDF file output support and rendering quality customization controls (`fileQuality`, `fileScale`, `fileMaxWidth`) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.
- README/Contributors: rank contributor avatars by composite score (commits + merged PRs + code LOC), excluding docs-only LOC to prevent bulk-generated files from inflating rankings. (#23970) Thanks @tyler6204.
### Breaking
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
- Docs/Web search: remove outdated Brave free-tier wording and replace prescriptive AI ToS guidance with neutral compliance language in Brave setup docs. (#26860) Thanks @HenryLoenwind.
- Tools/Diffs guidance loading: move diffs usage guidance from unconditional prompt-hook injection to the plugin companion skill path, reducing unrelated-turn prompt noise while keeping diffs tool behavior unchanged. (#32630) thanks @sircrumpet.
### Fixes
- macOS/LaunchAgent security defaults: write `Umask=63` (octal `077`) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system `022`. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.
- Daemon/macOS TLS trust defaults: set `NODE_USE_SYSTEM_CA=1` by default in gateway/node supervised service environments on macOS (while preserving explicit env overrides), so launchd-managed installs trust enterprise system keychains without manual shell env wiring. (#32205) Thanks @magos-minor.
- Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @Glucksberg.
- Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from `HTTPS_PROXY`/`HTTP_PROXY` env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.
- Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.
- Media understanding/audio transcription guard: skip tiny/empty audio files (<1024 bytes) before provider/CLI transcription to avoid noisy invalid-audio failures and preserve clean fallback behavior. (#8388) Thanks @Glucksberg.
- OpenAI media capabilities: include `audio` in the OpenAI provider capability list so audio transcription models are eligible in media-understanding provider selection. (#12717) Thanks @openjay.
- Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example `env sh -c ...`) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.
- Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.
- Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.
- Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example `(a|aa)+`), and bound large regex-evaluation inputs for session-filter and log-redaction paths.
- Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.
- Security/Web tools SSRF guard: keep DNS pinning for untrusted `web_fetch` and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.
- Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded `/api/channels/*` variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.
- Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like `[System Message]` and line-leading `System:` in untrusted message content. (#30448)
- Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.
- Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so `HEARTBEAT_OK` noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.
- Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing `HEARTBEAT_OK` from being delivered to users. (#32131) Thanks @adhishthite.
- Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing `starttime` when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.
- Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (`mtimeMs` + `sizeBytes`), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.
- Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like `source`/`provider`), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.
- Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so `openclaw models status` no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.
- Config/backups hardening: enforce owner-only (`0600`) permissions on rotated config backups and clean orphan `.bak.*` files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.
- Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like `/usr/bin/g++` and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.
- Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.
- Webchat/stream finalization: persist streamed assistant text when final events omit `message`, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.
- Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.
- Gateway/Heartbeat model reload: treat `models.*` and `agents.defaults.model` config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.
- Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of `NO_REPLY` and keep final-message buffering in sync, preventing partial `NO` leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.
- Tools/fsPolicy propagation: honor `tools.fs.workspaceOnly` for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.
- Memory/LanceDB embeddings: forward configured `embedding.dimensions` into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.
- Failover/error classification: treat HTTP `529` (provider overloaded, common with Anthropic-compatible APIs) as `rate_limit` so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.
- Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.
- Exec heartbeat routing: scope exec-triggered heartbeat wakes to agent session keys so unrelated agents are no longer awakened by exec events, while preserving legacy unscoped behavior for non-canonical session keys. (#32724) thanks @altaywtf
- macOS/Tailscale remote gateway discovery: add a Tailscale Serve fallback peer probe path (`wss://<peer>.ts.net`) when Bonjour and wide-area DNS-SD discovery return no gateways, and refresh both discovery paths from macOS onboarding. (#32860) Thanks @ngutman.
- Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add `openclaw doctor` warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin.
- Telegram/plugin outbound hook parity: run `message_sending` + `message_sent` in Telegram reply delivery, include reply-path hook metadata (`mediaUrls`, `threadId`), and report `message_sent.success=false` when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee.
- Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras.
- Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.
- Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai.
- Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind.
- LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman.
- LINE/media download synthesis: fix file-media download handling and M4A audio classification across overlapping LINE regressions. (from #26386, #27761, #27787, #29509, #29755, #29776, #29785, #32240) Thanks @kevinWangSheng, @loiie45e, @carrotRakko, @Sid-Qin, @codeafridi, and @bmendonca3.
- LINE/context and routing synthesis: fix group/room peer routing and command-authorization context propagation, and keep processing later events in mixed-success webhook batches. (from #21955, #24475, #27035, #28286) Thanks @lailoo, @mcaxtr, @jervyclaw, @Glucksberg, and @Takhoffman.
- LINE/status/config/webhook synthesis: fix status false positives from snapshot/config state and accept LINE webhook HEAD probes for compatibility. (from #10487, #25726, #27537, #27908, #31387) Thanks @BlueBirdBack, @stakeswky, @loiie45e, @puritysb, and @mcaxtr.
- LINE cleanup/test follow-ups: fold cleanup/test learnings into the synthesis review path while keeping runtime changes focused on regression fixes. (from #17630, #17289) Thanks @Clawborn and @davidahmann.
## 2026.3.2
### Changes
- Secrets/SecretRef coverage: expand SecretRef support across the full supported user-supplied credential surface (64 targets total), including runtime collectors, `openclaw secrets` planning/apply/audit flows, onboarding SecretInput UX, and related docs; unresolved refs now fail fast on active surfaces while inactive surfaces report non-blocking diagnostics. (#29580) Thanks @joshavant.
- Tools/PDF analysis: add a first-class `pdf` tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (`agents.defaults.pdfModel`, `pdfMaxBytesMb`, `pdfMaxPages`), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.
- Outbound adapters/plugins: add shared `sendPayload` support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.
- Models/MiniMax: add first-class `MiniMax-M2.5-highspeed` support across built-in provider catalogs, onboarding flows, and MiniMax OAuth plugin defaults, while keeping legacy `MiniMax-M2.5-Lightning` compatibility for existing configs.
- Sessions/Attachments: add inline file attachment support for `sessions_spawn` (subagent runtime only) with base64/utf8 encoding, transcript content redaction, lifecycle cleanup, and configurable limits via `tools.sessions_spawn.attachments`. (#16761) Thanks @napetrov.
- Telegram/Streaming defaults: default `channels.telegram.streaming` to `partial` (from `off`) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.
- Telegram/DM streaming: use `sendMessageDraft` for private preview streaming, keep reasoning/answer preview lanes separated in DM reasoning-stream mode. (#31824) Thanks @obviyus.
- Telegram/voice mention gating: add optional `disableAudioPreflight` on group/topic config to skip mention-detection preflight transcription for inbound voice notes where operators want text-only mention checks. (#23067) Thanks @yangnim21029.
- CLI/Config validation: add `openclaw config validate` (with `--json`) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.
- Tools/Diffs: add PDF file output support and rendering quality customization controls (`fileQuality`, `fileScale`, `fileMaxWidth`) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.
- Memory/Ollama embeddings: add `memorySearch.provider = "ollama"` and `memorySearch.fallback = "ollama"` support, honor `models.providers.ollama` settings for memory embedding requests, and document Ollama embedding usage. (#26349) Thanks @nico-hoff.
- Zalo Personal plugin (`@openclaw/zalouser`): rebuilt channel runtime to use native `zca-js` integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.
- Plugin SDK/channel extensibility: expose `channelRuntime` on `ChannelGatewayContext` so external channel plugins can access shared runtime helpers (reply/routing/session/text/media/commands) without internal imports. (#25462) Thanks @guxiaobo.
- Plugin runtime/STT: add `api.runtime.stt.transcribeAudioFile(...)` so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.
- Plugin hooks/session lifecycle: include `sessionKey` in `session_start`/`session_end` hook events and contexts so plugins can correlate lifecycle callbacks with routing identity. (#26394) Thanks @tempeste.
- Hooks/message lifecycle: add internal hook events `message:transcribed` and `message:preprocessed`, plus richer outbound `message:sent` context (`isGroup`, `groupId`) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon.
- Media understanding/audio echo: add optional `tools.media.audio.echoTranscript` + `echoFormat` to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.
- Plugin runtime/system: expose `runtime.system.requestHeartbeatNow(...)` so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral.
- Plugin runtime/events: expose `runtime.events.onAgentEvent` and `runtime.events.onSessionTranscriptUpdate` for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.
- CLI/Banner taglines: add `cli.banner.taglineMode` (`random` | `default` | `off`) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.
### Breaking
- **BREAKING:** Onboarding now defaults `tools.profile` to `messaging` for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.
- **BREAKING:** ACP dispatch now defaults to enabled unless explicitly disabled (`acp.dispatch.enabled=false`). If you need to pause ACP turn routing while keeping `/acp` controls, set `acp.dispatch.enabled=false`. Docs: https://docs.openclaw.ai/tools/acp-agents
- **BREAKING:** Plugin SDK removed `api.registerHttpHandler(...)`. Plugins must register explicit HTTP routes via `api.registerHttpRoute({ path, auth, match, handler })`, and dynamic webhook lifecycles should use `registerPluginHttpRoute(...)`.
- **BREAKING:** Zalo Personal plugin (`@openclaw/zalouser`) no longer depends on external `zca`-compatible CLI binaries (`openzca`, `zca-cli`) for runtime send/listen/login; operators should use `openclaw channels login --channel zalouser` after upgrade to refresh sessions in the new JS-native path.
### Fixes
- Feishu/Outbound render mode: respect Feishu account `renderMode` in outbound sends so card mode (and auto-detected markdown tables/code blocks) uses markdown card delivery instead of always sending plain text. (#31562) Thanks @arkyu2077.
- Plugin command/runtime hardening: validate and normalize plugin command name/description at registration boundaries, and guard Telegram native menu normalization paths so malformed plugin command specs cannot crash startup (`trim` on undefined). (#31997) Fixes #31944. Thanks @liuxiaopai-ai.
- Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (`pnpm`, `bun`) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.
- Web UI/config form: support SecretInput string-or-secret-ref unions in map `additionalProperties`, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.
- Plugins/install diagnostics: reject legacy plugin package shapes without `openclaw.extensions` and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.
- Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example `diffs` -> bundled `@openclaw/diffs`), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.
- Browser/default profile selection: default `browser.defaultProfile` behavior now prefers `openclaw` (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the `chrome` relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.
- Doctor/local memory provider checks: stop false-positive local-provider warnings when `provider=local` and no explicit `modelPath` is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.
- Cron/session reaper reliability: move cron session reaper sweeps into `onTimer` `finally` and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.
- Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.
- Sandbox/Docker setup command parsing: accept `agents.*.sandbox.docker.setupCommand` as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.
- Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.
- Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction `AGENTS.md` context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.
- Gateway/Node dangerous-command parity: include `sms.send` in default onboarding node `denyCommands`, share onboarding deny defaults with the gateway dangerous-command source of truth, and include `sms.send` in phone-control `/phone arm writes` handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.
- Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.
- Gateway/Plugin HTTP hardening: require explicit `auth` for plugin route registration, add route ownership guards for duplicate `path+match` registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.
- Agents/Subagents `sessions_spawn`: reject malformed `agentId` inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.
- Webchat/Feishu session continuation: preserve routable `OriginatingChannel`/`OriginatingTo` metadata from session delivery context in `chat.send`, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Agents/Sandbox workdir mapping: map container workdir paths (for example `/workspace`) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.
- Gateway/Control UI basePath POST handling: return 405 for `POST` on exact basePath routes (for example `/openclaw`) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.
- Authentication: classify `permission_error` as `auth_permanent` for profile fallback. (#31324) Thanks @Sid-Qin.
- Gateway/Node browser proxy routing: honor `profile` from `browser.request` JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.
- Browser/Extension relay reconnect tolerance: keep `/json/version` and `/cdp` reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.
- Browser/Extension re-announce reliability: keep relay state in `connecting` when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.
- Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.
- Browser/Profile defaults: prefer `openclaw` profile over `chrome` in headless/no-sandbox environments unless an explicit `defaultProfile` is configured. (#14944) Thanks @BenediktSchackenberg.
- Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.
- Browser/Profile attach-only override: support `browser.profiles.<name>.attachOnly` (fallback to global `browser.attachOnly`) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.
- Browser/Act request compatibility: accept legacy flattened `action="act"` params (`kind/ref/text/...`) in addition to `request={...}` so browser act calls no longer fail with `request required`. (#15120) Thanks @vincentkoc.
- Browser/Extension relay stale tabs: evict stale cached targets from `/json/list` when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.
- CLI/Browser start timeout: honor `openclaw browser --timeout <ms> start` and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.
- Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.
- Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up `PortInUseError` races after `browser start`/`open`. (#29538) Thanks @AaronWander.
- Browser/Managed tab cap: limit loopback managed `openclaw` page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.
- Browser/CDP proxy bypass: force direct loopback agent paths and scoped `NO_PROXY` expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.
- Browser/Gateway hardening: preserve env credentials for `OPENCLAW_GATEWAY_URL` / `CLAWDBOT_GATEWAY_URL` while treating explicit `--url` as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.
- Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for `.cmd` shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.
- Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.
- Gateway/WS security: keep plaintext `ws://` loopback-only by default, with explicit break-glass private-network opt-in via `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.
- Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing `token.trim()` crashes during status/start flows. (#31973) Thanks @ningding97.
- Discord/lifecycle startup status: push an immediate `connected` status snapshot when the gateway is already connected before lifecycle debug listeners attach, with abort-guarding to avoid contradictory status flips during pre-aborted startup. (#32336) Thanks @mitchmcalister.
- Feishu/inbound mention normalization: preserve all inbound mention semantics by normalizing Feishu mention placeholders into explicit `<at user_id=\"...\">name</at>` tags (instead of stripping them), improving multi-mention context fidelity in agent prompts while retaining bot/self mention disambiguation. (#30252) Thanks @Lanfei.
- Feishu/multi-app mention routing: guard mention detection in multi-bot groups by validating mention display name alongside bot `open_id`, preventing false-positive self-mentions from Feishu WebSocket remapping so only the actually mentioned bot responds under `requireMention`. (#30315) Thanks @teaguexiao.
- Feishu/session-memory hook parity: trigger the shared `before_reset` session-memory hook path when Feishu `/new` and `/reset` commands execute so reset flows preserve memory behavior consistent with other channels. (#31437) Thanks @Linux2010.
- Feishu/LINE group system prompts: forward per-group `systemPrompt` config into inbound context `GroupSystemPrompt` for Feishu and LINE group/room events so configured group-specific behavior actually applies at dispatch time. (#31713) Thanks @whiskyboy.
- Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.
- Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older `openclaw/plugin-sdk` builds omit webhook default constants. (#31606)
- Feishu/group broadcast dispatch: add configurable multi-agent group broadcast dispatch with observer-session isolation, cross-account dedupe safeguards, and non-mention history buffering rules that avoid duplicate replay in broadcast/topic workflows. (#29575) Thanks @ohmyskyhigh.
- Gateway/Subagent TLS pairing: allow authenticated local `gateway-client` backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring `sessions_spawn` with `gateway.tls.enabled=true` in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.
- Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file `starttime` with `/proc/<pid>/stat` starttime, so stale `.jsonl.lock` files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.
- Gateway/macOS LaunchAgent hardening: write `Umask=077` in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
- OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit `doctor --deep`) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.
- Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.
- Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.
- Feishu/Dedup restart resilience: warm persistent dedup state into memory on monitor startup so retry events after gateway restart stay suppressed without requiring initial on-disk probe misses. (#31605)
- Voice-call/runtime lifecycle: prevent `EADDRINUSE` loops by resetting failed runtime promises, making webhook `start()` idempotent with the actual bound port, and fully cleaning up webhook/tunnel/tailscale resources after startup failures. (#32395) Thanks @scoootscooob.
- Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example `(a|aa)+`), and bound large regex-evaluation inputs for session-filter and log-redaction paths.
- Gateway/Plugin HTTP hardening: require explicit `auth` for plugin route registration, add route ownership guards for duplicate `path+match` registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.
- Browser/Profile defaults: prefer `openclaw` profile over `chrome` in headless/no-sandbox environments unless an explicit `defaultProfile` is configured. (#14944) Thanks @BenediktSchackenberg.
- Gateway/WS security: keep plaintext `ws://` loopback-only by default, with explicit break-glass private-network opt-in via `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.
- OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit `doctor --deep`) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.
- Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.
- CLI/Config validation and routing hardening: dedupe `openclaw config validate` failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including `--json` fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed `config get/unset` with split root options). Thanks @gumadeiras.
- Browser/Extension relay reconnect tolerance: keep `/json/version` and `/cdp` reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.
- CLI/Browser start timeout: honor `openclaw browser --timeout <ms> start` and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.
- Synology Chat/gateway lifecycle: keep `startAccount` pending until abort for inactive and active account paths to prevent webhook route restart loops under gateway supervision. (#23074) Thanks @druide67.
- Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.
- Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like `/usr/bin/g++` and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
- Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.
- Slack/Bolt startup compatibility: remove invalid `message.channels` and `message.groups` event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified `message` handler (`channel_type`). (#32033) Thanks @mahopan.
- Slack/socket auth failure handling: fail fast on non-recoverable auth errors (`account_inactive`, `invalid_auth`, etc.) during startup and reconnect instead of retry-looping indefinitely, including `unable_to_socket_mode_start` error payload propagation. (#32377) Thanks @scoootscooob.
- Gateway/macOS LaunchAgent hardening: write `Umask=077` in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.
- macOS/LaunchAgent security defaults: write `Umask=63` (octal `077`) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system `022`. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.
- Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from `HTTPS_PROXY`/`HTTP_PROXY` env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.
- Sandbox/workspace mount permissions: make primary `/workspace` bind mounts read-only whenever `workspaceAccess` is not `rw` (including `none`) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.
- Tools/fsPolicy propagation: honor `tools.fs.workspaceOnly` for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.
- Daemon/Homebrew runtime pinning: resolve Homebrew Cellar Node paths to stable Homebrew-managed symlinks (including versioned formulas like `node@22`) so gateway installs keep the intended runtime across brew upgrades. (#32185) Thanks @scoootscooob.
- Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.
- Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded `/api/channels/*` variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.
- Browser/Gateway hardening: preserve env credentials for `OPENCLAW_GATEWAY_URL` / `CLAWDBOT_GATEWAY_URL` while treating explicit `--url` as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
- Gateway/Webchat streaming finalization: flush throttled trailing assistant text before `final` chat events so streaming consumers do not miss tail content, while preserving duplicate suppression and heartbeat/silent lead-fragment guards. (#24856) Thanks @visionik and @vincentkoc.
- Control UI/Legacy browser compatibility: replace `toSorted`-dependent cron suggestion sorting in `app-render` with a compatibility helper so older browsers without `Array.prototype.toSorted` no longer white-screen. (#31775) Thanks @liuxiaopai-ai.
- macOS/PeekabooBridge: add compatibility socket symlinks for legacy `clawdbot`, `clawdis`, and `moltbot` Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.
- Gateway/message tool reliability: avoid false `Unknown channel` failures when `message.*` actions receive platform-specific channel ids by falling back to `toolContext.currentChannelProvider`, and prevent health-monitor restart thrash for channels that just (re)started by adding a per-channel startup-connect grace window. (from #32367) Thanks @MunemHashmi.
- Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for `.cmd` shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.
- Security/ACP sandbox inheritance: enforce fail-closed runtime guardrails for `sessions_spawn` with `runtime="acp"` by rejecting ACP spawns from sandboxed requester sessions and rejecting `sandbox="require"` for ACP runtime, preventing sandbox-boundary bypass via host-side ACP initialization. (#32254) Thanks @tdjackey for reporting, and @dutifulbob for the fix.
- Security/Web tools SSRF guard: keep DNS pinning for untrusted `web_fetch` and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.
- Gemini schema sanitization: coerce malformed JSON Schema `properties` values (`null`, arrays, primitives) to `{}` before provider validation, preventing downstream strict-validator crashes on invalid plugin/tool schemas. (#32332) Thanks @webdevtodayjason.
- Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.
- Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.
- Browser/Extension relay stale tabs: evict stale cached targets from `/json/list` when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.
- Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up `PortInUseError` races after `browser start`/`open`. (#29538) Thanks @AaronWander.
- OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty `function_call_output.call_id` payloads in the WS conversion path to avoid OpenAI 400 errors (`Invalid 'input[n].call_id': empty string`), with regression coverage for both inbound stream normalization and outbound payload guards.
- Security/Nodes camera URL downloads: bind node `camera.snap`/`camera.clip` URL payload downloads to the resolved node host, enforce fail-closed behavior when node `remoteIp` is unavailable, and use SSRF-guarded fetch with redirect host/protocol checks to prevent off-node fetch pivots. Thanks @tdjackey for reporting.
- Config/backups hardening: enforce owner-only (`0600`) permissions on rotated config backups and clean orphan `.bak.*` files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.
- Telegram/inbound media filenames: preserve original `file_name` metadata for document/audio/video/animation downloads (with fetch/path fallbacks), so saved inbound attachments keep sender-provided names instead of opaque Telegram file paths. (#31837) Thanks @Kay-051.
- Gateway/OpenAI chat completions: honor `x-openclaw-message-channel` when building `agentCommand` input for `/v1/chat/completions`, preserving caller channel identity instead of forcing `webchat`. (#30462) Thanks @bmendonca3.
- Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @bmendonca3.
- Media/MIME normalization: normalize parameterized/case-variant MIME strings in `kindFromMime` (for example `Audio/Ogg; codecs=opus`) so WhatsApp voice notes are classified as audio and routed through transcription correctly. (#32280) Thanks @Lucenx9.
- Discord/audio preflight mentions: detect audio attachments via Discord `content_type` and gate preflight transcription on typed text (not media placeholders), so guild voice-note mentions are transcribed and matched correctly. (#32136) Thanks @jnMetaCode.
- Feishu/topic session routing: use `thread_id` as topic session scope fallback when `root_id` is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun.
- Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of `NO_REPLY` and keep final-message buffering in sync, preventing partial `NO` leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.
- Telegram/models picker callbacks: keep long model buttons selectable by falling back to compact callback payloads and resolving provider ids on selection (with provider re-prompt on ambiguity), avoiding Telegram 64-byte callback truncation failures. (#31857) Thanks @bmendonca3.
- WhatsApp/inbound self-message context: propagate inbound `fromMe` through the web inbox pipeline and annotate direct self messages as `(self)` in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.
- Context-window metadata warmup: add exponential config-load retry backoff (1s -> 2s -> 4s, capped at 60s) so transient startup failures recover automatically without hot-loop retries.
- Voice-call/Twilio external outbound: auto-register webhook-first `outbound-api` calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.
- Feishu/topic root replies: prefer `root_id` as outbound `replyTargetMessageId` when present, and parse millisecond `message_create_time` values correctly so topic replies anchor to the root message in grouped thread flows. (#29968) Thanks @bmendonca3.
- Feishu/DM pairing reply target: send pairing challenge replies to `chat:<chat_id>` instead of `user:<sender_open_id>` so Lark/Feishu private chats with user-id-only sender payloads receive pairing messages reliably. (#31403) Thanks @stakeswky.
- Feishu/Lark private DM routing: treat inbound `chat_type: "private"` as direct-message context for pairing/mention-forward/reaction synthetic handling so Lark private chats behave like Feishu p2p DMs. (#31400) Thanks @stakeswky.
- Signal/message actions: allow `react` to fall back to `toolContext.currentMessageId` when `messageId` is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax.
- Discord/message actions: allow `react` to fall back to `toolContext.currentMessageId` when `messageId` is omitted, matching Telegram/Signal reaction ergonomics in inbound turns.
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.
- Slack/thread context payloads: only inject thread starter/history text on first thread turn for new sessions while preserving thread metadata, reducing repeated context-token bloat on long-lived thread sessions. (#32133) Thanks @sourman.
- Slack/session routing: keep top-level channel messages in one shared session when `replyToMode=off`, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193) Thanks @bmendonca3.
- Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.
- OpenRouter/x-ai compatibility: skip `reasoning.effort` injection for `x-ai/*` models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.
- Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.
- Voice-call/webhook routing: require exact webhook path matches (instead of prefix matches) so lookalike paths cannot reach provider verification/dispatch logic. (#31930) Thanks @afurm.
- Slack/Bolt startup compatibility: remove invalid `message.channels` and `message.groups` event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified `message` handler (`channel_type`). (#32033) Thanks @mahopan.
- Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing `token.trim()` crashes during status/start flows. (#31973) Thanks @ningding97.
- Skills/sherpa-onnx-tts: run the `sherpa-onnx-tts` bin under ESM (replace CommonJS `require` imports) and add regression coverage to prevent `require is not defined in ES module scope` startup crashes. (#31965) Thanks @bmendonca3.
- Feishu/Run channel fallback: prefer `Provider` over `Surface` when inferring queued run `messageProvider` fallback (when `OriginatingChannel` is missing), preventing Feishu turns from being mislabeled as `webchat` in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.
- Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.
- Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (`monitor.account-scope.test.ts`) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.
- macOS/PeekabooBridge: add compatibility socket symlinks for legacy `clawdbot`, `clawdis`, and `moltbot` Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.
- Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)
- Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older `openclaw/plugin-sdk` builds omit webhook default constants. (#31606)
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
- Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.
- Docker/Sandbox bootstrap hardening: make `OPENCLAW_SANDBOX` opt-in parsing explicit (`1|true|yes|on`), support custom Docker socket paths via `OPENCLAW_DOCKER_SOCKET`, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to `off` when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.
- Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.
- Feishu/Send target prefixes: normalize explicit `group:`/`dm:` send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai.
- Webchat/Feishu session continuation: preserve routable `OriginatingChannel`/`OriginatingTo` metadata from session delivery context in `chat.send`, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)
- Telegram/implicit mention forum handling: exclude Telegram forum system service messages (`forum_topic_*`, `general_forum_topic_*`) from reply-chain implicit mention detection so `requireMention` does not get bypassed inside bot-created topic lifecycle events. (#32262) Thanks @scoootscooob.
- Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.
- Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (`provider: "message"`) and normalize `lark`/`feishu` provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)
- Webchat/silent token leak: filter assistant `NO_REPLY`-only transcript entries from `chat.history` responses and add client-side defense-in-depth guards in the chat controller so internal silent tokens never render as visible chat bubbles. (#32015) Consolidates overlap from #32183, #32082, #32045, #32052, #32172, and #32112. Thanks @ademczuk, @liuxiaopai-ai, @ningding97, @bmendonca3, and @x4v13r1120.
- Doctor/local memory provider checks: stop false-positive local-provider warnings when `provider=local` and no explicit `modelPath` is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.
- Media understanding/parakeet CLI output parsing: read `parakeet-mlx` transcripts from `--output-dir/<media-basename>.txt` when txt output is requested (or default), with stdout fallback for non-txt formats. (#9177) Thanks @mac-110.
- Media understanding/audio transcription guard: skip tiny/empty audio files (<1024 bytes) before provider/CLI transcription to avoid noisy invalid-audio failures and preserve clean fallback behavior. (#8388) Thanks @Glucksberg.
- Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.
- Gateway/Node browser proxy routing: honor `profile` from `browser.request` JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.
- Gateway/Control UI basePath POST handling: return 405 for `POST` on exact basePath routes (for example `/openclaw`) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.
- Browser/default profile selection: default `browser.defaultProfile` behavior now prefers `openclaw` (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the `chrome` relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.
- Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.
- Models/config env propagation: apply `config.env.vars` before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.
- Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so `openclaw models status` no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.
- Gateway/Heartbeat model reload: treat `models.*` and `agents.defaults.model` config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.
- Memory/LanceDB embeddings: forward configured `embedding.dimensions` into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.
- Browser/CDP status accuracy: require a successful `Browser.getVersion` response over the CDP websocket (not just socket-open) before reporting `cdpReady`, so stale idle command channels are surfaced as unhealthy. (#23427) Thanks @vincentkoc.
- Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.
- Security/Node exec approvals: revalidate approval-bound `cwd` identity immediately before execution/forwarding and fail closed with an explicit denial when `cwd` drifts after approval hardening.
- Security audit/skills workspace hardening: add `skills.workspace.symlink_escape` warning in `openclaw security audit` when workspace `skills/**/SKILL.md` resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary.
- Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example `env sh -c ...`) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.
- Security/fs-safe write hardening: make `writeFileWithinRoot` use same-directory temp writes plus atomic rename, add post-write inode/hardlink revalidation with security warnings on boundary drift, and avoid truncating existing targets when final rename fails.
- Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.
- Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like `[System Message]` and line-leading `System:` in untrusted message content. (#30448)
- Sandbox/Docker setup command parsing: accept `agents.*.sandbox.docker.setupCommand` as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.
- Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction `AGENTS.md` context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.
- Agents/Sandbox workdir mapping: map container workdir paths (for example `/workspace`) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.
- Docker/Sandbox bootstrap hardening: make `OPENCLAW_SANDBOX` opt-in parsing explicit (`1|true|yes|on`), support custom Docker socket paths via `OPENCLAW_DOCKER_SOCKET`, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to `off` when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.
- Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @AIflow-Labs.
- Feishu/Run channel fallback: prefer `Provider` over `Surface` when inferring queued run `messageProvider` fallback (when `OriginatingChannel` is missing), preventing Feishu turns from being mislabeled as `webchat` in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.
- Skills/sherpa-onnx-tts: run the `sherpa-onnx-tts` bin under ESM (replace CommonJS `require` imports) and add regression coverage to prevent `require is not defined in ES module scope` startup crashes. (#31965) Thanks @bmendonca3.
- Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.
- Slack/Channel message subscriptions: register explicit `message.channels` and `message.groups` monitor handlers (alongside generic `message`) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674.
- Hooks/session-scoped memory context: expose ephemeral `sessionId` in embedded plugin tool contexts and `before_tool_call`/`after_tool_call` hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across `/new` and `/reset`. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex.
- Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.
- Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.
- Feishu/File upload filenames: percent-encode non-ASCII/special-character `file_name` values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.
- Media/MIME channel parity: route Telegram/Signal/iMessage media-kind checks through normalized `kindFromMime` so mixed-case/parameterized MIME values classify consistently across message channels.
- WhatsApp/inbound self-message context: propagate inbound `fromMe` through the web inbox pipeline and annotate direct self messages as `(self)` in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.
- Webchat/stream finalization: persist streamed assistant text when final events omit `message`, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.
- Feishu/Inbound ordering: serialize message handling per chat while preserving cross-chat concurrency to avoid same-chat race drops under bursty inbound traffic. (#31807)
- Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580)
- Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970)
- Feishu/Streaming block fallback: preserve markdown block stream text as final streaming-card content when final payload text is missing, while still suppressing non-card internal block chunk delivery. (#30663)
- Feishu/Bitable API errors: unify Feishu Bitable tool error handling with structured `LarkApiError` responses and consistent API/context attribution across wiki/base metadata, field, and record operations. (#31450)
- Feishu/Missing-scope grant URL fix: rewrite known invalid scope aliases (`contact:contact.base:readonly`) to valid scope names in permission grant links, so remediation URLs open with correct Feishu consent scopes. (#31943)
- BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound `message_id` selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.
- WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack.
- Feishu/default account resolution: always honor explicit `channels.feishu.defaultAccount` during outbound account selection (including top-level-credential setups where the preferred id is not present in `accounts`), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3.
- Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (`contact:contact.base:readonly`) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761)
- Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.
- Feishu/Inbound debounce: debounce rapid same-chat sender bursts into one ordered dispatch turn, skip already-processed retries when composing merged text, and preserve bot-mention intent across merged entries to reduce duplicate or late inbound handling. (#31548)
- Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.
- Browser/Extension re-announce reliability: keep relay state in `connecting` when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.
- Browser/Act request compatibility: accept legacy flattened `action="act"` params (`kind/ref/text/...`) in addition to `request={...}` so browser act calls no longer fail with `request required`. (#15120) Thanks @vincentkoc.
- OpenRouter/x-ai compatibility: skip `reasoning.effort` injection for `x-ai/*` models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.
- Models/openai-completions developer-role compatibility: force `supportsDeveloperRole=false` for non-native endpoints, treat unparseable `baseUrl` values as non-native, and add regression coverage for empty/malformed baseUrl plus explicit-true override behavior. (#29479) thanks @akramcodez.
- Browser/Profile attach-only override: support `browser.profiles.<name>.attachOnly` (fallback to global `browser.attachOnly`) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.
- Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file `starttime` with `/proc/<pid>/stat` starttime, so stale `.jsonl.lock` files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.
- Cron/isolated delivery target fallback: remove early unresolved-target return so cron delivery can flow through shared outbound target resolution (including per-channel `resolveDefaultTo` fallback) when `delivery.to` is omitted. (#32364) Thanks @hclsys.
- OpenAI media capabilities: include `audio` in the OpenAI provider capability list so audio transcription models are eligible in media-understanding provider selection. (#12717) Thanks @openjay.
- Browser/Managed tab cap: limit loopback managed `openclaw` page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.
- Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.
- Gateway/Node dangerous-command parity: include `sms.send` in default onboarding node `denyCommands`, share onboarding deny defaults with the gateway dangerous-command source of truth, and include `sms.send` in phone-control `/phone arm writes` handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
- Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.
- Browser/CDP proxy bypass: force direct loopback agent paths and scoped `NO_PROXY` expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.
- Sessions/idle reset correctness: preserve existing `updatedAt` during inbound metadata-only writes so idle-reset boundaries are not unintentionally refreshed before actual user turns. (#32379) Thanks @romeodiaz.
- Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing `starttime` when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.
- Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (`mtimeMs` + `sizeBytes`), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.
- Agents/Subagents `sessions_spawn`: reject malformed `agentId` inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.
- CLI/installer Node preflight: enforce Node.js `v22.12+` consistently in both `openclaw.mjs` runtime bootstrap and installer active-shell checks, with actionable nvm recovery guidance for mismatched shell PATH/defaults. (#32356) Thanks @jasonhargrove.
- Web UI/config form: support SecretInput string-or-secret-ref unions in map `additionalProperties`, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.
- Auto-reply/inline command cleanup: preserve newline structure when stripping inline `/status` and extracting inline slash commands by collapsing only horizontal whitespace, preventing paragraph flattening in multi-line replies. (#32224) Thanks @scoootscooob.
- Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like `source`/`provider`), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.
- Hooks/runtime stability: keep the internal hook handler registry on a `globalThis` singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.
- Hooks/after_tool_call: include embedded session context (`sessionKey`, `agentId`) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc.
- Hooks/tool-call correlation: include `runId` and `toolCallId` in plugin tool hook payloads/context and scope tool start/adjusted-param tracking by run to prevent cross-run collisions in `before_tool_call` and `after_tool_call`. (#32360) Thanks @vincentkoc.
- Plugins/install diagnostics: reject legacy plugin package shapes without `openclaw.extensions` and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.
- Hooks/plugin context parity: ensure `llm_input` hooks in embedded attempts receive the same `trigger` and `channelId`-aware `hookCtx` used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.
- Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (`pnpm`, `bun`) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.
- Cron/session reaper reliability: move cron session reaper sweeps into `onTimer` `finally` and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.
- Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so `HEARTBEAT_OK` noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.
- Authentication: classify `permission_error` as `auth_permanent` for profile fallback. (#31324) Thanks @Sid-Qin.
- Agents/host edit reliability: treat host edit-tool throws as success only when on-disk post-check confirms replacement likely happened (`newText` present and `oldText` absent), preventing false failure reports while avoiding pre-write false positives. (#32383) Thanks @polooooo.
- Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example `diffs` -> bundled `@openclaw/diffs`), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.
- Web UI/inline code copy fidelity: disable forced mid-token wraps on inline `<code>` spans so copied UUID/hash/token strings preserve exact content instead of inserting line-break spaces. (#32346) Thanks @hclsys.
- Restart sentinel formatting: avoid duplicate `Reason:` lines when restart message text already matches `stats.reason`, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2.
- Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.
- Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.
- Failover/error classification: treat HTTP `529` (provider overloaded, common with Anthropic-compatible APIs) as `rate_limit` so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.
- Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
- Secrets/exec resolver timeout defaults: use provider `timeoutMs` as the default inactivity (`noOutputTimeoutMs`) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.
- Auto-reply/reminder guard note suppression: when a turn makes reminder-like commitments but schedules no new cron jobs, suppress the unscheduled-reminder warning note only if an enabled cron already exists for the same session; keep warnings for unrelated sessions, disabled jobs, or unreadable cron store paths. (#32255) Thanks @scoootscooob.
- Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing `HEARTBEAT_OK` from being delivered to users. (#32131) Thanks @adhishthite.
- Cron/store migration: normalize legacy cron jobs with string `schedule` and top-level `command`/`timeout` fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.
- Tests/Windows backup rotation: skip chmod-only backup permission assertions on Windows while retaining compose/rotation/prune coverage across platforms to avoid false CI failures from Windows non-POSIX mode semantics. (#32286) Thanks @jalehman.
- Tests/Subagent announce: set `OPENCLAW_TEST_FAST=1` before importing `subagent-announce` format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.
## 2026.3.1
@@ -168,16 +274,18 @@ Docs: https://docs.openclaw.ai
### Fixes
- Feishu/Streaming card text fidelity: merge throttled/fragmented partial updates without dropping content and avoid newline injection when stitching chunk-style deltas so card-stream output matches final reply text. (#29616) Thanks @HaoHuaqing.
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
- Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.
- Web tools/RFC2544 fake-IP compatibility: allow RFC2544 benchmark range (`198.18.0.0/15`) for trusted web-tool fetch endpoints so proxy fake-IP networking modes do not trigger false SSRF blocks. Landed from contributor PR #31176 by @sunkinux. Thanks @sunkinux.
- Feishu/Sessions announce group targets: normalize `group:` and `channel:` Feishu targets to `chat_id` routing so `sessions_send` announce delivery no longer sends group chat IDs via `user_id` API params. Fixes #31426.
- Windows/Plugin install: avoid `spawn EINVAL` on Windows npm/npx invocations by resolving to `node` + npm CLI scripts instead of spawning `.cmd` directly. Landed from contributor PR #31147 by @codertony. Thanks @codertony.
- Web UI/Cron: include configured agent model defaults/fallbacks in cron model suggestions so scheduled-job model autocomplete reflects configured models. (#29709) Thanks @Sid-Qin.
- Cron/Delivery: disable the agent messaging tool when `delivery.mode` is `"none"` so cron output is not sent to Telegram or other channels. (#21808) Thanks @lailoo.
- CLI/Cron: clarify `cron list` output by renaming `Agent` to `Agent ID` and adding a `Model` column for isolated agent-turn jobs. (#26259) Thanks @openperf.
- Gateway/Control UI origins: honor `gateway.controlUi.allowedOrigins: ["*"]` wildcard entries (including trimmed values) and lock behavior with regression tests. Landed from contributor PR #31058 by @byungsker. Thanks @byungsker.
- Agents/Sessions list transcript paths: handle missing/non-string/relative `sessions.list.path` values and per-agent `{agentId}` templates when deriving `transcriptPath`, so cross-agent session listings resolve to concrete agent session files instead of workspace-relative paths. (#24775) Thanks @martinfrancois.
- Gateway/Control UI CSP: allow required Google Fonts origins in Control UI CSP. (#29279) Thanks @Glucksberg and @vincentkoc.
- Gateway/Control UI CSP: allow required Google Fonts origins in Control UI CSP. (#29279) Thanks @vincentkoc.
- CLI/Install: add an npm-link fallback to fix CLI startup `Permission denied` failures (`exit 127`) on affected installs. (#17151) Thanks @sskyu and @vincentkoc.
- Plugins/NPM spec install: fix npm-spec plugin installs when `npm pack` output is empty by detecting newly created `.tgz` archives in the pack directory. (#21039) Thanks @graysurf and @vincentkoc.
- Plugins/Install: clear stale install errors when an npm package is not found so follow-up install attempts report current state correctly. (#25073) Thanks @dalefrieswthat.
@@ -203,12 +311,12 @@ Docs: https://docs.openclaw.ai
- Android/Voice screen TTS: stream assistant speech via ElevenLabs WebSocket in Talk Mode, stop cleanly on speaker mute/barge-in, and ignore stale out-of-order stream events. (#29521) Thanks @gregmousseau.
- Android/Photos permissions: declare Android 14+ selected-photo access permission (`READ_MEDIA_VISUAL_USER_SELECTED`) and align Android permission/settings paths with current minSdk behavior for more reliable permission state handling.
- Feishu/Reply media attachments: send Feishu reply `mediaUrl`/`mediaUrls` payloads as attachments alongside text/streamed replies in the reply dispatcher, including legacy fallback when `mediaUrls` is empty. (#28959) Thanks @icesword0760.
- Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (`SLACK_USER_TOKEN` env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @Glucksberg.
- Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (`SLACK_USER_TOKEN` env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @chilu18.
- Feishu/Outbound session routing: stop assuming bare `oc_` identifiers are always group chats, honor explicit `dm:`/`group:` prefixes for `oc_` chat IDs, and default ambiguous bare `oc_` targets to direct routing to avoid DM session misclassification. (#10407) Thanks @Bermudarat.
- Feishu/Group session routing: add configurable group session scopes (`group`, `group_sender`, `group_topic`, `group_topic_sender`) with legacy `topicSessionMode=enabled` compatibility so Feishu group conversations can isolate sessions by sender/topic as configured. (#17798) Thanks @yfge.
- Feishu/Reply-in-thread routing: add `replyInThread` config (`disabled|enabled`) for group replies, propagate `reply_in_thread` across text/card/media/streaming sends, and align topic-scoped session routing so newly created reply threads stay on the same session root. (#27325) Thanks @kcinzgg.
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg.
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg.
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @hou-rong.
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @PinoHouse.
- Feishu/Mobile video media type: treat inbound `message_type: "media"` as video-equivalent for media key extraction, placeholder inference, and media download resolution so mobile-app video sends ingest correctly. (#25502) Thanks @4ier.
- Feishu/Inbound sender fallback: fall back to `sender_id.user_id` when `sender_id.open_id` is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl.
- Feishu/Reply context metadata: include inbound `parent_id` and `root_id` as `ReplyToId`/`RootMessageId` in inbound context, and parse interactive-card quote bodies into readable text when fetching replied messages. (#18529) Thanks @qiangu.
@@ -269,7 +377,7 @@ Docs: https://docs.openclaw.ai
- Matrix/Directory room IDs: preserve original room-ID casing for direct `!roomId` group lookups (without `:server`) so allowlist checks do not fail on case-sensitive IDs. Landed from contributor PR #31201 by @williamos-dev. Thanks @williamos-dev.
- Discord/Inbound media fallback: preserve attachment and sticker metadata when Discord CDN fetch/save fails by keeping URL-based media entries in context, with regression coverage for save failures and mixed success/failure ordering. Landed from contributor PR #28906 by @Sid-Qin. Thanks @Sid-Qin.
- Auto-reply/Block reply timeout path: normalize `onBlockReply(...)` execution through `Promise.resolve(...)` before timeout wrapping so mixed sync/async callbacks keep deterministic timeout behavior across strict TypeScript build paths. (#19779) Thanks @dalefrieswthat and @vincentkoc.
- Cron/One-shot reschedule re-arm: allow completed `at` jobs to run again when rescheduled to a later time than `lastRunAtMs`, while keeping completed non-rescheduled one-shot jobs inactive. (#28915) Thanks @Glucksberg.
- Cron/One-shot reschedule re-arm: allow completed `at` jobs to run again when rescheduled to a later time than `lastRunAtMs`, while keeping completed non-rescheduled one-shot jobs inactive. (#28915) Thanks @arosstale.
- Docs/Docker images: clarify the official GHCR image source and tag guidance (`main`, `latest`, `<version>`), and document that `OPENCLAW_IMAGE` skips local image builds but still uses the repo-local compose/setup flow. (#27214, #31180) Fixes #15655. Thanks @ipl31.
- Docs/Gateway Docker bind guidance: clarify bridge-network loopback behavior and require bind mode values (`auto`/`loopback`/`lan`/`tailnet`/`custom`) instead of host aliases in `gateway.bind`. (#28001) Thanks @Anandesh-Sharma and @vincentkoc.
- Docker/Image base annotations: add OCI labels for base image plus source/documentation/license metadata, include revision/version/created labels in Docker release builds, and document annotation keys/release context in install docs. Fixes #27945. Thanks @vincentkoc.
@@ -281,7 +389,7 @@ Docs: https://docs.openclaw.ai
- Discord/Ack reactions: add Discord-account-level `ackReactionScope` override and support explicit `off`/`none` values in shared config schemas to disable ack reactions per account. Landed from contributor PR #30400 by @BlueBirdBack. Thanks @BlueBirdBack.
- Discord/Forum thread tags: support `appliedTags` on Discord thread-create actions and map to `applied_tags` for forum/media starter posts, with targeted thread-creation regression coverage. Landed from contributor PR #30358 by @pushkarsingh32. Thanks @pushkarsingh32.
- Discord/Application ID fallback: parse bot application IDs from token prefixes without numeric precision loss and use token fallback only on transport/timeout failures when probing `/oauth2/applications/@me`. Landed from contributor PR #29695 by @dhananjai1729. Thanks @dhananjai1729.
- Discord/EventQueue timeout config: expose per-account `channels.discord.accounts.<id>.eventQueue.listenerTimeout` (and related queue options) so long-running handlers can avoid Carbon listener timeout drops. Landed from contributor PR #28945 by @Glucksberg. Thanks @Glucksberg.
- Discord/EventQueue timeout config: expose per-account `channels.discord.accounts.<id>.eventQueue.listenerTimeout` (and related queue options) so long-running handlers can avoid Carbon listener timeout drops. Landed from contributor PR #24270 by @pdd-cli. Thanks @pdd-cli.
- CLI/Cron run exit code: return exit code `0` only when `cron run` reports `{ ok: true, ran: true }`, and `1` for non-run/error outcomes so scripting/debugging reflects actual execution status. Landed from contributor PR #31121 by @Sid-Qin. Thanks @Sid-Qin.
- Cron/Failure delivery routing: add `failureAlert.mode` (`announce|webhook`) and `failureAlert.accountId` support, plus `cron.failureDestination` and per-job `delivery.failureDestination` routing with duplicate-target suppression, best-effort skip behavior, and global+job merge semantics. Landed from contributor PR #31059 by @kesor. Thanks @kesor.
- CLI/JSON preflight output: keep `--json` command stdout machine-readable by suppressing doctor preflight note output while still running legacy migration/config doctor flow. (#24368) Thanks @altaywtf.
@@ -363,7 +471,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Control UI API routing: when `gateway.controlUi.basePath` is unset (default), stop serving Control UI SPA HTML for `/api` and `/api/*` so API paths fall through to normal gateway handlers/404 responses instead of `index.html`. (#30333) Fixes #30295. thanks @Sid-Qin.
- Cron/One-shot reliability: retry transient one-shot failures with bounded backoff and configurable retry policy before disabling. (#24435) Thanks @hugenshen.
- Gateway/Cron auditability: add gateway info logs for successful cron create, update, and remove operations. (#25090) Thanks @MoerAI.
- Gateway/Tailscale onboarding origin allowlist: auto-add the detected Tailnet HTTPS origin during interactive configure/onboarding flows (including IPv6-safe origin formatting and binary-path reuse), so Tailscale serve/funnel Control UI access works without manual `allowedOrigins` edits. Landed from contributor PR #28960 by @Glucksberg. Thanks @Glucksberg.
- Gateway/Tailscale onboarding origin allowlist: auto-add the detected Tailnet HTTPS origin during interactive configure/onboarding flows (including IPv6-safe origin formatting and binary-path reuse), so Tailscale serve/funnel Control UI access works without manual `allowedOrigins` edits. Landed from contributor PR #26157 by @stakeswky. Thanks @stakeswky.
- Gateway/Upgrade migration for Control UI origins: seed `gateway.controlUi.allowedOrigins` on startup for legacy non-loopback configs (`lan`/`tailnet`/`custom`) when origins are missing or blank, preventing post-upgrade crash loops while preserving explicit existing policy. Landed from contributor PR #29394 by @synchronic1. Thanks @synchronic1.
- Gateway/Plugin HTTP auth hardening: require gateway auth for protected plugin paths and explicit `registerHttpRoute` paths (while preserving wildcard-handler behavior for signature-auth webhooks), and run plugin handlers after built-in handlers for deterministic route precedence. Landed from contributor PR #29198 by @Mariana-Codebase. Thanks @Mariana-Codebase.
- Gateway/Config patch guard: reject `config.patch` updates that set non-loopback `gateway.bind` while `gateway.tailscale.mode` is `serve`/`funnel`, preventing restart crash loops from invalid bind/tailscale combinations. Landed from contributor PR #30910 by @liuxiaopai-ai. Thanks @liuxiaopai-ai.
@@ -374,10 +482,10 @@ Docs: https://docs.openclaw.ai
- Slack/Transient request errors: classify Slack request-error messages like `Client network socket disconnected before secure TLS connection was established` as transient in unhandled-rejection fatal detection, preventing temporary network drops from crash-looping the gateway. (#23169) Thanks @graysurf.
- Slack/Usage footer formatting: wrap session keys in inline code in full response-usage footers so Slack does not parse colon-delimited session segments as emoji shortcodes. (#30258) Thanks @pushkarsingh32.
- Slack/Thread session isolation: route channel/group top-level messages into thread-scoped sessions (`:thread:<ts>`) and read inbound `previousTimestamp` from the resolved thread session key, preventing cross-thread context bleed and stale timestamp lookups. (#10686) Thanks @pablohrcarvalho.
- Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715) Thanks @Glucksberg.
- Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715) Thanks @AIflow-Labs.
- Slack/Legacy streaming config: map boolean `channels.slack.streaming=false` to unified streaming mode `off` (with `nativeStreaming=false`) so legacy configs correctly disable draft preview/native streaming instead of defaulting to `partial`. (#25990) Thanks @chilu18.
- Slack/Socket reconnect reliability: reconnect Socket Mode after disconnect/start failures using bounded exponential backoff with abort-aware waits, while preserving clean shutdown behavior and adding disconnect/error helper tests. (#27232) Thanks @pandego.
- Memory/QMD update+embed output cap: discard captured stdout for `qmd update` and `qmd embed` runs (while keeping stderr diagnostics) so large index progress output no longer fails sync with `produced too much output` during boot/refresh. (#28900) Thanks @Glucksberg.
- Memory/QMD update+embed output cap: discard captured stdout for `qmd update` and `qmd embed` runs (while keeping stderr diagnostics) so large index progress output no longer fails sync with `produced too much output` during boot/refresh. (#28900; landed from contributor PR #23311 by @haitao-sjsu) Thanks @haitao-sjsu.
- Onboarding/Custom providers: raise default custom-provider model context window to the runtime hard minimum (16k) and auto-heal existing custom model entries below that threshold during reconfiguration, preventing immediate `Model context window too small (4096 tokens)` failures. (#21653) Thanks @r4jiv007.
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
@@ -619,7 +727,7 @@ Docs: https://docs.openclaw.ai
- WhatsApp/Web reconnect: treat close status `440` as non-retryable (including string-form status values), stop reconnect loops immediately, and emit operator guidance to relink after resolving session conflicts. (#25858) Thanks @markmusson.
- WhatsApp/Reasoning safety: suppress outbound payloads marked as reasoning and hard-drop text payloads that begin with `Reasoning:` before WhatsApp delivery, preventing hidden thinking blocks from leaking to end users through final-message paths. (#25804, #25214, #24328)
- Matrix/Read receipts: send read receipts as soon as Matrix messages arrive (before handler pipeline work), so clients no longer show long-lived unread/sent states while replies are processing. (#25841, #25840) Thanks @joshjhall.
- Telegram/Replies: when markdown formatting renders to empty HTML (for example syntax-only chunks in threaded replies), retry delivery with plain text, and fail loud when both formatted and plain payloads are empty to avoid false delivered states. (#25096, #25091) Thanks @Glucksberg.
- Telegram/Replies: when markdown formatting renders to empty HTML (for example syntax-only chunks in threaded replies), retry delivery with plain text, and fail loud when both formatted and plain payloads are empty to avoid false delivered states. (#25096, #25091) Thanks @ArsalanShakil.
- Telegram/Media fetch: prioritize IPv4 before IPv6 in SSRF pinned DNS address ordering so media downloads still work on hosts with broken IPv6 routing. (#24295, #23975) Thanks @Glucksberg.
- Telegram/Outbound API: replace Node 22's global undici dispatcher when applying Telegram `autoSelectFamily` decisions so outbound `fetch` calls inherit IPv4 fallback instead of staying pinned to stale dispatcher settings. (#25682, #25676) Thanks @lairtonlelis.
- Onboarding/Telegram: keep core-channel onboarding available when plugin registry population is missing by falling back to built-in adapters and continuing wizard setup with actionable recovery guidance. (#25803) Thanks @Suko.
@@ -710,11 +818,11 @@ Docs: https://docs.openclaw.ai
- Agents/Reasoning: when model-default thinking is active (for example `thinking=low`), keep auto-reasoning disabled unless explicitly enabled, preventing `Reasoning:` thinking-block leakage in channel replies. (#24335, #24290) thanks @Kay-051.
- Agents/Reasoning: avoid classifying provider reasoning-required errors as context overflows so these failures no longer trigger compaction-style overflow recovery. (#24593) Thanks @vincentkoc.
- Agents/Models: codify `agents.defaults.model` / `agents.defaults.imageModel` config-boundary input as `string | {primary,fallbacks}`, split explicit vs effective model resolution, and fix `models status --agent` source attribution so defaults-inherited agents are labeled as `defaults` while runtime selection still honors defaults fallback. (#24210) thanks @bianbiandashen.
- Agents/Compaction: pass `agentDir` into manual `/compact` command runs so compaction auth/profile resolution stays scoped to the active agent. (#24133) thanks @Glucksberg.
- Agents/Compaction: pass `agentDir` into manual `/compact` command runs so compaction auth/profile resolution stays scoped to the active agent. (#24133) thanks @miloudbelarebia.
- Agents/Compaction: pass model metadata through the embedded runtime so safeguard summarization can run when `ctx.model` is unavailable, avoiding repeated `"Summary unavailable due to context limits"` fallback summaries. (#3479) Thanks @battman21, @hanxiao and @vincentkoc.
- Agents/Compaction: cancel safeguard compaction when summary generation cannot run (missing model/API key or summarization failure), preserving history instead of truncating to fallback `"Summary unavailable"` text. (#10711) Thanks @DukeDeSouth and @vincentkoc.
- Agents/Tools: make `session_status` read transcript-derived usage mid-turn and tail-read session logs for cache-aware context reporting without full-log scans. (#22387) Thanks @1ucian.
- Agents/Overflow: detect additional provider context-overflow error shapes (including `input length` + `max_tokens` exceed-context variants) so failures route through compaction/recovery paths instead of leaking raw provider errors to users. (#9951) Thanks @echoVic and @Glucksberg.
- Agents/Overflow: detect additional provider context-overflow error shapes (including `input length` + `max_tokens` exceed-context variants) so failures route through compaction/recovery paths instead of leaking raw provider errors to users. (#9951) Thanks @echoVic.
- Agents/Overflow: add Chinese context-overflow pattern detection in `isContextOverflowError` so localized provider errors route through overflow recovery paths. (#22855) Thanks @Clawborn.
- Agents/Failover: treat HTTP 502/503/504 errors as failover-eligible transient timeouts so fallback chains can switch providers/models during upstream outages instead of retrying the same failing target. (#20999) Thanks @taw0002 and @vincentkoc.
- Auto-reply/Inbound metadata: hide direct-chat `message_id`/`message_id_full` and sender metadata only from normalized chat type (not sender-id sentinels), preserving group metadata visibility and preventing sender-id spoofed direct-mode classification. (#24373) thanks @jd316.
@@ -860,7 +968,7 @@ Docs: https://docs.openclaw.ai
- Config/Memory: allow `"mistral"` in `agents.defaults.memorySearch.provider` and `agents.defaults.memorySearch.fallback` schema validation. (#14934) Thanks @ThomsenDrake.
- Feishu/Commands: in group chats, command authorization now falls back to top-level `channels.feishu.allowFrom` when per-group `allowFrom` is not set, so `/command` no longer gets blocked by an unintended empty allowlist. (#23756) Thanks @steipete.
- Dev tooling: prevent `CLAUDE.md` symlink target regressions by excluding CLAUDE symlink sentinels from `oxfmt` and marking them `-text` in `.gitattributes`, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc.
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349; landed from contributor PR #5005 by @Diaspar4u) Thanks @Diaspar4u.
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633) Thanks @steipete.
- Hooks/Loader: avoid redundant hook-module recompilation on gateway restart by skipping cache-busting for bundled hooks and using stable file metadata keys (`mtime+size`) for mutable workspace/managed/plugin hook imports. (#16953) Thanks @mudrii.
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
@@ -1151,6 +1259,8 @@ Docs: https://docs.openclaw.ai
- iOS/Watch: add an Apple Watch companion MVP with watch inbox UI, watch notification relay handling, and gateway command surfaces for watch status/send flows. (#20054) Thanks @mbelinky.
- iOS/Gateway: wake disconnected iOS nodes via APNs before `nodes.invoke` and auto-reconnect gateway sessions on silent push wake to reduce invoke failures while the app is backgrounded. (#20332) Thanks @mbelinky.
- Gateway/CLI: add paired-device hygiene flows with `device.pair.remove`, plus `openclaw devices remove` and guarded `openclaw devices clear --yes [--pending]` commands for removing paired entries and optionally rejecting pending requests. (#20057) Thanks @mbelinky.
- Mattermost: add opt-in native slash command support with registration lifecycle, callback route/token validation, multi-account token routing, and callback URL/path configuration (`channels.mattermost.commands.*`). (#16515) Thanks @echo931.
- Mattermost: harden native slash callback auth-bypass behavior for configurable callback paths, add callback validation coverage, and clarify callback reachability/allowlist docs. (#32467) Thanks @mukhtharcm and @echo931.
- iOS/APNs: add push registration and notification-signing configuration for node delivery. (#20308) Thanks @mbelinky.
- Gateway/APNs: add a push-test pipeline for APNs delivery validation in gateway flows. (#20307) Thanks @mbelinky.
- Security/Audit: add `gateway.http.no_auth` findings when `gateway.auth.mode="none"` leaves Gateway HTTP APIs reachable, with loopback warning and remote-exposure critical severity, plus regression coverage and docs updates.
@@ -1550,7 +1660,7 @@ Docs: https://docs.openclaw.ai
- Agents: treat `read` tool `file_path` arguments as valid in tool-start diagnostics to avoid false “read tool called without path” warnings when alias parameters are used. (#16717) Thanks @Stache73.
- Agents/Transcript: drop malformed tool-call blocks with blank required fields (`id`/`name` or missing `input`/`arguments`) during session transcript repair to prevent persistent tool-call corruption on future turns. (#15485) Thanks @mike-zachariades.
- Tools/Write/Edit: normalize structured text-block arguments for `content`/`oldText`/`newText` before filesystem edits, preventing JSON-like file corruption and false “exact text not found” misses from block-form params. (#16778) Thanks @danielpipernz.
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg.
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @briancolinger.
- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
- Agents/Process: supervise PTY/child process lifecycles with explicit ownership, cancellation, timeouts, and deterministic cleanup, preventing Codex/Pi PTY sessions from dying or stalling on resume. (#14257) Thanks @onutc.
- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard.
@@ -1873,7 +1983,7 @@ Docs: https://docs.openclaw.ai
- Cron: prevent one-shot `at` jobs from re-firing on gateway restart when previously skipped or errored. (#13845)
- Discord: add exec approval cleanup option to delete DMs after approval/denial/timeout. (#13205) Thanks @thewilloftheshadow.
- Sessions: prune stale entries, cap session store size, rotate large stores, accept duration/size thresholds, default to warn-only maintenance, and prune cron run sessions after retention windows. (#13083) Thanks @skyfallsin, @Glucksberg, @gumadeiras.
- Sessions: prune stale entries, cap session store size, rotate large stores, accept duration/size thresholds, default to warn-only maintenance, and prune cron run sessions after retention windows. (#13083) Thanks @skyfallsin, @gumadeiras.
- CI: Implement pipeline and workflow order. Thanks @quotentiroler.
- WhatsApp: preserve original filenames for inbound documents. (#12691) Thanks @akramcodez.
- Telegram: harden quote parsing; preserve quote context; avoid QUOTE_TEXT_INVALID; avoid nested reply quote misclassification. (#12156) Thanks @rybnikov.
@@ -2023,7 +2133,7 @@ Docs: https://docs.openclaw.ai
- Cron: accept epoch timestamps and 0ms durations in CLI `--at` parsing.
- Cron: reload store data when the store file is recreated or mtime changes.
- Cron: deliver announce runs directly, honor delivery mode, and respect wakeMode for summaries. (#8540) Thanks @tyler6204.
- Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @Glucksberg.
- Telegram: include forward_from_chat metadata in forwarded messages and harden cron delivery target checks. (#8392) Thanks @sleontenko.
- macOS: fix cron payload summary rendering and ISO 8601 formatter concurrency safety.
- Discord: enforce DM allowlists for agent components (buttons/select menus), honoring pairing store approvals and tag matches. (#11254) Thanks @thedudeabidesai.
@@ -2353,7 +2463,7 @@ Docs: https://docs.openclaw.ai
- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.openclaw.ai/tools/web
- UI: refresh Control UI dashboard design system (colors, icons, typography). (#1745, #1786) Thanks @EnzeD, @mousberg.
- Exec approvals: forward approval prompts to chat with `/approve` for all channels (including plugins). (#1621) Thanks @czekaj. https://docs.openclaw.ai/tools/exec-approvals https://docs.openclaw.ai/tools/slash-commands
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @Glucksberg.
- Gateway: expose config.patch in the gateway tool with safe partial updates + restart sentinel. (#1653) Thanks @steipete.
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.openclaw.ai/diagnostics/flags
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
- Docs: add verbose installer troubleshooting guidance.
@@ -2366,7 +2476,7 @@ Docs: https://docs.openclaw.ai
- Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @steipete.
- Web UI: hide internal `message_id` hints in chat bubbles.
- Gateway: allow Control UI token-only auth to skip device pairing even when device identity is present (`gateway.controlUi.allowInsecureAuth`). (#1679) Thanks @steipete.
- Matrix: decrypt E2EE media attachments with preflight size guard. (#1744) Thanks @araa47.

View File

@@ -72,7 +72,7 @@ RUN if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \
# Update OPENCLAW_DOCKER_GPG_FINGERPRINT when Docker rotates release keys.
curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \
expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == \"fpr\" { print toupper($10); exit }')" && \
actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "fpr" { print toupper($10); exit }')" && \
if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-<empty>})" >&2; \
exit 1; \

View File

@@ -40,7 +40,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
- **[OpenAI](https://openai.com/)** (ChatGPT/Codex)
Model note: while any model is supported, I strongly recommend **Anthropic Pro/Max (100/200) + Opus 4.6** for longcontext strength and better promptinjection resistance. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
Model note: while many providers/models are supported, for the best experience and lower prompt-injection risk use the strongest latest-generation model available to you. See [Onboarding](https://docs.openclaw.ai/start/onboarding).
## Models (selection + auth)

View File

@@ -57,6 +57,8 @@ These are frequently reported but are typically closed with no code change:
- Reports that only show differences in heuristic detection/parity (for example obfuscation-pattern detection on one exec path but not another, such as `node.invoke -> system.run` parity gaps) without demonstrating bypass of auth, approvals, allowlist enforcement, sandboxing, or other documented trust boundaries.
- ReDoS/DoS claims that require trusted operator configuration input (for example catastrophic regex in `sessionFilter` or `logging.redactPatterns`) without a trust-boundary bypass.
- Archive/install extraction claims that require pre-existing local filesystem priming in trusted state (for example planting symlink/hardlink aliases under destination directories such as skills/tools paths) without showing an untrusted path that can create/control that primitive.
- Reports that depend on replacing or rewriting an already-approved executable path on a trusted host (same-path inode/content swap) without showing an untrusted path to perform that write.
- Reports that depend on pre-existing symlinked skill/workspace filesystem state (for example symlink chains involving `skills/*/SKILL.md`) without showing an untrusted path that can create/control that state.
- Missing HSTS findings on default local/loopback deployments.
- Slack webhook signature findings when HTTP mode already uses signing-secret verification.
- Discord inbound webhook signature findings for paths not used by this repo's Discord integration.
@@ -114,6 +116,8 @@ Plugins/extensions are part of OpenClaw's trusted computing base for a gateway.
- Prompt-injection-only attacks (without a policy/auth/sandbox boundary bypass)
- Reports that require write access to trusted local state (`~/.openclaw`, workspace files like `MEMORY.md` / `memory/*.md`)
- Reports where exploitability depends on attacker-controlled pre-existing symlink/hardlink filesystem state in trusted local paths (for example extraction/install target trees) unless a separate untrusted boundary bypass is shown that creates that state.
- Reports whose only claim is sandbox/workspace read expansion through trusted local skill/workspace symlink state (for example `skills/*/SKILL.md` symlink chains) unless a separate untrusted boundary bypass is shown that creates/controls that state.
- Reports whose only claim is post-approval executable identity drift on a trusted host via same-path file replacement/rewrite unless a separate untrusted boundary bypass is shown for that host write primitive.
- Reports where the only demonstrated impact is an already-authorized sender intentionally invoking a local-action command (for example `/export-session` writing to an absolute host path) without bypassing auth, sandbox, or another documented boundary
- Reports where the only claim is that a trusted-installed/enabled plugin can execute with gateway/host privileges (documented trust model behavior).
- Any report whose only claim is that an operator-enabled `dangerous*`/`dangerously*` config option weakens defaults (these are explicit break-glass tradeoffs by design)

View File

@@ -2,6 +2,225 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.3.2</title>
<pubDate>Tue, 03 Mar 2026 04:30:29 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026030290</sparkle:version>
<sparkle:shortVersionString>2026.3.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.2</h2>
<h3>Changes</h3>
<ul>
<li>Secrets/SecretRef coverage: expand SecretRef support across the full supported user-supplied credential surface (64 targets total), including runtime collectors, <code>openclaw secrets</code> planning/apply/audit flows, onboarding SecretInput UX, and related docs; unresolved refs now fail fast on active surfaces while inactive surfaces report non-blocking diagnostics. (#29580) Thanks @joshavant.</li>
<li>Tools/PDF analysis: add a first-class <code>pdf</code> tool with native Anthropic and Google PDF provider support, extraction fallback for non-native models, configurable defaults (<code>agents.defaults.pdfModel</code>, <code>pdfMaxBytesMb</code>, <code>pdfMaxPages</code>), and docs/tests covering routing, validation, and registration. (#31319) Thanks @tyler6204.</li>
<li>Outbound adapters/plugins: add shared <code>sendPayload</code> support across direct-text-media, Discord, Slack, WhatsApp, Zalo, and Zalouser with multi-media iteration and chunk-aware text fallback. (#30144) Thanks @nohat.</li>
<li>Models/MiniMax: add first-class <code>MiniMax-M2.5-highspeed</code> support across built-in provider catalogs, onboarding flows, and MiniMax OAuth plugin defaults, while keeping legacy <code>MiniMax-M2.5-Lightning</code> compatibility for existing configs.</li>
<li>Sessions/Attachments: add inline file attachment support for <code>sessions_spawn</code> (subagent runtime only) with base64/utf8 encoding, transcript content redaction, lifecycle cleanup, and configurable limits via <code>tools.sessions_spawn.attachments</code>. (#16761) Thanks @napetrov.</li>
<li>Telegram/Streaming defaults: default <code>channels.telegram.streaming</code> to <code>partial</code> (from <code>off</code>) so new Telegram setups get live preview streaming out of the box, with runtime fallback to message-edit preview when native drafts are unavailable.</li>
<li>Telegram/DM streaming: use <code>sendMessageDraft</code> for private preview streaming, keep reasoning/answer preview lanes separated in DM reasoning-stream mode. (#31824) Thanks @obviyus.</li>
<li>Telegram/voice mention gating: add optional <code>disableAudioPreflight</code> on group/topic config to skip mention-detection preflight transcription for inbound voice notes where operators want text-only mention checks. (#23067) Thanks @yangnim21029.</li>
<li>CLI/Config validation: add <code>openclaw config validate</code> (with <code>--json</code>) to validate config files before gateway startup, and include detailed invalid-key paths in startup invalid-config errors. (#31220) thanks @Sid-Qin.</li>
<li>Tools/Diffs: add PDF file output support and rendering quality customization controls (<code>fileQuality</code>, <code>fileScale</code>, <code>fileMaxWidth</code>) for generated diff artifacts, and document PDF as the preferred option when messaging channels compress images. (#31342) Thanks @gumadeiras.</li>
<li>Memory/Ollama embeddings: add <code>memorySearch.provider = "ollama"</code> and <code>memorySearch.fallback = "ollama"</code> support, honor <code>models.providers.ollama</code> settings for memory embedding requests, and document Ollama embedding usage. (#26349) Thanks @nico-hoff.</li>
<li>Zalo Personal plugin (<code>@openclaw/zalouser</code>): rebuilt channel runtime to use native <code>zca-js</code> integration in-process, removing external CLI transport usage and keeping QR/login + send/listen flows fully inside OpenClaw.</li>
<li>Plugin SDK/channel extensibility: expose <code>channelRuntime</code> on <code>ChannelGatewayContext</code> so external channel plugins can access shared runtime helpers (reply/routing/session/text/media/commands) without internal imports. (#25462) Thanks @guxiaobo.</li>
<li>Plugin runtime/STT: add <code>api.runtime.stt.transcribeAudioFile(...)</code> so extensions can transcribe local audio files through OpenClaw's configured media-understanding audio providers. (#22402) Thanks @benthecarman.</li>
<li>Plugin hooks/session lifecycle: include <code>sessionKey</code> in <code>session_start</code>/<code>session_end</code> hook events and contexts so plugins can correlate lifecycle callbacks with routing identity. (#26394) Thanks @tempeste.</li>
<li>Hooks/message lifecycle: add internal hook events <code>message:transcribed</code> and <code>message:preprocessed</code>, plus richer outbound <code>message:sent</code> context (<code>isGroup</code>, <code>groupId</code>) for group-conversation correlation and post-transcription automations. (#9859) Thanks @Drickon.</li>
<li>Media understanding/audio echo: add optional <code>tools.media.audio.echoTranscript</code> + <code>echoFormat</code> to send a pre-agent transcript confirmation message to the originating chat, with echo disabled by default. (#32150) Thanks @AytuncYildizli.</li>
<li>Plugin runtime/system: expose <code>runtime.system.requestHeartbeatNow(...)</code> so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral.</li>
<li>Plugin runtime/events: expose <code>runtime.events.onAgentEvent</code> and <code>runtime.events.onSessionTranscriptUpdate</code> for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic.</li>
<li>CLI/Banner taglines: add <code>cli.banner.taglineMode</code> (<code>random</code> | <code>default</code> | <code>off</code>) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li><strong>BREAKING:</strong> Onboarding now defaults <code>tools.profile</code> to <code>messaging</code> for new local installs (interactive + non-interactive). New setups no longer start with broad coding/system tools unless explicitly configured.</li>
<li><strong>BREAKING:</strong> ACP dispatch now defaults to enabled unless explicitly disabled (<code>acp.dispatch.enabled=false</code>). If you need to pause ACP turn routing while keeping <code>/acp</code> controls, set <code>acp.dispatch.enabled=false</code>. Docs: https://docs.openclaw.ai/tools/acp-agents</li>
<li><strong>BREAKING:</strong> Plugin SDK removed <code>api.registerHttpHandler(...)</code>. Plugins must register explicit HTTP routes via <code>api.registerHttpRoute({ path, auth, match, handler })</code>, and dynamic webhook lifecycles should use <code>registerPluginHttpRoute(...)</code>.</li>
<li><strong>BREAKING:</strong> Zalo Personal plugin (<code>@openclaw/zalouser</code>) no longer depends on external <code>zca</code>-compatible CLI binaries (<code>openzca</code>, <code>zca-cli</code>) for runtime send/listen/login; operators should use <code>openclaw channels login --channel zalouser</code> after upgrade to refresh sessions in the new JS-native path.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Plugin command/runtime hardening: validate and normalize plugin command name/description at registration boundaries, and guard Telegram native menu normalization paths so malformed plugin command specs cannot crash startup (<code>trim</code> on undefined). (#31997) Fixes #31944. Thanks @liuxiaopai-ai.</li>
<li>Telegram: guard duplicate-token checks and gateway startup token normalization when account tokens are missing, preventing <code>token.trim()</code> crashes during status/start flows. (#31973) Thanks @ningding97.</li>
<li>Discord/lifecycle startup status: push an immediate <code>connected</code> status snapshot when the gateway is already connected before lifecycle debug listeners attach, with abort-guarding to avoid contradictory status flips during pre-aborted startup. (#32336) Thanks @mitchmcalister.</li>
<li>Feishu/LINE group system prompts: forward per-group <code>systemPrompt</code> config into inbound context <code>GroupSystemPrompt</code> for Feishu and LINE group/room events so configured group-specific behavior actually applies at dispatch time. (#31713) Thanks @whiskyboy.</li>
<li>Mentions/Slack formatting hardening: add null-safe guards for runtime text normalization paths so malformed/undefined text payloads do not crash mention stripping or mrkdwn conversion. (#31865) Thanks @stone-jin.</li>
<li>Feishu/Plugin sdk compatibility: add safe webhook default fallbacks when loading Feishu monitor state so mixed-version installs no longer crash if older <code>openclaw/plugin-sdk</code> builds omit webhook default constants. (#31606)</li>
<li>Feishu/group broadcast dispatch: add configurable multi-agent group broadcast dispatch with observer-session isolation, cross-account dedupe safeguards, and non-mention history buffering rules that avoid duplicate replay in broadcast/topic workflows. (#29575) Thanks @ohmyskyhigh.</li>
<li>Gateway/Subagent TLS pairing: allow authenticated local <code>gateway-client</code> backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring <code>sessions_spawn</code> with <code>gateway.tls.enabled=true</code> in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.</li>
<li>Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast.</li>
<li>Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.</li>
<li>Feishu/Dedup restart resilience: warm persistent dedup state into memory on monitor startup so retry events after gateway restart stay suppressed without requiring initial on-disk probe misses. (#31605)</li>
<li>Voice-call/runtime lifecycle: prevent <code>EADDRINUSE</code> loops by resetting failed runtime promises, making webhook <code>start()</code> idempotent with the actual bound port, and fully cleaning up webhook/tunnel/tailscale resources after startup failures. (#32395) Thanks @scoootscooob.</li>
<li>Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when <code>gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback</code> accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example <code>(a|aa)+</code>), and bound large regex-evaluation inputs for session-filter and log-redaction paths.</li>
<li>Gateway/Plugin HTTP hardening: require explicit <code>auth</code> for plugin route registration, add route ownership guards for duplicate <code>path+match</code> registrations, centralize plugin path matching/auth logic into dedicated modules, and share webhook target-route lifecycle wiring across channel monitors to avoid stale or conflicting registrations. Thanks @tdjackey for reporting.</li>
<li>Browser/Profile defaults: prefer <code>openclaw</code> profile over <code>chrome</code> in headless/no-sandbox environments unless an explicit <code>defaultProfile</code> is configured. (#14944) Thanks @BenediktSchackenberg.</li>
<li>Gateway/WS security: keep plaintext <code>ws://</code> loopback-only by default, with explicit break-glass private-network opt-in via <code>OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1</code>; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.</li>
<li>OpenAI Codex OAuth/TLS prerequisites: add an OAuth TLS cert-chain preflight with actionable remediation for cert trust failures, and gate doctor TLS prerequisite probing to OpenAI Codex OAuth-configured installs (or explicit <code>doctor --deep</code>) to avoid unconditional outbound probe latency. (#32051) Thanks @alexfilatov.</li>
<li>Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting.</li>
<li>CLI/Config validation and routing hardening: dedupe <code>openclaw config validate</code> failures to a single authoritative report, expose allowed-values metadata/hints across core Zod and plugin AJV validation (including <code>--json</code> fields), sanitize terminal-rendered validation text, and make command-path parsing root-option-aware across preaction/route/lazy registration (including routed <code>config get/unset</code> with split root options). Thanks @gumadeiras.</li>
<li>Browser/Extension relay reconnect tolerance: keep <code>/json/version</code> and <code>/cdp</code> reachable during short MV3 worker disconnects when attached targets still exist, and retain clients across reconnect grace windows. (#30232) Thanks @Sid-Qin.</li>
<li>CLI/Browser start timeout: honor <code>openclaw browser --timeout <ms> start</code> and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc.</li>
<li>Synology Chat/gateway lifecycle: keep <code>startAccount</code> pending until abort for inactive and active account paths to prevent webhook route restart loops under gateway supervision. (#23074) Thanks @druide67.</li>
<li>Exec approvals/allowlist matching: escape regex metacharacters in path-pattern literals (while preserving glob wildcards), preventing crashes on allowlisted executables like <code>/usr/bin/g++</code> and correctly matching mixed wildcard/literal token paths. (#32162) Thanks @stakeswky.</li>
<li>Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with <code>204</code> to avoid persistent <code>Processing...</code> states in Synology Chat clients. (#26635) Thanks @memphislee09-source.</li>
<li>Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.</li>
<li>Slack/Bolt startup compatibility: remove invalid <code>message.channels</code> and <code>message.groups</code> event registrations so Slack providers no longer crash on startup with Bolt 4.6+; channel/group traffic continues through the unified <code>message</code> handler (<code>channel_type</code>). (#32033) Thanks @mahopan.</li>
<li>Slack/socket auth failure handling: fail fast on non-recoverable auth errors (<code>account_inactive</code>, <code>invalid_auth</code>, etc.) during startup and reconnect instead of retry-looping indefinitely, including <code>unable_to_socket_mode_start</code> error payload propagation. (#32377) Thanks @scoootscooob.</li>
<li>Gateway/macOS LaunchAgent hardening: write <code>Umask=077</code> in generated gateway LaunchAgent plists so npm upgrades preserve owner-only default file permissions for gateway-created state files. (#31919) Fixes #31905. Thanks @liuxiaopai-ai.</li>
<li>macOS/LaunchAgent security defaults: write <code>Umask=63</code> (octal <code>077</code>) into generated gateway launchd plists so post-update service reinstalls keep owner-only file permissions by default instead of falling back to system <code>022</code>. (#32022) Fixes #31905. Thanks @liuxiaopai-ai.</li>
<li>Media understanding/provider HTTP proxy routing: pass a proxy-aware fetch function from <code>HTTPS_PROXY</code>/<code>HTTP_PROXY</code> env vars into audio/video provider calls (with graceful malformed-proxy fallback) so transcription/video requests honor configured outbound proxies. (#27093) Thanks @mcaxtr.</li>
<li>Sandbox/workspace mount permissions: make primary <code>/workspace</code> bind mounts read-only whenever <code>workspaceAccess</code> is not <code>rw</code> (including <code>none</code>) across both core sandbox container and sandbox browser create flows. (#32227) Thanks @guanyu-zhang.</li>
<li>Tools/fsPolicy propagation: honor <code>tools.fs.workspaceOnly</code> for image/pdf local-root allowlists so non-sandbox media paths outside workspace are rejected when workspace-only mode is enabled. (#31882) Thanks @justinhuangcode.</li>
<li>Daemon/Homebrew runtime pinning: resolve Homebrew Cellar Node paths to stable Homebrew-managed symlinks (including versioned formulas like <code>node@22</code>) so gateway installs keep the intended runtime across brew upgrades. (#32185) Thanks @scoootscooob.</li>
<li>Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting.</li>
<li>Gateway/Security canonicalization hardening: decode plugin route path variants to canonical fixpoint (with bounded depth), fail closed on canonicalization anomalies, and enforce gateway auth for deeply encoded <code>/api/channels/*</code> variants to prevent alternate-path auth bypass through plugin handlers. Thanks @tdjackey for reporting.</li>
<li>Browser/Gateway hardening: preserve env credentials for <code>OPENCLAW_GATEWAY_URL</code> / <code>CLAWDBOT_GATEWAY_URL</code> while treating explicit <code>--url</code> as override-only auth, and make container browser hardening flags optional with safer defaults for Docker/LXC stability. (#31504) Thanks @vincentkoc.</li>
<li>Gateway/Control UI basePath webhook passthrough: let non-read methods under configured <code>controlUiBasePath</code> fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.</li>
<li>Control UI/Legacy browser compatibility: replace <code>toSorted</code>-dependent cron suggestion sorting in <code>app-render</code> with a compatibility helper so older browsers without <code>Array.prototype.toSorted</code> no longer white-screen. (#31775) Thanks @liuxiaopai-ai.</li>
<li>macOS/PeekabooBridge: add compatibility socket symlinks for legacy <code>clawdbot</code>, <code>clawdis</code>, and <code>moltbot</code> Application Support socket paths so pre-rename clients can still connect. (#6033) Thanks @lumpinif and @vincentkoc.</li>
<li>Gateway/message tool reliability: avoid false <code>Unknown channel</code> failures when <code>message.*</code> actions receive platform-specific channel ids by falling back to <code>toolContext.currentChannelProvider</code>, and prevent health-monitor restart thrash for channels that just (re)started by adding a per-channel startup-connect grace window. (from #32367) Thanks @MunemHashmi.</li>
<li>Windows/Spawn canonicalization: unify non-core Windows spawn handling across ACP client, QMD/mcporter memory paths, and sandbox Docker execution using the shared wrapper-resolution policy, with targeted regression coverage for <code>.cmd</code> shim unwrapping and shell fallback behavior. (#31750) Thanks @Takhoffman.</li>
<li>Security/ACP sandbox inheritance: enforce fail-closed runtime guardrails for <code>sessions_spawn</code> with <code>runtime="acp"</code> by rejecting ACP spawns from sandboxed requester sessions and rejecting <code>sandbox="require"</code> for ACP runtime, preventing sandbox-boundary bypass via host-side ACP initialization. (#32254) Thanks @tdjackey for reporting, and @dutifulbob for the fix.</li>
<li>Security/Web tools SSRF guard: keep DNS pinning for untrusted <code>web_fetch</code> and citation-redirect URL checks when proxy env vars are set, and require explicit dangerous opt-in before env-proxy routing can bypass pinned dispatch for trusted/operator-controlled endpoints. Thanks @tdjackey for reporting.</li>
<li>Gemini schema sanitization: coerce malformed JSON Schema <code>properties</code> values (<code>null</code>, arrays, primitives) to <code>{}</code> before provider validation, preventing downstream strict-validator crashes on invalid plugin/tool schemas. (#32332) Thanks @webdevtodayjason.</li>
<li>Media understanding/malformed attachment guards: harden attachment selection and decision summary formatting against non-array or malformed attachment payloads to prevent runtime crashes on invalid inbound metadata shapes. (#28024) Thanks @claw9267.</li>
<li>Browser/Extension navigation reattach: preserve debugger re-attachment when relay is temporarily disconnected by deferring relay attach events until reconnect/re-announce, reducing post-navigation tab loss. (#28725) Thanks @stone-jin.</li>
<li>Browser/Extension relay stale tabs: evict stale cached targets from <code>/json/list</code> when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc.</li>
<li>Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up <code>PortInUseError</code> races after <code>browser start</code>/<code>open</code>. (#29538) Thanks @AaronWander.</li>
<li>OpenAI/Responses WebSocket tool-call id hygiene: normalize blank/whitespace streamed tool-call ids before persistence, and block empty <code>function_call_output.call_id</code> payloads in the WS conversion path to avoid OpenAI 400 errors (<code>Invalid 'input[n].call_id': empty string</code>), with regression coverage for both inbound stream normalization and outbound payload guards.</li>
<li>Security/Nodes camera URL downloads: bind node <code>camera.snap</code>/<code>camera.clip</code> URL payload downloads to the resolved node host, enforce fail-closed behavior when node <code>remoteIp</code> is unavailable, and use SSRF-guarded fetch with redirect host/protocol checks to prevent off-node fetch pivots. Thanks @tdjackey for reporting.</li>
<li>Config/backups hardening: enforce owner-only (<code>0600</code>) permissions on rotated config backups and clean orphan <code>.bak.*</code> files outside the managed backup ring, reducing credential leakage risk from stale or permissive backup artifacts. (#31718) Thanks @YUJIE2002.</li>
<li>Telegram/inbound media filenames: preserve original <code>file_name</code> metadata for document/audio/video/animation downloads (with fetch/path fallbacks), so saved inbound attachments keep sender-provided names instead of opaque Telegram file paths. (#31837) Thanks @Kay-051.</li>
<li>Gateway/OpenAI chat completions: honor <code>x-openclaw-message-channel</code> when building <code>agentCommand</code> input for <code>/v1/chat/completions</code>, preserving caller channel identity instead of forcing <code>webchat</code>. (#30462) Thanks @bmendonca3.</li>
<li>Plugin SDK/runtime hardening: add package export verification in CI/release checks to catch missing runtime exports before publish-time regressions. (#28575) Thanks @Glucksberg.</li>
<li>Media/MIME normalization: normalize parameterized/case-variant MIME strings in <code>kindFromMime</code> (for example <code>Audio/Ogg; codecs=opus</code>) so WhatsApp voice notes are classified as audio and routed through transcription correctly. (#32280) Thanks @Lucenx9.</li>
<li>Discord/audio preflight mentions: detect audio attachments via Discord <code>content_type</code> and gate preflight transcription on typed text (not media placeholders), so guild voice-note mentions are transcribed and matched correctly. (#32136) Thanks @jnMetaCode.</li>
<li>Feishu/topic session routing: use <code>thread_id</code> as topic session scope fallback when <code>root_id</code> is absent, keep first-turn topic keys stable across thread creation, and force thread replies when inbound events already carry topic/thread context. (#29788) Thanks @songyaolun.</li>
<li>Gateway/Webchat NO_REPLY streaming: suppress assistant lead-fragment deltas that are prefixes of <code>NO_REPLY</code> and keep final-message buffering in sync, preventing partial <code>NO</code> leaks on silent-response runs while preserving legitimate short replies. (#32073) Thanks @liuxiaopai-ai.</li>
<li>Telegram/models picker callbacks: keep long model buttons selectable by falling back to compact callback payloads and resolving provider ids on selection (with provider re-prompt on ambiguity), avoiding Telegram 64-byte callback truncation failures. (#31857) Thanks @bmendonca3.</li>
<li>Context-window metadata warmup: add exponential config-load retry backoff (1s -> 2s -> 4s, capped at 60s) so transient startup failures recover automatically without hot-loop retries.</li>
<li>Voice-call/Twilio external outbound: auto-register webhook-first <code>outbound-api</code> calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.</li>
<li>Feishu/topic root replies: prefer <code>root_id</code> as outbound <code>replyTargetMessageId</code> when present, and parse millisecond <code>message_create_time</code> values correctly so topic replies anchor to the root message in grouped thread flows. (#29968) Thanks @bmendonca3.</li>
<li>Feishu/DM pairing reply target: send pairing challenge replies to <code>chat:<chat_id></code> instead of <code>user:<sender_open_id></code> so Lark/Feishu private chats with user-id-only sender payloads receive pairing messages reliably. (#31403) Thanks @stakeswky.</li>
<li>Feishu/Lark private DM routing: treat inbound <code>chat_type: "private"</code> as direct-message context for pairing/mention-forward/reaction synthetic handling so Lark private chats behave like Feishu p2p DMs. (#31400) Thanks @stakeswky.</li>
<li>Signal/message actions: allow <code>react</code> to fall back to <code>toolContext.currentMessageId</code> when <code>messageId</code> is omitted, matching Telegram behavior and unblocking agent-initiated reactions on inbound turns. (#32217) Thanks @dunamismax.</li>
<li>Discord/message actions: allow <code>react</code> to fall back to <code>toolContext.currentMessageId</code> when <code>messageId</code> is omitted, matching Telegram/Signal reaction ergonomics in inbound turns.</li>
<li>Synology Chat/reply delivery: resolve webhook usernames to Chat API <code>user_id</code> values for outbound chatbot replies, avoiding mismatches between webhook user IDs and <code>method=chatbot</code> recipient IDs in multi-account setups. (#23709) Thanks @druide67.</li>
<li>Slack/thread context payloads: only inject thread starter/history text on first thread turn for new sessions while preserving thread metadata, reducing repeated context-token bloat on long-lived thread sessions. (#32133) Thanks @sourman.</li>
<li>Slack/session routing: keep top-level channel messages in one shared session when <code>replyToMode=off</code>, while preserving thread-scoped keys for true thread replies and non-off modes. (#32193) Thanks @bmendonca3.</li>
<li>Voice-call/webhook routing: require exact webhook path matches (instead of prefix matches) so lookalike paths cannot reach provider verification/dispatch logic. (#31930) Thanks @afurm.</li>
<li>Zalo/Pairing auth tests: add webhook regression coverage asserting DM pairing-store reads/writes remain account-scoped, preventing cross-account authorization bleed in multi-account setups. (#26121) Thanks @bmendonca3.</li>
<li>Zalouser/Pairing auth tests: add account-scoped DM pairing-store regression coverage (<code>monitor.account-scope.test.ts</code>) to prevent cross-account allowlist bleed in multi-account setups. (#26672) Thanks @bmendonca3.</li>
<li>Feishu/Send target prefixes: normalize explicit <code>group:</code>/<code>dm:</code> send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai.</li>
<li>Webchat/Feishu session continuation: preserve routable <code>OriginatingChannel</code>/<code>OriginatingTo</code> metadata from session delivery context in <code>chat.send</code>, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)</li>
<li>Telegram/implicit mention forum handling: exclude Telegram forum system service messages (<code>forum_topic_*</code>, <code>general_forum_topic_*</code>) from reply-chain implicit mention detection so <code>requireMention</code> does not get bypassed inside bot-created topic lifecycle events. (#32262) Thanks @scoootscooob.</li>
<li>Slack/inbound debounce routing: isolate top-level non-DM message debounce keys by message timestamp to avoid cross-thread collisions, preserve DM batching, and flush pending top-level buffers before immediate non-debounce follow-ups to keep ordering stable. (#31951) Thanks @scoootscooob.</li>
<li>Feishu/Duplicate replies: suppress same-target reply dispatch when message-tool sends use generic provider metadata (<code>provider: "message"</code>) and normalize <code>lark</code>/<code>feishu</code> provider aliases during duplicate-target checks, preventing double-delivery in Feishu sessions. (#31526)</li>
<li>Webchat/silent token leak: filter assistant <code>NO_REPLY</code>-only transcript entries from <code>chat.history</code> responses and add client-side defense-in-depth guards in the chat controller so internal silent tokens never render as visible chat bubbles. (#32015) Consolidates overlap from #32183, #32082, #32045, #32052, #32172, and #32112. Thanks @ademczuk, @liuxiaopai-ai, @ningding97, @bmendonca3, and @x4v13r1120.</li>
<li>Doctor/local memory provider checks: stop false-positive local-provider warnings when <code>provider=local</code> and no explicit <code>modelPath</code> is set by honoring default local model fallback while still warning when gateway probe reports local embeddings not ready. (#32014) Fixes #31998. Thanks @adhishthite.</li>
<li>Media understanding/parakeet CLI output parsing: read <code>parakeet-mlx</code> transcripts from <code>--output-dir/<media-basename>.txt</code> when txt output is requested (or default), with stdout fallback for non-txt formats. (#9177) Thanks @mac-110.</li>
<li>Media understanding/audio transcription guard: skip tiny/empty audio files (<1024 bytes) before provider/CLI transcription to avoid noisy invalid-audio failures and preserve clean fallback behavior. (#8388) Thanks @Glucksberg.</li>
<li>Gateway/Plugin HTTP route precedence: run explicit plugin HTTP routes before the Control UI SPA catch-all so registered plugin webhook/custom paths remain reachable, while unmatched paths still fall through to Control UI handling. (#31885) Thanks @Sid-Qin.</li>
<li>Gateway/Node browser proxy routing: honor <code>profile</code> from <code>browser.request</code> JSON body when query params omit it, while preserving query-profile precedence when both are present. (#28852) Thanks @Sid-Qin.</li>
<li>Gateway/Control UI basePath POST handling: return 405 for <code>POST</code> on exact basePath routes (for example <code>/openclaw</code>) instead of redirecting, and add end-to-end regression coverage that root-mounted webhook POST paths still pass through to plugin handlers. (#31349) Thanks @Sid-Qin.</li>
<li>Browser/default profile selection: default <code>browser.defaultProfile</code> behavior now prefers <code>openclaw</code> (managed standalone CDP) when no explicit default is configured, while still auto-provisioning the <code>chrome</code> relay profile for explicit opt-in use. (#32031) Fixes #31907. Thanks @liuxiaopai-ai.</li>
<li>Sandbox/mkdirp boundary checks: allow existing in-boundary directories to pass mkdirp boundary validation when directory open probes return platform-specific I/O errors, with regression coverage for directory-safe fallback behavior. (#31547) Thanks @stakeswky.</li>
<li>Models/config env propagation: apply <code>config.env.vars</code> before implicit provider discovery in models bootstrap so config-scoped credentials are visible to implicit provider resolution paths. (#32295) Thanks @hsiaoa.</li>
<li>Models/Codex usage labels: infer weekly secondary usage windows from reset cadence when API window seconds are ambiguously reported as 24h, so <code>openclaw models status</code> no longer mislabels weekly limits as daily. (#31938) Thanks @bmendonca3.</li>
<li>Gateway/Heartbeat model reload: treat <code>models.*</code> and <code>agents.defaults.model</code> config updates as heartbeat hot-reload triggers so heartbeat picks up model changes without a full gateway restart. (#32046) Thanks @stakeswky.</li>
<li>Memory/LanceDB embeddings: forward configured <code>embedding.dimensions</code> into OpenAI embeddings requests so vector size and API output dimensions stay aligned when dimensions are explicitly configured. (#32036) Thanks @scotthuang.</li>
<li>Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.</li>
<li>Browser/CDP status accuracy: require a successful <code>Browser.getVersion</code> response over the CDP websocket (not just socket-open) before reporting <code>cdpReady</code>, so stale idle command channels are surfaced as unhealthy. (#23427) Thanks @vincentkoc.</li>
<li>Daemon/systemd checks in containers: treat missing <code>systemctl</code> invocations (including <code>spawn systemctl ENOENT</code>/<code>EACCES</code>) as unavailable service state during <code>is-enabled</code> checks, preventing container flows from failing with <code>Gateway service check failed</code> before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc.</li>
<li>Security/Node exec approvals: revalidate approval-bound <code>cwd</code> identity immediately before execution/forwarding and fail closed with an explicit denial when <code>cwd</code> drifts after approval hardening.</li>
<li>Security audit/skills workspace hardening: add <code>skills.workspace.symlink_escape</code> warning in <code>openclaw security audit</code> when workspace <code>skills/**/SKILL.md</code> resolves outside the workspace root (for example symlink-chain drift), plus docs coverage in the security glossary.</li>
<li>Security/Node exec approvals: preserve shell/dispatch-wrapper argv semantics during approval hardening so approved wrapper commands (for example <code>env sh -c ...</code>) cannot drift into a different runtime command shape, and add regression coverage for both approval-plan generation and approved runtime execution paths. Thanks @tdjackey for reporting.</li>
<li>Security/fs-safe write hardening: make <code>writeFileWithinRoot</code> use same-directory temp writes plus atomic rename, add post-write inode/hardlink revalidation with security warnings on boundary drift, and avoid truncating existing targets when final rename fails.</li>
<li>Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting.</li>
<li>Security/Prompt spoofing hardening: stop injecting queued runtime events into user-role prompt text, route them through trusted system-prompt context, and neutralize inbound spoof markers like <code>[System Message]</code> and line-leading <code>System:</code> in untrusted message content. (#30448)</li>
<li>Sandbox/Docker setup command parsing: accept <code>agents.*.sandbox.docker.setupCommand</code> as either a string or a string array, and normalize arrays to newline-delimited shell scripts so multi-step setup commands no longer concatenate without separators. (#31953) Thanks @liuxiaopai-ai.</li>
<li>Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction <code>AGENTS.md</code> context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting.</li>
<li>Agents/Sandbox workdir mapping: map container workdir paths (for example <code>/workspace</code>) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.</li>
<li>Docker/Sandbox bootstrap hardening: make <code>OPENCLAW_SANDBOX</code> opt-in parsing explicit (<code>1|true|yes|on</code>), support custom Docker socket paths via <code>OPENCLAW_DOCKER_SOCKET</code>, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to <code>off</code> when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc.</li>
<li>Hooks/webhook ACK compatibility: return <code>200</code> (instead of <code>202</code>) for successful <code>/hooks/agent</code> requests so providers that require <code>200</code> (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg.</li>
<li>Feishu/Run channel fallback: prefer <code>Provider</code> over <code>Surface</code> when inferring queued run <code>messageProvider</code> fallback (when <code>OriginatingChannel</code> is missing), preventing Feishu turns from being mislabeled as <code>webchat</code> in mixed relay metadata contexts. (#31880) Fixes #31859. Thanks @liuxiaopai-ai.</li>
<li>Skills/sherpa-onnx-tts: run the <code>sherpa-onnx-tts</code> bin under ESM (replace CommonJS <code>require</code> imports) and add regression coverage to prevent <code>require is not defined in ES module scope</code> startup crashes. (#31965) Thanks @bmendonca3.</li>
<li>Inbound metadata/direct relay context: restore direct-channel conversation metadata blocks for external channels (for example WhatsApp) while preserving webchat-direct suppression, so relay agents recover sender/message identifiers without reintroducing internal webchat metadata noise. (#31969) Fixes #29972. Thanks @Lucenx9.</li>
<li>Slack/Channel message subscriptions: register explicit <code>message.channels</code> and <code>message.groups</code> monitor handlers (alongside generic <code>message</code>) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674.</li>
<li>Hooks/session-scoped memory context: expose ephemeral <code>sessionId</code> in embedded plugin tool contexts and <code>before_tool_call</code>/<code>after_tool_call</code> hook contexts (including compaction and client-tool wiring) so plugins can isolate per-conversation state across <code>/new</code> and <code>/reset</code>. Related #31253 and #31304. Thanks @Sid-Qin and @Servo-AIpex.</li>
<li>Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.</li>
<li>Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.</li>
<li>Feishu/File upload filenames: percent-encode non-ASCII/special-character <code>file_name</code> values in Feishu multipart uploads so Chinese/symbol-heavy filenames are sent as proper attachments instead of plain text links. (#31179) Thanks @Kay-051.</li>
<li>Media/MIME channel parity: route Telegram/Signal/iMessage media-kind checks through normalized <code>kindFromMime</code> so mixed-case/parameterized MIME values classify consistently across message channels.</li>
<li>WhatsApp/inbound self-message context: propagate inbound <code>fromMe</code> through the web inbox pipeline and annotate direct self messages as <code>(self)</code> in envelopes so agents can distinguish owner-authored turns from contact turns. (#32167) Thanks @scoootscooob.</li>
<li>Webchat/stream finalization: persist streamed assistant text when final events omit <code>message</code>, while keeping final payload precedence and skipping empty stream buffers to prevent disappearing replies after tool turns. (#31920) Thanks @Sid-Qin.</li>
<li>Feishu/Inbound ordering: serialize message handling per chat while preserving cross-chat concurrency to avoid same-chat race drops under bursty inbound traffic. (#31807)</li>
<li>Feishu/Typing notification suppression: skip typing keepalive reaction re-adds when the indicator is already active, preventing duplicate notification pings from repeated identical emoji adds. (#31580)</li>
<li>Feishu/Probe failure backoff: cache API and timeout probe failures for one minute per account key while preserving abort-aware probe timeouts, reducing repeated health-check retries during transient credential/network outages. (#29970)</li>
<li>Feishu/Streaming block fallback: preserve markdown block stream text as final streaming-card content when final payload text is missing, while still suppressing non-card internal block chunk delivery. (#30663)</li>
<li>Feishu/Bitable API errors: unify Feishu Bitable tool error handling with structured <code>LarkApiError</code> responses and consistent API/context attribution across wiki/base metadata, field, and record operations. (#31450)</li>
<li>Feishu/Missing-scope grant URL fix: rewrite known invalid scope aliases (<code>contact:contact.base:readonly</code>) to valid scope names in permission grant links, so remediation URLs open with correct Feishu consent scopes. (#31943)</li>
<li>BlueBubbles/Message metadata: harden send response ID extraction, include sender identity in DM context, and normalize inbound <code>message_id</code> selection to avoid duplicate ID metadata. (#23970) Thanks @tyler6204.</li>
<li>WebChat/markdown tables: ensure GitHub-flavored markdown table parsing is explicitly enabled at render time and add horizontal overflow handling for wide tables, with regression coverage for table-only and mixed text+table content. (#32365) Thanks @BlueBirdBack.</li>
<li>Feishu/default account resolution: always honor explicit <code>channels.feishu.defaultAccount</code> during outbound account selection (including top-level-credential setups where the preferred id is not present in <code>accounts</code>), instead of silently falling back to another account id. (#32253) Thanks @bmendonca3.</li>
<li>Feishu/Sender lookup permissions: suppress user-facing grant prompts for stale non-existent scope errors (<code>contact:contact.base:readonly</code>) during best-effort sender-name resolution so inbound messages continue without repeated false permission notices. (#31761)</li>
<li>Discord/dispatch + Slack formatting: restore parallel outbound dispatch across Discord channels with per-channel queues while preserving in-channel ordering, and run Slack preview/stream update text through mrkdwn normalization for consistent formatting. (#31927) Thanks @Sid-Qin.</li>
<li>Feishu/Inbound debounce: debounce rapid same-chat sender bursts into one ordered dispatch turn, skip already-processed retries when composing merged text, and preserve bot-mention intent across merged entries to reduce duplicate or late inbound handling. (#31548)</li>
<li>Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale.</li>
<li>Browser/Extension re-announce reliability: keep relay state in <code>connecting</code> when re-announce forwarding fails and extend debugger re-attach retries after navigation to reduce false attached states and post-nav disconnect loops. (#27630) Thanks @markmusson.</li>
<li>Browser/Act request compatibility: accept legacy flattened <code>action="act"</code> params (<code>kind/ref/text/...</code>) in addition to <code>request={...}</code> so browser act calls no longer fail with <code>request required</code>. (#15120) Thanks @vincentkoc.</li>
<li>OpenRouter/x-ai compatibility: skip <code>reasoning.effort</code> injection for <code>x-ai/*</code> models (for example Grok) so OpenRouter requests no longer fail with invalid-arguments errors on unsupported reasoning params. (#32054) Thanks @scoootscooob.</li>
<li>Models/openai-completions developer-role compatibility: force <code>supportsDeveloperRole=false</code> for non-native endpoints, treat unparseable <code>baseUrl</code> values as non-native, and add regression coverage for empty/malformed baseUrl plus explicit-true override behavior. (#29479) thanks @akramcodez.</li>
<li>Browser/Profile attach-only override: support <code>browser.profiles.<name>.attachOnly</code> (fallback to global <code>browser.attachOnly</code>) so loopback proxy profiles can skip local launch/port-ownership checks without forcing attach-only mode for every profile. (#20595) Thanks @unblockedgamesstudio and @vincentkoc.</li>
<li>Sessions/Lock recovery: detect recycled Linux PIDs by comparing lock-file <code>starttime</code> with <code>/proc/<pid>/stat</code> starttime, so stale <code>.jsonl.lock</code> files are reclaimed immediately in containerized PID-reuse scenarios while preserving compatibility for older lock files. (#26443) Fixes #27252. Thanks @HirokiKobayashi-R and @vincentkoc.</li>
<li>Cron/isolated delivery target fallback: remove early unresolved-target return so cron delivery can flow through shared outbound target resolution (including per-channel <code>resolveDefaultTo</code> fallback) when <code>delivery.to</code> is omitted. (#32364) Thanks @hclsys.</li>
<li>OpenAI media capabilities: include <code>audio</code> in the OpenAI provider capability list so audio transcription models are eligible in media-understanding provider selection. (#12717) Thanks @openjay.</li>
<li>Browser/Managed tab cap: limit loopback managed <code>openclaw</code> page tabs to 8 via best-effort cleanup after tab opens to reduce long-running renderer buildup while preserving attach-only and remote profile behavior. (#29724) Thanks @pandego.</li>
<li>Docker/Image health checks: add Dockerfile <code>HEALTHCHECK</code> that probes gateway <code>GET /healthz</code> so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc.</li>
<li>Gateway/Node dangerous-command parity: include <code>sms.send</code> in default onboarding node <code>denyCommands</code>, share onboarding deny defaults with the gateway dangerous-command source of truth, and include <code>sms.send</code> in phone-control <code>/phone arm writes</code> handling so SMS follows the same break-glass flow as other dangerous node commands. Thanks @zpbrent.</li>
<li>Pairing/AllowFrom account fallback: handle omitted <code>accountId</code> values in <code>readChannelAllowFromStore</code> and <code>readChannelAllowFromStoreSync</code> as <code>default</code>, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.</li>
<li>Browser/Remote CDP ownership checks: skip local-process ownership errors for non-loopback remote CDP profiles when HTTP is reachable but the websocket handshake fails, and surface the remote websocket attach/retry path instead. (#15582) Landed from contributor (#28780) Thanks @stubbi, @bsormagec, @unblockedgamesstudio and @vincentkoc.</li>
<li>Browser/CDP proxy bypass: force direct loopback agent paths and scoped <code>NO_PROXY</code> expansion for localhost CDP HTTP/WS connections when proxy env vars are set, so browser relay/control still works behind global proxy settings. (#31469) Thanks @widingmarcus-cyber.</li>
<li>Sessions/idle reset correctness: preserve existing <code>updatedAt</code> during inbound metadata-only writes so idle-reset boundaries are not unintentionally refreshed before actual user turns. (#32379) Thanks @romeodiaz.</li>
<li>Sessions/lock recovery: reclaim orphan legacy same-PID lock files missing <code>starttime</code> when no in-process lock ownership exists, avoiding false lock timeouts after PID reuse while preserving active lock safety checks. (#32081) Thanks @bmendonca3.</li>
<li>Sessions/store cache invalidation: reload cached session stores when file size changes within the same mtime tick by keying cache validation on a single file-stat snapshot (<code>mtimeMs</code> + <code>sizeBytes</code>), with regression coverage for same-tick rewrites. (#32191) Thanks @jalehman.</li>
<li>Agents/Subagents <code>sessions_spawn</code>: reject malformed <code>agentId</code> inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.</li>
<li>CLI/installer Node preflight: enforce Node.js <code>v22.12+</code> consistently in both <code>openclaw.mjs</code> runtime bootstrap and installer active-shell checks, with actionable nvm recovery guidance for mismatched shell PATH/defaults. (#32356) Thanks @jasonhargrove.</li>
<li>Web UI/config form: support SecretInput string-or-secret-ref unions in map <code>additionalProperties</code>, so provider API key fields stay editable instead of being marked unsupported. (#31866) Thanks @ningding97.</li>
<li>Auto-reply/inline command cleanup: preserve newline structure when stripping inline <code>/status</code> and extracting inline slash commands by collapsing only horizontal whitespace, preventing paragraph flattening in multi-line replies. (#32224) Thanks @scoootscooob.</li>
<li>Config/raw redaction safety: preserve non-sensitive literals during raw redaction round-trips, scope SecretRef redaction to secret IDs (not structural fields like <code>source</code>/<code>provider</code>), and fall back to structured raw redaction when text replacement cannot restore the original config shape. (#32174) Thanks @bmendonca3.</li>
<li>Hooks/runtime stability: keep the internal hook handler registry on a <code>globalThis</code> singleton so hook registration/dispatch remains consistent when bundling emits duplicate module copies. (#32292) Thanks @Drickon.</li>
<li>Hooks/after_tool_call: include embedded session context (<code>sessionKey</code>, <code>agentId</code>) and fire the hook exactly once per tool execution by removing duplicate adapter-path dispatch in embedded runs. (#32201) Thanks @jbeno, @scoootscooob, @vincentkoc.</li>
<li>Hooks/tool-call correlation: include <code>runId</code> and <code>toolCallId</code> in plugin tool hook payloads/context and scope tool start/adjusted-param tracking by run to prevent cross-run collisions in <code>before_tool_call</code> and <code>after_tool_call</code>. (#32360) Thanks @vincentkoc.</li>
<li>Plugins/install diagnostics: reject legacy plugin package shapes without <code>openclaw.extensions</code> and return an explicit upgrade hint with troubleshooting docs for repackaging. (#32055) Thanks @liuxiaopai-ai.</li>
<li>Hooks/plugin context parity: ensure <code>llm_input</code> hooks in embedded attempts receive the same <code>trigger</code> and <code>channelId</code>-aware <code>hookCtx</code> used by the other hook phases, preserving channel/trigger-scoped plugin behavior. (#28623) Thanks @davidrudduck and @vincentkoc.</li>
<li>Plugins/hardlink install compatibility: allow bundled plugin manifests and entry files to load when installed via hardlink-based package managers (<code>pnpm</code>, <code>bun</code>) while keeping hardlink rejection enabled for non-bundled plugin sources. (#32119) Fixes #28175, #28404, #29455. Thanks @markfietje.</li>
<li>Cron/session reaper reliability: move cron session reaper sweeps into <code>onTimer</code> <code>finally</code> and keep pruning active even when timer ticks fail early (for example cron store parse failures), preventing stale isolated run sessions from accumulating indefinitely. (#31996) Fixes #31946. Thanks @scoootscooob.</li>
<li>Cron/HEARTBEAT_OK summary leak: suppress fallback main-session enqueue for heartbeat/internal ack summaries in isolated announce mode so <code>HEARTBEAT_OK</code> noise never appears in user chat while real summaries still forward. (#32093) Thanks @scoootscooob.</li>
<li>Authentication: classify <code>permission_error</code> as <code>auth_permanent</code> for profile fallback. (#31324) Thanks @Sid-Qin.</li>
<li>Agents/host edit reliability: treat host edit-tool throws as success only when on-disk post-check confirms replacement likely happened (<code>newText</code> present and <code>oldText</code> absent), preventing false failure reports while avoiding pre-write false positives. (#32383) Thanks @polooooo.</li>
<li>Plugins/install fallback safety: resolve bare install specs to bundled plugin ids before npm lookup (for example <code>diffs</code> -> bundled <code>@openclaw/diffs</code>), keep npm fallback limited to true package-not-found errors, and continue rejecting non-plugin npm packages that fail manifest validation. (#32096) Thanks @scoootscooob.</li>
<li>Web UI/inline code copy fidelity: disable forced mid-token wraps on inline <code><code></code> spans so copied UUID/hash/token strings preserve exact content instead of inserting line-break spaces. (#32346) Thanks @hclsys.</li>
<li>Restart sentinel formatting: avoid duplicate <code>Reason:</code> lines when restart message text already matches <code>stats.reason</code>, keeping restart notifications concise for users and downstream parsers. (#32083) Thanks @velamints2.</li>
<li>Auto-reply/followup queue: avoid stale callback reuse across idle-window restarts by caching the followup runner only when a drain actually starts, preserving enqueue ordering after empty-finalize paths. (#31902) Thanks @Lanfei.</li>
<li>Agents/tool-result guard: always clear pending tool-call state on interruptions even when synthetic tool results are disabled, preventing orphaned tool-use transcripts that cause follow-up provider request failures. (#32120) Thanks @jnMetaCode.</li>
<li>Failover/error classification: treat HTTP <code>529</code> (provider overloaded, common with Anthropic-compatible APIs) as <code>rate_limit</code> so model failover can engage instead of misclassifying the error path. (#31854) Thanks @bugkill3r.</li>
<li>Logging: use local time for logged timestamps instead of UTC, aligning log output with documented local timezone behavior and avoiding confusion during local diagnostics. (#28434) Thanks @liuy.</li>
<li>Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.</li>
<li>Secrets/exec resolver timeout defaults: use provider <code>timeoutMs</code> as the default inactivity (<code>noOutputTimeoutMs</code>) watchdog for exec secret providers, preventing premature no-output kills for resolvers that start producing output after 2s. (#32235) Thanks @bmendonca3.</li>
<li>Auto-reply/reminder guard note suppression: when a turn makes reminder-like commitments but schedules no new cron jobs, suppress the unscheduled-reminder warning note only if an enabled cron already exists for the same session; keep warnings for unrelated sessions, disabled jobs, or unreadable cron store paths. (#32255) Thanks @scoootscooob.</li>
<li>Cron/isolated announce heartbeat suppression: treat multi-payload runs as skippable when any payload is a heartbeat ack token and no payload has media, preventing internal narration + trailing <code>HEARTBEAT_OK</code> from being delivered to users. (#32131) Thanks @adhishthite.</li>
<li>Cron/store migration: normalize legacy cron jobs with string <code>schedule</code> and top-level <code>command</code>/<code>timeout</code> fields into canonical schedule/payload/session-target shape on load, preventing schedule-error loops on old persisted stores. (#31926) Thanks @bmendonca3.</li>
<li>Tests/Windows backup rotation: skip chmod-only backup permission assertions on Windows while retaining compose/rotation/prune coverage across platforms to avoid false CI failures from Windows non-POSIX mode semantics. (#32286) Thanks @jalehman.</li>
<li>Tests/Subagent announce: set <code>OPENCLAW_TEST_FAST=1</code> before importing <code>subagent-announce</code> format suites so module-level fast-mode constants are captured deterministically on Windows CI, preventing timeout flakes in nested completion announce coverage. (#31370) Thanks @zwffff.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.2/OpenClaw-2026.3.2.zip" length="23181513" type="application/octet-stream" sparkle:edSignature="THMgkcoMgz2vv5zse3Po3K7l3Or2RhBKurXZIi8iYVXN76yJy1YXAY6kXi6ovD+dbYn68JKYDIKA1Ya78bO7BQ=="/>
</item>
<item>
<title>2026.3.1</title>
<pubDate>Mon, 02 Mar 2026 04:40:59 +0000</pubDate>
@@ -140,175 +359,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.1/OpenClaw-2026.3.1.zip" length="12804155" type="application/octet-stream" sparkle:edSignature="TF1otD4Vk3pG0iViX7mvix5DQEgAsk4JkSFvH7opjf9aawV16f29SUa2wRmiCFU6HEgyNgnGI/078O+A27eXCA=="/>
</item>
<item>
<title>2026.2.15</title>
<pubDate>Mon, 16 Feb 2026 05:04:34 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>202602150</sparkle:version>
<sparkle:shortVersionString>2026.2.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.15</h2>
<h3>Changes</h3>
<ul>
<li>Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.</li>
<li>Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.</li>
<li>Plugins: expose <code>llm_input</code> and <code>llm_output</code> hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.</li>
<li>Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set <code>agents.defaults.subagents.maxSpawnDepth: 2</code> to allow sub-agents to spawn their own children. Includes <code>maxChildrenPerAgent</code> limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.</li>
<li>Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.</li>
<li>Cron/Gateway: add finished-run webhook delivery toggle (<code>notify</code>) and dedicated webhook auth token support (<code>cron.webhookToken</code>) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.</li>
<li>Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.</li>
<li>Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.</li>
<li>Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.</li>
<li>Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.</li>
<li>Gateway/Security: redact sensitive session/path details from <code>status</code> responses for non-admin clients; full details remain available to <code>operator.admin</code>. (#8590) Thanks @fr33d3m0n.</li>
<li>Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (<code>allowInsecureAuth</code> / <code>dangerouslyDisableDeviceAuth</code>) when device identity is unavailable, preventing false <code>missing scope</code> failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.</li>
<li>LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.</li>
<li>Skills/Security: restrict <code>download</code> installer <code>targetDir</code> to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.</li>
<li>Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.</li>
<li>Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.</li>
<li>Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving <code>passwordFile</code> path exemptions, preventing accidental redaction of non-secret config values like <code>maxTokens</code> and IRC password-file paths. (#16042) Thanks @akramcodez.</li>
<li>Dev tooling: harden git <code>pre-commit</code> hook against option injection from malicious filenames (for example <code>--force</code>), preventing accidental staging of ignored files. Thanks @mrthankyou.</li>
<li>Gateway/Agent: reject malformed <code>agent:</code>-prefixed session keys (for example, <code>agent:main</code>) in <code>agent</code> and <code>agent.identity.get</code> instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.</li>
<li>Gateway/Chat: harden <code>chat.send</code> inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.</li>
<li>Gateway/Send: return an actionable error when <code>send</code> targets internal-only <code>webchat</code>, guiding callers to use <code>chat.send</code> or a deliverable channel. (#15703) Thanks @rodrigouroz.</li>
<li>Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing <code>script-src 'self'</code>. Thanks @Adam55A-code.</li>
<li>Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.</li>
<li>Agents/Sandbox: clarify system prompt path guidance so sandbox <code>bash/exec</code> uses container paths (for example <code>/workspace</code>) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.</li>
<li>Agents/Context: apply configured model <code>contextWindow</code> overrides after provider discovery so <code>lookupContextTokens()</code> honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.</li>
<li>Agents/Context: derive <code>lookupContextTokens()</code> from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.</li>
<li>Agents/OpenAI: force <code>store=true</code> for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.</li>
<li>Memory/FTS: make <code>buildFtsQuery</code> Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.</li>
<li>Auto-reply/Compaction: resolve <code>memory/YYYY-MM-DD.md</code> placeholders with timezone-aware runtime dates and append a <code>Current time:</code> line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.</li>
<li>Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.</li>
<li>Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.</li>
<li>Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.</li>
<li>Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.</li>
<li>Subagents/Models: preserve <code>agents.defaults.model.fallbacks</code> when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.</li>
<li>Telegram: omit <code>message_thread_id</code> for DM sends/draft previews and keep forum-topic handling (<code>id=1</code> general omitted, non-general kept), preventing DM failures with <code>400 Bad Request: message thread not found</code>. (#10942) Thanks @garnetlyx.</li>
<li>Telegram: replace inbound <code><media:audio></code> placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.</li>
<li>Telegram: retry inbound media <code>getFile</code> calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.</li>
<li>Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.</li>
<li>Discord: preserve channel session continuity when runtime payloads omit <code>message.channelId</code> by falling back to event/raw <code>channel_id</code> values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as <code>sessionKey=unknown</code>. (#17622) Thanks @shakkernerd.</li>
<li>Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with <code>_2</code> suffixes. (#17365) Thanks @seewhyme.</li>
<li>Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.</li>
<li>Web UI/Agents: hide <code>BOOTSTRAP.md</code> in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.</li>
<li>Auto-reply/WhatsApp/TUI/Web: when a final assistant message is <code>NO_REPLY</code> and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show <code>NO_REPLY</code> placeholders. (#7010) Thanks @Morrowind-Xie.</li>
<li>Cron: infer <code>payload.kind="agentTurn"</code> for model-only <code>cron.update</code> payload patches, so partial agent-turn updates do not fail validation when <code>kind</code> is omitted. (#15664) Thanks @rodrigouroz.</li>
<li>TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.</li>
<li>TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.</li>
<li>TUI: suppress false <code>(no output)</code> placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.</li>
<li>TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.</li>
<li>CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.15/OpenClaw-2026.2.15.zip" length="22896513" type="application/octet-stream" sparkle:edSignature="MLGsd2NeHXFRH1Or0bFQnAjqfuuJDuhl1mvKFIqTQcRvwbeyvOyyLXrqSbmaOgJR3wBQBKLs6jYQ9dQ/3R8RCg=="/>
</item>
<item>
<title>2026.2.26</title>
<pubDate>Thu, 26 Feb 2026 23:37:15 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>202602260</sparkle:version>
<sparkle:shortVersionString>2026.2.26</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.26</h2>
<h3>Changes</h3>
<ul>
<li>Highlight: External Secrets Management introduces a full <code>openclaw secrets</code> workflow (<code>audit</code>, <code>configure</code>, <code>apply</code>, <code>reload</code>) with runtime snapshot activation, strict <code>secrets apply</code> target-path validation, safer migration scrubbing, ref-only auth-profile support, and dedicated docs. (#26155) Thanks @joshavant.</li>
<li>ACP/Thread-bound agents: make ACP agents first-class runtimes for thread sessions with <code>acp</code> spawn/send dispatch integration, acpx backend bridging, lifecycle controls, startup reconciliation, runtime cleanup, and coalesced thread replies. (#23580) thanks @osolmaz.</li>
<li>Agents/Routing CLI: add <code>openclaw agents bindings</code>, <code>openclaw agents bind</code>, and <code>openclaw agents unbind</code> for account-scoped route management, including channel-only to account-scoped binding upgrades, role-aware binding identity handling, plugin-resolved binding account IDs, and optional account-binding prompts in <code>openclaw channels add</code>. (#27195) thanks @gumadeiras.</li>
<li>Codex/WebSocket transport: make <code>openai-codex</code> WebSocket-first by default (<code>transport: "auto"</code> with SSE fallback), keep explicit per-model/runtime transport overrides, and add regression coverage + docs for transport selection.</li>
<li>Onboarding/Plugins: let channel plugins own interactive onboarding flows with optional <code>configureInteractive</code> and <code>configureWhenConfigured</code> hooks while preserving the generic fallback path. (#27191) thanks @gumadeiras.</li>
<li>Android/Nodes: add Android <code>device</code> capability plus <code>device.status</code> and <code>device.info</code> node commands, including runtime handler wiring and protocol/registry coverage for device status/info payloads. (#27664) Thanks @obviyus.</li>
<li>Android/Nodes: add <code>notifications.list</code> support on Android nodes and expose <code>nodes notifications_list</code> in agent tooling for listing active device notifications. (#27344) thanks @obviyus.</li>
<li>Docs/Contributing: add Nimrod Gutman to the maintainer roster in <code>CONTRIBUTING.md</code>. (#27840) Thanks @ngutman.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Telegram/DM allowlist runtime inheritance: enforce <code>dmPolicy: "allowlist"</code> <code>allowFrom</code> requirements using effective account-plus-parent config across account-capable channels (Telegram, Discord, Slack, Signal, iMessage, IRC, BlueBubbles, WhatsApp), and align <code>openclaw doctor</code> checks to the same inheritance logic so DM traffic is not silently dropped after upgrades. (#27936) Thanks @widingmarcus-cyber.</li>
<li>Delivery queue/recovery backoff: prevent retry starvation by persisting <code>lastAttemptAt</code> on failed sends and deferring recovery retries until each entry's <code>lastAttemptAt + backoff</code> window is eligible, while continuing to recover ready entries behind deferred ones. Landed from contributor PR #27710 by @Jimmy-xuzimo. Thanks @Jimmy-xuzimo.</li>
<li>Google Chat/Lifecycle: keep Google Chat <code>startAccount</code> pending until abort in webhook mode so startup is no longer interpreted as immediate exit, preventing auto-restart loops and webhook-target churn. (#27384) thanks @junsuwhy.</li>
<li>Temp dirs/Linux umask: force <code>0700</code> permissions after temp-dir creation and self-heal existing writable temp dirs before trust checks so <code>umask 0002</code> installs no longer crash-loop on startup. Landed from contributor PR #27860 by @stakeswky. (#27853) Thanks @stakeswky.</li>
<li>Nextcloud Talk/Lifecycle: keep <code>startAccount</code> pending until abort and stop the webhook monitor on shutdown, preventing <code>EADDRINUSE</code> restart loops when the gateway manages account lifecycle. (#27897)</li>
<li>Microsoft Teams/File uploads: acknowledge <code>fileConsent/invoke</code> immediately (<code>invokeResponse</code> before upload + file card send) so Teams no longer shows false "Something went wrong" timeout banners while upload completion continues asynchronously; includes updated async regression coverage. Landed from contributor PR #27641 by @scz2011.</li>
<li>Queue/Drain/Cron reliability: harden lane draining with guaranteed <code>draining</code> flag reset on synchronous pump failures, reject new queue enqueues during gateway restart drain windows (instead of silently killing accepted tasks), add <code>/stop</code> queued-backlog cutoff metadata with stale-message skipping (while avoiding cross-session native-stop cutoff bleed), and raise isolated cron <code>agentTurn</code> outer safety timeout to avoid false 10-minute timeout races against longer agent session timeouts. (#27407, #27332, #27427)</li>
<li>Typing/Main reply pipeline: always mark dispatch idle in <code>agent-runner</code> finalization so typing cleanup runs even when dispatcher <code>onIdle</code> does not fire, preventing stuck typing indicators after run completion. (#27250) Thanks @Sid-Qin.</li>
<li>Typing/TTL safety net: add max-duration guardrails to shared typing callbacks so stuck lifecycle edges auto-stop typing indicators even when explicit idle/cleanup signals are missed. (#27428) Thanks @Crpdim.</li>
<li>Typing/Cross-channel leakage: unify run-scoped typing suppression for cross-channel/internal-webchat routes, preserve current inbound origin as embedded run message channel context, harden shared typing keepalive with consecutive-failure circuit breaker edge-case handling, and enforce dispatcher completion/idle waits in extension dispatcher callsites (Feishu, Matrix, Mattermost, MSTeams) so typing indicators always clean up on success/error paths. Related: #27647, #27493, #27598. Supersedes/replaces draft PRs: #27640, #27593, #27540.</li>
<li>Telegram/sendChatAction 401 handling: add bounded exponential backoff + temporary local typing suppression after repeated unauthorized failures to stop unbounded <code>sendChatAction</code> retry loops that can trigger Telegram abuse enforcement and bot deletion. (#27415) Thanks @widingmarcus-cyber.</li>
<li>Telegram/Webhook startup: clarify webhook config guidance, allow <code>channels.telegram.webhookPort: 0</code> for ephemeral listener binding, and log both the local listener URL and Telegram-advertised webhook URL with the bound port. (#25732) thanks @huntharo.</li>
<li>Browser/Chrome extension handshake: bind relay WS message handling before <code>onopen</code> and add non-blocking <code>connect.challenge</code> response handling for gateway-style handshake frames, avoiding stuck <code>…</code> badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553)</li>
<li>Browser/Extension relay init: dedupe concurrent same-port relay startup with shared in-flight initialization promises so callers await one startup lifecycle and receive consistent success/failure results. Landed from contributor PR #21277 by @HOYALIM. (Related #20688)</li>
<li>Browser/Fill relay + CLI parity: accept <code>act.fill</code> fields without explicit <code>type</code> by defaulting missing/empty <code>type</code> to <code>text</code> in both browser relay route parsing and <code>openclaw browser fill</code> CLI field parsing, so relay calls no longer fail when the model omits field type metadata. Landed from contributor PR #27662 by @Uface11. (#27296) Thanks @Uface11.</li>
<li>Feishu/Permission error dispatch: merge sender-name permission notices into the main inbound dispatch so one user message produces one agent turn/reply (instead of a duplicate permission-notice turn), with regression coverage. (#27381) thanks @byungsker.</li>
<li>Agents/Canvas default node resolution: when multiple connected canvas-capable nodes exist and no single <code>mac-*</code> candidate is selected, default to the first connected candidate instead of failing with <code>node required</code> for implicit-node canvas tool calls. Landed from contributor PR #27444 by @carbaj03. Thanks @carbaj03.</li>
<li>TUI/stream assembly: preserve streamed text across real tool-boundary drops without keeping stale streamed text when non-text blocks appear only in the final payload. Landed from contributor PR #27711 by @scz2011. (#27674)</li>
<li>Hooks/Internal <code>message:sent</code>: forward <code>sessionKey</code> on outbound sends from agent delivery, cron isolated delivery, gateway receipt acks, heartbeat sends, session-maintenance warnings, and restart-sentinel recovery so internal <code>message:sent</code> hooks consistently dispatch with session context, including <code>openclaw agent --deliver</code> runs resumed via <code>--session-id</code> (without explicit <code>--session-key</code>). Landed from contributor PR #27584 by @qualiobra. Thanks @qualiobra.</li>
<li>Pi image-token usage: stop re-injecting history image blocks each turn, process image references from the current prompt only, and prune already-answered user-image blocks in stored history to prevent runaway token growth. (#27602)</li>
<li>BlueBubbles/SSRF: auto-allowlist the configured <code>serverUrl</code> hostname for attachment fetches so localhost/private-IP BlueBubbles setups are no longer false-blocked by default SSRF checks. Landed from contributor PR #27648 by @lailoo. (#27599) Thanks @taylorhou for reporting.</li>
<li>Agents/Compaction + onboarding safety: prevent destructive double-compaction by stripping stale assistant usage around compaction boundaries, skipping post-compaction custom metadata writes in the same attempt, and cancelling safeguard compaction when there are no real conversation messages to summarize; harden workspace/bootstrap detection for memory-backed workspaces; and change <code>openclaw onboard --reset</code> default scope to <code>config+creds+sessions</code> (workspace deletion now requires <code>--reset-scope full</code>). (#26458, #27314) Thanks @jaden-clovervnd, @Sid-Qin, and @widingmarcus-cyber for fix direction in #26502, #26529, and #27492.</li>
<li>NO_REPLY suppression: suppress <code>NO_REPLY</code> before Slack API send and in sub-agent announce completion flow so sentinel text no longer leaks into user channels. Landed from contributor PRs #27529 (by @Sid-Qin) and #27535 (rewritten minimal landing by maintainers). (#27387, #27531)</li>
<li>Matrix/Group sender identity: preserve sender labels in Matrix group inbound prompt text (<code>BodyForAgent</code>) for both channel and threaded messages, and align group envelopes with shared inbound sender-prefix formatting so first-person requests resolve against the current sender. (#27401) thanks @koushikxd.</li>
<li>Auto-reply/Streaming: suppress only exact <code>NO_REPLY</code> final replies while still filtering streaming partial sentinel fragments (<code>NO_</code>, <code>NO_RE</code>, <code>HEARTBEAT_...</code>) so substantive replies ending with <code>NO_REPLY</code> are delivered and partial silent tokens do not leak during streaming. (#19576) Thanks @aldoeliacim.</li>
<li>Auto-reply/Inbound metadata: add a readable <code>timestamp</code> field to conversation info and ignore invalid/out-of-range timestamp values so prompt assembly never crashes on malformed timestamp inputs. (#17017) thanks @liuy.</li>
<li>Typing/Run completion race: prevent post-run keepalive ticks from re-triggering typing callbacks by guarding <code>triggerTyping()</code> with <code>runComplete</code>, with regression coverage for no-restart behavior during run-complete/dispatch-idle boundaries. (#27413) Thanks @widingmarcus-cyber.</li>
<li>Typing/Dispatch idle: force typing cleanup when <code>markDispatchIdle</code> never arrives after run completion, avoiding leaked typing keepalive loops in cron/announce edges. Landed from contributor PR #27541 by @Sid-Qin. (#27493)</li>
<li>Telegram/Inline buttons: allow callback-query button handling in groups (including <code>/models</code> follow-up buttons) when group policy authorizes the sender, by removing the redundant callback allowlist gate that blocked open-policy groups. (#27343) Thanks @GodsBoy.</li>
<li>Telegram/Streaming preview: when finalizing without an existing preview message, prime pending preview text with final answer before stop-flush so users do not briefly see stale 1-2 word fragments (for example <code>no</code> before <code>no problem</code>). (#27449) Thanks @emanuelst for the original fix direction in #19673.</li>
<li>Browser/Extension relay CORS: handle <code>/json*</code> <code>OPTIONS</code> preflight before auth checks, allow Chrome extension origins, and return extension-origin CORS headers on relay HTTP responses so extension token validation no longer fails cross-origin. Landed from contributor PR #23962 by @miloudbelarebia. (#23842)</li>
<li>Browser/Extension relay auth: allow <code>?token=</code> query-param auth on relay <code>/json*</code> endpoints (consistent with relay WebSocket auth) so curl/devtools-style <code>/json/version</code> and <code>/json/list</code> probes work without requiring custom headers. Landed from contributor PR #26015 by @Sid-Qin. (#25928)</li>
<li>Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay <code>stop()</code> before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng.</li>
<li>Browser/Extension relay reconnect resilience: keep CDP clients alive across brief MV3 extension disconnect windows, wait briefly for extension reconnect before failing in-flight CDP commands, and only tear down relay target/client state after reconnect grace expires. Landed from contributor PR #27617 by @davidemanuelDEV.</li>
<li>Browser/Route decode hardening: guard malformed percent-encoding in relay target action routes and browser route-param decoding so crafted <code>%</code> paths return <code>400</code> instead of crashing/unhandled URI decode failures. Landed from contributor PR #11880 by @Yida-Dev.</li>
<li>Feishu/Inbound message metadata: include inbound <code>message_id</code> in <code>BodyForAgent</code> on a dedicated metadata line so agents can reliably correlate and act on media/message operations that require message IDs, with regression coverage. (#27253) thanks @xss925175263.</li>
<li>Feishu/Doc tools: route <code>feishu_doc</code> and <code>feishu_app_scopes</code> through the active agent account context (with explicit <code>accountId</code> override support) so multi-account agents no longer default to the first configured app, with regression coverage for context routing and explicit override behavior. (#27338) thanks @AaronL725.</li>
<li>LINE/Inline directives auth: gate directive parsing (<code>/model</code>, <code>/think</code>, <code>/verbose</code>, <code>/reasoning</code>, <code>/queue</code>) on resolved authorization (<code>command.isAuthorizedSender</code>) so <code>commands.allowFrom</code>-authorized LINE senders are not silently stripped when raw <code>CommandAuthorized</code> is unset. Landed from contributor PR #27248 by @kevinWangSheng. (#27240)</li>
<li>Onboarding/Gateway: seed default Control UI <code>allowedOrigins</code> for non-loopback binds during onboarding (<code>localhost</code>/<code>127.0.0.1</code> plus custom bind host) so fresh non-loopback setups do not fail startup due to missing origin policy. (#26157) thanks @stakeswky.</li>
<li>Docker/GCP onboarding: reduce first-build OOM risk by capping Node heap during <code>pnpm install</code>, reuse existing gateway token during <code>docker-setup.sh</code> reruns so <code>.env</code> stays aligned with config, auto-bootstrap Control UI allowed origins for non-loopback Docker binds, and add GCP docs guidance for tokenized dashboard links + pairing recovery commands. (#26253) Thanks @pandego.</li>
<li>CLI/Gateway <code>--force</code> in non-root Docker: recover from <code>lsof</code> permission failures (<code>EACCES</code>/<code>EPERM</code>) by falling back to <code>fuser</code> kill + probe-based port checks, so <code>openclaw gateway --force</code> works for default container <code>node</code> user flows. (#27941)</li>
<li>Gateway/Bind visibility: emit a startup warning when binding to non-loopback addresses so operators get explicit exposure guidance in runtime logs. (#25397) thanks @let5sne.</li>
<li>Sessions cleanup/Doctor: add <code>openclaw sessions cleanup --fix-missing</code> to prune store entries whose transcript files are missing, including doctor guidance and CLI coverage. Landed from contributor PR #27508 by @Sid-Qin. (#27422)</li>
<li>Doctor/State integrity: ignore metadata-only slash routing sessions when checking recent missing transcripts so <code>openclaw doctor</code> no longer reports false-positive transcript-missing warnings for <code>*:slash:*</code> keys. (#27375) thanks @gumadeiras.</li>
<li>CLI/Gateway status: force local <code>gateway status</code> probe host to <code>127.0.0.1</code> for <code>bind=lan</code> so co-located probes do not trip non-loopback plaintext WebSocket checks. (#26997) thanks @chikko80.</li>
<li>CLI/Gateway auth: align <code>gateway run --auth</code> parsing/help text with supported gateway auth modes by accepting <code>none</code> and <code>trusted-proxy</code> (in addition to <code>token</code>/<code>password</code>) for CLI overrides. (#27469) thanks @s1korrrr.</li>
<li>CLI/Daemon status TLS probe: use <code>wss://</code> and forward local TLS certificate fingerprint for TLS-enabled gateway daemon probes so <code>openclaw daemon status</code> works with <code>gateway.bind=lan</code> + <code>gateway.tls.enabled=true</code>. (#24234) thanks @liuy.</li>
<li>Podman/Default bind: change <code>run-openclaw-podman.sh</code> default gateway bind from <code>lan</code> to <code>loopback</code> and document explicit LAN opt-in with Control UI origin configuration. (#27491) thanks @robbyczgw-cla.</li>
<li>Daemon/macOS launchd: forward proxy env vars into supervised service environments, keep LaunchAgent <code>KeepAlive=true</code> semantics, and harden restart sequencing to <code>print -> bootout -> wait old pid exit -> bootstrap -> kickstart</code>. (#27276) thanks @frankekn.</li>
<li>Gateway/macOS restart-loop hardening: detect OpenClaw-managed supervisor markers during SIGUSR1 restart handoff, clean stale gateway PIDs before <code>/restart</code> launchctl/systemctl triggers, and set LaunchAgent <code>ThrottleInterval=60</code> to bound launchd retry storms during lock-release races. Landed from contributor PRs #27655 (@taw0002), #27448 (@Sid-Qin), and #27650 (@kevinWangSheng). (#27605, #27590, #26904, #26736)</li>
<li>Models/MiniMax auth header defaults: set <code>authHeader: true</code> for both onboarding-generated MiniMax API providers and implicit built-in MiniMax (<code>minimax</code>, <code>minimax-portal</code>) provider templates so first requests no longer fail with MiniMax <code>401 authentication_error</code> due to missing <code>Authorization</code> header. Landed from contributor PRs #27622 by @riccoyuanft and #27631 by @kevinWangSheng. (#27600, #15303)</li>
<li>Auth/Auth profiles: normalize <code>auth-profiles.json</code> alias fields (<code>mode -> type</code>, <code>apiKey -> key</code>) before credential validation so entries copied from <code>openclaw.json</code> auth examples are no longer silently dropped. (#26950) thanks @byungsker.</li>
<li>Models/Profile suffix parsing: centralize trailing <code>@profile</code> parsing and only treat <code>@</code> as a profile separator when it appears after the final <code>/</code>, preserving model IDs like <code>openai/@cf/...</code> and <code>openrouter/@preset/...</code> across <code>/model</code> directive parsing and allowlist model resolution, with regression coverage.</li>
<li>Models/OpenAI Codex config schema parity: accept <code>openai-codex-responses</code> in the config model API schema and TypeScript <code>ModelApi</code> union, with regression coverage for config validation. Landed from contributor PR #27501 by @AytuncYildizli. Thanks @AytuncYildizli.</li>
<li>Agents/Models config: preserve agent-level provider <code>apiKey</code> and <code>baseUrl</code> during merge-mode <code>models.json</code> updates when agent values are present. (#27293) thanks @Sid-Qin.</li>
<li>Azure OpenAI Responses: force <code>store=true</code> for <code>azure-openai-responses</code> direct responses API calls to avoid multi-turn 400 failures. Landed from contributor PR #27499 by @polarbear-Yang. (#27497)</li>
<li>Security/Node exec approvals: require structured <code>commandArgv</code> approvals for <code>host=node</code>, enforce versioned <code>systemRunBindingV1</code> matching for argv/cwd/session/agent/env context with fail-closed behavior on missing/mismatched bindings, and add <code>GIT_EXTERNAL_DIFF</code> to blocked host env keys. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Plugin channel HTTP auth: normalize protected <code>/api/channels</code> path checks against canonicalized request paths (case + percent-decoding + slash normalization), resolve encoded dot-segment traversal variants, and fail closed on malformed <code>%</code>-encoded channel prefixes so alternate-path variants cannot bypass gateway auth. This ships in the next npm release (<code>2026.2.26</code>). Thanks @zpbrent for reporting.</li>
<li>Security/Gateway node pairing: pin paired-device <code>platform</code>/<code>deviceFamily</code> metadata across reconnects and bind those fields into device-auth signatures, so reconnect metadata spoofing cannot expand node command allowlists without explicit repair pairing. This ships in the next npm release (<code>2026.2.26</code>). Thanks @76embiid21 for reporting.</li>
<li>Security/Sandbox path alias guard: reject broken symlink targets by resolving through existing ancestors and failing closed on out-of-root targets, preventing workspace-only <code>apply_patch</code> writes from escaping sandbox/workspace boundaries via dangling symlinks. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Workspace FS boundary aliases: harden canonical boundary resolution for non-existent-leaf symlink aliases while preserving valid in-root aliases, preventing first-write workspace escapes via out-of-root symlink targets. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Config includes: harden <code>$include</code> file loading with verified-open reads, reject hardlinked include aliases, and enforce include file-size guardrails so config include resolution remains bounded to trusted in-root files. This ships in the next npm release (<code>2026.2.26</code>). Thanks @zpbrent for reporting.</li>
<li>Security/Node exec approvals hardening: freeze immutable approval-time execution plans (<code>argv</code>/<code>cwd</code>/<code>agentId</code>/<code>sessionKey</code>) via <code>system.run.prepare</code>, enforce those canonical plan values during approval forwarding/execution, and reject mutable parent-symlink cwd paths during approval-plan building to prevent approval bypass via symlink rebind. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Microsoft Teams media fetch: route Graph message/hosted-content/attachment fetches and auth-scope fallback attachment downloads through shared SSRF-guarded fetch paths, and centralize hostname-suffix allowlist policy helpers in the plugin SDK to remove channel/plugin drift. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Voice Call (Twilio): bind webhook replay + manager dedupe identity to authenticated request material, remove unsigned <code>i-twilio-idempotency-token</code> trust from replay/dedupe keys, and thread verified request identity through provider parse flow to harden cross-provider event dedupe. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting.</li>
<li>Security/Exec approvals forwarding: prefer turn-source channel/account/thread metadata when resolving approval delivery targets so stale session routes do not misroute approval prompts.</li>
<li>Security/Pairing multi-account isolation: enforce account-scoped pairing allowlists and pending-request storage across core + extension message channels while preserving channel-scoped defaults for the default account. This ships in the next npm release (<code>2026.2.26</code>). Thanks @tdjackey for reporting and @gumadeiras for implementation.</li>
<li>Config/Plugins entries: treat unknown <code>plugins.entries.*</code> ids as startup warnings (ignored stale keys) instead of hard validation failures that can crash-loop gateway boot. Landed from contributor PR #27506 by @Sid-Qin. (#27455)</li>
<li>Telegram native commands: degrade command registration on <code>BOT_COMMANDS_TOO_MUCH</code> by retrying with fewer commands instead of crash-looping startup sync. Landed from contributor PR #27512 by @Sid-Qin. (#27456)</li>
<li>Web tools/Proxy: route <code>web_search</code> provider HTTP calls (Brave, Perplexity, xAI, Gemini, Kimi), redirect resolution, and <code>web_fetch</code> through a shared proxy-aware SSRF guard path so gateway installs behind <code>HTTP_PROXY</code>/<code>HTTPS_PROXY</code>/<code>ALL_PROXY</code> no longer fail with transport <code>fetch failed</code> errors. (#27430) thanks @kevinWangSheng.</li>
<li>Android/Node invoke: remove native gateway WebSocket <code>Origin</code> header to avoid false origin rejections, unify invoke command registry/policy/error parsing paths, and keep command availability checks centralized to reduce dispatcher/advertisement drift. (#27257) Thanks @obviyus.</li>
<li>Gateway shared-auth scopes: preserve requested operator scopes for shared-token clients when device identity is unavailable, instead of clearing scopes during auth handling. Landed from contributor PR #27498 by @kevinWangSheng. (#27494)</li>
<li>Cron/Hooks isolated routing: preserve canonical <code>agent:*</code> session keys in isolated runs so already-qualified keys are not double-prefixed (for example <code>agent:main:main</code> no longer becomes <code>agent:main:agent:main:main</code>). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282)</li>
<li>Channels/Multi-account config: when adding a non-default channel account to a single-account top-level channel setup, move existing account-scoped top-level single-account values into <code>channels.<channel>.accounts.default</code> before writing the new account so the original account keeps working without duplicated account values at channel root; <code>openclaw doctor --fix</code> now repairs previously mixed channel account shapes the same way. (#27334) thanks @gumadeiras.</li>
<li>iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman.</li>
<li>CI/Windows: shard the Windows <code>checks-windows</code> test lane into two matrix jobs and honor explicit shard index overrides in <code>scripts/test-parallel.mjs</code> to reduce CI critical-path wall time. (#27234) Thanks @joshavant.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.26/OpenClaw-2026.2.26.zip" length="12796628" type="application/octet-stream" sparkle:edSignature="qqVJfkQS9Q4LCTlGtOyXzORWZWWnOkWyiJ6DVX27oPF8aeUlUyfHrmb51sFiNjSuCJC2xmJW1Mi1CAHl/I1pCw=="/>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,238 @@
# OpenClaw iOS App - Comprehensive Audit Report 2026
**Date:** 2026-03-02
**Scope:** `apps/ios/Sources/` (63 files, ~16,244 LOC), `apps/ios/Tests/` (25 files, 1,884 LOC)
**Deployment Target:** iOS 18.0 / watchOS 11.0 | Swift 6.0 (strict concurrency: complete)
**Audit Team:** 5 specialized Opus 4.6 agents (Concurrency, API Modernization, Architecture, UI/UX, Security)
---
## Executive Summary
The OpenClaw iOS app is a **well-engineered codebase** that has adopted many 2026 best practices: Swift 6 strict concurrency, the Observation framework (`@Observable`), `NavigationStack`, Keychain credential storage, and TLS certificate pinning. However, the audit identified **9 critical findings**, **17 high findings**, **29 medium findings**, and **25 low findings** across 5 audit domains.
### Overall Health Score: **B+** (74/100)
| Domain | Score | Grade | Key Issue |
|--------|-------|-------|-----------|
| Swift 6 Concurrency | 78/100 | B+ | 3 data race risks, 5 unsafe patterns |
| iOS 26 API Modernization | 82/100 | A- | 1 deprecated framework, 4 dead code paths |
| Architecture & Code Quality | 62/100 | C+ | 2 god objects, 11.6% test coverage ratio |
| UI/UX & Accessibility | 65/100 | C+ | Zero Dynamic Type, zero localization |
| Security & Performance | 85/100 | A | No critical vulns, 3 high storage issues |
---
## Critical Findings (9)
### Concurrency (3)
| ID | Finding | File | Risk |
|----|---------|------|------|
| CON-C1 | `GatewayTLSFingerprintProbe` data race: `objc_sync_enter` with unsynchronized `didFinish`/`session`/`task` reads in `start()` | `Gateway/GatewayConnectionController.swift:992-1058` | Crash/undefined behavior |
| CON-C2 | `PhotoCaptureDelegate` & `MovieFileDelegate` unsynchronized `didResume` flag can double-resume `CheckedContinuation` | `Camera/CameraController.swift:260-339` | Fatal crash (debug), UB (release) |
| CON-C3 | `GatewayDiagnostics.logWritesSinceCheck` uses `nonisolated(unsafe)` suppressing all compiler race checks | `Gateway/GatewaySettingsStore.swift:358` | Silent data race |
### Architecture (2)
| ID | Finding | File | Risk |
|----|---------|------|------|
| ARC-C1 | `NodeAppModel` is a 2,787 LOC god object with ~17 responsibilities | `Model/NodeAppModel.swift` | Untestable, unmaintainable |
| ARC-C2 | `TalkModeManager` is a 2,153 LOC god object centralizing speech, audio, PTT, and gateway comms | `Voice/TalkModeManager.swift` | Same as above |
### UI/UX (3)
| ID | Finding | File | Risk |
|----|---------|------|------|
| UIX-C1 | `RootCanvas` voiceWakeToast animations ignore `accessibilityReduceMotion` | `RootCanvas.swift:159-167` | Accessibility violation |
| UIX-C2 | `TalkOrbOverlay` perpetual pulse animations ignore `accessibilityReduceMotion` | `Voice/TalkOrbOverlay.swift:15-26` | Vestibular disorder risk |
| UIX-C3 | `CameraFlashOverlay` has no VoiceOver announcement and no reduced motion check | `RootCanvas.swift:405-429` | Accessibility violation, photosensitivity |
### API Modernization (1)
| ID | Finding | File | Risk |
|----|---------|------|------|
| API-C1 | `NetService` usage (deprecated since iOS 16, removed in future SDKs) while `NWBrowser` already used for discovery | `Gateway/GatewayServiceResolver.swift`, `Gateway/GatewayConnectionController.swift:560-657` | Future SDK breakage |
---
## High Findings (17)
### Concurrency (5)
| ID | Finding | File |
|----|---------|------|
| CON-H1 | `ScreenRecordService` `UncheckedSendableBox<T>` wraps any T as Sendable, silencing compiler | `Screen/ScreenRecordService.swift:4-11` |
| CON-H2 | `WatchMessagingService` `@unchecked Sendable` with `WCSession` property reads unprotected | `Services/WatchMessagingService.swift:23-28` |
| CON-H3 | `LocationService` stores `CheckedContinuation` as instance vars with `nonisolated` delegate callbacks hopping to `@MainActor` | `Location/LocationService.swift:13-14` |
| CON-H4 | `LiveNotificationCenter` wraps non-Sendable `UNUserNotificationCenter` in `@unchecked Sendable` | `Services/NotificationService.swift:18-58` |
| CON-H5 | `NetworkStatusService` is `@unchecked Sendable` but stateless - unnecessary annotation | `Device/NetworkStatusService.swift:5` |
### Security (3)
| ID | Finding | File |
|----|---------|------|
| SEC-H1 | TLS fingerprints stored in UserDefaults (backup-extractable trust anchor) | `OpenClawKit/GatewayTLSPinning.swift:19-38` |
| SEC-H2 | `KeychainStore` update path doesn't enforce `kSecAttrAccessible` on existing items | `Gateway/KeychainStore.swift:20-37` |
| SEC-H3 | Gateway connection metadata (host/port/topology) in UserDefaults | `Gateway/GatewaySettingsStore.swift:170-217` |
### Architecture (2)
| ID | Finding | File |
|----|---------|------|
| ARC-H1 | 3 oversized files: `GatewayConnectionController` (1,058 LOC), `SettingsTab` (1,032 LOC), `OnboardingWizardView` (884 LOC) | Various |
| ARC-H2 | 17 source modules with zero test coverage; 11.6% test LOC ratio | See gap analysis |
### UI/UX (5)
| ID | Finding | File |
|----|---------|------|
| UIX-H1 | Zero Dynamic Type support (no `@ScaledMetric`, no `dynamicTypeSize`) | All view files |
| UIX-H2 | Zero localization infrastructure (all hardcoded English) | All source files |
| UIX-H3 | Zero haptic feedback in entire app | All source files |
| UIX-H4 | OnboardingWizardView missing accessibility labels on mode selection rows | `Onboarding/OnboardingWizardView.swift` |
| UIX-H5 | `GatewayTrustPromptAlert` and `DeepLinkAgentPromptAlert` use deprecated `Alert` API | `Gateway/GatewayTrustPromptAlert.swift` |
### API Modernization (2)
| ID | Finding | File |
|----|---------|------|
| API-H1 | Dead `#available(iOS 15/18)` checks (deployment target is iOS 18.0) | `OpenClawApp.swift:344`, `Camera/CameraController.swift:222-249` |
| API-H2 | `UNUserNotificationCenter` callback APIs wrapped in continuations instead of native async | `OpenClawApp.swift:429-462` |
---
## Cross-Cutting Themes
### 1. God Object Pattern
`NodeAppModel` (2,787 LOC) and `TalkModeManager` (2,153 LOC) together represent **30%** of the entire codebase. Both have `// swiftlint:disable` suppressions acknowledging the problem. This is the single highest-impact improvement opportunity.
### 2. Inconsistent Synchronization Primitives
The codebase uses 4 different synchronization mechanisms: `NSLock` (6 usages), `OSAllocatedUnfairLock` (1 usage), `objc_sync_enter/exit` (1 usage), and `DispatchQueue` serialization (7 usages). Standardizing on `OSAllocatedUnfairLock` + actors would improve consistency and safety.
### 3. UserDefaults Overuse
~70+ direct `UserDefaults.standard` reads/writes with raw string keys across the codebase. TLS fingerprints, gateway metadata, and connection details stored in UserDefaults should be in Keychain. Non-sensitive preferences lack a typed key registry.
### 4. Missing Accessibility Infrastructure
Dynamic Type, localization, and haptic feedback are completely absent. Three views ignore `accessibilityReduceMotion`. This represents the largest gap relative to Apple's 2026 HIG expectations.
### 5. Test Coverage Gaps
11.6% test LOC ratio with 17 untested modules. The gateway reconnect state machine (most complex logic), background lifecycle, onboarding flow, and TalkModeManager have minimal or zero test coverage.
---
## Test Coverage Gap Analysis (Top 15 Gaps)
| Module | Source LOC | Test LOC | Coverage |
|--------|-----------|----------|----------|
| `NodeAppModel.swift` | 2,787 | 478 (invoke only) | Partial - reconnect/background/deep links untested |
| `TalkModeManager.swift` | 2,153 | 31 (config only) | Minimal |
| `GatewayConnectionController.swift` | 1,058 | 226 | Partial - no TLS/Bonjour/autoconnect tests |
| `SettingsTab.swift` | 1,032 | 8 (smoke) | Smoke only |
| `OnboardingWizardView.swift` | 884 | 0 | None |
| `OpenClawApp.swift` | 541 | 0 | None |
| `RootCanvas.swift` | 429 | 8 (smoke) | Smoke only |
| `GatewayOnboardingView.swift` | 371 | 0 | None |
| `WatchMessagingService.swift` | 284 | 0 | None |
| `ContactsService.swift` | 210 | 0 | None |
| `LocationService.swift` | 177 | 0 | None |
| `PhotoLibraryService.swift` | 164 | 0 | None |
| `CalendarService.swift` | 135 | 0 | None |
| `RemindersService.swift` | 133 | 0 | None |
| `MotionService.swift` | 100 | 0 | None |
---
## Prioritized Action Plan
### Phase 1: Critical Fixes (Immediate)
| # | Action | Effort | Impact |
|---|--------|--------|--------|
| 1 | Fix `PhotoCaptureDelegate`/`MovieFileDelegate` `didResume` synchronization (CON-C2) | Small | Prevents crashes |
| 2 | Fix `GatewayTLSFingerprintProbe` data race (CON-C1) | Small | Prevents undefined behavior |
| 3 | Add `accessibilityReduceMotion` checks to `RootCanvas` and `TalkOrbOverlay` (UIX-C1, C2, C3) | Small | Accessibility compliance |
| 4 | Replace `nonisolated(unsafe)` in `GatewayDiagnostics` (CON-C3) | Small | Compiler safety |
### Phase 2: High-Priority Improvements (Next Sprint)
| # | Action | Effort | Impact |
|---|--------|--------|--------|
| 5 | Move TLS fingerprints to Keychain (SEC-H1) | Medium | Security hardening |
| 6 | Fix `KeychainStore` update accessibility enforcement (SEC-H2) | Small | Security correctness |
| 7 | Migrate `NetService` to Network framework (API-C1) | Large | Future-proofing |
| 8 | Remove dead `#available` checks (API-H1) | Small | Code cleanup |
| 9 | Replace `UNUserNotificationCenter` callbacks with async APIs (API-H2) | Small | Modernization |
| 10 | Add `@ScaledMetric` Dynamic Type support to key views (UIX-H1) | Medium | Accessibility |
### Phase 3: Architecture Refactoring (Planned)
| # | Action | Effort | Impact |
|---|--------|--------|--------|
| 11 | Split `NodeAppModel` into 5-6 focused types (ARC-C1) | Large | Testability, maintainability |
| 12 | Split `TalkModeManager` into 3-4 focused types (ARC-C2) | Large | Same |
| 13 | Extract `SettingsTab` into section sub-views (ARC-H1) | Medium | Maintainability |
| 14 | Create typed UserDefaults key registry | Medium | Type safety |
| 15 | Add test coverage for gateway reconnect state machine | Large | Regression safety |
| 16 | Add test coverage for background lifecycle management | Medium | Regression safety |
### Phase 4: Polish & Hardening (Opportunistic)
| # | Action | Effort | Impact |
|---|--------|--------|--------|
| 17 | Add localization infrastructure with `String(localized:)` (UIX-H2) | Large | International users |
| 18 | Add haptic feedback to key interactions (UIX-H3) | Small | UX polish |
| 19 | Standardize on `OSAllocatedUnfairLock` across codebase | Small | Consistency |
| 20 | Replace Combine `Timer.publish`/`onReceive` with async patterns | Small | Modernization |
| 21 | Add keyboard shortcuts for iPad (UIX-M5) | Small | iPad UX |
| 22 | Gate `ELEVENLABS_API_KEY` env var behind `#if DEBUG` (SEC-M3) | Small | Security |
| 23 | Enforce minimum interval between deep link prompts (SEC-M5) | Small | Security |
| 24 | Add HMAC verification to QR setup codes (SEC-M6) | Medium | Security |
---
## Positive Patterns Worth Preserving
1. **Observation framework adoption** - Zero `ObservableObject` usage; consistent `@Observable` + `@Environment` throughout
2. **Protocol-based DI** - `NodeServiceProtocols.swift` defines clean interfaces for all device capabilities with default implementations
3. **Keychain for credentials** - Tokens, passwords, instance IDs stored with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
4. **TLS certificate pinning** - TOFU model with SHA-256 fingerprint verification and user confirmation
5. **`CameraController` as actor** - Exemplary Swift concurrency pattern for hardware resource management
6. **Dual WebSocket sessions** - Node/operator separation provides good privilege scoping
7. **Non-persistent WKWebView** - Canvas prevents session data leakage
8. **Swift 6 strict concurrency** - Enabled project-wide with `SWIFT_STRICT_CONCURRENCY: complete`
9. **`@Sendable` service protocols** - All service protocols correctly require `Sendable` conformance
10. **Deep link confirmation** - Agent deep links require explicit user approval with length limits
---
## OWASP Mobile Top 10 Summary
| Category | Status |
|----------|--------|
| M1: Improper Credential Usage | PASS |
| M2: Inadequate Supply Chain Security | PASS |
| M3: Insecure Authentication/Authorization | PASS |
| M4: Insufficient Input/Output Validation | PASS |
| M5: Insecure Communication | PASS (note: HTTP allowed in web views) |
| M6: Inadequate Privacy Controls | PASS (note: location sent over TLS) |
| M7: Insufficient Binary Protections | N/A |
| M8: Security Misconfiguration | PASS (notes: H-1, H-3) |
| M9: Insecure Data Storage | PASS (notes: H-1, H-3, M-2) |
| M10: Insufficient Cryptography | PASS |
---
## Detailed Reports
Individual audit reports with full code snippets and line-by-line analysis:
- [`audit-concurrency.md`](./audit-concurrency.md) - Swift 6 strict concurrency (20 findings)
- [`audit-api-modernization.md`](./audit-api-modernization.md) - iOS 26 API modernization (19 findings)
- [`audit-architecture.md`](./audit-architecture.md) - Architecture & test coverage (16 findings)
- [`audit-uiux.md`](./audit-uiux.md) - UI/UX & accessibility (24 findings)
- [`audit-security.md`](./audit-security.md) - Security & performance (18 findings)
---
*Generated by OpenClaw iOS Audit Team (5x Opus 4.6 agents) on 2026-03-02*

View File

@@ -1,6 +1,7 @@
import AVFoundation
import OpenClawKit
import Foundation
import os
actor CameraController {
struct CameraDeviceInfo: Codable, Sendable {
@@ -120,7 +121,8 @@ actor CameraController {
Self.pickCamera(facing: preferFrontCamera ? .front : .back, deviceId: deviceId)
},
cameraUnavailableError: CameraError.cameraUnavailable,
mapSetupError: Self.mapMovieSetupError) { output in
mapSetupError: Self.mapMovieSetupError,
operation: { output in
var delegate: MovieFileDelegate?
let recordedURL: URL = try await withCheckedThrowingContinuation { cont in
let d = MovieFileDelegate(cont)
@@ -131,7 +133,7 @@ actor CameraController {
// Transcode .mov -> .mp4 for easier downstream handling.
try await Self.exportToMP4(inputURL: recordedURL, outputURL: mp4URL)
return try Data(contentsOf: mp4URL)
}
})
return (
format: format.rawValue,
base64: data.base64EncodedString(),
@@ -259,7 +261,7 @@ actor CameraController {
private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
private let continuation: CheckedContinuation<Data, Error>
private var didResume = false
private let resumed = OSAllocatedUnfairLock(initialState: false)
init(_ continuation: CheckedContinuation<Data, Error>) {
self.continuation = continuation
@@ -270,8 +272,12 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
guard !self.didResume else { return }
self.didResume = true
let alreadyResumed = self.resumed.withLock { old in
let was = old
old = true
return was
}
guard !alreadyResumed else { return }
if let error {
self.continuation.resume(throwing: error)
@@ -300,15 +306,19 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat
error: Error?
) {
guard let error else { return }
guard !self.didResume else { return }
self.didResume = true
let alreadyResumed = self.resumed.withLock { old in
let was = old
old = true
return was
}
guard !alreadyResumed else { return }
self.continuation.resume(throwing: error)
}
}
private final class MovieFileDelegate: NSObject, AVCaptureFileOutputRecordingDelegate {
private let continuation: CheckedContinuation<URL, Error>
private var didResume = false
private let resumed = OSAllocatedUnfairLock(initialState: false)
init(_ continuation: CheckedContinuation<URL, Error>) {
self.continuation = continuation
@@ -320,8 +330,12 @@ private final class MovieFileDelegate: NSObject, AVCaptureFileOutputRecordingDel
from connections: [AVCaptureConnection],
error: Error?)
{
guard !self.didResume else { return }
self.didResume = true
let alreadyResumed = self.resumed.withLock { old in
let was = old
old = true
return was
}
guard !alreadyResumed else { return }
if let error {
let ns = error as NSError

View File

@@ -9,6 +9,7 @@ import Darwin
import OpenClawKit
import Network
import Observation
import os
import Photos
import ReplayKit
import Security
@@ -990,12 +991,16 @@ extension GatewayConnectionController {
#endif
private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @unchecked Sendable {
private struct ProbeState {
var didFinish = false
var session: URLSession?
var task: URLSessionWebSocketTask?
}
private let url: URL
private let timeoutSeconds: Double
private let onComplete: (String?) -> Void
private var didFinish = false
private var session: URLSession?
private var task: URLSessionWebSocketTask?
private let state = OSAllocatedUnfairLock(initialState: ProbeState())
init(url: URL, timeoutSeconds: Double, onComplete: @escaping (String?) -> Void) {
self.url = url
@@ -1008,9 +1013,11 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u
config.timeoutIntervalForRequest = self.timeoutSeconds
config.timeoutIntervalForResource = self.timeoutSeconds
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
self.session = session
let task = session.webSocketTask(with: self.url)
self.task = task
self.state.withLock { s in
s.session = session
s.task = task
}
task.resume()
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + self.timeoutSeconds) { [weak self] in
@@ -1036,12 +1043,18 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u
}
private func finish(_ fingerprint: String?) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }
guard !self.didFinish else { return }
self.didFinish = true
self.task?.cancel(with: .goingAway, reason: nil)
self.session?.invalidateAndCancel()
let (shouldComplete, taskToCancel, sessionToInvalidate) = self.state.withLock { s -> (Bool, URLSessionWebSocketTask?, URLSession?) in
guard !s.didFinish else { return (false, nil, nil) }
s.didFinish = true
let task = s.task
let session = s.session
s.task = nil
s.session = nil
return (true, task, session)
}
guard shouldComplete else { return }
taskToCancel?.cancel(with: .goingAway, reason: nil)
sessionToInvalidate?.invalidateAndCancel()
self.onComplete(fingerprint)
}

View File

@@ -25,6 +25,7 @@ enum GatewaySettingsStore {
private static let instanceIdAccount = "instanceId"
private static let preferredGatewayStableIDAccount = "preferredStableID"
private static let lastDiscoveredGatewayStableIDAccount = "lastDiscoveredStableID"
private static let lastGatewayConnectionAccount = "lastConnection"
private static let talkProviderApiKeyAccountPrefix = "provider.apiKey."
static func bootstrapPersistence() {
@@ -140,11 +141,20 @@ enum GatewaySettingsStore {
}
}
private enum LastGatewayKind: String {
private enum LastGatewayKind: String, Codable {
case manual
case discovered
}
/// JSON-serializable envelope stored as a single Keychain entry.
private struct LastGatewayConnectionData: Codable {
var kind: LastGatewayKind
var stableID: String
var useTLS: Bool
var host: String?
var port: Int?
}
static func loadTalkProviderApiKey(provider: String) -> String? {
guard let providerId = self.normalizedTalkProviderID(provider) else { return nil }
let account = self.talkProviderApiKeyAccount(providerId: providerId)
@@ -168,47 +178,93 @@ enum GatewaySettingsStore {
}
static func saveLastGatewayConnectionManual(host: String, port: Int, useTLS: Bool, stableID: String) {
let defaults = UserDefaults.standard
defaults.set(LastGatewayKind.manual.rawValue, forKey: self.lastGatewayKindDefaultsKey)
defaults.set(host, forKey: self.lastGatewayHostDefaultsKey)
defaults.set(port, forKey: self.lastGatewayPortDefaultsKey)
defaults.set(useTLS, forKey: self.lastGatewayTlsDefaultsKey)
defaults.set(stableID, forKey: self.lastGatewayStableIDDefaultsKey)
let payload = LastGatewayConnectionData(
kind: .manual, stableID: stableID, useTLS: useTLS, host: host, port: port)
self.saveLastGatewayConnectionData(payload)
}
static func saveLastGatewayConnectionDiscovered(stableID: String, useTLS: Bool) {
let defaults = UserDefaults.standard
defaults.set(LastGatewayKind.discovered.rawValue, forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.set(useTLS, forKey: self.lastGatewayTlsDefaultsKey)
defaults.set(stableID, forKey: self.lastGatewayStableIDDefaultsKey)
let payload = LastGatewayConnectionData(
kind: .discovered, stableID: stableID, useTLS: useTLS)
self.saveLastGatewayConnectionData(payload)
}
static func loadLastGatewayConnection() -> LastGatewayConnection? {
// Migrate legacy UserDefaults entries on first access.
self.migrateLastGatewayFromUserDefaultsIfNeeded()
guard let json = KeychainStore.loadString(
service: self.gatewayService, account: self.lastGatewayConnectionAccount),
let data = json.data(using: .utf8),
let stored = try? JSONDecoder().decode(LastGatewayConnectionData.self, from: data)
else { return nil }
let stableID = stored.stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !stableID.isEmpty else { return nil }
if stored.kind == .discovered {
return .discovered(stableID: stableID, useTLS: stored.useTLS)
}
let host = (stored.host ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let port = stored.port ?? 0
guard !host.isEmpty, port > 0, port <= 65535 else { return nil }
return .manual(host: host, port: port, useTLS: stored.useTLS, stableID: stableID)
}
static func clearLastGatewayConnection(defaults: UserDefaults = .standard) {
_ = KeychainStore.delete(
service: self.gatewayService, account: self.lastGatewayConnectionAccount)
// Clean up any legacy UserDefaults entries.
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey)
}
@discardableResult
private static func saveLastGatewayConnectionData(_ payload: LastGatewayConnectionData) -> Bool {
guard let data = try? JSONEncoder().encode(payload),
let json = String(data: data, encoding: .utf8)
else { return false }
return KeychainStore.saveString(
json, service: self.gatewayService, account: self.lastGatewayConnectionAccount)
}
/// Migrate legacy UserDefaults gateway.last.* keys into a single Keychain entry.
private static func migrateLastGatewayFromUserDefaultsIfNeeded() {
let defaults = UserDefaults.standard
let stableID = defaults.string(forKey: self.lastGatewayStableIDDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !stableID.isEmpty else { return nil }
guard !stableID.isEmpty else { return }
// Already migrated if Keychain entry exists.
if KeychainStore.loadString(
service: self.gatewayService, account: self.lastGatewayConnectionAccount) != nil
{
// Clean up legacy keys.
self.removeLastGatewayDefaults(defaults)
return
}
let useTLS = defaults.bool(forKey: self.lastGatewayTlsDefaultsKey)
let kindRaw = defaults.string(forKey: self.lastGatewayKindDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let kind = LastGatewayKind(rawValue: kindRaw) ?? .manual
if kind == .discovered {
return .discovered(stableID: stableID, useTLS: useTLS)
}
let host = defaults.string(forKey: self.lastGatewayHostDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let port = defaults.integer(forKey: self.lastGatewayPortDefaultsKey)
.trimmingCharacters(in: .whitespacesAndNewlines)
let port = defaults.object(forKey: self.lastGatewayPortDefaultsKey) as? Int
// Back-compat: older builds persisted manual-style host/port without a kind marker.
guard !host.isEmpty, port > 0, port <= 65535 else { return nil }
return .manual(host: host, port: port, useTLS: useTLS, stableID: stableID)
let payload = LastGatewayConnectionData(
kind: kind, stableID: stableID, useTLS: useTLS,
host: kind == .manual ? host : nil,
port: kind == .manual ? port : nil)
guard self.saveLastGatewayConnectionData(payload) else { return }
self.removeLastGatewayDefaults(defaults)
}
static func clearLastGatewayConnection(defaults: UserDefaults = .standard) {
private static func removeLastGatewayDefaults(_ defaults: UserDefaults) {
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
@@ -355,9 +411,15 @@ enum GatewayDiagnostics {
private static let maxLogBytes: Int64 = 512 * 1024
private static let keepLogBytes: Int64 = 256 * 1024
private static let logSizeCheckEveryWrites = 50
nonisolated(unsafe) private static var logWritesSinceCheck = 0
private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0)
private static let isoFormatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
private static var fileURL: URL? {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
.appendingPathComponent("openclaw-gateway.log")
}
@@ -404,32 +466,41 @@ enum GatewayDiagnostics {
}
}
private static func applyFileProtection(url: URL) {
try? FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
ofItemAtPath: url.path)
}
static func bootstrap() {
guard let url = fileURL else { return }
queue.async {
self.truncateLogIfNeeded(url: url)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let timestamp = formatter.string(from: Date())
let timestamp = self.isoFormatter.string(from: Date())
let line = "[\(timestamp)] gateway diagnostics started\n"
if let data = line.data(using: .utf8) {
self.appendToLog(url: url, data: data)
self.applyFileProtection(url: url)
}
}
}
static func log(_ message: String) {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let timestamp = formatter.string(from: Date())
let timestamp = self.isoFormatter.string(from: Date())
let line = "[\(timestamp)] \(message)"
logger.info("\(line, privacy: .public)")
guard let url = fileURL else { return }
queue.async {
self.logWritesSinceCheck += 1
if self.logWritesSinceCheck >= self.logSizeCheckEveryWrites {
self.logWritesSinceCheck = 0
let shouldTruncate = self.logWritesSinceCheck.withLock { count in
count += 1
if count >= self.logSizeCheckEveryWrites {
count = 0
return true
}
return false
}
if shouldTruncate {
self.truncateLogIfNeeded(url: url)
}
let entry = line + "\n"

View File

@@ -18,6 +18,9 @@ enum KeychainStore {
}
static func saveString(_ value: String, service: String, account: String) -> Bool {
// Delete-then-add ensures kSecAttrAccessible is always applied.
// SecItemUpdate cannot change the accessibility level of an existing item,
// so a stale item created with a weaker policy would retain it on update.
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
@@ -25,10 +28,7 @@ enum KeychainStore {
kSecAttrAccount as String: account,
]
let update: [String: Any] = [kSecValueData as String: data]
let status = SecItemUpdate(query as CFDictionary, update as CFDictionary)
if status == errSecSuccess { return true }
if status != errSecItemNotFound { return false }
SecItemDelete(query as CFDictionary)
var insert = query
insert[kSecValueData as String] = data

View File

@@ -21,7 +21,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
self.manager
}
var locationRequestContinuation: CheckedContinuation<CLLocation, Error>? {
var locationRequestContinuation: CheckedContinuation<CLLocation, Swift.Error>? {
get { self.locationContinuation }
set { self.locationContinuation = newValue }
}
@@ -65,9 +65,10 @@ final class LocationService: NSObject, CLLocationManagerDelegate, LocationServic
desiredAccuracy: desiredAccuracy,
maxAgeMs: maxAgeMs,
timeoutMs: timeoutMs,
request: { try await self.requestLocationOnce() }) { timeoutMs, operation in
request: { try await self.requestLocationOnce() },
withTimeout: { timeoutMs, operation in
try await self.withTimeout(timeoutMs: timeoutMs, operation: operation)
}
})
}
private func awaitAuthorizationChange() async -> CLAuthorizationStatus {

View File

@@ -2591,8 +2591,8 @@ extension NodeAppModel {
"agent deep link rejected: unkeyed message too long chars=\(message.count, privacy: .public)")
return
}
if Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt) < 1.0 {
self.deepLinkLogger.debug("agent deep link prompt throttled")
if Date().timeIntervalSince(self.lastAgentDeepLinkPromptAt) < 5.0 {
self.deepLinkLogger.debug("agent deep link prompt rate-limited (min 5 s interval)")
return
}
self.lastAgentDeepLinkPromptAt = Date()

View File

@@ -302,6 +302,7 @@ private struct ManualEntryStep: View {
// (GatewaySetupCode) decode raw setup codes.
}
@MainActor
private func gatewayConnectionStatusLines(
appModel: NodeAppModel,
gatewayController: GatewayConnectionController) -> [String]
@@ -309,6 +310,7 @@ private func gatewayConnectionStatusLines(
ConnectionStatusBox.defaultLines(appModel: appModel, gatewayController: gatewayController)
}
@MainActor
private func resetGatewayConnectionState(
appModel: NodeAppModel,
connectStatusText: inout String?,
@@ -319,6 +321,7 @@ private func resetGatewayConnectionState(
connectingGatewayID = nil
}
@MainActor
@ViewBuilder
private func gatewayConnectionStatusSection(
appModel: NodeAppModel,

View File

@@ -1,4 +1,5 @@
import AVFoundation
import OpenClawKit
import ReplayKit
final class ScreenRecordService: @unchecked Sendable {

View File

@@ -72,6 +72,9 @@ final class TalkModeManager: NSObject {
private var mainSessionKey: String = "main"
private var fallbackVoiceId: String?
private var lastPlaybackWasPCM: Bool = false
/// Set when the ElevenLabs API rejects PCM format (e.g. 403 subscription_required).
/// Once set, all subsequent requests in this session use MP3 instead of re-trying PCM.
private var pcmFormatUnavailable: Bool = false
var pcmPlayer: PCMStreamingAudioPlaying = PCMStreamingAudioPlayer.shared
var mp3Player: StreamingAudioPlaying = StreamingAudioPlayer.shared
@@ -987,9 +990,12 @@ final class TalkModeManager: NSObject {
self.logger.warning("unknown voice alias \(requestedVoice ?? "?", privacy: .public)")
}
let resolvedKey =
(self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil) ??
ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
let configuredKey = self.apiKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? self.apiKey : nil
#if DEBUG
let resolvedKey = configuredKey ?? ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
#else
let resolvedKey = configuredKey
#endif
let apiKey = resolvedKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let preferredVoice = resolvedVoice ?? self.currentVoiceId ?? self.defaultVoiceId
let voiceId: String? = if let apiKey, !apiKey.isEmpty {
@@ -1004,7 +1010,8 @@ final class TalkModeManager: NSObject {
let desiredOutputFormat = (directive?.outputFormat ?? self.defaultOutputFormat)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let requestedOutputFormat = (desiredOutputFormat?.isEmpty == false) ? desiredOutputFormat : nil
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(requestedOutputFormat ?? "pcm_44100")
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(
requestedOutputFormat ?? self.effectiveDefaultOutputFormat)
if outputFormat == nil, let requestedOutputFormat {
self.logger.warning(
"talk output_format unsupported for local playback: \(requestedOutputFormat, privacy: .public)")
@@ -1051,8 +1058,9 @@ final class TalkModeManager: NSObject {
self.lastPlaybackWasPCM = true
var playback = await self.pcmPlayer.play(stream: stream, sampleRate: sampleRate)
if !playback.finished, playback.interruptedAt == nil {
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100_128")
self.logger.warning("pcm playback failed; retrying mp3")
self.pcmFormatUnavailable = true
self.lastPlaybackWasPCM = false
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
@@ -1388,7 +1396,7 @@ final class TalkModeManager: NSObject {
private func resolveIncrementalPrefetchOutputFormat(context: IncrementalSpeechContext) -> String? {
if TalkTTSValidation.pcmSampleRate(from: context.outputFormat) != nil {
return ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
return ElevenLabsTTSClient.validatedOutputFormat("mp3_44100_128")
}
return context.outputFormat
}
@@ -1477,7 +1485,8 @@ final class TalkModeManager: NSObject {
let desiredOutputFormat = (directive?.outputFormat ?? self.defaultOutputFormat)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let requestedOutputFormat = (desiredOutputFormat?.isEmpty == false) ? desiredOutputFormat : nil
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(requestedOutputFormat ?? "pcm_44100")
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(
requestedOutputFormat ?? self.effectiveDefaultOutputFormat)
if outputFormat == nil, let requestedOutputFormat {
self.logger.warning(
"talk output_format unsupported for local playback: \(requestedOutputFormat, privacy: .public)")
@@ -1528,6 +1537,11 @@ final class TalkModeManager: NSObject {
latencyTier: TalkTTSValidation.validatedLatencyTier(context.directive?.latencyTier))
}
/// Returns `mp3_44100_128` when the API has already rejected PCM, otherwise `pcm_44100`.
private var effectiveDefaultOutputFormat: String {
self.pcmFormatUnavailable ? "mp3_44100_128" : "pcm_44100"
}
private static func makeBufferedAudioStream(chunks: [Data]) -> AsyncThrowingStream<Data, Error> {
AsyncThrowingStream { continuation in
for chunk in chunks {
@@ -1583,8 +1597,9 @@ final class TalkModeManager: NSObject {
var playback = await self.pcmPlayer.play(stream: stream, sampleRate: sampleRate)
if !playback.finished, playback.interruptedAt == nil {
self.logger.warning("pcm playback failed; retrying mp3")
self.pcmFormatUnavailable = true
self.lastPlaybackWasPCM = false
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100_128")
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
request: self.makeIncrementalTTSRequest(
@@ -1920,6 +1935,7 @@ extension TalkModeManager {
func reloadConfig() async {
guard let gateway else { return }
self.pcmFormatUnavailable = false
do {
let res = try await gateway.request(
method: "talk.config",

View File

@@ -441,13 +441,6 @@ final class VoiceWakeManager: NSObject {
}
}
private static func permissionMessage(
kind: String,
status: AVAudioSession.RecordPermission) -> String
{
self.deniedByDefaultPermissionMessage(kind: kind, isUndetermined: status == .undetermined)
}
private static func permissionMessage(
kind: String,
status: SFSpeechRecognizerAuthorizationStatus) -> String
@@ -466,7 +459,7 @@ final class VoiceWakeManager: NSObject {
}
}
private static func deniedByDefaultPermissionMessage(kind: String, isUndetermined: Bool) -> String {
private nonisolated static func deniedByDefaultPermissionMessage(kind: String, isUndetermined: Bool) -> String {
if isUndetermined {
return "\(kind) permission not granted"
}

View File

@@ -71,18 +71,37 @@ import UIKit
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
withUserDefaults([:]) {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "gateway.example.com",
port: 443,
useTLS: true,
stableID: "manual|gateway.example.com|443")
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "gateway.example.com", port: 443, useTLS: true, stableID: "manual|gateway.example.com|443"))
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {
if let prior {
_ = KeychainStore.saveString(prior, service: "ai.openclaw.gateway", account: "lastConnection")
} else {
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
}
}
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "gateway.example.com",
port: 443,
useTLS: true,
stableID: "manual|gateway.example.com|443")
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "gateway.example.com", port: 443, useTLS: true, stableID: "manual|gateway.example.com|443"))
}
@Test @MainActor func loadLastConnectionReturnsNilForInvalidData() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {
if let prior {
_ = KeychainStore.saveString(prior, service: "ai.openclaw.gateway", account: "lastConnection")
} else {
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
}
}
_ = KeychainStore.delete(service: "ai.openclaw.gateway", account: "lastConnection")
// Plant legacy UserDefaults with invalid host/port to exercise migration + validation.
withUserDefaults([
"gateway.last.kind": "manual",
"gateway.last.host": "",

View File

@@ -27,6 +27,7 @@ private let lastGatewayDefaultsKeys = [
"gateway.last.tls",
"gateway.last.stableID",
]
private let lastGatewayKeychainEntry = KeychainEntry(service: gatewayService, account: "lastConnection")
private func snapshotDefaults(_ keys: [String]) -> [String: Any?] {
let defaults = UserDefaults.standard
@@ -84,9 +85,13 @@ private func withBootstrapSnapshots(_ body: () -> Void) {
body()
}
private func withLastGatewayDefaultsSnapshot(_ body: () -> Void) {
let snapshot = snapshotDefaults(lastGatewayDefaultsKeys)
defer { restoreDefaults(snapshot) }
private func withLastGatewaySnapshot(_ body: () -> Void) {
let defaultsSnapshot = snapshotDefaults(lastGatewayDefaultsKeys)
let keychainSnapshot = snapshotKeychain([lastGatewayKeychainEntry])
defer {
restoreDefaults(defaultsSnapshot)
restoreKeychain(keychainSnapshot)
}
body()
}
@@ -135,7 +140,7 @@ private func withLastGatewayDefaultsSnapshot(_ body: () -> Void) {
}
@Test func lastGateway_manualRoundTrip() {
withLastGatewayDefaultsSnapshot {
withLastGatewaySnapshot {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "example.com",
port: 443,
@@ -147,28 +152,24 @@ private func withLastGatewayDefaultsSnapshot(_ body: () -> Void) {
}
}
@Test func lastGateway_discoveredDoesNotPersistResolvedHostPort() {
withLastGatewayDefaultsSnapshot {
// Simulate a prior manual record that included host/port.
applyDefaults([
"gateway.last.host": "10.0.0.99",
"gateway.last.port": 18789,
"gateway.last.tls": true,
"gateway.last.stableID": "manual|10.0.0.99|18789",
"gateway.last.kind": "manual",
])
@Test func lastGateway_discoveredOverwritesManual() {
withLastGatewaySnapshot {
GatewaySettingsStore.saveLastGatewayConnectionManual(
host: "10.0.0.99",
port: 18789,
useTLS: true,
stableID: "manual|10.0.0.99|18789")
GatewaySettingsStore.saveLastGatewayConnectionDiscovered(stableID: "gw|abc", useTLS: true)
let defaults = UserDefaults.standard
#expect(defaults.object(forKey: "gateway.last.host") == nil)
#expect(defaults.object(forKey: "gateway.last.port") == nil)
#expect(GatewaySettingsStore.loadLastGatewayConnection() == .discovered(stableID: "gw|abc", useTLS: true))
}
}
@Test func lastGateway_backCompat_manualLoadsWhenKindMissing() {
withLastGatewayDefaultsSnapshot {
@Test func lastGateway_migratesFromUserDefaults() {
withLastGatewaySnapshot {
// Clear Keychain entry and plant legacy UserDefaults values.
applyKeychain([lastGatewayKeychainEntry: nil])
applyDefaults([
"gateway.last.kind": nil,
"gateway.last.host": "example.org",
@@ -179,6 +180,11 @@ private func withLastGatewayDefaultsSnapshot(_ body: () -> Void) {
let loaded = GatewaySettingsStore.loadLastGatewayConnection()
#expect(loaded == .manual(host: "example.org", port: 18789, useTLS: false, stableID: "manual|example.org|18789"))
// Legacy keys should be cleaned up after migration.
let defaults = UserDefaults.standard
#expect(defaults.object(forKey: "gateway.last.stableID") == nil)
#expect(defaults.object(forKey: "gateway.last.host") == nil)
}
}

View File

@@ -0,0 +1,478 @@
# iOS API Modernization Audit Report
**Date:** 2026-03-02
**Auditor:** API Modernization Expert (Claude Opus 4.6)
**Scope:** All Swift source files in `apps/ios/Sources/`, `apps/ios/WatchExtension/Sources/`, and `apps/ios/ShareExtension/`
**Deployment Target:** iOS 18.0 / watchOS 11.0
**Swift Version:** 6.0 (strict concurrency: complete)
**Xcode Version:** 16.0
---
## Executive Summary
The OpenClaw iOS codebase is well-maintained and has already adopted many modern Swift and iOS patterns. The Observation framework (`@Observable`, `@Bindable`, `@Environment(ModelType.self)`) is used consistently throughout. `NavigationStack` is used instead of the deprecated `NavigationView`. Swift 6 strict concurrency is enabled project-wide.
However, there are several areas where deprecated APIs remain in use, unnecessary availability checks exist (dead code given iOS 18.0 deployment target), and legacy callback-based APIs are wrapped in continuations where native async alternatives are available.
### Summary by Severity
| Severity | Count | Description |
|----------|-------|-------------|
| Critical | 1 | Deprecated `NetService` usage (removed in future SDKs) |
| High | 4 | Dead availability-check code, legacy callback wrapping |
| Medium | 8 | Callback APIs with async alternatives, legacy patterns |
| Low | 6 | Minor modernization opportunities, style improvements |
---
## Critical Findings
### C-1: `NetService` Usage (Deprecated Since iOS 16)
**Files:**
- `apps/ios/Sources/Gateway/GatewayServiceResolver.swift` (entire file)
- `apps/ios/Sources/Gateway/GatewayConnectionController.swift` (lines ~560-657)
**Current Code:**
`GatewayServiceResolver` is built entirely on `NetService` and `NetServiceDelegate`, which have been deprecated since iOS 16. `GatewayConnectionController` uses `NetService` for Bonjour resolution in `resolveBonjourServiceToHostPort`.
**Risk:** Apple may remove `NetService` entirely in a future SDK. The app already uses `NWBrowser` (Network framework) for discovery in `GatewayDiscoveryModel.swift`, creating an inconsistency where discovery uses the modern API but resolution falls back to the deprecated one.
**Recommended Replacement:** Migrate to `NWConnection` for TCP connection establishment and use the endpoint information from `NWBrowser` results directly, eliminating the need for a separate `NetService`-based resolver. The `NWBrowser.Result` already provides `NWEndpoint` values that can be used with `NWConnection` without resolution.
---
## High Findings
### H-1: Unnecessary `#available(iOS 15.0, *)` Check
**File:** `apps/ios/Sources/OpenClawApp.swift`, line 344
**Current Code:**
```swift
if #available(iOS 15.0, *) { ... }
```
**Issue:** The deployment target is iOS 18.0, so this check is always true. The code inside the `#available` block executes unconditionally, and the compiler may warn about this.
**Recommended Fix:** Remove the `#available` check and keep only the body.
### H-2: Dead `AVAssetExportSession` Fallback Code
**File:** `apps/ios/Sources/Camera/CameraController.swift`, lines ~222-249
**Current Code:**
```swift
if #available(iOS 18.0, tvOS 18.0, visionOS 2.0, *) {
try await exportSession.export(to: fileURL, as: .mp4)
} else {
exportSession.outputURL = fileURL
exportSession.outputFileType = .mp4
await exportSession.export()
// ...legacy error check...
}
```
**Issue:** The `else` branch is dead code since the deployment target is iOS 18.0. The `#available` check is always true.
**Recommended Fix:** Remove the `#available` check and the `else` branch entirely. Use only the modern `export(to:as:)` API.
### H-3: Callback-Based `UNUserNotificationCenter` APIs Wrapped in Continuations
**File:** `apps/ios/Sources/OpenClawApp.swift`, lines ~429-462
**Current Code:**
```swift
let settings = await withCheckedContinuation { cont in
center.getNotificationSettings { settings in
cont.resume(returning: settings)
}
}
```
**Issue:** `UNUserNotificationCenter` has had native async APIs since iOS 15:
- `center.notificationSettings()` (replaces `getNotificationSettings`)
- `center.notificationCategories()` (replaces `getNotificationCategories`)
- `try await center.add(request)` (replaces `add(_:completionHandler:)`)
The Watch app (`WatchInboxStore.swift`, line 161) already correctly uses the modern async pattern: `await center.notificationSettings()`.
**Recommended Fix:** Replace all `withCheckedContinuation` wrappers around `UNUserNotificationCenter` with their native async equivalents.
### H-4: `NSItemProvider.loadItem` Callback Pattern in Share Extension
**File:** `apps/ios/ShareExtension/ShareViewController.swift`, lines ~501-547
**Current Code:**
```swift
await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
// ...
continuation.resume(returning: ...)
}
}
```
**Issue:** `NSItemProvider` has had modern async alternatives since iOS 16:
- `try await provider.loadItem(forTypeIdentifier:)` for basic loading
- `try await provider.loadDataRepresentation(for:)` with `UTType` parameter
- `try await provider.loadFileRepresentation(for:)`
Three separate methods (`loadURLValue`, `loadTextValue`, `loadDataValue`) all wrap callbacks in continuations.
**Recommended Fix:** Adopt the modern `NSItemProvider` async APIs, using `UTType` parameters instead of string identifiers where possible.
---
## Medium Findings
### M-1: `CLLocationManager` Delegate Pattern vs Modern `CLLocationUpdate` API
**File:** `apps/ios/Sources/Location/LocationService.swift` (entire file)
**Current Code:** Uses `CLLocationManagerDelegate` with:
- `startUpdatingLocation()` / `stopUpdatingLocation()`
- `startMonitoringSignificantLocationChanges()`
- `requestWhenInUseAuthorization()` / `requestAlwaysAuthorization()`
- `locationManager(_:didUpdateLocations:)` delegate callback
**Modern Alternative (iOS 17+):**
- `CLLocationUpdate.liveUpdates()` async sequence for continuous location
- `CLMonitor` for region monitoring and significant location changes
- `CLLocationManager.requestWhenInUseAuthorization()` still required for authorization, but updates are consumed via async sequences
**Impact:** The delegate pattern works but requires more boilerplate and is harder to compose with async/await code.
**Recommended Fix:** Migrate `startLocationUpdates` to use `CLLocationUpdate.liveUpdates()` and consider `CLMonitor` for significant location changes. Keep the authorization request methods as-is (no async alternative for those).
### M-2: `CMMotionActivityManager` and `CMPedometer` Callback Wrapping
**File:** `apps/ios/Sources/Motion/MotionService.swift`, lines 23-81
**Current Code:**
```swift
return try await withCheckedThrowingContinuation { continuation in
activityManager.queryActivityStarting(from: startDate, to: endDate, to: OperationQueue.main) { activities, error in
// ...
}
}
```
**Issue:** CoreMotion APIs still use callbacks; there are no native async versions. However, wrapping in `withCheckedThrowingContinuation` is currently the correct approach.
**Recommended Fix:** No change needed at this time. Monitor for async CoreMotion APIs in future SDK releases.
### M-3: `EKEventStore.fetchReminders` Callback Wrapping
**File:** `apps/ios/Sources/Reminders/RemindersService.swift`, lines 20-45
**Current Code:**
```swift
return try await withCheckedThrowingContinuation { continuation in
store.fetchReminders(matching: predicate) { reminders in
// ...
}
}
```
**Issue:** EventKit still uses callbacks for `fetchReminders`. The continuation wrapper is the correct approach for now.
**Recommended Fix:** No change needed. This is the standard pattern for callback-based EventKit APIs.
### M-4: `PHImageManager.requestImage` Synchronous Callback Pattern
**File:** `apps/ios/Sources/Media/PhotoLibraryService.swift`, line ~82
**Current Code:**
```swift
let options = PHImageRequestOptions()
options.isSynchronous = true
// ...
imageManager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { image, _ in
resultImage = image
}
```
**Issue:** Uses `isSynchronous = true` which blocks the calling thread. Modern iOS apps should prefer async image loading. Consider using `PHImageManager`'s async image loading or the newer `PHPickerViewController` patterns for user-initiated selection.
**Recommended Fix:** If this code runs on a background thread (inside an actor), the synchronous pattern is acceptable for simplicity. Consider wrapping in a continuation if thread blocking becomes an issue.
### M-5: `NotificationCenter` Observer Callback Pattern
**File:** `apps/ios/Sources/Voice/VoiceWakeManager.swift`, lines 105-113
**Current Code:**
```swift
self.userDefaultsObserver = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: UserDefaults.standard,
queue: .main,
using: { [weak self] _ in
Task { @MainActor in
self?.handleUserDefaultsDidChange()
}
})
```
**Modern Alternative (iOS 15+):**
```swift
// Use async notification sequence
for await _ in NotificationCenter.default.notifications(named: UserDefaults.didChangeNotification) {
self.handleUserDefaultsDidChange()
}
```
**Also in:** `apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift`, line 55 (uses `onReceive` with Combine publisher -- see M-8).
**Recommended Fix:** Replace callback-based observers with `NotificationCenter.default.notifications(named:)` async sequences in a `.task` modifier or dedicated Task.
### M-6: `DispatchQueue.asyncAfter` Usage
**Files:**
- `apps/ios/Sources/Gateway/TCPProbe.swift`, line 39
- `apps/ios/Sources/Gateway/GatewayConnectionController.swift`, line ~1016
- `apps/ios/ShareExtension/ShareViewController.swift`, line 142
**Current Code:**
```swift
queue.asyncAfter(deadline: .now() + timeoutSeconds) { finish(false) }
```
**Issue:** `DispatchQueue.asyncAfter` is a legacy GCD pattern. In Swift concurrency, `Task.sleep(nanoseconds:)` or `Task.sleep(for:)` is preferred. However, in `TCPProbe`, the GCD pattern is used within an `NWConnection` state handler context where a DispatchQueue is already in use, making it acceptable.
**Recommended Fix:**
- `TCPProbe.swift`: Acceptable as-is (NWConnection requires a DispatchQueue).
- `GatewayConnectionController.swift`: Replace with `Task.sleep` pattern.
- `ShareViewController.swift`: Replace with `Task.sleep` + `MainActor.run`.
### M-7: `objc_sync_enter`/`objc_sync_exit` and `objc_setAssociatedObject`
**File:** `apps/ios/Sources/Gateway/GatewayConnectionController.swift`, lines ~1039-1040, ~653
**Current Code:**
```swift
objc_sync_enter(connection)
// ...
objc_sync_exit(connection)
```
and
```swift
objc_setAssociatedObject(service, &resolvedKey, resolvedBox, .OBJC_ASSOCIATION_RETAIN)
```
**Issue:** These are Objective-C runtime patterns. Swift has modern alternatives:
- `OSAllocatedUnfairLock` (iOS 16+) or `Mutex` (proposed) for synchronization
- Property wrappers or Swift-native patterns for associated state
Note: `TCPProbe.swift` correctly uses `OSAllocatedUnfairLock` already.
**Recommended Fix:** Replace `objc_sync_enter`/`objc_sync_exit` with `OSAllocatedUnfairLock`. For `objc_setAssociatedObject`, this will naturally be eliminated when migrating away from `NetService` (see C-1).
### M-8: Combine `Timer.publish` and `onReceive` Usage
**Files:**
- `apps/ios/Sources/Onboarding/OnboardingWizardView.swift`, line ~72 (`Timer.publish`)
- `apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift`, line 55 (`.onReceive(NotificationCenter.default.publisher(...))`)
**Current Code:**
```swift
@State private var autoAdvanceTimer = Timer.publish(every: 5.5, on: .main, in: .common).autoconnect()
// ...
.onReceive(self.autoAdvanceTimer) { _ in ... }
```
**Issue:** `Timer.publish` is a Combine pattern. Modern SwiftUI alternatives include:
- `.task { while !Task.isCancelled { ... try? await Task.sleep(...) } }` for recurring timers
- `TimelineView(.periodic(from:, by:))` for UI-driven periodic updates
**Recommended Fix:** Replace `Timer.publish` with a `.task`-based loop using `Task.sleep`. Replace `onReceive(NotificationCenter.default.publisher(...))` with `.task` + `NotificationCenter.default.notifications(named:)` async sequence.
---
## Low Findings
### L-1: `@unchecked Sendable` on `WatchConnectivityReceiver`
**File:** `apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift`, line 21
**Current Code:**
```swift
final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { ... }
```
**Issue:** `@unchecked Sendable` bypasses the compiler's sendability checks. The class holds a `WCSession?` and `WatchInboxStore` reference. Since `WatchInboxStore` is `@MainActor @Observable`, the receiver should ideally be restructured to use actor isolation or be marked `@MainActor`.
**Recommended Fix:** Consider making `WatchConnectivityReceiver` `@MainActor` or using an actor to protect shared state. The `WCSessionDelegate` methods dispatch to `@MainActor` already.
### L-2: `@unchecked Sendable` on `ScreenRecordService`
**File:** `apps/ios/Sources/Screen/ScreenRecordService.swift`
**Current Code:** Uses `@unchecked Sendable` with manual `NSLock`-based `CaptureState` synchronization.
**Issue:** Manual lock-based synchronization is error-prone. An actor would provide compiler-verified thread safety.
**Recommended Fix:** Consider converting `ScreenRecordService` to an actor, or at minimum replace `NSLock` with `OSAllocatedUnfairLock` for consistency with other parts of the codebase (e.g., `TCPProbe.swift`).
### L-3: `NSLock` Usage in `AudioBufferQueue`
**File:** `apps/ios/Sources/Voice/VoiceWakeManager.swift`, lines 15-38
**Current Code:**
```swift
private final class AudioBufferQueue: @unchecked Sendable {
private let lock = NSLock()
// ...
}
```
**Issue:** `NSLock` is a valid synchronization primitive but `OSAllocatedUnfairLock` (iOS 16+) is more efficient and is already used elsewhere in the codebase.
**Recommended Fix:** Replace `NSLock` with `OSAllocatedUnfairLock` for consistency and performance. Note: this class is intentionally `@unchecked Sendable` because it runs on a realtime audio thread where actor isolation is not appropriate -- the manual lock pattern is correct here; just the lock type could be modernized.
### L-4: `DateFormatter` Usage Instead of `.formatted()`
**File:** `apps/ios/Sources/Gateway/GatewayDiscoveryDebugLogView.swift`, lines 49-67
**Current Code:**
```swift
private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter
}()
```
**Issue:** Since iOS 15, Swift provides `Date.formatted()` with `FormatStyle` which is more type-safe and concise. The `WatchInboxView.swift` already uses the modern pattern: `updatedAt.formatted(date: .omitted, time: .shortened)`.
**Recommended Fix:** Replace `DateFormatter` with `Date.formatted(.dateTime.hour().minute().second())` for the time format and `Date.ISO8601FormatStyle` for ISO formatting.
### L-5: `UIScreen.main.bounds` Usage
**File:** `apps/ios/ShareExtension/ShareViewController.swift`, line 31
**Current Code:**
```swift
self.preferredContentSize = CGSize(width: UIScreen.main.bounds.width, height: 420)
```
**Issue:** `UIScreen.main` is deprecated in iOS 16. In an extension context, `view.window?.windowScene?.screen` may not be available at `viewDidLoad` time, so the deprecation is harder to address here.
**Recommended Fix:** Since this is a share extension with limited lifecycle, this is acceptable. If refactoring, consider using trait collection or a fixed width, since the system manages extension sizing.
### L-6: String-Based `NSSortDescriptor` Key Path
**File:** `apps/ios/Sources/Media/PhotoLibraryService.swift`
**Current Code:**
```swift
NSSortDescriptor(key: "creationDate", ascending: false)
```
**Issue:** String-based key paths are not type-safe. While Photos framework requires `NSSortDescriptor`, this is a known limitation of the framework.
**Recommended Fix:** No change needed. The Photos framework API requires `NSSortDescriptor` with string keys.
---
## Positive Findings (Already Modern)
The following modern patterns are already correctly adopted throughout the codebase:
| Pattern | Status | Files |
|---------|--------|-------|
| `@Observable` (Observation framework) | Adopted | `NodeAppModel`, `GatewayConnectionController`, `GatewayDiscoveryModel`, `ScreenController`, `VoiceWakeManager`, `TalkModeManager`, `WatchInboxStore` |
| `@Environment(ModelType.self)` | Adopted | All views consistently use this pattern |
| `@Bindable` for two-way bindings | Adopted | `WatchInboxView`, various settings views |
| `NavigationStack` (not `NavigationView`) | Adopted | All navigation uses `NavigationStack` |
| Modern `onChange(of:) { _, newValue in }` | Adopted | All `onChange` modifiers use the two-parameter variant |
| `NWBrowser` (Network framework) | Adopted | `GatewayDiscoveryModel` for Bonjour discovery |
| `NWPathMonitor` (Network framework) | Adopted | `NetworkStatusService` |
| `DataScannerViewController` (VisionKit) | Adopted | `QRScannerView` for QR code scanning |
| `PhotosPicker` (PhotosUI) | Adopted | `OnboardingWizardView` |
| `OSAllocatedUnfairLock` | Adopted | `TCPProbe` |
| Swift 6 strict concurrency | Adopted | Project-wide `SWIFT_STRICT_CONCURRENCY: complete` |
| `actor` isolation | Adopted | `CameraController` uses `actor` |
| `@ObservationIgnored` | Adopted | `NodeAppModel` for non-tracked properties |
| `OSLog` / `Logger` | Adopted | Throughout the codebase |
| `async`/`await` | Adopted | Pervasive throughout the codebase |
| No `ObservableObject` / `@StateObject` | Correct | No legacy `ObservableObject` usage found |
---
## Prioritized Action Plan
### Phase 1: Critical (Immediate)
1. **Migrate `NetService` to Network framework** (C-1) -- `GatewayServiceResolver` and `GatewayConnectionController` Bonjour resolution
### Phase 2: High (Next Sprint)
2. **Remove dead `#available` checks** (H-1, H-2) -- `OpenClawApp.swift`, `CameraController.swift`
3. **Replace `UNUserNotificationCenter` callback wrappers** (H-3) -- `OpenClawApp.swift`
4. **Modernize `NSItemProvider` loading in Share Extension** (H-4) -- `ShareViewController.swift`
### Phase 3: Medium (Planned)
5. **Migrate `CLLocationManager` delegate to `CLLocationUpdate`** (M-1) -- `LocationService.swift`
6. **Replace `DispatchQueue.asyncAfter`** (M-6) -- `GatewayConnectionController.swift`, `ShareViewController.swift`
7. **Replace `objc_sync` with `OSAllocatedUnfairLock`** (M-7) -- `GatewayConnectionController.swift`
8. **Replace Combine `Timer.publish` and `onReceive`** (M-8) -- `OnboardingWizardView.swift`, `VoiceWakeWordsSettingsView.swift`
9. **Replace callback-based `NotificationCenter` observers** (M-5) -- `VoiceWakeManager.swift`
### Phase 4: Low (Opportunistic)
10. **Replace `NSLock` with `OSAllocatedUnfairLock`** (L-3) -- `VoiceWakeManager.swift`
11. **Modernize `DateFormatter` to `FormatStyle`** (L-4) -- `GatewayDiscoveryDebugLogView.swift`
12. **Address `@unchecked Sendable` patterns** (L-1, L-2) -- `WatchConnectivityReceiver`, `ScreenRecordService`
---
## Files Not Requiring Changes
The following files were audited and found to use modern patterns appropriately:
- `apps/ios/Sources/RootView.swift`
- `apps/ios/Sources/RootTabs.swift`
- `apps/ios/Sources/RootCanvas.swift`
- `apps/ios/Sources/Model/NodeAppModel+Canvas.swift`
- `apps/ios/Sources/Model/NodeAppModel+WatchNotifyNormalization.swift`
- `apps/ios/Sources/Chat/ChatSheet.swift`
- `apps/ios/Sources/Chat/IOSGatewayChatTransport.swift`
- `apps/ios/Sources/Voice/VoiceTab.swift`
- `apps/ios/Sources/Voice/VoiceWakePreferences.swift`
- `apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift`
- `apps/ios/Sources/Gateway/GatewaySettingsStore.swift`
- `apps/ios/Sources/Gateway/GatewayHealthMonitor.swift`
- `apps/ios/Sources/Gateway/GatewayConnectConfig.swift`
- `apps/ios/Sources/Gateway/GatewayConnectionIssue.swift`
- `apps/ios/Sources/Gateway/GatewaySetupCode.swift`
- `apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift`
- `apps/ios/Sources/Gateway/GatewayTrustPromptAlert.swift`
- `apps/ios/Sources/Gateway/DeepLinkAgentPromptAlert.swift`
- `apps/ios/Sources/Gateway/KeychainStore.swift`
- `apps/ios/Sources/Screen/ScreenTab.swift`
- `apps/ios/Sources/Screen/ScreenWebView.swift`
- `apps/ios/Sources/Onboarding/GatewayOnboardingView.swift`
- `apps/ios/Sources/Onboarding/OnboardingStateStore.swift`
- `apps/ios/Sources/Status/StatusPill.swift`
- `apps/ios/Sources/Status/StatusGlassCard.swift`
- `apps/ios/Sources/Status/StatusActivityBuilder.swift`
- `apps/ios/Sources/Status/GatewayStatusBuilder.swift`
- `apps/ios/Sources/Status/GatewayActionsDialog.swift`
- `apps/ios/Sources/Status/VoiceWakeToast.swift`
- `apps/ios/Sources/Device/DeviceInfoHelper.swift`
- `apps/ios/Sources/Device/DeviceStatusService.swift`
- `apps/ios/Sources/Device/NetworkStatusService.swift`
- `apps/ios/Sources/Device/NodeDisplayName.swift`
- `apps/ios/Sources/Services/NodeServiceProtocols.swift`
- `apps/ios/Sources/Services/WatchMessagingService.swift`
- `apps/ios/Sources/Services/NotificationService.swift`
- `apps/ios/Sources/Settings/SettingsNetworkingHelpers.swift`
- `apps/ios/Sources/Capabilities/NodeCapabilityRouter.swift`
- `apps/ios/Sources/SessionKey.swift`
- `apps/ios/Sources/Calendar/CalendarService.swift`
- `apps/ios/Sources/Contacts/ContactsService.swift`
- `apps/ios/Sources/EventKit/EventKitAuthorization.swift`
- `apps/ios/Sources/Location/SignificantLocationMonitor.swift`
- `apps/ios/WatchExtension/Sources/OpenClawWatchApp.swift`
- `apps/ios/WatchExtension/Sources/WatchInboxStore.swift`
- `apps/ios/WatchExtension/Sources/WatchInboxView.swift`
- `apps/ios/WatchApp/` (asset catalog only)

View File

@@ -0,0 +1,324 @@
# iOS App Architecture, Code Quality & Test Coverage Audit
**Date:** 2026-03-02
**Scope:** `apps/ios/Sources/` (63 files, 16,244 LOC) and `apps/ios/Tests/` (25 files, 1,884 LOC)
---
## Architecture Overview
```
OpenClawApp (@main)
|
+----------------+----------------+
| |
NodeAppModel GatewayConnectionController
(@Observable) (@Observable)
[God Object] [Discovery + Connect]
| |
+-----------+-----------+ GatewayDiscoveryModel
| | | | | GatewaySettingsStore
| | | | | GatewayHealthMonitor
| | | | |
v v v v v
Screen Voice Camera Services Gateway Sessions
Ctrl Wake Ctrl (proto) (node + operator)
Talk
Mode
UI Layer (SwiftUI):
RootCanvas -> ScreenWebView + StatusPill + Overlays
RootTabs -> ScreenTab, VoiceTab, SettingsTab
Onboarding -> OnboardingWizardView, QRScannerView
Chat -> ChatSheet (wraps OpenClawChatUI package)
Service Layer (protocols in NodeServiceProtocols.swift):
CameraServicing, ScreenRecordingServicing, LocationServicing,
DeviceStatusServicing, PhotosServicing, ContactsServicing,
CalendarServicing, RemindersServicing, MotionServicing,
WatchMessagingServicing
Routing: NodeCapabilityRouter (command -> handler dictionary)
Shared Packages: OpenClawKit, OpenClawChatUI, OpenClawProtocol, SwabbleKit
```
---
## Findings by Severity
### CRITICAL
#### C1. NodeAppModel is a God Object (2,787 LOC)
- **File:** `Sources/Model/NodeAppModel.swift`
- **Lines:** 1-2787 (entire file)
- **Description:** NodeAppModel concentrates ~17 distinct responsibilities in a single 2,787-line class:
1. Gateway WebSocket lifecycle (two sessions: node + operator)
2. Gateway reconnect state machine with exponential backoff
3. Background task management (grace periods, leases, suppression)
4. Deep link handling and agent prompt routing
5. Voice wake coordination (suspend/resume around other audio)
6. Talk mode coordination
7. Camera HUD state management
8. Screen recording state
9. Canvas/A2UI invoke handling (present, hide, navigate, evalJS, snapshot, push, reset)
10. Camera invoke handling (list, snap, clip)
11. Location invoke handling
12. Device/Photos/Contacts/Calendar/Reminders/Motion invoke handling
13. Watch messaging and notification mirroring
14. Push notification (APNs) token management
15. Share extension relay configuration
16. Branding/config refresh from gateway
17. Session key management and agent selection
- **Impact:** Extremely difficult to test in isolation, reason about, or modify safely. The file already has `// swiftlint:disable type_body_length file_length` which indicates a known but unaddressed problem.
- **Recommendation:** Extract at least these into separate types:
- `GatewayConnectionLoop` (reconnect state machine, background lease management)
- `NodeInvokeDispatcher` (all `handleXxxInvoke` methods, currently ~600 LOC)
- `VoiceAudioCoordinator` (voice wake + talk mode suspend/resume logic)
- `BackgroundLifecycleManager` (grace periods, suppression, leases)
- `DeepLinkHandler` (agent prompt, deep link parsing/routing)
- `PushNotificationManager` (APNs token, notification authorization)
#### C2. TalkModeManager is a God Object (2,153 LOC)
- **File:** `Sources/Voice/TalkModeManager.swift`
- **Lines:** 1-2153 (entire file, saved to disk due to size)
- **Description:** TalkModeManager contains speech recognition, audio playback, gateway communication, provider API key management, push-to-talk state machine, and TTS. The file header acknowledges this: "This file intentionally centralizes talk mode state + behavior. It's large, and splitting would force `private` -> `fileprivate` across many members."
- **Impact:** The `private` -> `fileprivate` concern is valid but solvable with extensions in the same file or a dedicated module with `internal` access.
- **Recommendation:** Extract `TalkAudioPlayer`, `TalkSpeechRecognitionEngine`, `TalkConfigLoader`, `TalkPTTStateMachine` into separate files.
---
### HIGH
#### H1. GatewayConnectionController is oversized (1,058 LOC)
- **File:** `Sources/Gateway/GatewayConnectionController.swift`
- **Lines:** 1-1058
- **Description:** Exceeds the 500 LOC guideline. Mixes discovery coordination, TLS fingerprint verification, Bonjour service resolution, loopback IP detection, URL building, capability/command/permission registration, and auto-connect logic.
- **Recommendation:** Extract `GatewayTLSVerifier`, `LoopbackHostDetector` (static utility), and `GatewayCapabilityRegistrar` (caps/commands/permissions).
#### H2. SettingsTab is oversized (1,032 LOC)
- **File:** `Sources/Settings/SettingsTab.swift`
- **Lines:** 1-1032
- **Description:** A single monolithic SwiftUI view with ~30 `@AppStorage` properties and multiple nested sections.
- **Recommendation:** Extract section views: `GatewaySettingsSection`, `VoiceSettingsSection`, `DeviceSettingsSection`, `AdvancedSettingsSection`.
#### H3. OnboardingWizardView is oversized (884 LOC)
- **File:** `Sources/Onboarding/OnboardingWizardView.swift`
- **Lines:** 1-884
- **Description:** Multi-step wizard with QR scanning, manual connection, photo picker, and pairing logic all in one view.
- **Recommendation:** Extract per-step views: `OnboardingWelcomeStep`, `OnboardingConnectStep`, `OnboardingAuthStep`.
#### H4. Heavy UserDefaults coupling (no abstraction layer)
- **Files:** `NodeAppModel.swift`, `GatewayConnectionController.swift`, `SettingsTab.swift`, `GatewaySettingsStore.swift`, `RootCanvas.swift`
- **Description:** `UserDefaults.standard` is accessed directly throughout the codebase (~70+ direct reads/writes with raw string keys). There is no typed key registry or wrapper, so:
- Key typos compile silently
- Default values are duplicated (e.g., `"camera.enabled"` checked with fallback `true` in two places)
- Testing requires the `withUserDefaults` helper which mutates the shared `UserDefaults.standard`
- **Recommendation:** Create a `Settings` enum with typed keys (similar to `VoiceWakePreferences`) and use dependency injection for `UserDefaults`.
#### H5. Significant test coverage gaps for critical paths
- **Description:** Several critical modules have zero test coverage. See the Test Coverage Gap Analysis table below.
- **Impact:** Changes to gateway connection lifecycle, background task management, voice/talk coordination, and canvas interaction cannot be regression-tested.
---
### MEDIUM
#### M1. Inconsistent module boundary patterns
- **Description:** Some modules use proper protocol-based DI (camera, screen recording, location, device status, photos, contacts, calendar, reminders, motion, watch messaging via `NodeServiceProtocols.swift`), while others use concrete types directly:
- `VoiceWakeManager` and `TalkModeManager` are concrete, not protocol-backed
- `GatewayHealthMonitor` is concrete (but has testable init with sleep injection)
- `ScreenController` is concrete with no protocol
- `NotificationCentering` protocol exists but is ad hoc (not in `NodeServiceProtocols.swift`)
- **Recommendation:** Add protocols for `VoiceWakeServicing`, `TalkModeServicing`, `ScreenControlling` to enable test doubles.
#### M2. Closure-based wiring instead of protocol conformance
- **Files:** `NodeAppModel.swift:178-216`, `ScreenController.swift:14-18`
- **Description:** `ScreenController.onDeepLink` and `ScreenController.onA2UIAction` are closure properties rather than delegate protocols. Similarly, `VoiceWakeManager.configure(onCommand:)` uses a closure. This makes the dependency graph harder to trace.
- **Recommendation:** Consider delegate protocols for clearer contracts, or at minimum document the callback contracts.
#### M3. OpenClawApp.swift mixes concerns (541 LOC)
- **File:** `Sources/OpenClawApp.swift`
- **Lines:** 1-541
- **Description:** Contains three distinct concerns in one file:
1. `OpenClawAppDelegate` (push notifications, background tasks)
2. `WatchPromptNotificationBridge` (notification category management, 200+ LOC)
3. `OpenClawApp` (SwiftUI app entry point)
- **Recommendation:** Extract `WatchPromptNotificationBridge` to its own file.
#### M4. GatewayDiagnostics embedded in GatewaySettingsStore file
- **File:** `Sources/Gateway/GatewaySettingsStore.swift:352-448`
- **Description:** `GatewayDiagnostics` enum (file-based logging) is defined at the bottom of `GatewaySettingsStore.swift` with no relation to settings storage.
- **Recommendation:** Move to its own file `Gateway/GatewayDiagnostics.swift`.
#### M5. Duplicate code patterns in invoke handlers
- **File:** `Sources/Model/NodeAppModel.swift:1213-1358`
- **Description:** Every `handleXxxInvoke` method follows the same pattern: decode params -> call service -> encode payload -> return response. The 12 invoke handlers repeat this boilerplate with minor variations. The `default:` case error response is duplicated 9 times verbatim.
- **Recommendation:** Create a generic `invokeServiceMethod<P: Decodable, R: Encodable>` helper that handles the decode-call-encode-response cycle.
#### M6. No formal error domain or error catalog
- **Description:** Errors are constructed ad hoc using `NSError(domain:code:userInfo:)` with inconsistent domains ("Screen", "Gateway", "Camera", "NodeAppModel", "GatewayHealthMonitor", "VoiceWake") and magic number codes. Only `CameraController.CameraError` uses a proper Swift error enum.
- **Recommendation:** Define a unified `OpenClawIOSError` enum with cases for each domain, or at minimum use consistent error domains and documented code ranges.
#### M7. Two gateway sessions managed in parallel without shared state machine
- **File:** `Sources/Model/NodeAppModel.swift:96-98`
- **Description:** `nodeGateway` and `operatorGateway` are two independent `GatewayNodeSession` instances with separate reconnect loops. Their connected states (`gatewayConnected`, `operatorConnected`) are tracked independently, but the UI only shows one "gateway status". Disconnect/reconnect of one does not coordinate with the other.
- **Recommendation:** Extract a `DualGatewaySessionManager` that manages both sessions' lifecycles as a coordinated unit.
---
### LOW
#### L1. `RootView.swift` is a trivial wrapper (7 LOC)
- **File:** `Sources/RootView.swift`
- **Description:** Contains only `struct RootView: View { var body: some View { RootCanvas() } }`. This adds an unnecessary layer of indirection.
- **Recommendation:** Remove and use `RootCanvas` directly, or document why the indirection exists.
#### L2. Access control could be tighter
- **Description:** Many types use default `internal` access where `private` or `fileprivate` would be more appropriate. For example:
- `NodeAppModel.gatewayStatusText`, `nodeStatusText`, `operatorStatusText` are `var` (settable) from outside
- `GatewayDiscoveryModel.gateways` is `var` (not `private(set)`)
- `VoiceWakeManager.isEnabled`, `isListening` are publicly settable
- **Recommendation:** Prefer `private(set)` for observable properties that should only be modified internally.
#### L3. `#if DEBUG` test hooks pattern
- **Files:** `GatewayConnectionController.swift:929-989`, `VoiceWakeManager.swift:477-483`, `NodeAppModel.swift` (via `_test_` prefixed methods)
- **Description:** Test hooks are exposed via `#if DEBUG` extensions with `_test_` prefixes. While functional, this pollutes the type's API surface.
- **Recommendation:** This is a reasonable pattern for host-app tests. Consider using `@_spi(Testing)` when available in Swift 6 for cleaner separation.
#### L4. Naming inconsistency: `ThrowingContinuationSupport`
- **File:** `Sources/OpenClawApp.swift:459`
- **Description:** References `ThrowingContinuationSupport.resumeVoid` which appears to be defined in OpenClawKit. The name is verbose; a simple extension on `CheckedContinuation` would be more idiomatic.
#### L5. `GatewayTLSFingerprintProbe` uses `objc_sync_enter/exit` instead of a lock
- **File:** `Sources/Gateway/GatewayConnectionController.swift:1039-1040`
- **Description:** `objc_sync_enter(self)` / `objc_sync_exit(self)` is an Objective-C runtime synchronization primitive. Modern Swift code should use `NSLock`, `os_unfair_lock`, or `Mutex` (Swift 6).
- **Recommendation:** Replace with `NSLock` or `Mutex` for consistency with other lock usage (e.g., `NotificationInvokeLatch` uses `NSLock`).
---
## Test Coverage Gap Analysis
| Source File | LOC | Test File | Test LOC | Coverage |
|---|---|---|---|---|
| `Model/NodeAppModel.swift` | 2787 | `NodeAppModelInvokeTests.swift` | 478 | **Partial** - invoke dispatch only; no tests for reconnect, background, deep links |
| `Voice/TalkModeManager.swift` | 2153 | `TalkModeConfigParsingTests.swift` | 31 | **Minimal** - config parsing only; no PTT, speech, or playback tests |
| `Gateway/GatewayConnectionController.swift` | 1058 | `GatewayConnectionControllerTests.swift` + `GatewayConnectionSecurityTests.swift` | 226 | **Partial** - security + basic flow; no TLS probe, Bonjour resolve, or autoconnect tests |
| `Settings/SettingsTab.swift` | 1032 | `SwiftUIRenderSmokeTests.swift` (1 test) | ~8 | **Smoke only** - verifies view hierarchy builds |
| `Onboarding/OnboardingWizardView.swift` | 884 | None | 0 | **None** |
| `OpenClawApp.swift` | 541 | None | 0 | **None** - WatchPromptNotificationBridge untested |
| `Voice/VoiceWakeManager.swift` | 483 | `VoiceWakeManagerStateTests.swift` + `VoiceWakeManagerExtractCommandTests.swift` | 144 | **Good** - state transitions + command extraction |
| `Gateway/GatewaySettingsStore.swift` | 448 | `GatewaySettingsStoreTests.swift` | 197 | **Good** |
| `RootCanvas.swift` | 429 | `SwiftUIRenderSmokeTests.swift` | ~8 | **Smoke only** |
| `Onboarding/GatewayOnboardingView.swift` | 371 | None | 0 | **None** |
| `Screen/ScreenRecordService.swift` | 350 | `ScreenRecordServiceTests.swift` | 32 | **Minimal** |
| `Camera/CameraController.swift` | 339 | `CameraControllerClampTests.swift` + `CameraControllerErrorTests.swift` | 38 | **Minimal** - clamp/error only; no capture flow tests |
| `Services/WatchMessagingService.swift` | 284 | None (mock in NodeAppModelInvokeTests) | 0 | **None** |
| `Screen/ScreenController.swift` | 267 | `ScreenControllerTests.swift` | 87 | **Good** |
| `Contacts/ContactsService.swift` | 210 | None | 0 | **None** |
| `Screen/ScreenWebView.swift` | 193 | None | 0 | **None** |
| `Gateway/GatewayDiscoveryModel.swift` | 181 | `GatewayDiscoveryModelTests.swift` | 22 | **Minimal** |
| `Location/LocationService.swift` | 177 | None | 0 | **None** |
| `Media/PhotoLibraryService.swift` | 164 | None | 0 | **None** |
| `Chat/IOSGatewayChatTransport.swift` | 142 | `IOSGatewayChatTransportTests.swift` | 30 | **Minimal** |
| `Calendar/CalendarService.swift` | 135 | None | 0 | **None** |
| `Reminders/RemindersService.swift` | 133 | None | 0 | **None** |
| `Motion/MotionService.swift` | 100 | None | 0 | **None** |
| `Model/NodeAppModel+WatchNotifyNormalization.swift` | 103 | `VoiceWakeGatewaySyncTests.swift` (partial) | 22 | **Minimal** |
| `Model/NodeAppModel+Canvas.swift` | 59 | None | 0 | **None** |
| `Gateway/GatewayHealthMonitor.swift` | 85 | None | 0 | **None** |
| `Gateway/KeychainStore.swift` | 48 | `KeychainStoreTests.swift` | 22 | **Minimal** |
| `Onboarding/OnboardingStateStore.swift` | 52 | `OnboardingStateStoreTests.swift` | 57 | **Good** |
| `Gateway/GatewayConnectionIssue.swift` | 71 | `GatewayConnectionIssueTests.swift` | 33 | **Good** |
| `SessionKey.swift` | 23 | Tested via `NodeAppModelInvokeTests` | - | **Good** (indirectly) |
| `Settings/SettingsNetworkingHelpers.swift` | 40 | `SettingsNetworkingHelpersTests.swift` | 50 | **Good** |
| `Voice/VoiceWakePreferences.swift` | 44 | `VoiceWakePreferencesTests.swift` | 38 | **Good** |
| `Device/NodeDisplayName.swift` | 48 | Tested via GatewayConnectionControllerTests | - | **Partial** |
### Coverage Summary
- **63 source files**, **25 test files** (24 test + 1 helper)
- **17 source modules with zero test coverage** (service implementations, onboarding views, several gateway files)
- **Test LOC ratio:** 1,884 / 16,244 = **11.6%** (low for a production app)
- **Test framework:** Swift Testing (`@Test`, `#expect`) -- modern and correct
- **Test patterns:** Good use of mocks (MockWatchMessagingService), `withUserDefaults` helper for isolation, `_test_` hooks for internal access. SwiftUI render smoke tests validate view hierarchy construction.
### Critical Untested Paths
1. **Gateway reconnect state machine** - the most complex logic in the app (background lease, pairing pause, backoff) has zero tests
2. **Background lifecycle management** - grace periods, suppression, wake handling untested
3. **Onboarding flow** - 1,255 LOC across 3 files with zero tests
4. **Push notification handling** - APNs registration, silent push, background refresh untested
5. **TalkModeManager** - 2,153 LOC with only 31 LOC of config parsing tests
---
## Dependency Injection Assessment
### Well-Injected (protocol-based, testable)
All services in `NodeServiceProtocols.swift` are protocol-based with default production implementations:
- `CameraServicing` -> `CameraController`
- `ScreenRecordingServicing` -> `ScreenRecordService`
- `LocationServicing` -> `LocationService`
- `DeviceStatusServicing` -> `DeviceStatusService`
- `PhotosServicing` -> `PhotoLibraryService`
- `ContactsServicing` -> `ContactsService`
- `CalendarServicing` -> `CalendarService`
- `RemindersServicing` -> `RemindersService`
- `MotionServicing` -> `MotionService`
- `WatchMessagingServicing` -> `WatchMessagingService`
- `NotificationCentering` -> `LiveNotificationCenter`
`NodeAppModel.init()` accepts all of these via parameters with defaults -- excellent DI pattern.
### Not Injected (hardcoded dependencies)
- `GatewaySettingsStore` - static enum, not injectable. Tests must use real `UserDefaults`/Keychain.
- `GatewayDiagnostics` - static enum with file I/O, not injectable.
- `GatewayDiscoveryModel` - concrete class created inside `GatewayConnectionController.init`.
- `GatewayHealthMonitor` - created internally by `NodeAppModel` (but has testable init).
- `VoiceWakeManager` - created internally, injected into SwiftUI environment.
- `TalkModeManager` - injected via `NodeAppModel.init` parameter (good).
- `ScreenController` - injected via `NodeAppModel.init` parameter (good).
---
## Data Flow Patterns
### Observation Framework Usage
The app uses Swift's `Observation` framework (`@Observable`) consistently:
- `NodeAppModel`, `GatewayConnectionController`, `GatewayDiscoveryModel`, `VoiceWakeManager`, `TalkModeManager`, `ScreenController` are all `@Observable`.
- SwiftUI views access them via `@Environment(Type.self)`.
- No legacy `ObservableObject` / `@StateObject` patterns found -- this is correct per CLAUDE.md guidance.
### Environment Propagation
```
OpenClawApp
|-- @State NodeAppModel -> .environment(appModel)
|-- @State GatewayConnectionController -> .environment(gatewayController)
|-- appModel.voiceWake (VoiceWakeManager) -> .environment(appModel.voiceWake)
```
This is clean, though `voiceWake` being both a property of `NodeAppModel` AND injected separately into the environment creates a potential consistency issue if they ever diverge.
---
## Architectural Strengths
1. **Strong protocol-based DI for services** - `NodeServiceProtocols.swift` defines clean interfaces for all device capabilities, enabling easy mocking in tests.
2. **Modern Swift 6 / Observation adoption** - No legacy `ObservableObject` patterns; strict concurrency enabled.
3. **NodeCapabilityRouter** - Clean command-routing pattern that decouples command registration from handling.
4. **Dual gateway session architecture** - Separating node (device capabilities) from operator (chat/config) connections is architecturally sound.
5. **GatewayConnectConfig** - Single source of truth struct for connection parameters.
6. **Consistent input validation** - Nearly every string input is trimmed and empty-checked.
7. **Keychain-based credential storage** - Sensitive data (tokens, passwords) stored in Keychain, not UserDefaults.
8. **`CameraController` uses actor isolation** - Correct concurrency pattern for hardware resource.
---
## Recommended Refactoring Priority
1. **[CRITICAL]** Split `NodeAppModel` into 5-6 focused types (highest ROI for testability)
2. **[CRITICAL]** Split `TalkModeManager` into 3-4 focused types
3. **[HIGH]** Add tests for gateway reconnect state machine
4. **[HIGH]** Add tests for background lifecycle management
5. **[HIGH]** Extract `SettingsTab` into section views
6. **[MEDIUM]** Create typed `UserDefaults` key registry
7. **[MEDIUM]** Unify error handling with a proper error catalog
8. **[MEDIUM]** Extract duplicate invoke handler boilerplate

View File

@@ -0,0 +1,399 @@
# Swift 6 Concurrency Audit: OpenClaw iOS App
**Scope:** `apps/ios/Sources/` (63 files, ~15K LOC)
**Date:** 2026-03-02
**Auditor:** Concurrency Auditor Agent
---
## Summary Statistics
| Metric | Count |
|--------|-------|
| Files audited | 63 |
| `@MainActor` classes | 8 |
| `actor` types | 1 (`CameraController`) |
| `@unchecked Sendable` types | 9 |
| `@preconcurrency` imports | 2 (`UserNotifications`, `WatchConnectivity`) |
| `@preconcurrency` conformances | 2 (`UNUserNotificationCenterDelegate`, `NetServiceDelegate`) |
| `nonisolated(unsafe)` usages | 1 |
| `NSLock` usages | 6 |
| `DispatchQueue` usages | 7 |
| `objc_sync_enter/exit` usages | 1 |
| `CheckedContinuation` usages | ~25 |
| `@Observable` (Observation framework) types | 6 |
| `ObservableObject` types | 0 |
### Overall Assessment
The codebase is in **good shape for Swift 6 strict concurrency**. The major model types use `@MainActor` + `@Observable` (Observation framework), there are zero `ObservableObject` usages, and the actor model is applied consistently. There are no `@Sendable` annotations missing on closure parameters in any obvious way, and the use of `@unchecked Sendable` is confined to genuine low-level synchronization wrappers. However, there are several areas that warrant attention.
---
## Critical Findings
### C-1: `GatewayTLSFingerprintProbe` uses `objc_sync_enter` + `@unchecked Sendable` with unsynchronized `didFinish` read
**File:** `Gateway/GatewayConnectionController.swift:992-1058`
**Severity:** Critical (potential data race)
```swift
private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @unchecked Sendable {
private var didFinish = false // line 996
private var session: URLSession? // line 997
private var task: URLSessionWebSocketTask? // line 998
...
private func finish(_ fingerprint: String?) {
objc_sync_enter(self) // line 1039
defer { objc_sync_exit(self) }
guard !self.didFinish else { return }
...
}
}
```
**Issue:** The `start()` method (line 1006) reads and writes `self.session` and `self.task` without any lock. The `DispatchQueue.global().asyncAfter` timeout on line 1016 calls `finish()` from a background queue while `start()` sets properties on the caller's thread. Additionally, `URLSession` delegate callbacks arrive on an arbitrary delegate queue (nil was passed for `delegateQueue`), which means `urlSession(_:didReceive:completionHandler:)` and `finish()` can race.
**Recommendation:** Replace `objc_sync_enter/exit` with `NSLock` or `OSAllocatedUnfairLock`. Ensure all mutable state (`didFinish`, `session`, `task`) is accessed under the lock. Better yet, convert to an `actor` since this is a short-lived async operation. Alternatively, use `OSAllocatedUnfairLock<State>` wrapping a struct.
---
### C-2: `PhotoCaptureDelegate` and `MovieFileDelegate` lack synchronization on `didResume`
**File:** `Camera/CameraController.swift:260-339`
**Severity:** Critical (potential double continuation resume)
```swift
private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
private let continuation: CheckedContinuation<Data, Error>
private var didResume = false // NOT thread-safe
func photoOutput(...) {
guard !self.didResume else { return } // line 273
self.didResume = true
...
}
func photoOutput(...didFinishCaptureFor...) {
guard let error else { return }
guard !self.didResume else { return } // line 303
self.didResume = true
...
}
}
```
**Issue:** `AVCapturePhotoCaptureDelegate` callbacks can arrive on different queues. The `didResume` flag is a plain `Bool` with no synchronization. If `didFinishProcessingPhoto` and `didFinishCaptureFor` are called concurrently (possible under certain error conditions), both could read `didResume` as `false` and resume the continuation twice, which is a fatal crash in debug builds and undefined behavior in release.
**Recommendation:** Protect `didResume` with `OSAllocatedUnfairLock<Bool>` or `NSLock`. The same issue applies to `MovieFileDelegate` on line 309.
---
### C-3: `GatewayDiagnostics.logWritesSinceCheck` is `nonisolated(unsafe)` static var
**File:** `Gateway/GatewaySettingsStore.swift:358`
**Severity:** Critical (data race)
```swift
nonisolated(unsafe) private static var logWritesSinceCheck = 0
```
**Issue:** This counter is read and written inside `queue.async {}` blocks on `GatewayDiagnostics.queue`, but `nonisolated(unsafe)` tells the compiler to skip checking. The access is actually serialized by the private `DispatchQueue`, so it is functionally safe -- however, `nonisolated(unsafe)` is a red flag for Swift 6 audits because it permanently suppresses the compiler's data-race safety checks.
**Recommendation:** Replace with proper synchronization visible to the compiler. Either:
1. Make it a local variable inside the `DispatchQueue` closure scope, or
2. Wrap in `OSAllocatedUnfairLock<Int>` or a dedicated `actor`, or
3. Since all accesses are on `GatewayDiagnostics.queue`, convert to a `@Sendable`-safe pattern that doesn't require `nonisolated(unsafe)`.
---
## High Findings
### H-1: `ScreenRecordService` is `@unchecked Sendable` but holds no state -- its inner `CaptureState` synchronizes via NSLock but `UncheckedSendableBox` silences Sendable checks
**File:** `Screen/ScreenRecordService.swift:4-11`
**Severity:** High
```swift
final class ScreenRecordService: @unchecked Sendable {
private struct UncheckedSendableBox<T>: @unchecked Sendable {
let value: T
}
```
**Issue:** `UncheckedSendableBox` wraps **any** `T` (including non-Sendable types like `CMSampleBuffer`) and marks it `@unchecked Sendable`. This is used to pass `CMSampleBuffer` across threads in the capture handler. While `CMSampleBuffer` is effectively thread-safe for read-only access, this pattern silences the compiler completely and could mask future issues if the box is used for other types.
**Recommendation:** Use `nonisolated(unsafe) let value: T` instead if on Swift 6.2+, or document the specific thread-safety invariant. Consider constraining `T: Sendable` on the generic and handling `CMSampleBuffer` separately with a targeted unsafe annotation.
### H-2: `WatchMessagingService` is `@unchecked Sendable` with mutable `replyHandler` protected only by NSLock
**File:** `Services/WatchMessagingService.swift:23-28`
**Severity:** High
```swift
final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked Sendable {
private let replyHandlerLock = NSLock()
private var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
```
**Issue:** While the `replyHandler` is properly protected by `NSLock`, the `session` property (`WCSession?`) is accessed from both the main thread (via delegate callbacks forwarded with `@preconcurrency`) and potentially from WatchConnectivity's internal threads. The `WCSession` properties like `isPaired`, `isWatchAppInstalled`, `isReachable` are read in `status(for:)` without synchronization and could race with delegate callbacks.
**Recommendation:** Convert to an `actor` or ensure all `WCSession` property reads happen on a specific isolation context. The lock properly protects `replyHandler`, so this is a moderate risk.
### H-3: `LocationService` stores `CheckedContinuation` as instance vars without synchronization between `nonisolated` delegate callbacks and `@MainActor` methods
**File:** `Location/LocationService.swift:13-14, 136-176`
**Severity:** High
```swift
@MainActor
final class LocationService: NSObject, CLLocationManagerDelegate, LocationServiceCommon {
private var authContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
private var locationContinuation: CheckedContinuation<CLLocation, Swift.Error>?
```
The delegate methods are marked `nonisolated`:
```swift
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
Task { @MainActor in
if let cont = self.authContinuation { ... }
}
}
```
**Issue:** The `nonisolated` delegate methods create `Task { @MainActor in }` to hop back to the main actor before accessing continuations. This is the correct pattern. However, there is a subtle race: if two delegate callbacks arrive in rapid succession, both could queue `@MainActor` tasks, and the second one would find the continuation already `nil`. This is handled (the `if let` guards), but the pattern is fragile. More importantly, `CLLocationManager` requires its delegate methods to be called on the queue it was created on. Since the class is `@MainActor`, the manager is created on main, and iOS should deliver delegate callbacks on main -- making the `nonisolated` annotation somewhat misleading.
**Recommendation:** Since `CLLocationManager` delivers callbacks on the thread/queue of the delegate's assigned queue (main in this case), the `nonisolated` annotation is technically unnecessary and may confuse future maintainers. Consider removing `nonisolated` and letting `@MainActor` inheritance apply. This would also let the compiler verify the continuation access is safe.
### H-4: `LiveNotificationCenter` is `@unchecked Sendable` wrapping a non-Sendable `UNUserNotificationCenter`
**File:** `Services/NotificationService.swift:18-58`
**Severity:** High
```swift
struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
private let center: UNUserNotificationCenter
```
**Issue:** `UNUserNotificationCenter` is not `Sendable`. Wrapping it in `@unchecked Sendable` silences the compiler. In practice, `UNUserNotificationCenter.current()` returns a singleton that is thread-safe, so this is functionally fine -- but the compiler cannot verify this.
**Recommendation:** This is acceptable given `UNUserNotificationCenter.current()` is a thread-safe singleton. Document the invariant with a comment explaining why `@unchecked Sendable` is safe here. Alternatively, access the center via `UNUserNotificationCenter.current()` each time instead of storing it.
### H-5: `NetworkStatusService` is `@unchecked Sendable` but has no mutable state
**File:** `Device/NetworkStatusService.swift:5`
**Severity:** High (misleading annotation)
```swift
final class NetworkStatusService: @unchecked Sendable {
```
**Issue:** `NetworkStatusService` has no stored properties at all. It creates `NWPathMonitor` locally in each method call. The `@unchecked Sendable` is unnecessary because a stateless final class is inherently `Sendable`.
**Recommendation:** Remove `@unchecked` -- just conform to `Sendable` directly. The class has no mutable state and is `final`, so it qualifies for automatic Sendable conformance.
---
## Medium Findings
### M-1: `TalkModeManager` `pttCompletion` continuation stored as instance var could leak
**File:** `Voice/TalkModeManager.swift:43`
**Severity:** Medium
```swift
private var pttCompletion: CheckedContinuation<OpenClawTalkPTTStopPayload, Never>?
```
**Issue:** If `pttCompletion` is set but the manager is deinitialized or the PTT session is interrupted without resuming it, the continuation will leak. `CheckedContinuation` logs a warning in debug builds when it is never resumed, and in production the caller will hang indefinitely.
**Recommendation:** Add a `deinit` or cleanup path that resumes `pttCompletion` with a default/error value. Also verify that all code paths that set `pttCompletion` eventually resume it (including error paths, cancellation, and mode changes).
### M-2: Heavy use of `Task { @MainActor in }` hops in code that is already `@MainActor`
**Files:** Multiple (OpenClawApp.swift:30-47, NodeAppModel.swift:179-207, etc.)
**Severity:** Medium (performance/clarity)
```swift
// In OpenClawAppDelegate which is already @MainActor:
Task { @MainActor in
model.updateAPNsDeviceToken(token)
}
```
**Issue:** When code is already on `@MainActor`, creating `Task { @MainActor in }` is redundant in terms of isolation but does defer execution to the next event loop tick. If the intent is immediate execution, this is a performance anti-pattern. If the intent is deferral, it should be documented.
**Recommendation:** Where immediate execution is intended, call the method directly. Where deferral is intentional, add a comment explaining why. In Swift 6.2 with `nonisolated(nonsending)` defaults, these patterns will behave differently.
### M-3: `GatewayDiscoveryModel` browser callbacks use closures that capture `self` without explicit `@Sendable`
**File:** `Gateway/GatewayDiscoveryModel.swift:60-96`
**Severity:** Medium
```swift
let browser = GatewayDiscoveryBrowserSupport.makeBrowser(
...
onState: { [weak self] state in
guard let self else { return }
self.statesByDomain[domain] = state // MainActor state access
},
onResults: { [weak self] results in
guard let self else { return }
self.gatewaysByDomain[domain] = results.compactMap { ... }
```
**Issue:** These closures capture `self` (a `@MainActor` `@Observable` class) and mutate its state. If `GatewayDiscoveryBrowserSupport.makeBrowser` dispatches these callbacks on a background queue (which NWBrowser does by default), this would be a main-actor isolation violation. The callbacks access `@MainActor`-isolated properties without explicitly hopping to the main actor.
**Recommendation:** Verify that `GatewayDiscoveryBrowserSupport.makeBrowser` dispatches callbacks on the main queue. If not, wrap the callback bodies in `Task { @MainActor in ... }` or `await MainActor.run { ... }`. This is a potential data race if callbacks arrive off-main.
### M-4: `withObservationTracking` + `onChange` pattern in `GatewayConnectionController.observeDiscovery()` could miss updates
**File:** `Gateway/GatewayConnectionController.swift:293-305`
**Severity:** Medium
```swift
private func observeDiscovery() {
withObservationTracking {
_ = self.discovery.gateways
_ = self.discovery.statusText
_ = self.discovery.debugLog
} onChange: { [weak self] in
Task { @MainActor in
guard let self else { return }
self.updateFromDiscovery()
self.observeDiscovery() // re-register
}
}
}
```
**Issue:** The `onChange` handler in `withObservationTracking` fires at most once per registration. The recursive re-registration inside `Task { @MainActor in }` means there is a window between when the `onChange` fires and when the new tracking is registered where changes could be missed. In practice, the `Task` hop is fast, but under heavy load or if the main actor queue is busy, rapid changes to `discovery.gateways` could be dropped.
**Recommendation:** This is a known limitation of `withObservationTracking` outside SwiftUI. Consider using `AsyncStream` or `Combine` publisher from the discovery model instead, which provides continuous observation without re-registration gaps.
### M-5: `GatewayServiceResolver` does not protect `didFinish` flag with a lock
**File:** `Gateway/GatewayServiceResolver.swift:9, 41-47`
**Severity:** Medium
```swift
final class GatewayServiceResolver: NSObject, NetServiceDelegate {
private var didFinish = false
private func finish(result: ...) {
guard !self.didFinish else { return }
self.didFinish = true
...
}
}
```
**Issue:** `NetServiceDelegate` callbacks can theoretically arrive on multiple threads (depending on how the service is scheduled). The `didFinish` flag is not synchronized. If `netServiceDidResolveAddress` and `netService(_:didNotResolve:)` are called concurrently, `finish` could be called twice.
**Recommendation:** Add `NSLock` protection or use `OSAllocatedUnfairLock<Bool>` for `didFinish`. Alternatively, ensure the service is always scheduled on the main run loop (which `BonjourServiceResolverSupport.start` may already do).
### M-6: `ContactsService`, `CalendarService`, `RemindersService`, `MotionService`, `PhotoLibraryService` conform to `Sendable` protocols but are plain classes without actor isolation
**Files:** Various service files
**Severity:** Medium
```swift
final class ContactsService: ContactsServicing { ... }
// ContactsServicing: Sendable
```
**Issue:** These classes have no mutable stored properties and are `final`, which technically makes them safe to mark `Sendable`. However, they don't explicitly declare `Sendable` conformance -- they inherit it through their protocol conformances (`ContactsServicing: Sendable`). The Swift 6 compiler will flag this because a `final class` without explicit `Sendable` or `@unchecked Sendable` conformance cannot implicitly satisfy `Sendable` requirements from protocols unless it is provably safe (no mutable state).
**Recommendation:** Since these classes are stateless and `final`, add explicit `: Sendable` conformance or verify they compile cleanly under strict concurrency.
---
## Low Findings
### L-1: `@preconcurrency import UserNotifications` and `@preconcurrency import WatchConnectivity` suppress Sendable warnings
**Files:** `OpenClawApp.swift:7`, `Services/WatchMessagingService.swift:4`
**Severity:** Low
**Issue:** `@preconcurrency` imports suppress sendability diagnostics for types from those modules. As Apple updates these frameworks for Sendable conformance in newer SDKs, the `@preconcurrency` should be removed to benefit from the compiler's checks.
**Recommendation:** Periodically check if these frameworks have been updated with Sendable annotations in newer Xcode versions and remove `@preconcurrency` when possible.
### L-2: `VoiceWakeManager.makeRecognitionResultHandler()` returns `@Sendable` closure that captures `[weak self]` correctly
**File:** `Voice/VoiceWakeManager.swift:301-313`
**Severity:** Low (informational -- this is well done)
The recognition result handler correctly captures `[weak self]` and hops to `@MainActor` before accessing any state. This is a good pattern.
### L-3: `CameraController` is an `actor` -- exemplary usage
**File:** `Camera/CameraController.swift:5`
**Severity:** Low (informational -- this is well done)
`CameraController` is the only `actor` in the codebase. It properly uses `nonisolated static` for pure functions and `async` for all state-mutating operations. This is a model for how other services could be structured.
### L-4: Several `Task { }` in `@MainActor` context don't explicitly annotate `@MainActor`
**Files:** Multiple
**Severity:** Low
```swift
// Inside @MainActor class:
Task { [weak self] in
guard let self else { return }
_ = await self.connectDiscoveredGateway(target)
}
```
**Issue:** In Swift 6.0, an unstructured `Task { }` created from `@MainActor` context inherits the actor context. However, in Swift 6.2 with `nonisolated(nonsending)` defaults, this behavior may change. Explicitly annotating `Task { @MainActor in }` makes the intent clear and forward-compatible.
**Recommendation:** Add explicit `@MainActor` annotation to `Task { }` blocks in `@MainActor` types where main-actor isolation is required.
### L-5: Consider migrating `NSLock` to `OSAllocatedUnfairLock` for better performance
**Files:** Multiple (6 usages)
**Severity:** Low
`OSAllocatedUnfairLock` (available since iOS 16) is faster than `NSLock` for short critical sections. The existing `NSLock` usages in `AudioBufferQueue`, `NotificationInvokeLatch`, `CaptureState`, etc. are all protecting brief property accesses and would benefit from the switch.
**Recommendation:** Migrate `NSLock` to `OSAllocatedUnfairLock` where deployment target allows (iOS 16+). `TCPProbe.swift` already uses `OSAllocatedUnfairLock` -- apply the same pattern to other files.
### L-6: `NodeAppModel` is very large (~1500+ lines) which makes concurrency reasoning difficult
**File:** `Model/NodeAppModel.swift`
**Severity:** Low (maintainability)
**Issue:** The large file size with many Task/async operations, multiple gateway sessions, and deeply nested closures makes it harder to reason about concurrency invariants. All state is `@MainActor` which is safe, but the complexity makes it harder to verify no accidental non-isolated access exists.
**Recommendation:** Consider splitting into smaller focused files (already noted with `NodeAppModel+Canvas.swift` and `NodeAppModel+WatchNotifyNormalization.swift` extensions). Further decomposition would improve auditability.
---
## Positive Patterns Found
1. **Consistent `@MainActor` + `@Observable` usage**: All major model types (`NodeAppModel`, `GatewayConnectionController`, `GatewayDiscoveryModel`, `TalkModeManager`, `VoiceWakeManager`, `ScreenController`) use the Observation framework with `@MainActor` isolation. Zero `ObservableObject` usages.
2. **Zero `@Sendable` protocol conformance issues**: All service protocols (`CameraServicing`, `LocationServicing`, `DeviceStatusServicing`, etc.) correctly require `Sendable`.
3. **`CameraController` as `actor`**: Properly models concurrent camera access.
4. **`@Sendable` closures in callback APIs**: Callback closures (e.g., `onCommand` in `VoiceWakeManager`, `replyHandler` in `WatchMessagingService`) are properly annotated `@Sendable`.
5. **`CheckedContinuation` usage**: All continuation usages properly handle the single-resume invariant with `didResume`/`finished` flags (though some lack synchronization -- see C-2 and M-5).
6. **No `DispatchQueue.main.async` for UI updates**: All UI-related state mutations go through `@MainActor` or `Task { @MainActor in }`, not legacy GCD patterns.
7. **`ThrowingContinuationSupport.resumeVoid`**: Custom helper for void continuations reduces boilerplate and potential mistakes.
---
## Swift 6.2 / iOS 26 Forward-Compatibility Notes
1. **`nonisolated(nonsending)` default**: Several `nonisolated` functions and closures may need `@concurrent` annotation if they are intended to run off the caller's actor. Review all `nonisolated` methods.
2. **Default `@MainActor` isolation**: If the project opts into Swift 6.2's `MainActorByDefault`, most explicit `@MainActor` annotations become redundant. The current architecture is well-positioned for this.
3. **`@preconcurrency` removal**: As Apple frameworks adopt Sendable, remove `@preconcurrency` imports for `UserNotifications` and `WatchConnectivity`.
4. **`sending` parameter keyword**: New `sending` keyword in Swift 6.2 may replace some `@Sendable` closure annotations for parameters that are consumed (not stored).

376
apps/ios/audit-security.md Normal file
View File

@@ -0,0 +1,376 @@
# iOS App Security, Networking & Performance Audit
**Date:** 2026-03-02
**Scope:** `apps/ios/Sources/`, `apps/shared/OpenClawKit/Sources/` (security-relevant shared code), `apps/ios/project.yml`, entitlements
**Auditor:** Security & Performance Audit Agent
---
## 1. Security Posture Overview
The OpenClaw iOS app demonstrates a **generally strong security posture** for a local-network gateway client. Key strengths include:
- **Keychain usage for credentials:** Gateway tokens, passwords, instance IDs, and API keys are stored in Keychain (not UserDefaults).
- **TLS certificate pinning:** SHA-256 certificate fingerprint pinning is implemented for gateway WebSocket connections via `GatewayTLSPinningSession`.
- **Trust-on-first-use (TOFU) with user confirmation:** New gateway TLS fingerprints require explicit user approval before trust is established.
- **Deep link confirmation:** Agent deep links (the `openclaw://` URL scheme) require user confirmation before execution, with message length limits.
- **Web view security:** The canvas WKWebView uses `.nonPersistent()` data store and validates that A2UI action messages originate only from trusted/local-network URLs.
- **Input sanitization:** Consistent `.trimmingCharacters(in: .whitespacesAndNewlines)` throughout, input length limits on contacts/calendar/photos queries.
- **Permission gating:** All hardware capabilities (camera, location, microphone, contacts, calendar, photos) check authorization status before access.
- **No hardcoded secrets:** No API keys, tokens, or credentials are hardcoded in the source.
- **Swift 6 strict concurrency:** Enabled project-wide (`SWIFT_STRICT_CONCURRENCY: complete`), reducing data race risks.
---
## 2. Critical Severity Findings
*No critical vulnerabilities identified.*
The app does not store plaintext passwords in UserDefaults, does not embed secrets, does not disable ATS globally, and does not allow arbitrary code execution from untrusted sources. The attack surface is primarily local-network, which limits remote exploitation vectors.
---
## 3. High Severity Findings
### H-1: TLS Fingerprints Stored in UserDefaults Instead of Keychain
**File:** `apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift:19-38`
**Severity:** HIGH
`GatewayTLSStore` stores TLS certificate fingerprints in `UserDefaults(suiteName: "ai.openclaw.shared")`. While fingerprints themselves are not secrets, they serve as the trust anchor for the TLS pinning system. An attacker with access to the device backup (unencrypted iTunes/Finder backup) or a compromised app extension sharing the same suite could modify these fingerprints and redirect gateway connections to a malicious server.
**Exploit scenario:** An attacker with physical or backup access modifies the stored fingerprint for a known gateway stableID, then performs a MITM attack on the LAN. The app connects using the attacker's fingerprint as the expected pin.
**Recommended fix:** Store TLS fingerprints in Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` (matching the existing `KeychainStore` pattern). This prevents backup extraction and cross-device compromise.
---
### H-2: KeychainStore Update Path Does Not Set Accessibility Level
**File:** `apps/ios/Sources/Gateway/KeychainStore.swift:20-37`
**Severity:** HIGH
In `saveString()`, when the item already exists (`SecItemUpdate` succeeds), the update does not set or enforce the `kSecAttrAccessible` attribute. Only new items (via `SecItemAdd`) get `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. If a Keychain item was originally created with a less restrictive accessibility level (e.g., during a migration or by an older version), it retains that weaker level after updates.
**Exploit scenario:** An older app version or a migration path creates a Keychain item without specifying `kSecAttrAccessible` (defaults to `kSecAttrAccessibleWhenUnlocked`). After upgrading, the item retains the old accessibility level, potentially making it accessible via iCloud Keychain sync.
**Recommended fix:** Before `SecItemUpdate`, delete and re-add the item with the correct accessibility attribute, or explicitly include `kSecAttrAccessible` in the update query attributes. Example:
```swift
// Delete-then-add pattern for consistent accessibility
SecItemDelete(query as CFDictionary)
var insert = query
insert[kSecValueData as String] = data
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
return SecItemAdd(insert as CFDictionary, nil) == errSecSuccess
```
---
### H-3: Gateway Connection Metadata in UserDefaults
**File:** `apps/ios/Sources/Gateway/GatewaySettingsStore.swift:170-217`
**Severity:** HIGH
Last-known gateway connection details (host, port, TLS flag, stableID, connection kind) are stored in `UserDefaults.standard`. This data reveals which gateway servers the user connects to, their network topology, and connection preferences. UserDefaults are included in unencrypted device backups and can be read by MDM profiles or forensic tools.
**Affected keys:** `gateway.last.kind`, `gateway.last.host`, `gateway.last.port`, `gateway.last.tls`, `gateway.last.stableID`, `gateway.manual.host`, `gateway.manual.port`, `gateway.manual.tls`, `gateway.manual.clientId`, `gateway.clientIdOverride.*`, `gateway.selectedAgentId.*`.
**Recommended fix:** Move gateway connection metadata that reveals network topology to Keychain or use `NSFileProtectionCompleteUntilFirstUserAuthentication` on a dedicated plist file in the app's data directory.
---
## 4. Medium Severity Findings
### M-1: `NSAllowsArbitraryLoadsInWebContent` Enabled
**File:** `apps/ios/project.yml:110`
**Severity:** MEDIUM
```yaml
NSAppTransportSecurity:
NSAllowsArbitraryLoadsInWebContent: true
```
This disables ATS protections for WKWebView content. While necessary for the canvas to load user-specified URLs from the gateway (including local-network HTTP servers), it means the web view can load insecure HTTP resources. The `ScreenController.navigate()` method does filter out loopback URLs but does not enforce HTTPS for remote URLs.
**Exploit scenario:** A gateway instructs the canvas to load an HTTP URL on a public network. The content is intercepted/modified via MITM.
**Recommended fix:** This is largely an accepted risk given the product's design (canvas loads gateway-specified URLs). Consider adding a user-visible indicator when the canvas is loading non-HTTPS content, and log a warning.
---
### M-2: Diagnostic Log File Written to Documents Directory Without Protection
**File:** `apps/ios/Sources/Gateway/GatewaySettingsStore.swift:359-448`
**Severity:** MEDIUM
`GatewayDiagnostics` writes logs to `Documents/openclaw-gateway.log`. The Documents directory is accessible via iTunes file sharing (if enabled) and is included in device backups. Log entries include timestamps and gateway connection events which could reveal usage patterns.
Logs are written with `privacy: .public` in the `os.Logger` calls, meaning they are also visible in `Console.app` sysdiagnose captures without redaction.
**Recommended fix:** Write diagnostic logs to `Library/Caches/` instead (excluded from backups), apply `NSFileProtectionCompleteUntilFirstUserAuthentication`, and consider using `privacy: .private` or `privacy: .auto` for log messages that may contain sensitive connection details.
---
### M-3: Environment Variable Fallback for ElevenLabs API Key
**File:** `apps/ios/Sources/Voice/TalkModeManager.swift:991-992`
**Severity:** MEDIUM
```swift
ProcessInfo.processInfo.environment["ELEVENLABS_API_KEY"]
```
The talk mode manager reads API keys from environment variables as a fallback. While environment variables are not accessible to other apps on iOS, they persist in process memory and could be captured in crash reports. This pattern is more suitable for development/debugging and should not ship in production builds.
**Recommended fix:** Gate this fallback behind `#if DEBUG` to prevent production builds from reading API keys from environment variables.
---
### M-4: Instance ID Dual-Storage Creates Sync Risk
**File:** `apps/ios/Sources/Gateway/GatewaySettingsStore.swift:291-312`
**Severity:** MEDIUM
`ensureStableInstanceID()` maintains the instance ID in both Keychain and UserDefaults (`node.instanceId`). If either store is cleared independently (e.g., Keychain reset during device restore without backup, or UserDefaults cleared by storage pressure), the sync logic may create a new UUID, effectively orphaning the device's gateway registration.
While this is a robustness concern rather than a direct vulnerability, an attacker who can clear UserDefaults (e.g., via an MDM-deployed configuration profile) could force a device identity reset.
**Recommended fix:** Designate Keychain as the single source of truth and only mirror to UserDefaults for read convenience. Document the recovery flow for identity reset.
---
### M-5: No Rate Limiting on Deep Link Agent Prompts
**File:** `apps/ios/Sources/Model/NodeAppModel.swift:43-45, 92`
**Severity:** MEDIUM
The `IOSDeepLinkAgentPolicy` defines `maxMessageChars = 20000` and `maxUnkeyedConfirmChars = 240`, and there is a `lastAgentDeepLinkPromptAt` timestamp. However, without a minimum interval check, a malicious webpage or app could rapidly fire `openclaw://` deep links, creating a flood of confirmation dialogs that degrade UX and potentially trick users into accepting a malicious prompt through fatigue.
**Recommended fix:** Enforce a minimum interval (e.g., 5 seconds) between successive deep link prompts, silently dropping duplicates. The `lastAgentDeepLinkPromptAt` field exists but its enforcement should be verified.
---
### M-6: QR Code Parsing Accepts Multiple Formats Without Strict Validation
**File:** `apps/ios/Sources/Onboarding/QRScannerView.swift:63-85`
**Severity:** MEDIUM
The QR scanner tries two parsing strategies: `GatewayConnectDeepLink.fromSetupCode(payload)` (base64url JSON) and `DeepLinkParser.parse(url)` (URL format). The `GatewaySetupCode.decode()` method (`apps/ios/Sources/Gateway/GatewaySetupCode.swift`) accepts arbitrary base64-encoded JSON payloads that decode into `GatewaySetupPayload`. There is no signature verification or HMAC on the QR code content.
**Exploit scenario:** An attacker places a malicious QR code that encodes a gateway URL pointing to their controlled server. When scanned, the user is prompted to connect to the attacker's gateway.
**Mitigating factors:** The TLS trust prompt still fires for new gateways, requiring explicit user approval of the certificate fingerprint.
**Recommended fix:** Consider adding an HMAC or signing mechanism to QR setup codes so the app can verify they were generated by the user's own gateway. At minimum, clearly display the gateway URL/host to the user during the onboarding flow.
---
### M-7: WebSocket Maximum Message Size Set to 16 MB
**File:** `apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift:55`
**Severity:** MEDIUM (Performance/DoS)
```swift
task.maximumMessageSize = 16 * 1024 * 1024
```
A malicious or compromised gateway could send a 16 MB WebSocket message, causing a significant memory spike on the iOS device.
**Recommended fix:** Evaluate whether 16 MB is necessary. Consider progressive parsing or streaming for large payloads. Add a sanity check on incoming message size.
---
## 5. Low Severity Findings
### L-1: Location Data Sent Over Gateway WebSocket Without End-to-End Encryption
**File:** `apps/ios/Sources/Location/SignificantLocationMonitor.swift:21-38`
**Severity:** LOW
Significant location updates (lat, lon, accuracy) are sent as JSON over the gateway WebSocket. While the WebSocket uses TLS (wss://), the gateway server itself can read the location data in plaintext. This is by design (the gateway processes location for hooks), but users should be informed that location data is accessible to the gateway process.
**Recommended fix:** Document this clearly in privacy documentation. Consider allowing users to configure location precision (rounding to neighborhood-level vs. exact coordinates).
---
### L-2: Camera Photo/Video Base64 Encoding in Memory
**Files:** `apps/ios/Sources/Camera/CameraController.swift:84`, `apps/ios/Sources/Media/PhotoLibraryService.swift:105`
**Severity:** LOW
Camera captures and photo library images are base64-encoded in memory before being sent over the gateway. For large images (up to 1600px wide at 0.9 quality), this means the raw image data plus the base64 string (33% larger) coexist in memory briefly.
**Mitigating factors:** The app already clamps max width to 1600px and applies quality compression. Temporary files are cleaned up via `defer` blocks.
**Recommended fix:** Consider streaming base64 encoding or using a memory-mapped approach for very large payloads. Current implementation is adequate for the existing size limits.
---
### L-3: Screen Recording Output Path User-Controllable
**File:** `apps/ios/Sources/Screen/ScreenRecordService.swift:103-109`
**Severity:** LOW
The `makeOutputURL` method accepts an optional `outPath` parameter. If this comes from a gateway command, a malicious gateway could specify a path outside the app's sandbox (which iOS would block) or overwrite files within the sandbox.
**Mitigating factors:** iOS sandbox prevents writing outside the app container. The `defer` cleanup in the caller should handle temporary files.
**Recommended fix:** Validate that `outPath` is within the app's temporary or documents directory before using it. Reject absolute paths that don't start with the app's known writable directories.
---
### L-4: Voice Wake Preferences Stored in UserDefaults
**File:** `apps/ios/Sources/Voice/VoiceWakePreferences.swift:23-29`
**Severity:** LOW
Trigger words and voice wake enabled state are stored in `UserDefaults.standard`. While trigger words are not sensitive per se, they reveal user behavior patterns.
**Recommended fix:** Acceptable for non-sensitive preferences. No action needed unless trigger words become user-configurable sensitive phrases.
---
### L-5: `nonisolated(unsafe)` in GatewayDiagnostics
**File:** `apps/ios/Sources/Gateway/GatewaySettingsStore.swift:358`
**Severity:** LOW
```swift
nonisolated(unsafe) private static var logWritesSinceCheck = 0
```
This counter is accessed from the `DispatchQueue` without the lock that protects the file I/O. While this is a benign data race (used only for approximate frequency gating), it could theoretically cause the log size check to be skipped or double-triggered.
**Recommended fix:** Move the counter into the `queue.async` block or use an atomic counter.
---
### L-6: `objc_sync_enter` Used for Synchronization
**File:** `apps/ios/Sources/Gateway/GatewayConnectionController.swift:1039-1040`
**Severity:** LOW
`GatewayTLSFingerprintProbe.finish()` uses `objc_sync_enter/exit` for synchronization. This is the Objective-C `@synchronized` primitive. While functional, modern Swift best practice prefers `OSAllocatedUnfairLock` (as used correctly in `TCPProbe`), `NSLock`, or actor isolation.
**Recommended fix:** Replace with `OSAllocatedUnfairLock` for consistency with the rest of the codebase.
---
### L-7: No Certificate Revocation Checking
**File:** `apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift:59-96`
**Severity:** LOW
The TLS pinning implementation checks the certificate fingerprint but does not perform OCSP or CRL revocation checking. For self-signed certificates (typical in local gateway setups), this is expected. For publicly-signed certificates, revocation checking would add defense in depth.
**Recommended fix:** For the current use case (self-signed gateway certs on LAN), this is acceptable. If public CA certificates are used in future, consider enabling revocation checking via `SecTrustSetOptions`.
---
## 6. Performance Concerns
### P-1: ISO8601DateFormatter Created Per Log Entry
**File:** `apps/ios/Sources/Gateway/GatewaySettingsStore.swift:422-424`
**Severity:** LOW
`GatewayDiagnostics.log()` creates a new `ISO8601DateFormatter` for every log call. `ISO8601DateFormatter` is relatively expensive to initialize.
**Recommended fix:** Cache a static formatter instance (thread-safety is acceptable for `ISO8601DateFormatter` as it is immutable after configuration).
---
### P-2: Observation Tracking Re-registration Pattern
**File:** `apps/ios/Sources/Gateway/GatewayConnectionController.swift:293-305`
**Severity:** LOW
The `observeDiscovery()` method uses `withObservationTracking` and recursively calls itself in the `onChange` closure. This is the standard Swift Observation pattern, but each change creates a new `Task` and re-registers tracking. Under rapid discovery state changes, this could create a burst of Task allocations.
**Mitigating factors:** Discovery state changes are infrequent (Bonjour events).
**Recommended fix:** Consider debouncing or coalescing rapid state changes.
---
### P-3: Synchronous Photo Library Access
**File:** `apps/ios/Sources/Media/PhotoLibraryService.swift:69-71`
**Severity:** MEDIUM (Performance)
```swift
options.isSynchronous = true
```
`PHImageManager.requestImage` is called synchronously, which blocks the calling thread until the image is loaded and decoded. For network-backed assets (iCloud Photo Library), this could block for seconds.
**Recommended fix:** Use asynchronous image loading with a continuation wrapper to avoid blocking.
---
### P-4: Camera Clip Base64 Encoding of Video Data
**File:** `apps/ios/Sources/Camera/CameraController.swift:89-140`
**Severity:** LOW
Video clips (up to 60 seconds) are fully loaded into memory as `Data` and then base64-encoded. A 60-second medium-quality MP4 could be 10-30 MB, producing a 13-40 MB base64 string in memory.
**Mitigating factors:** Default duration is 3 seconds, keeping typical payloads small. The 60-second max is enforced at `CameraController.clampDurationMs`.
**Recommended fix:** Consider a streaming upload mechanism for clips longer than ~10 seconds.
---
## 7. OWASP Mobile Top 10 2024 Checklist
| # | OWASP Category | Status | Notes |
|---|---------------|--------|-------|
| M1 | Improper Credential Usage | **PASS** | Credentials stored in Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. No hardcoded secrets. API keys from env vars gated to dev builds (recommended). |
| M2 | Inadequate Supply Chain Security | **PASS** | Dependencies are version-pinned via Package.resolved. SwiftLint and SwiftFormat enforce code quality. |
| M3 | Insecure Authentication/Authorization | **PASS** | Gateway authentication uses token + password stored in Keychain. TLS pinning prevents MITM. Deep links require user confirmation. |
| M4 | Insufficient Input/Output Validation | **PASS** | Input trimming and length limits applied consistently. QR code parsing has two validated paths. Calendar/contacts sanitize inputs. |
| M5 | Insecure Communication | **PASS with notes** | TLS required for non-loopback connections. Certificate pinning implemented. `NSAllowsArbitraryLoadsInWebContent` allows HTTP in web views (accepted risk for canvas). |
| M6 | Inadequate Privacy Controls | **PASS with notes** | All sensitive permissions have usage descriptions. Location data sent to gateway in plaintext over TLS. Photo library access checks authorization. Logging uses `privacy: .public` for some potentially sensitive data. |
| M7 | Insufficient Binary Protections | **N/A** | Standard Xcode compilation. No jailbreak detection implemented (acceptable for non-financial app). |
| M8 | Security Misconfiguration | **PASS with notes** | TLS fingerprints in UserDefaults (H-1). Gateway metadata in UserDefaults (H-3). Entitlements minimal (only `aps-environment`). |
| M9 | Insecure Data Storage | **PASS with notes** | Credentials in Keychain (good). Gateway connection metadata in UserDefaults (H-3). Diagnostic logs in Documents directory (M-2). |
| M10 | Insufficient Cryptography | **PASS** | SHA-256 for certificate fingerprinting via CryptoKit. No custom/weak crypto implementations. |
---
## 8. Summary of Recommendations by Priority
### Immediate (High)
1. **H-1:** Move TLS fingerprint storage from UserDefaults to Keychain
2. **H-2:** Fix KeychainStore to enforce accessibility level on updates (delete + re-add)
3. **H-3:** Move gateway connection metadata out of UserDefaults
### Short-term (Medium)
4. **M-3:** Gate `ELEVENLABS_API_KEY` env var fallback behind `#if DEBUG`
5. **M-2:** Move diagnostic logs to Caches directory, apply file protection
6. **M-5:** Enforce minimum interval between deep link prompts
7. **M-6:** Add HMAC/signature to QR setup codes
8. **M-7:** Evaluate reducing WebSocket max message size from 16 MB
9. **P-3:** Convert synchronous photo library loading to async
### Long-term (Low / Hardening)
10. **L-6:** Replace `objc_sync_enter` with `OSAllocatedUnfairLock`
11. **L-3:** Validate screen recording output paths
12. **P-1:** Cache ISO8601DateFormatter instances
13. **M-1:** Add indicator for non-HTTPS canvas content
14. **L-5:** Fix `nonisolated(unsafe)` data race in log counter
---
## 9. Positive Security Patterns Worth Preserving
- **TOFU with explicit user confirmation** for TLS fingerprints is a pragmatic and user-friendly approach for self-signed certificates.
- **Dual WebSocket sessions** (node + operator) with separate role scoping provides good privilege separation.
- **`websiteDataStore = .nonPersistent()`** for the canvas WKWebView prevents session data leakage.
- **Origin validation in `CanvasA2UIActionMessageHandler`** (checking `isTrustedCanvasUIURL` and `isLocalNetworkCanvasURL`) is a strong defense against arbitrary web content triggering actions.
- **Loopback URL rejection** in `ScreenController.navigate()` prevents SSRF-like attacks from the gateway.
- **Autoconnect only to previously trusted gateways** (stored TLS pin required) prevents connecting to rogue gateways after TOFU.
- **Permission checks before hardware access** with clear error messages is well-implemented.
- **`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`** is the correct Keychain accessibility level for this use case (device-local, available after first unlock for background operation).

318
apps/ios/audit-uiux.md Normal file
View File

@@ -0,0 +1,318 @@
# iOS App UI/UX & Accessibility Audit
**Audit Date:** 2026-03-02
**Scope:** All SwiftUI view files in `apps/ios/Sources/`
**Reference Standards:** Apple HIG (iOS 26), WCAG 2.1 AA, Liquid Glass design language, SwiftUI accessibility best practices
---
## UI/UX Health Overview
The OpenClaw iOS app demonstrates a well-structured SwiftUI codebase with several accessibility-conscious patterns already in place. The app uses the modern `@Observable` / `Observation` framework consistently, respects `accessibilityReduceMotion`, responds to `colorSchemeContrast`, and provides accessibility labels on key interactive elements. However, there are significant gaps in Dynamic Type support, localization readiness, haptic feedback, and iPad adaptivity that should be addressed before the next major release.
**Strengths:**
- Good use of `@Environment(\.accessibilityReduceMotion)` in animation-heavy views (RootTabs, StatusPill)
- `StatusGlassCard` correctly responds to `colorSchemeContrast` for increased visibility
- `StatusPill` has proper `accessibilityLabel`, `accessibilityValue`, and `accessibilityHint`
- `TalkOrbOverlay` uses `accessibilityElement(children: .combine)` to present a single VoiceOver element
- Consistent use of `@Observable` macro (Observation framework) over legacy `ObservableObject`
- Glass material effects on overlays (`.ultraThinMaterial`) with light/dark mode awareness
**Weaknesses:**
- Zero Dynamic Type support (no `@ScaledMetric`, no `dynamicTypeSize` environment usage)
- Zero localization infrastructure (no `NSLocalizedString`, `String(localized:)`, or `.strings` files)
- Zero haptic feedback across the entire app
- Several views lack accessibility labels entirely
- Hardcoded dimensions in TalkOrbOverlay will break on small screens
- SettingsTab is a monolithic ~650 LOC file
- No iPad-specific layout adaptations
- `RootCanvas` voiceWakeToast animation does not respect `reduceMotion` (unlike `RootTabs`)
---
## Critical Findings
### C-1: RootCanvas animations ignore `accessibilityReduceMotion`
**File:** `Sources/RootCanvas.swift:159-167`
**Description:** The `voiceWakeToastText` animation in `RootCanvas` uses hardcoded `.spring()` and `.easeOut()` animations without checking `@Environment(\.accessibilityReduceMotion)`. The sibling `RootTabs` view correctly guards the same toast animation with `reduceMotion ? .none : .spring(...)`.
**Impact:** Users who require reduced motion will see unexpected animations in the canvas view.
**Recommended Fix:**
```swift
// In RootCanvas, add the environment property:
@Environment(\.accessibilityReduceMotion) private var reduceMotion
// Then guard animations:
withAnimation(self.reduceMotion ? .none : .spring(response: 0.25, dampingFraction: 0.85)) {
self.voiceWakeToastText = trimmed
}
// ...
withAnimation(self.reduceMotion ? .none : .easeOut(duration: 0.25)) {
self.voiceWakeToastText = nil
}
```
### C-2: TalkOrbOverlay perpetual animations ignore `accessibilityReduceMotion`
**File:** `Sources/Voice/TalkOrbOverlay.swift:15-26`
**Description:** The pulsing ring animations use `.repeatForever(autoreverses: false)` without checking `reduceMotion`. These are high-frequency, continuous animations that can cause discomfort for users with vestibular disorders.
**Recommended Fix:**
```swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion
// Replace pulse animations with:
if !reduceMotion {
Circle()
.scaleEffect(self.pulse ? 1.15 : 0.96)
.animation(.easeOut(duration: 1.3).repeatForever(autoreverses: false), value: self.pulse)
}
```
### C-3: CameraFlashOverlay has no accessibility announcement
**File:** `Sources/RootCanvas.swift:405-429`
**Description:** `CameraFlashOverlay` flashes the screen white at 85% opacity. VoiceOver users have no indication that a photo was taken. There is no `AccessibilityNotification.Announcement` posted, and the flash itself could trigger photosensitive reactions.
**Recommended Fix:**
```swift
// Post an accessibility announcement:
AccessibilityNotification.Announcement("Photo captured").post()
// Add prefers-reduced-motion check to skip or soften the flash:
if reduceMotion {
// Skip flash, or use subtle opacity change
}
```
---
## High Findings
### H-1: Zero Dynamic Type support across the entire app
**Files:** All view files in `Sources/`
**Description:** No view uses `@ScaledMetric`, `@Environment(\.dynamicTypeSize)`, or `ContentSizeCategory`. All hardcoded font sizes and dimensions (e.g., `font(.system(size: 16))` in `OverlayButton`, `font(.system(size: 12))` in monospaced debug text, `frame(width: 320, height: 320)` in TalkOrbOverlay) will not scale with the user's preferred text size. Apple's HIG strongly recommends supporting Dynamic Type for all text.
**Key locations:**
- `Sources/RootCanvas.swift:358` - OverlayButton uses fixed `size: 16`
- `Sources/Voice/TalkOrbOverlay.swift:16,23,39` - Fixed 320pt and 190pt circles
- `Sources/Status/StatusPill.swift:52` - Fixed `width: 9, height: 9` indicator dot
- `Sources/Gateway/GatewayDiscoveryDebugLogView.swift:24` - Fixed `font(.callout)`
- `Sources/Gateway/GatewayOnboardingView.swift:345-346` - Fixed `.system(size: 12)` monospaced text
**Recommended Fix:** Use semantic font styles (`.body`, `.headline`, etc.) instead of fixed sizes where possible. For custom dimensions, use `@ScaledMetric`:
```swift
@ScaledMetric(relativeTo: .body) private var orbSize: CGFloat = 190
@ScaledMetric(relativeTo: .caption) private var dotSize: CGFloat = 9
```
### H-2: OnboardingWizardView missing accessibility labels on interactive elements
**File:** `Sources/Onboarding/OnboardingWizardView.swift`
**Description:** Multiple interactive elements lack accessibility labels:
- `OnboardingModeRow` (line 861-884): Radio-style selection buttons have no `accessibilityAddTraits(.isButton)` or clear selection state announcement. VoiceOver users cannot tell which mode is selected.
- Gateway list connect buttons (line 453-465): `ProgressView` and "Resolving..." text lack accessibility context.
- QR scanner action (line 319-326): "Scan QR Code" button label is good, but the status line below it (line 340-345) is not connected as an accessibility value.
**Recommended Fix:**
```swift
// OnboardingModeRow:
.accessibilityElement(children: .combine)
.accessibilityAddTraits(self.selected ? [.isButton, .isSelected] : .isButton)
.accessibilityLabel("\(self.title), \(self.subtitle)")
.accessibilityValue(self.selected ? "Selected" : "Not selected")
```
### H-3: No localization infrastructure
**Files:** All source files
**Description:** The entire app uses hardcoded English strings with no localization wrapping. No `NSLocalizedString`, `String(localized:)`, `.strings`/`.stringsdict` files, or `LocalizedStringKey` usage was found. This makes the app inaccessible to non-English speakers and violates Apple's HIG recommendation to support multiple languages.
**Key examples:**
- `Sources/Settings/SettingsTab.swift`: All section headers, labels, help text
- `Sources/Onboarding/OnboardingWizardView.swift`: "Welcome", "Connected", all step descriptions
- `Sources/Status/StatusPill.swift`: "Connected", "Connecting...", "Error", "Offline"
- `Sources/Voice/VoiceTab.swift`: All list labels
**Recommended Fix:** Wrap all user-facing strings in `String(localized:)` or use `LocalizedStringResource`. Create a `Localizable.xcstrings` catalog.
### H-4: No haptic feedback anywhere in the app
**Files:** All source files
**Description:** No `UIImpactFeedbackGenerator`, `UINotificationFeedbackGenerator`, `UISelectionFeedbackGenerator`, or `.sensoryFeedback()` modifier usage found. Key interaction points that would benefit from haptics:
- Gateway connection success/failure
- Voice wake trigger detection
- Talk mode orb tap
- QR code successfully scanned
- Toggle state changes in Settings
**Recommended Fix:**
```swift
// iOS 17+ SwiftUI modifier:
.sensoryFeedback(.success, trigger: appModel.gatewayServerName != nil)
// For Talk orb tap:
.sensoryFeedback(.impact(weight: .medium), trigger: tapCount)
```
### H-5: GatewayTrustPromptAlert uses deprecated `Alert` API
**File:** `Sources/Gateway/GatewayTrustPromptAlert.swift:17-35`
**Description:** Uses the deprecated `Alert(title:message:primaryButton:secondaryButton:)` initializer pattern. This API was deprecated in iOS 15 in favor of the `alert(_:isPresented:actions:message:)` modifier. Same issue in `DeepLinkAgentPromptAlert.swift:15-33`.
**Recommended Fix:** Migrate to the modern `alert` modifier with `@ViewBuilder` actions.
---
## Medium Findings
### M-1: SettingsTab is a monolithic view (~650+ LOC)
**File:** `Sources/Settings/SettingsTab.swift`
**Description:** SettingsTab contains the entire settings UI, including gateway connection, device features, advanced debug options, agent picker, and reset logic. The file has a `// swiftlint:disable type_body_length` comment acknowledging this. This makes the view hard to maintain and test.
**Recommended Fix:** Extract into focused sub-views:
- `GatewaySettingsSection`
- `DeviceFeaturesSection`
- `AdvancedSettingsSection`
- `DeviceInfoSection`
### M-2: No empty states for VoiceTab when disconnected
**File:** `Sources/Voice/VoiceTab.swift`
**Description:** VoiceTab always shows the same status labels regardless of gateway connection state. When disconnected, it should show a clear empty state explaining that voice features require a gateway connection, with a CTA to connect.
**Recommended Fix:**
```swift
if appModel.gatewayServerName == nil {
ContentUnavailableView(
"Not Connected",
systemImage: "antenna.radiowaves.left.and.right.slash",
description: Text("Connect to a gateway to use voice features."))
}
```
### M-3: No loading/error states in GatewayQuickSetupSheet
**File:** `Sources/Gateway/GatewayQuickSetupSheet.swift`
**Description:** When `bestCandidate` is nil and no gateways are found, the sheet shows a text message but no visual indicator that discovery is actively running. No retry button or activity indicator is shown during the discovery phase.
### M-4: OverlayButton touch target may be too small
**File:** `Sources/RootCanvas.swift:348-403`
**Description:** `OverlayButton` uses `padding(10)` around a 16pt icon, resulting in a ~36pt touch target. Apple HIG recommends a minimum of 44pt x 44pt for touch targets.
**Recommended Fix:**
```swift
.frame(minWidth: 44, minHeight: 44)
// or increase padding to at least 14pt
```
### M-5: No keyboard shortcut support
**Files:** All view files
**Description:** No `.keyboardShortcut()` modifiers found anywhere. iPad users with external keyboards have no keyboard navigation shortcuts for common actions like opening chat, settings, or toggling voice.
### M-6: TalkOrbOverlay hardcoded dimensions break on small screens
**File:** `Sources/Voice/TalkOrbOverlay.swift:16,23,39`
**Description:** The pulse rings are hardcoded at 320pt width/height, and the inner orb at 190pt. On iPhone SE (320pt logical width), the rings will extend beyond screen bounds. On iPad, the orb will appear relatively small.
**Recommended Fix:** Use `GeometryReader` or `@ScaledMetric` for adaptive sizing:
```swift
GeometryReader { proxy in
let size = min(proxy.size.width, proxy.size.height) * 0.65
Circle().frame(width: size, height: size)
}
```
### M-7: ScreenTab error overlay not accessible
**File:** `Sources/Screen/ScreenTab.swift:12-21`
**Description:** The error text overlay appears only when `errorText` is set and the gateway is disconnected, but there is no VoiceOver announcement when the error appears or disappears. Screen reader users may not notice the error.
### M-8: No pull-to-refresh on any list view
**Files:** `Sources/Voice/VoiceTab.swift`, `Sources/Gateway/GatewayDiscoveryDebugLogView.swift`
**Description:** List views do not support `.refreshable {}` for pull-to-refresh, which is a standard iOS interaction pattern.
---
## Low Findings
### L-1: Inconsistent glass card styling between RootTabs and RootCanvas
**Files:** `Sources/RootTabs.swift`, `Sources/RootCanvas.swift`
**Description:** `RootTabs` shows `StatusPill` without the `brighten` parameter (defaults to false), while `RootCanvas.CanvasContent` passes `brighten` based on color scheme. This can cause visual inconsistency if both code paths are reachable.
### L-2: VoiceWakeToast hardcoded top offset
**Files:** `Sources/RootTabs.swift:47`, `Sources/RootCanvas.swift:329`
**Description:** `.safeAreaPadding(.top, 58)` is a magic number that assumes the StatusPill height. If the pill height changes (e.g., with Dynamic Type), the toast will overlap.
### L-3: No app-wide tint/accent color configuration
**Files:** `Sources/OpenClawApp.swift`
**Description:** No `.tint()` or `accentColor` is set at the app level. The default blue accent is used for buttons and toggles, but the app uses `appModel.seamColor` for some elements. This creates visual inconsistency.
### L-4: ConnectionStatusBox uses hardcoded monospaced font size
**File:** `Sources/Onboarding/GatewayOnboardingView.swift:345-346`
**Description:** `.font(.system(size: 12, weight: .regular, design: .monospaced))` will not scale with Dynamic Type.
### L-5: DateFormatter instances in GatewayDiscoveryDebugLogView are not locale-aware
**File:** `Sources/Gateway/GatewayDiscoveryDebugLogView.swift:49-53`
**Description:** `DateFormatter` with hardcoded `dateFormat = "HH:mm:ss"` does not respect the user's locale for time formatting. Should use `.dateStyle`/`.timeStyle` or `formatted()`.
### L-6: No transition animations on sheet presentations
**File:** `Sources/RootCanvas.swift:92-111`
**Description:** The `.sheet(item:)` presentations for settings, chat, and quick setup use default sheet transitions. Custom `presentationDetents` could improve the UX for smaller sheets like Quick Setup.
### L-7: Onboarding wizard duplicate padding
**File:** `Sources/Onboarding/OnboardingWizardView.swift:344-346`
**Description:** The welcome step has duplicate `.padding(.horizontal, 24)` on the status line (lines 344 and 345), which doubles the intended padding.
### L-8: No VoiceOver rotor actions
**Files:** All view files
**Description:** No `.accessibilityAction(named:)` or custom rotor items are defined. Power VoiceOver users could benefit from custom actions for common operations.
---
## Accessibility Compliance Checklist
| Criterion | Status | Notes |
|---|---|---|
| VoiceOver labels on all interactive elements | Partial | Overlay buttons, StatusPill, ChatSheet close, SettingsTab close have labels. OnboardingModeRow, gateway list items, many settings toggles missing. |
| VoiceOver hints for non-obvious actions | Partial | StatusPill has hint. Most buttons lack hints. |
| VoiceOver value for stateful elements | Partial | StatusPill has value. Toggle states auto-announced by SwiftUI. OnboardingModeRow selection not announced. |
| Dynamic Type support | Missing | No `@ScaledMetric`, no `dynamicTypeSize` environment, fixed font sizes throughout. |
| Reduce Motion respected | Partial | RootTabs and StatusPill respect it. RootCanvas, TalkOrbOverlay, CameraFlashOverlay do not. |
| Increased Contrast support | Partial | StatusGlassCard adjusts border for increased contrast. Other views do not check. |
| Color not sole indicator | Pass | Status uses both color dots and text labels. |
| Minimum touch target 44pt | Partial | Standard buttons OK. OverlayButton (~36pt) and StatusPill dot are undersized. |
| Keyboard navigation (iPad) | Missing | No keyboard shortcuts defined. |
| Localization readiness | Missing | All strings hardcoded in English. |
| Haptic feedback | Missing | No haptic feedback in any interaction. |
| iPad layout adaptation | Missing | No `horizontalSizeClass` or iPad-specific layouts. |
| Dark mode support | Pass | Uses semantic colors, materials, and `.preferredColorScheme(.dark)` for canvas. |
| Safe area handling | Pass | Correct use of `.ignoresSafeArea()` for screen, `.safeAreaPadding()` for overlays. |
| Error state announcements | Missing | No `AccessibilityNotification.Announcement` for state changes. |
| Focus management | Partial | `@FocusState` used in VoiceWakeWordsSettingsView. No focus management in onboarding. |
---
## Summary by Priority
| Priority | Count | Key Themes |
|---|---|---|
| Critical | 3 | Reduce Motion violations, flash accessibility |
| High | 5 | Dynamic Type, localization, haptics, deprecated APIs, missing labels |
| Medium | 8 | Monolithic views, empty states, touch targets, iPad, hardcoded sizes |
| Low | 8 | Styling consistency, magic numbers, locale formatting, polish |

View File

@@ -164,7 +164,7 @@ actor CameraCaptureService {
}
private func ensureAccess(for mediaType: AVMediaType) async throws {
if !(await CameraAuthorization.isAuthorized(for: mediaType)) {
if await !(CameraAuthorization.isAuthorized(for: mediaType)) {
throw CameraError.permissionDenied(kind: mediaType == .video ? "Camera" : "Microphone")
}
}

View File

@@ -9,5 +9,4 @@ final class CanvasFileWatcher: @unchecked Sendable, SimpleFileWatcherOwner {
queueLabel: "ai.openclaw.canvaswatcher",
onChange: onChange))
}
}

View File

@@ -25,11 +25,22 @@ extension CanvasWindowController {
}
static func _testParseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
LoopbackHost.parseIPv4(host)
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count == 4 else { return nil }
let bytes: [UInt8] = parts.compactMap { UInt8($0) }
guard bytes.count == 4 else { return nil }
return (bytes[0], bytes[1], bytes[2], bytes[3])
}
static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
LoopbackHost.isLocalNetworkIPv4(ip)
let (a, b, _, _) = ip
if a == 10 { return true }
if a == 172, (16...31).contains(Int(b)) { return true }
if a == 192, b == 168 { return true }
if a == 127 { return true }
if a == 169, b == 254 { return true }
if a == 100, (64...127).contains(Int(b)) { return true }
return false
}
static func _testIsLocalNetworkCanvasURL(_ url: URL) -> Bool {

View File

@@ -30,5 +30,4 @@ final class ConfigFileWatcher: @unchecked Sendable, SimpleFileWatcherOwner {
},
onChange: onChange))
}
}

View File

@@ -124,12 +124,12 @@ enum ExecSystemRunCommandValidator {
let lower = token.lowercased()
let flag = lower.split(separator: "=", maxSplits: 1).first.map(String.init) ?? lower
if ExecEnvOptions.flagOnly.contains(flag) {
if ExecEnvOptions.flagOnly.contains(flag) {
usesModifiers = true
idx += 1
continue
}
if ExecEnvOptions.withValue.contains(flag) {
if ExecEnvOptions.withValue.contains(flag) {
usesModifiers = true
if !lower.contains("=") {
expectsOptionValue = true

View File

@@ -39,11 +39,12 @@ final class MacNodeLocationService: NSObject, CLLocationManagerDelegate, Locatio
desiredAccuracy: desiredAccuracy,
maxAgeMs: maxAgeMs,
timeoutMs: timeoutMs,
request: { try await self.requestLocationOnce() }) { timeoutMs, operation in
request: { try await self.requestLocationOnce() },
withTimeout: { timeoutMs, operation in
try await self.withTimeout(timeoutMs: timeoutMs) {
try await operation()
}
}
})
}
private func withTimeout<T: Sendable>(

View File

@@ -184,9 +184,9 @@ struct NodeMenuRowView: View {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(NodeMenuEntryFormatter.primaryName(self.entry))
.font(.callout.weight(NodeMenuEntryFormatter.isConnected(self.entry) ? .semibold : .regular))
.foregroundStyle(self.palette.primary)
Text(NodeMenuEntryFormatter.primaryName(self.entry))
.font(.callout.weight(NodeMenuEntryFormatter.isConnected(self.entry) ? .semibold : .regular))
.foregroundStyle(self.palette.primary)
.lineLimit(1)
.truncationMode(.middle)
.layoutPriority(1)
@@ -195,9 +195,9 @@ struct NodeMenuRowView: View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
if let right = NodeMenuEntryFormatter.headlineRight(self.entry) {
Text(right)
.font(.caption.monospacedDigit())
.foregroundStyle(self.palette.secondary)
Text(right)
.font(.caption.monospacedDigit())
.foregroundStyle(self.palette.secondary)
.lineLimit(1)
.truncationMode(.middle)
.layoutPriority(2)

View File

@@ -64,9 +64,10 @@ final class NotifyOverlayController {
OverlayPanelFactory.present(
window: self.window,
isVisible: &self.model.isVisible,
target: target) { window in
self.updateWindowFrame(animate: true)
window.orderFrontRegardless()
target: target)
{ window in
self.updateWindowFrame(animate: true)
window.orderFrontRegardless()
}
}

View File

@@ -134,10 +134,10 @@ extension OnboardingView {
if self.gatewayDiscovery.gateways.isEmpty {
ProgressView().controlSize(.small)
Button("Refresh") {
self.gatewayDiscovery.refreshWideAreaFallbackNow(timeoutSeconds: 5.0)
self.gatewayDiscovery.refreshRemoteFallbackNow(timeoutSeconds: 5.0)
}
.buttonStyle(.link)
.help("Retry Tailscale discovery (DNS-SD).")
.help("Retry remote discovery (Tailscale DNS-SD + Serve probe).")
}
Spacer(minLength: 0)
}
@@ -311,7 +311,7 @@ extension OnboardingView {
.font(.caption.monospaced())
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
.truncationMode(.middle)
}
}
Spacer(minLength: 0)

View File

@@ -134,7 +134,8 @@ enum PairingAlertSupport {
messageText: String,
informativeText: String,
alertHostWindow: inout NSWindow?,
completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) -> NSAlert {
completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) -> NSAlert
{
NSApp.activate(ignoringOtherApps: true)
let alert = NSAlert()
@@ -164,32 +165,6 @@ enum PairingAlertSupport {
completion: completion)
}
static func presentPairingAlert<Request>(
request: Request,
requestId: String,
messageText: String,
informativeText: String,
activeAlert: inout NSAlert?,
activeRequestId: inout String?,
alertHostWindow: inout NSWindow?,
clearActive: @escaping @MainActor (NSWindow) -> Void,
onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void)
{
self.presentPairingAlert(
requestId: requestId,
messageText: messageText,
informativeText: informativeText,
activeAlert: &activeAlert,
activeRequestId: &activeRequestId,
alertHostWindow: &alertHostWindow)
{ response, hostWindow in
Task { @MainActor in
clearActive(hostWindow)
await onResponse(response, request)
}
}
}
static func presentPairingAlert<Request>(
request: Request,
requestId: String,
@@ -199,19 +174,18 @@ enum PairingAlertSupport {
onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void)
{
self.presentPairingAlert(
request: request,
requestId: requestId,
messageText: messageText,
informativeText: informativeText,
activeAlert: &state.activeAlert,
activeRequestId: &state.activeRequestId,
alertHostWindow: &state.alertHostWindow)
{ response, hostWindow in
Task { @MainActor in
self.clearActivePairingAlert(state: state, hostWindow: hostWindow)
await onResponse(response, request)
}
}
alertHostWindow: &state.alertHostWindow,
completion: { response, hostWindow in
Task { @MainActor in
self.clearActivePairingAlert(state: state, hostWindow: hostWindow)
await onResponse(response, request)
}
})
}
static func clearActivePairingAlert(
@@ -231,12 +205,12 @@ enum PairingAlertSupport {
hostWindow: hostWindow)
}
static func stopPairingPrompter<Request>(
static func stopPairingPrompter(
isStopping: inout Bool,
activeAlert: inout NSAlert?,
activeRequestId: inout String?,
task: inout Task<Void, Never>?,
queue: inout [Request],
queue: inout [some Any],
isPresenting: inout Bool,
alertHostWindow: inout NSWindow?)
{
@@ -252,10 +226,10 @@ enum PairingAlertSupport {
alertHostWindow = nil
}
static func stopPairingPrompter<Request>(
static func stopPairingPrompter(
isStopping: inout Bool,
task: inout Task<Void, Never>?,
queue: inout [Request],
queue: inout [some Any],
isPresenting: inout Bool,
state: PairingAlertState)
{

View File

@@ -36,6 +36,7 @@ final class PeekabooBridgeHostCoordinator {
?? fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support")
return Self.legacySocketDirectoryNames.map { Self.makeSocketPath(for: $0, in: base) }
}
func setEnabled(_ enabled: Bool) async {
if enabled {
await self.startIfNeeded()
@@ -85,7 +86,7 @@ final class PeekabooBridgeHostCoordinator {
}
private func ensureLegacySocketSymlinks() {
Self.legacySocketPaths.forEach { legacyPath in
for legacyPath in Self.legacySocketPaths {
self.ensureLegacySocketSymlink(at: legacyPath)
}
}
@@ -116,7 +117,9 @@ final class PeekabooBridgeHostCoordinator {
}
try fileManager.createSymbolicLink(atPath: legacyPath, withDestinationPath: Self.openclawSocketPath)
} catch {
self.logger.debug("Failed to create legacy PeekabooBridge socket symlink: \(error.localizedDescription, privacy: .public)")
let message = "Failed to create legacy PeekabooBridge socket symlink: \(error.localizedDescription)"
self.logger
.debug("\(message, privacy: .public)")
}
}

View File

@@ -33,9 +33,10 @@ final class TalkOverlayController {
OverlayPanelFactory.present(
window: self.window,
isVisible: &self.model.isVisible,
target: target) { window in
window.setFrame(target, display: true)
window.orderFrontRegardless()
target: target)
{ window in
window.setFrame(target, display: true)
window.orderFrontRegardless()
}
}

View File

@@ -77,14 +77,14 @@ struct GatewayCostUsageDay: Codable {
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.date = try c.decode(String.self, forKey: .date)
self.totals = GatewayCostUsageTotals(
input: try c.decode(Int.self, forKey: .input),
output: try c.decode(Int.self, forKey: .output),
cacheRead: try c.decode(Int.self, forKey: .cacheRead),
cacheWrite: try c.decode(Int.self, forKey: .cacheWrite),
totalTokens: try c.decode(Int.self, forKey: .totalTokens),
totalCost: try c.decode(Double.self, forKey: .totalCost),
missingCostEntries: try c.decode(Int.self, forKey: .missingCostEntries))
self.totals = try GatewayCostUsageTotals(
input: c.decode(Int.self, forKey: .input),
output: c.decode(Int.self, forKey: .output),
cacheRead: c.decode(Int.self, forKey: .cacheRead),
cacheWrite: c.decode(Int.self, forKey: .cacheWrite),
totalTokens: c.decode(Int.self, forKey: .totalTokens),
totalCost: c.decode(Double.self, forKey: .totalCost),
missingCostEntries: c.decode(Int.self, forKey: .missingCostEntries))
}
func encode(to encoder: Encoder) throws {

View File

@@ -172,9 +172,9 @@ actor VoicePushToTalk {
let adoptedPrefix = self.adoptedPrefix
let adoptedAttributed: NSAttributedString? = adoptedPrefix.isEmpty ? nil : VoiceOverlayTextFormatting
.makeAttributed(
committed: adoptedPrefix,
volatile: "",
isFinal: false)
committed: adoptedPrefix,
volatile: "",
isFinal: false)
self.overlayToken = await MainActor.run {
VoiceSessionCoordinator.shared.startSession(
source: .pushToTalk,
@@ -406,5 +406,4 @@ actor VoicePushToTalk {
if suffix.isEmpty { return prefix }
return "\(prefix) \(suffix)"
}
}

View File

@@ -23,10 +23,11 @@ extension VoiceWakeOverlayController {
"overlay present windowShown textLen=\(self.model.text.count, privacy: .public)")
// Keep the status item in listening mode until we explicitly dismiss the overlay.
AppStateStore.shared.triggerVoiceEars(ttl: nil)
}) { window in
},
onAlreadyVisible: { window in
self.updateWindowFrame(animate: true)
window.orderFrontRegardless()
}
})
}
private func ensureWindow() {

View File

@@ -773,5 +773,4 @@ actor VoiceWakeRuntime {
}
#endif
}

View File

@@ -76,6 +76,8 @@ public final class GatewayDiscoveryModel {
private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:]
private var wideAreaFallbackTask: Task<Void, Never>?
private var wideAreaFallbackGateways: [DiscoveredGateway] = []
private var tailscaleServeFallbackTask: Task<Void, Never>?
private var tailscaleServeFallbackGateways: [DiscoveredGateway] = []
private let logger = Logger(subsystem: "ai.openclaw", category: "gateway-discovery")
public init(
@@ -111,6 +113,7 @@ public final class GatewayDiscoveryModel {
}
self.scheduleWideAreaFallback()
self.scheduleTailscaleServeFallback()
}
public func refreshWideAreaFallbackNow(timeoutSeconds: TimeInterval = 5.0) {
@@ -126,6 +129,23 @@ public final class GatewayDiscoveryModel {
}
}
public func refreshTailscaleServeFallbackNow(timeoutSeconds: TimeInterval = 5.0) {
Task.detached(priority: .utility) { [weak self] in
guard let self else { return }
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: timeoutSeconds)
await MainActor.run { [weak self] in
guard let self else { return }
self.tailscaleServeFallbackGateways = self.mapTailscaleServeBeacons(beacons)
self.recomputeGateways()
}
}
}
public func refreshRemoteFallbackNow(timeoutSeconds: TimeInterval = 5.0) {
self.refreshWideAreaFallbackNow(timeoutSeconds: timeoutSeconds)
self.refreshTailscaleServeFallbackNow(timeoutSeconds: timeoutSeconds)
}
public func stop() {
for browser in self.browsers.values {
browser.cancel()
@@ -140,6 +160,9 @@ public final class GatewayDiscoveryModel {
self.wideAreaFallbackTask?.cancel()
self.wideAreaFallbackTask = nil
self.wideAreaFallbackGateways = []
self.tailscaleServeFallbackTask?.cancel()
self.tailscaleServeFallbackTask = nil
self.tailscaleServeFallbackGateways = []
self.gateways = []
self.statusText = "Stopped"
}
@@ -168,22 +191,45 @@ public final class GatewayDiscoveryModel {
}
}
private func mapTailscaleServeBeacons(
_ beacons: [TailscaleServeGatewayBeacon]) -> [DiscoveredGateway]
{
beacons.map { beacon in
let stableID = "tailscale-serve|\(beacon.tailnetDns.lowercased())"
let isLocal = Self.isLocalGateway(
lanHost: nil,
tailnetDns: beacon.tailnetDns,
displayName: beacon.displayName,
serviceName: nil,
local: self.localIdentity)
return DiscoveredGateway(
displayName: beacon.displayName,
serviceHost: beacon.host,
servicePort: beacon.port,
lanHost: nil,
tailnetDns: beacon.tailnetDns,
sshPort: 22,
gatewayPort: beacon.port,
cliPath: nil,
stableID: stableID,
debugID: "\(beacon.host):\(beacon.port)",
isLocal: isLocal)
}
}
private func recomputeGateways() {
let primary = self.sortedDeduped(gateways: self.gatewaysByDomain.values.flatMap(\.self))
let primaryFiltered = self.filterLocalGateways ? primary.filter { !$0.isLocal } : primary
if !primaryFiltered.isEmpty {
self.gateways = primaryFiltered
return
}
// Bonjour can return only "local" results for the wide-area domain (or no results at all),
// which makes onboarding look empty even though Tailscale DNS-SD can already see gateways.
guard !self.wideAreaFallbackGateways.isEmpty else {
// and cross-network setups may rely on Tailscale Serve without DNS-SD.
let fallback = self.wideAreaFallbackGateways + self.tailscaleServeFallbackGateways
guard !fallback.isEmpty else {
self.gateways = primaryFiltered
return
}
let combined = self.sortedDeduped(gateways: primary + self.wideAreaFallbackGateways)
let combined = self.sortedDeduped(gateways: primary + fallback)
self.gateways = self.filterLocalGateways ? combined.filter { !$0.isLocal } : combined
}
@@ -284,6 +330,39 @@ public final class GatewayDiscoveryModel {
}
}
private func scheduleTailscaleServeFallback() {
if Self.isRunningTests { return }
guard self.tailscaleServeFallbackTask == nil else { return }
self.tailscaleServeFallbackTask = Task.detached(priority: .utility) { [weak self] in
guard let self else { return }
var attempt = 0
let startedAt = Date()
while !Task.isCancelled, Date().timeIntervalSince(startedAt) < 35.0 {
let hasResults = await MainActor.run {
if self.filterLocalGateways {
return !self.gateways.isEmpty
}
return self.gateways.contains(where: { !$0.isLocal })
}
if hasResults { return }
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: 2.4)
if !beacons.isEmpty {
await MainActor.run { [weak self] in
guard let self else { return }
self.tailscaleServeFallbackGateways = self.mapTailscaleServeBeacons(beacons)
self.recomputeGateways()
}
return
}
attempt += 1
let backoff = min(8.0, 0.8 + (Double(attempt) * 0.8))
try? await Task.sleep(nanoseconds: UInt64(backoff * 1_000_000_000))
}
}
}
private var hasUsableWideAreaResults: Bool {
guard let domain = OpenClawBonjour.wideAreaGatewayServiceDomain else { return false }
guard let gateways = self.gatewaysByDomain[domain], !gateways.isEmpty else { return false }
@@ -291,11 +370,25 @@ public final class GatewayDiscoveryModel {
return gateways.contains(where: { !$0.isLocal })
}
static func dedupeKey(for gateway: DiscoveredGateway) -> String {
if let host = gateway.serviceHost?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!host.isEmpty,
let port = gateway.servicePort,
port > 0
{
return "endpoint|\(host):\(port)"
}
return "stable|\(gateway.stableID)"
}
private func sortedDeduped(gateways: [DiscoveredGateway]) -> [DiscoveredGateway] {
var seen = Set<String>()
let deduped = gateways.filter { gateway in
if seen.contains(gateway.stableID) { return false }
seen.insert(gateway.stableID)
let key = Self.dedupeKey(for: gateway)
if seen.contains(key) { return false }
seen.insert(key)
return true
}
return deduped.sorted {

View File

@@ -13,8 +13,8 @@ public enum TailscaleNetwork {
}
public static func detectTailnetIPv4() -> String? {
for entry in NetworkInterfaceIPv4.addresses() {
if self.isTailnetIPv4(entry.ip) { return entry.ip }
for entry in NetworkInterfaceIPv4.addresses() where self.isTailnetIPv4(entry.ip) {
return entry.ip
}
return nil
}

View File

@@ -0,0 +1,315 @@
import Foundation
import OpenClawKit
struct TailscaleServeGatewayBeacon: Sendable, Equatable {
var displayName: String
var tailnetDns: String
var host: String
var port: Int
}
enum TailscaleServeGatewayDiscovery {
private static let maxCandidates = 32
private static let probeConcurrency = 6
private static let defaultProbeTimeoutSeconds: TimeInterval = 1.6
struct DiscoveryContext: Sendable {
var tailscaleStatus: @Sendable () async -> String?
var probeHost: @Sendable (_ host: String, _ timeout: TimeInterval) async -> Bool
static let live = DiscoveryContext(
tailscaleStatus: { await readTailscaleStatus() },
probeHost: { host, timeout in
await probeHostForGatewayChallenge(host: host, timeout: timeout)
})
}
static func discover(
timeoutSeconds: TimeInterval = 3.0,
context: DiscoveryContext = .live) async -> [TailscaleServeGatewayBeacon]
{
guard timeoutSeconds > 0 else { return [] }
guard let statusJson = await context.tailscaleStatus(),
let status = parseStatus(statusJson)
else {
return []
}
let candidates = self.collectCandidates(status: status)
if candidates.isEmpty { return [] }
let deadline = Date().addingTimeInterval(timeoutSeconds)
let perProbeTimeout = min(self.defaultProbeTimeoutSeconds, max(0.5, timeoutSeconds * 0.45))
var byHost: [String: TailscaleServeGatewayBeacon] = [:]
await withTaskGroup(of: TailscaleServeGatewayBeacon?.self) { group in
var index = 0
let workerCount = min(self.probeConcurrency, candidates.count)
func submitOne() {
guard index < candidates.count else { return }
let candidate = candidates[index]
index += 1
group.addTask {
let remaining = deadline.timeIntervalSinceNow
if remaining <= 0 {
return nil
}
let timeout = min(perProbeTimeout, remaining)
let reachable = await context.probeHost(candidate.dnsName, timeout)
if !reachable {
return nil
}
return TailscaleServeGatewayBeacon(
displayName: candidate.displayName,
tailnetDns: candidate.dnsName,
host: candidate.dnsName,
port: 443)
}
}
for _ in 0..<workerCount {
submitOne()
}
while let beacon = await group.next() {
if let beacon {
byHost[beacon.host.lowercased()] = beacon
}
submitOne()
}
}
return byHost.values.sorted {
$0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending
}
}
private struct Candidate: Sendable {
var dnsName: String
var displayName: String
}
private static func collectCandidates(status: TailscaleStatus) -> [Candidate] {
let selfDns = normalizeDnsName(status.selfNode?.dnsName)
var out: [Candidate] = []
var seen = Set<String>()
for node in status.peer.values {
if node.online == false {
continue
}
guard let dnsName = normalizeDnsName(node.dnsName) else {
continue
}
if dnsName == selfDns {
continue
}
if seen.contains(dnsName) {
continue
}
seen.insert(dnsName)
out.append(Candidate(
dnsName: dnsName,
displayName: displayName(hostName: node.hostName, dnsName: dnsName)))
if out.count >= self.maxCandidates {
break
}
}
return out
}
private static func displayName(hostName: String?, dnsName: String) -> String {
if let hostName {
let trimmed = hostName.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
return trimmed
}
}
return dnsName
.split(separator: ".")
.first
.map(String.init) ?? dnsName
}
private static func normalizeDnsName(_ raw: String?) -> String? {
guard let raw else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return nil }
let withoutDot = trimmed.hasSuffix(".") ? String(trimmed.dropLast()) : trimmed
let lower = withoutDot.lowercased()
return lower.isEmpty ? nil : lower
}
private static func readTailscaleStatus() async -> String? {
let candidates = [
"/usr/local/bin/tailscale",
"/opt/homebrew/bin/tailscale",
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
"tailscale",
]
for candidate in candidates {
guard let executable = self.resolveExecutablePath(candidate) else { continue }
if let stdout = await self.run(path: executable, args: ["status", "--json"], timeout: 1.0) {
return stdout
}
}
return nil
}
static func resolveExecutablePath(
_ candidate: String,
env: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
let trimmed = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let fileManager = FileManager.default
let hasPathSeparator = trimmed.contains("/")
if hasPathSeparator {
return fileManager.isExecutableFile(atPath: trimmed) ? trimmed : nil
}
let pathRaw = env["PATH"] ?? ""
let entries = pathRaw.split(separator: ":").map(String.init)
for entry in entries {
let dir = entry.trimmingCharacters(in: .whitespacesAndNewlines)
if dir.isEmpty { continue }
let fullPath = URL(fileURLWithPath: dir)
.appendingPathComponent(trimmed)
.path
if fileManager.isExecutableFile(atPath: fullPath) {
return fullPath
}
}
return nil
}
private static func run(path: String, args: [String], timeout: TimeInterval) async -> String? {
await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .utility).async {
continuation.resume(returning: self.runBlocking(path: path, args: args, timeout: timeout))
}
}
}
private static func runBlocking(path: String, args: [String], timeout: TimeInterval) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: path)
process.arguments = args
let outPipe = Pipe()
process.standardOutput = outPipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
} catch {
return nil
}
let deadline = Date().addingTimeInterval(timeout)
while process.isRunning, Date() < deadline {
Thread.sleep(forTimeInterval: 0.02)
}
if process.isRunning {
process.terminate()
}
process.waitUntilExit()
let data = (try? outPipe.fileHandleForReading.readToEnd()) ?? Data()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
return output?.isEmpty == false ? output : nil
}
private static func parseStatus(_ raw: String) -> TailscaleStatus? {
guard let data = raw.data(using: .utf8) else { return nil }
return try? JSONDecoder().decode(TailscaleStatus.self, from: data)
}
private static func probeHostForGatewayChallenge(host: String, timeout: TimeInterval) async -> Bool {
var components = URLComponents()
components.scheme = "wss"
components.host = host
guard let url = components.url else { return false }
let config = URLSessionConfiguration.ephemeral
config.timeoutIntervalForRequest = max(0.5, timeout)
config.timeoutIntervalForResource = max(0.5, timeout)
let session = URLSession(configuration: config)
let task = session.webSocketTask(with: url)
task.resume()
defer {
task.cancel(with: .goingAway, reason: nil)
session.invalidateAndCancel()
}
do {
return try await AsyncTimeout.withTimeout(
seconds: timeout,
onTimeout: { NSError(domain: "TailscaleServeDiscovery", code: 1, userInfo: nil) },
operation: {
while true {
let message = try await task.receive()
if isConnectChallenge(message: message) {
return true
}
}
})
} catch {
return false
}
}
private static func isConnectChallenge(message: URLSessionWebSocketTask.Message) -> Bool {
let data: Data
switch message {
case let .data(value):
data = value
case let .string(value):
guard let encoded = value.data(using: .utf8) else { return false }
data = encoded
@unknown default:
return false
}
guard let object = try? JSONSerialization.jsonObject(with: data),
let dict = object as? [String: Any],
let type = dict["type"] as? String,
type == "event",
let event = dict["event"] as? String
else {
return false
}
return event == "connect.challenge"
}
}
private struct TailscaleStatus: Decodable {
struct Node: Decodable {
let dnsName: String?
let hostName: String?
let online: Bool?
private enum CodingKeys: String, CodingKey {
case dnsName = "DNSName"
case hostName = "HostName"
case online = "Online"
}
}
let selfNode: Node?
let peer: [String: Node]
private enum CodingKeys: String, CodingKey {
case selfNode = "Self"
case peer = "Peer"
}
}

View File

@@ -1030,6 +1030,74 @@ public struct PushTestResult: Codable, Sendable {
}
}
public struct SecretsReloadParams: Codable, Sendable {}
public struct SecretsResolveParams: Codable, Sendable {
public let commandname: String
public let targetids: [String]
public init(
commandname: String,
targetids: [String])
{
self.commandname = commandname
self.targetids = targetids
}
private enum CodingKeys: String, CodingKey {
case commandname = "commandName"
case targetids = "targetIds"
}
}
public struct SecretsResolveAssignment: Codable, Sendable {
public let path: String?
public let pathsegments: [String]
public let value: AnyCodable
public init(
path: String?,
pathsegments: [String],
value: AnyCodable)
{
self.path = path
self.pathsegments = pathsegments
self.value = value
}
private enum CodingKeys: String, CodingKey {
case path
case pathsegments = "pathSegments"
case value
}
}
public struct SecretsResolveResult: Codable, Sendable {
public let ok: Bool?
public let assignments: [SecretsResolveAssignment]?
public let diagnostics: [String]?
public let inactiverefpaths: [String]?
public init(
ok: Bool?,
assignments: [SecretsResolveAssignment]?,
diagnostics: [String]?,
inactiverefpaths: [String]?)
{
self.ok = ok
self.assignments = assignments
self.diagnostics = diagnostics
self.inactiverefpaths = inactiverefpaths
}
private enum CodingKeys: String, CodingKey {
case ok
case assignments
case diagnostics
case inactiverefpaths = "inactiveRefPaths"
}
}
public struct SessionsListParams: Codable, Sendable {
public let limit: Int?
public let activeminutes: Int?

View File

@@ -1,4 +1,4 @@
import OpenClawDiscovery
@testable import OpenClawDiscovery
import Testing
@Suite
@@ -121,4 +121,50 @@ struct GatewayDiscoveryModelTests {
host: "studio.local",
port: 2201) == "peter@studio.local:2201")
}
@Test func dedupeKeyPrefersResolvedEndpointAcrossSources() {
let wideArea = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: "gateway-host.tailnet-example.ts.net",
servicePort: 443,
lanHost: nil,
tailnetDns: "gateway-host.tailnet-example.ts.net",
sshPort: 22,
gatewayPort: 443,
cliPath: nil,
stableID: "wide-area|openclaw.internal.|gateway-host",
debugID: "wide-area",
isLocal: false)
let serve = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: "gateway-host.tailnet-example.ts.net",
servicePort: 443,
lanHost: nil,
tailnetDns: "gateway-host.tailnet-example.ts.net",
sshPort: 22,
gatewayPort: 443,
cliPath: nil,
stableID: "tailscale-serve|gateway-host.tailnet-example.ts.net",
debugID: "serve",
isLocal: false)
#expect(GatewayDiscoveryModel.dedupeKey(for: wideArea) == GatewayDiscoveryModel.dedupeKey(for: serve))
}
@Test func dedupeKeyFallsBackToStableIDWithoutEndpoint() {
let unresolved = GatewayDiscoveryModel.DiscoveredGateway(
displayName: "Gateway",
serviceHost: nil,
servicePort: nil,
lanHost: nil,
tailnetDns: "gateway-host.tailnet-example.ts.net",
sshPort: 22,
gatewayPort: nil,
cliPath: nil,
stableID: "tailscale-serve|gateway-host.tailnet-example.ts.net",
debugID: "serve",
isLocal: false)
#expect(GatewayDiscoveryModel.dedupeKey(for: unresolved) == "stable|tailscale-serve|gateway-host.tailnet-example.ts.net")
}
}

View File

@@ -0,0 +1,77 @@
import Foundation
import Testing
@testable import OpenClawDiscovery
@Suite
struct TailscaleServeGatewayDiscoveryTests {
@Test func discoversServeGatewayFromTailnetPeers() async {
let statusJson = """
{
"Self": {
"DNSName": "local-mac.tailnet-example.ts.net.",
"HostName": "local-mac",
"Online": true
},
"Peer": {
"peer-1": {
"DNSName": "gateway-host.tailnet-example.ts.net.",
"HostName": "gateway-host",
"Online": true
},
"peer-2": {
"DNSName": "offline.tailnet-example.ts.net.",
"HostName": "offline-box",
"Online": false
},
"peer-3": {
"DNSName": "local-mac.tailnet-example.ts.net.",
"HostName": "local-mac",
"Online": true
}
}
}
"""
let context = TailscaleServeGatewayDiscovery.DiscoveryContext(
tailscaleStatus: { statusJson },
probeHost: { host, _ in
host == "gateway-host.tailnet-example.ts.net"
})
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: 2.0, context: context)
#expect(beacons.count == 1)
#expect(beacons.first?.displayName == "gateway-host")
#expect(beacons.first?.tailnetDns == "gateway-host.tailnet-example.ts.net")
#expect(beacons.first?.host == "gateway-host.tailnet-example.ts.net")
#expect(beacons.first?.port == 443)
}
@Test func returnsEmptyWhenStatusUnavailable() async {
let context = TailscaleServeGatewayDiscovery.DiscoveryContext(
tailscaleStatus: { nil },
probeHost: { _, _ in true })
let beacons = await TailscaleServeGatewayDiscovery.discover(timeoutSeconds: 2.0, context: context)
#expect(beacons.isEmpty)
}
@Test func resolvesBareExecutableFromPATH() throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDir) }
let executable = tempDir.appendingPathComponent("tailscale")
try "#!/bin/sh\necho ok\n".write(to: executable, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
let env: [String: String] = ["PATH": tempDir.path]
let resolved = TailscaleServeGatewayDiscovery.resolveExecutablePath("tailscale", env: env)
#expect(resolved == executable.path)
}
@Test func rejectsMissingExecutableCandidate() {
#expect(TailscaleServeGatewayDiscovery.resolveExecutablePath("", env: [:]) == nil)
#expect(TailscaleServeGatewayDiscovery.resolveExecutablePath("definitely-not-here", env: ["PATH": "/tmp"]) == nil)
}
}

View File

@@ -59,7 +59,7 @@ extension URLSession: WebSocketSessioning {
public func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
let task = self.webSocketTask(with: url)
// Avoid "Message too long" receive errors for large snapshots / history payloads.
task.maximumMessageSize = 16 * 1024 * 1024 // 16 MB
task.maximumMessageSize = 4 * 1024 * 1024 // 4 MB
return WebSocketTaskBox(task: task)
}
}

View File

@@ -17,23 +17,70 @@ public struct GatewayTLSParams: Sendable {
}
public enum GatewayTLSStore {
private static let suiteName = "ai.openclaw.shared"
private static let keyPrefix = "gateway.tls."
private static let keychainService = "ai.openclaw.tls-pinning"
private static var defaults: UserDefaults {
UserDefaults(suiteName: suiteName) ?? .standard
}
// Legacy UserDefaults location used before Keychain migration.
private static let legacySuiteName = "ai.openclaw.shared"
private static let legacyKeyPrefix = "gateway.tls."
public static func loadFingerprint(stableID: String) -> String? {
let key = self.keyPrefix + stableID
let raw = self.defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines)
self.migrateFromUserDefaultsIfNeeded(stableID: stableID)
let raw = self.keychainLoad(account: stableID)?.trimmingCharacters(in: .whitespacesAndNewlines)
if raw?.isEmpty == false { return raw }
return nil
}
public static func saveFingerprint(_ value: String, stableID: String) {
let key = self.keyPrefix + stableID
self.defaults.set(value, forKey: key)
self.keychainSave(value, account: stableID)
}
// MARK: - Migration
/// On first Keychain read for a given stableID, move any legacy UserDefaults
/// fingerprint into Keychain and remove the old entry.
private static func migrateFromUserDefaultsIfNeeded(stableID: String) {
guard let defaults = UserDefaults(suiteName: self.legacySuiteName) else { return }
let legacyKey = self.legacyKeyPrefix + stableID
guard let existing = defaults.string(forKey: legacyKey)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!existing.isEmpty
else { return }
if self.keychainLoad(account: stableID) == nil {
guard self.keychainSave(existing, account: stableID) else { return }
}
defaults.removeObject(forKey: legacyKey)
}
// MARK: - Self-contained Keychain helpers (OpenClawKit can't import iOS KeychainStore)
private static func keychainLoad(account: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.keychainService,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
@discardableResult
private static func keychainSave(_ value: String, account: String) -> Bool {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.keychainService,
kSecAttrAccount as String: account,
]
// Delete-then-add to enforce accessibility attribute.
SecItemDelete(query as CFDictionary)
var insert = query
insert[kSecValueData as String] = data
insert[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
return SecItemAdd(insert as CFDictionary, nil) == errSecSuccess
}
}
@@ -52,7 +99,7 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS
public func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
let task = self.session.webSocketTask(with: url)
task.maximumMessageSize = 16 * 1024 * 1024
task.maximumMessageSize = 4 * 1024 * 1024
return WebSocketTaskBox(task: task)
}

View File

@@ -1030,6 +1030,74 @@ public struct PushTestResult: Codable, Sendable {
}
}
public struct SecretsReloadParams: Codable, Sendable {}
public struct SecretsResolveParams: Codable, Sendable {
public let commandname: String
public let targetids: [String]
public init(
commandname: String,
targetids: [String])
{
self.commandname = commandname
self.targetids = targetids
}
private enum CodingKeys: String, CodingKey {
case commandname = "commandName"
case targetids = "targetIds"
}
}
public struct SecretsResolveAssignment: Codable, Sendable {
public let path: String?
public let pathsegments: [String]
public let value: AnyCodable
public init(
path: String?,
pathsegments: [String],
value: AnyCodable)
{
self.path = path
self.pathsegments = pathsegments
self.value = value
}
private enum CodingKeys: String, CodingKey {
case path
case pathsegments = "pathSegments"
case value
}
}
public struct SecretsResolveResult: Codable, Sendable {
public let ok: Bool?
public let assignments: [SecretsResolveAssignment]?
public let diagnostics: [String]?
public let inactiverefpaths: [String]?
public init(
ok: Bool?,
assignments: [SecretsResolveAssignment]?,
diagnostics: [String]?,
inactiverefpaths: [String]?)
{
self.ok = ok
self.assignments = assignments
self.diagnostics = diagnostics
self.inactiverefpaths = inactiverefpaths
}
private enum CodingKeys: String, CodingKey {
case ok
case assignments
case diagnostics
case inactiverefpaths = "inactiveRefPaths"
}
}
public struct SessionsListParams: Codable, Sendable {
public let limit: Int?
public let activeminutes: Int?

View File

@@ -1,13 +0,0 @@
# Changelog Fragments
Use this directory when a PR should not edit `CHANGELOG.md` directly.
- One fragment file per PR.
- File name recommendation: `pr-<number>.md`.
- Include at least one line with both `#<pr>` and `thanks @<contributor>`.
Example:
```md
- Fix LINE monitor lifecycle wait ownership (#27001) (thanks @alice)
```

View File

@@ -1 +0,0 @@
- Clarify block reply pipeline seen-check parameter naming for maintainability (#5080) (thanks @yassine20011)

View File

@@ -1 +0,0 @@
- Memory flush: fix usage-threshold gating and transcript fallback paths so flushes run reliably when expected (#5343) (thanks @jarvis-medmatic)

View File

@@ -258,7 +258,9 @@ Triggered when the gateway starts:
Triggered when messages are received or sent:
- **`message`**: All message events (general listener)
- **`message:received`**: When an inbound message is received from any channel
- **`message:received`**: When an inbound message is received from any channel. Fires early in processing before media understanding. Content may contain raw placeholders like `<media:audio>` for media attachments that haven't been processed yet.
- **`message:transcribed`**: When a message has been fully processed, including audio transcription and link understanding. At this point, `transcript` contains the full transcript text for audio messages. Use this hook when you need access to transcribed audio content.
- **`message:preprocessed`**: Fires for every message after all media + link understanding completes, giving hooks access to the fully enriched body (transcripts, image descriptions, link summaries) before the agent sees it.
- **`message:sent`**: When an outbound message is successfully sent
#### Message Event Context
@@ -297,6 +299,30 @@ Message events include rich context about the message:
accountId?: string, // Provider account ID
conversationId?: string, // Chat/conversation ID
messageId?: string, // Message ID returned by the provider
isGroup?: boolean, // Whether this outbound message belongs to a group/channel context
groupId?: string, // Group/channel identifier for correlation with message:received
}
// message:transcribed context
{
body?: string, // Raw inbound body before enrichment
bodyForAgent?: string, // Enriched body visible to the agent
transcript: string, // Audio transcript text
channelId: string, // Channel (e.g., "telegram", "whatsapp")
conversationId?: string,
messageId?: string,
}
// message:preprocessed context
{
body?: string, // Raw inbound body
bodyForAgent?: string, // Final enriched body after media/link understanding
transcript?: string, // Transcript when audio was present
channelId: string, // Channel (e.g., "telegram", "whatsapp")
conversationId?: string,
messageId?: string,
isGroup?: boolean,
groupId?: string,
}
```

View File

@@ -159,7 +159,7 @@ Mapping options (summary):
## Responses
- `200` for `/hooks/wake`
- `202` for `/hooks/agent` (async run started)
- `200` for `/hooks/agent` (async run accepted)
- `401` on auth failure
- `429` after repeated auth failures from the same client (check `Retry-After`)
- `400` on invalid payload

View File

@@ -36,6 +36,7 @@ OpenClaw uses Brave Search as the default provider for `web_search`.
## Notes
- The Data for AI plan is **not** compatible with `web_search`.
- Brave provides a free tier plus paid plans; check the Brave API portal for current limits.
- Brave provides paid plans; check the Brave API portal for current limits.
- Brave Terms include restrictions on some AI-related uses of Search Results. Review the Brave Terms of Service and confirm your intended use is compliant. For legal questions, consult your counsel.
See [Web tools](/tools/web) for the full web_search configuration.

View File

@@ -17,6 +17,7 @@ host configuration.
- **AccountId**: perchannel account instance (when supported).
- Optional channel default account: `channels.<channel>.defaultAccount` chooses
which account is used when an outbound path does not specify `accountId`.
- In multi-account setups, set an explicit default (`defaultAccount` or `accounts.default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID.
- **AgentId**: an isolated workspace + session store (“brain”).
- **SessionKey**: the bucket key used to store context and control concurrency.

View File

@@ -944,6 +944,7 @@ Auto-join example:
Notes:
- `voice.tts` overrides `messages.tts` for voice playback only.
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
- Voice is enabled by default; set `channels.discord.voice.enabled=false` to disable it.
- `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options.
- `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset.

View File

@@ -197,6 +197,17 @@ Edit `~/.openclaw/openclaw.json`:
If you use `connectionMode: "webhook"`, set `verificationToken`. The Feishu webhook server binds to `127.0.0.1` by default; set `webhookHost` only if you intentionally need a different bind address.
#### Verification Token (webhook mode)
When using webhook mode, set `channels.feishu.verificationToken` in your config. To get the value:
1. In Feishu Open Platform, open your app
2. Go to **Development****Events & Callbacks** (开发配置 → 事件与回调)
3. Open the **Encryption** tab (加密策略)
4. Copy **Verification Token**
![Verification Token location](../images/feishu-verification-token.png)
### Configure via environment variables
```bash
@@ -359,9 +370,9 @@ After approval, you can chat normally.
}
```
### Allow specific users to run control commands in a group (e.g. /reset, /new)
### Restrict which senders can message in a group (sender allowlist)
In addition to allowing the group itself, control commands are gated by the **sender** open_id.
In addition to allowing the group itself, **all messages** in that group are gated by the sender open_id: only users listed in `groups.<chat_id>.allowFrom` have their messages processed; messages from other members are ignored (this is full sender-level gating, not only for control commands like /reset or /new).
```json5
{

View File

@@ -55,6 +55,45 @@ Minimal config:
}
```
## Native slash commands
Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via
the Mattermost API and receives callback POSTs on the gateway HTTP server.
```json5
{
channels: {
mattermost: {
commands: {
native: true,
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Use when Mattermost cannot reach the gateway directly (reverse proxy/public URL).
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
},
},
}
```
Notes:
- `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable.
- If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`.
- For multi-account setups, `commands` can be set at the top level or under
`channels.mattermost.accounts.<id>.commands` (account values override top-level fields).
- Command callbacks are validated with per-command tokens and fail closed when token checks fail.
- Reachability requirement: the callback endpoint must be reachable from the Mattermost server.
- Do not set `callbackUrl` to `localhost` unless Mattermost runs on the same host/network namespace as OpenClaw.
- Do not set `callbackUrl` to your Mattermost base URL unless that URL reverse-proxies `/api/channels/mattermost/command` to OpenClaw.
- A quick check is `curl https://<gateway-host>/api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`.
- Mattermost egress allowlist requirement:
- If your callback targets private/tailnet/internal addresses, set Mattermost
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
- Use host/domain entries, not full URLs.
- Good: `gateway.tailnet-name.ts.net`
- Bad: `https://gateway.tailnet-name.ts.net`
## Environment variables (default account)
Set these on the gateway host if you prefer env vars:

View File

@@ -230,23 +230,31 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
## Feature reference
<AccordionGroup>
<Accordion title="Live stream preview (message edits)">
OpenClaw can stream partial replies by sending a temporary Telegram message and editing it as text arrives.
<Accordion title="Live stream preview (native drafts + message edits)">
OpenClaw can stream partial replies in real time:
- direct chats: Telegram native draft streaming via `sendMessageDraft`
- groups/topics: preview message + `editMessageText`
Requirement:
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `off`)
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
- `progress` maps to `partial` on Telegram (compat with cross-channel naming)
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
This works in direct chats and groups/topics.
Telegram enabled `sendMessageDraft` for all bots in Bot API 9.5 (March 1, 2026).
For text-only replies, OpenClaw keeps the same preview message and performs a final edit in place (no second message).
For text-only replies:
- DM: OpenClaw updates the draft in place (no extra preview message)
- group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
Preview streaming is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming.
If native draft transport is unavailable/rejected, OpenClaw automatically falls back to `sendMessage` + `editMessageText`.
Telegram-only reasoning stream:
- `/reasoning stream` sends reasoning to the live preview while generating
@@ -731,6 +739,8 @@ Primary reference:
- `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `channels.telegram.groupAllowFrom`: group sender allowlist (numeric Telegram user IDs). `openclaw doctor --fix` can resolve legacy `@username` entries to IDs. Non-numeric entries are ignored at auth time. Group auth does not use DM pairing-store fallback (`2026.2.25+`).
- Multi-account precedence:
- When two or more account IDs are configured, set `channels.telegram.defaultAccount` (or include `channels.telegram.accounts.default`) to make default routing explicit.
- If neither is set, OpenClaw falls back to the first normalized account ID and `openclaw doctor` warns.
- `channels.telegram.accounts.default.allowFrom` and `channels.telegram.accounts.default.groupAllowFrom` apply only to the `default` account.
- Named accounts inherit `channels.telegram.allowFrom` and `channels.telegram.groupAllowFrom` when account-level values are unset.
- Named accounts do not inherit `channels.telegram.accounts.default.allowFrom` / `groupAllowFrom`.
@@ -751,7 +761,7 @@ Primary reference:
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
- `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `off`; `progress` maps to `partial`; `block` is legacy preview mode compatibility).
- `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). In DMs, `partial` uses native `sendMessageDraft` when available.
- `channels.telegram.mediaMaxMb`: inbound Telegram media download/processing cap (MB).
- `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter).
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled.

View File

@@ -11,8 +11,8 @@ Tlon is a decentralized messenger built on Urbit. OpenClaw connects to your Urbi
respond to DMs and group chat messages. Group replies require an @ mention by default and can
be further restricted via allowlists.
Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback
(URL appended to caption). Reactions, polls, and native media uploads are not supported.
Status: supported via plugin. DMs, group mentions, thread replies, rich text formatting, and
image uploads are supported. Reactions and polls are not yet supported.
## Plugin required
@@ -50,27 +50,38 @@ Minimal config (single account):
ship: "~sampel-palnet",
url: "https://your-ship-host",
code: "lidlut-tabwed-pillex-ridrup",
ownerShip: "~your-main-ship", // recommended: your ship, always allowed
},
},
}
```
Private/LAN ship URLs (advanced):
## Private/LAN ships
By default, OpenClaw blocks private/internal hostnames and IP ranges for this plugin (SSRF hardening).
If your ship URL is on a private network (for example `http://192.168.1.50:8080` or `http://localhost:8080`),
By default, OpenClaw blocks private/internal hostnames and IP ranges for SSRF protection.
If your ship is running on a private network (localhost, LAN IP, or internal hostname),
you must explicitly opt in:
```json5
{
channels: {
tlon: {
url: "http://localhost:8080",
allowPrivateNetwork: true,
},
},
}
```
This applies to URLs like:
- `http://localhost:8080`
- `http://192.168.x.x:8080`
- `http://my-ship.local:8080`
⚠️ Only enable this if you trust your local network. This setting disables SSRF protections
for requests to your ship URL.
## Group channels
Auto-discovery is enabled by default. You can also pin channels manually:
@@ -99,7 +110,7 @@ Disable auto-discovery:
## Access control
DM allowlist (empty = allow all):
DM allowlist (empty = no DMs allowed, use `ownerShip` for approval flow):
```json5
{
@@ -134,6 +145,56 @@ Group authorization (restricted by default):
}
```
## Owner and approval system
Set an owner ship to receive approval requests when unauthorized users try to interact:
```json5
{
channels: {
tlon: {
ownerShip: "~your-main-ship",
},
},
}
```
The owner ship is **automatically authorized everywhere** — DM invites are auto-accepted and
channel messages are always allowed. You don't need to add the owner to `dmAllowlist` or
`defaultAuthorizedShips`.
When set, the owner receives DM notifications for:
- DM requests from ships not in the allowlist
- Mentions in channels without authorization
- Group invite requests
## Auto-accept settings
Auto-accept DM invites (for ships in dmAllowlist):
```json5
{
channels: {
tlon: {
autoAcceptDmInvites: true,
},
},
}
```
Auto-accept group invites:
```json5
{
channels: {
tlon: {
autoAcceptGroupInvites: true,
},
},
}
```
## Delivery targets (CLI/cron)
Use these with `openclaw message send` or cron delivery:
@@ -141,8 +202,75 @@ Use these with `openclaw message send` or cron delivery:
- DM: `~sampel-palnet` or `dm/~sampel-palnet`
- Group: `chat/~host-ship/channel` or `group:~host-ship/channel`
## Bundled skill
The Tlon plugin includes a bundled skill ([`@tloncorp/tlon-skill`](https://github.com/tloncorp/tlon-skill))
that provides CLI access to Tlon operations:
- **Contacts**: get/update profiles, list contacts
- **Channels**: list, create, post messages, fetch history
- **Groups**: list, create, manage members
- **DMs**: send messages, react to messages
- **Reactions**: add/remove emoji reactions to posts and DMs
- **Settings**: manage plugin permissions via slash commands
The skill is automatically available when the plugin is installed.
## Capabilities
| Feature | Status |
| --------------- | --------------------------------------- |
| Direct messages | ✅ Supported |
| Groups/channels | ✅ Supported (mention-gated by default) |
| Threads | ✅ Supported (auto-replies in thread) |
| Rich text | ✅ Markdown converted to Tlon format |
| Images | ✅ Uploaded to Tlon storage |
| Reactions | ✅ Via [bundled skill](#bundled-skill) |
| Polls | ❌ Not yet supported |
| Native commands | ✅ Supported (owner-only by default) |
## Troubleshooting
Run this ladder first:
```bash
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
```
Common failures:
- **DMs ignored**: sender not in `dmAllowlist` and no `ownerShip` configured for approval flow.
- **Group messages ignored**: channel not discovered or sender not authorized.
- **Connection errors**: check ship URL is reachable; enable `allowPrivateNetwork` for local ships.
- **Auth errors**: verify login code is current (codes rotate).
## Configuration reference
Full configuration: [Configuration](/gateway/configuration)
Provider options:
- `channels.tlon.enabled`: enable/disable channel startup.
- `channels.tlon.ship`: bot's Urbit ship name (e.g. `~sampel-palnet`).
- `channels.tlon.url`: ship URL (e.g. `https://sampel-palnet.tlon.network`).
- `channels.tlon.code`: ship login code.
- `channels.tlon.allowPrivateNetwork`: allow localhost/LAN URLs (SSRF bypass).
- `channels.tlon.ownerShip`: owner ship for approval system (always authorized).
- `channels.tlon.dmAllowlist`: ships allowed to DM (empty = none).
- `channels.tlon.autoAcceptDmInvites`: auto-accept DMs from allowlisted ships.
- `channels.tlon.autoAcceptGroupInvites`: auto-accept all group invites.
- `channels.tlon.autoDiscoverChannels`: auto-discover group channels (default: true).
- `channels.tlon.groupChannels`: manually pinned channel nests.
- `channels.tlon.defaultAuthorizedShips`: ships authorized for all channels.
- `channels.tlon.authorization.channelRules`: per-channel auth rules.
- `channels.tlon.showModelSignature`: append model name to messages.
## Notes
- Group replies require a mention (e.g. `~your-bot-ship`) to respond.
- Thread replies: if the inbound message is in a thread, OpenClaw replies in-thread.
- Media: `sendMedia` falls back to text + URL (no native upload).
- Rich text: Markdown formatting (bold, italic, code, headers, lists) is converted to Tlon's native format.
- Images: URLs are uploaded to Tlon storage and embedded as image blocks.

View File

@@ -13,20 +13,20 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
## Job Overview
| Job | Purpose | When it runs |
| ----------------- | ----------------------------------------------- | ------------------------- |
| `docs-scope` | Detect docs-only changes | Always |
| `changed-scope` | Detect which areas changed (node/macos/android) | Non-docs PRs |
| `check` | TypeScript types, lint, format | Non-docs changes |
| `check-docs` | Markdown lint + broken link check | Docs changed |
| `code-analysis` | LOC threshold check (1000 lines) | PRs only |
| `secrets` | Detect leaked secrets | Always |
| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes |
| `release-check` | Validate npm pack contents | After build |
| `checks` | Node/Bun tests + protocol check | Non-docs, node changes |
| `checks-windows` | Windows-specific tests | Non-docs, node changes |
| `macos` | Swift lint/build/test + TS tests | PRs with macos changes |
| `android` | Gradle build + tests | Non-docs, android changes |
| Job | Purpose | When it runs |
| ----------------- | ------------------------------------------------------- | ------------------------------------------------- |
| `docs-scope` | Detect docs-only changes | Always |
| `changed-scope` | Detect which areas changed (node/macos/android/windows) | Non-docs PRs |
| `check` | TypeScript types, lint, format | Push to `main`, or PRs with Node-relevant changes |
| `check-docs` | Markdown lint + broken link check | Docs changed |
| `code-analysis` | LOC threshold check (1000 lines) | PRs only |
| `secrets` | Detect leaked secrets | Always |
| `build-artifacts` | Build dist once, share with other jobs | Non-docs, node changes |
| `release-check` | Validate npm pack contents | After build |
| `checks` | Node/Bun tests + protocol check | Non-docs, node changes |
| `checks-windows` | Windows-specific tests | Non-docs, windows-relevant changes |
| `macos` | Swift lint/build/test + TS tests | PRs with macos changes |
| `android` | Gradle build + tests | Non-docs, android changes |
## Fail-Fast Order
@@ -36,12 +36,14 @@ Jobs are ordered so cheap checks fail before expensive ones run:
2. `build-artifacts` (blocked on above)
3. `checks`, `checks-windows`, `macos`, `android` (blocked on build)
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
## Runners
| Runner | Jobs |
| -------------------------------- | ------------------------------------------ |
| `blacksmith-16vcpu-ubuntu-2404` | Most Linux jobs, including scope detection |
| `blacksmith-16vcpu-windows-2025` | `checks-windows` |
| `blacksmith-32vcpu-windows-2025` | `checks-windows` |
| `macos-latest` | `macos`, `ios` |
## Local Equivalents

View File

@@ -828,7 +828,7 @@ Tip: when calling `config.set`/`config.apply`/`config.patch` directly, pass `bas
See [/concepts/models](/concepts/models) for fallback behavior and scanning strategy.
Preferred Anthropic auth (setup-token):
Anthropic setup-token (supported):
```bash
claude setup-token
@@ -836,6 +836,10 @@ openclaw models auth setup-token --provider anthropic
openclaw models status
```
Policy note: this is technical compatibility. Anthropic has blocked some
subscription usage outside Claude Code in the past; verify current Anthropic
terms before relying on setup-token in production.
### `models` (root)
`openclaw models` is an alias for `models status`.

View File

@@ -50,3 +50,5 @@ Notes:
- `memory status --deep --index` runs a reindex if the store is dirty.
- `memory index --verbose` prints per-phase details (provider, model, sources, batch activity).
- `memory status` includes any extra paths configured via `memorySearch.extraPaths`.
- If effectively active memory remote API key fields are configured as SecretRefs, the command resolves those values from the active gateway snapshot. If gateway is unavailable, the command fails fast.
- Gateway version skew note: this command path requires a gateway that supports `secrets.resolve`; older gateways return an unknown-method error.

View File

@@ -77,3 +77,4 @@ Notes:
- `setup-token` prompts for a setup-token value (generate it with `claude setup-token` on any machine).
- `paste-token` accepts a token string generated elsewhere or from automation.
- Anthropic policy note: setup-token support is technical compatibility. Anthropic has blocked some subscription usage outside Claude Code in the past, so verify current terms before using it broadly.

View File

@@ -34,6 +34,9 @@ openclaw qr --url wss://gateway.example/ws --token '<token>'
## Notes
- `--token` and `--password` are mutually exclusive.
- With `--remote`, if effectively active remote credentials are configured as SecretRefs and you do not pass `--token` or `--password`, the command resolves them from the active gateway snapshot. If gateway is unavailable, the command fails fast.
- Without `--remote`, local `gateway.auth.password` SecretRefs are resolved when password auth can win (explicit `gateway.auth.mode="password"` or inferred password mode with no winning token from auth/env), and no CLI auth override is passed.
- Gateway version skew note: this command path requires a gateway that supports `secrets.resolve`; older gateways return an unknown-method error.
- After scanning, approve device pairing with:
- `openclaw devices list`
- `openclaw devices approve <requestId>`

View File

@@ -9,14 +9,14 @@ title: "secrets"
# `openclaw secrets`
Use `openclaw secrets` to migrate credentials from plaintext to SecretRefs and keep the active secrets runtime healthy.
Use `openclaw secrets` to manage SecretRefs and keep the active runtime snapshot healthy.
Command roles:
- `reload`: gateway RPC (`secrets.reload`) that re-resolves refs and swaps runtime snapshot only on full success (no config writes).
- `audit`: read-only scan of config + auth stores + legacy residues (`.env`, `auth.json`) for plaintext, unresolved refs, and precedence drift.
- `configure`: interactive planner for provider setup + target mapping + preflight (TTY required).
- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub migrated plaintext residues.
- `audit`: read-only scan of configuration/auth stores and legacy residues for plaintext, unresolved refs, and precedence drift.
- `configure`: interactive planner for provider setup, target mapping, and preflight (TTY required).
- `apply`: execute a saved plan (`--dry-run` for validation only), then scrub targeted plaintext residues.
Recommended operator loop:
@@ -31,11 +31,13 @@ openclaw secrets reload
Exit code note for CI/gates:
- `audit --check` returns `1` on findings, `2` when refs are unresolved.
- `audit --check` returns `1` on findings.
- unresolved refs return `2`.
Related:
- Secrets guide: [Secrets Management](/gateway/secrets)
- Credential surface: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- Security guide: [Security](/gateway/security)
## Reload runtime snapshot
@@ -59,8 +61,8 @@ Scan OpenClaw state for:
- plaintext secret storage
- unresolved refs
- precedence drift (`auth-profiles` shadowing config refs)
- legacy residues (`auth.json`, OAuth out-of-scope notes)
- precedence drift (`auth-profiles.json` credentials shadowing `openclaw.json` refs)
- legacy residues (legacy auth store entries, OAuth reminders)
```bash
openclaw secrets audit
@@ -71,7 +73,7 @@ openclaw secrets audit --json
Exit behavior:
- `--check` exits non-zero on findings.
- unresolved refs exit with a higher-priority non-zero code.
- unresolved refs exit with higher-priority non-zero code.
Report shape highlights:
@@ -85,7 +87,7 @@ Report shape highlights:
## Configure (interactive helper)
Build provider + SecretRef changes interactively, run preflight, and optionally apply:
Build provider and SecretRef changes interactively, run preflight, and optionally apply:
```bash
openclaw secrets configure
@@ -93,6 +95,7 @@ openclaw secrets configure --plan-out /tmp/openclaw-secrets-plan.json
openclaw secrets configure --apply --yes
openclaw secrets configure --providers-only
openclaw secrets configure --skip-provider-setup
openclaw secrets configure --agent ops
openclaw secrets configure --json
```
@@ -106,23 +109,26 @@ Flags:
- `--providers-only`: configure `secrets.providers` only, skip credential mapping.
- `--skip-provider-setup`: skip provider setup and map credentials to existing providers.
- `--agent <id>`: scope `auth-profiles.json` target discovery and writes to one agent store.
Notes:
- Requires an interactive TTY.
- You cannot combine `--providers-only` with `--skip-provider-setup`.
- `configure` targets secret-bearing fields in `openclaw.json`.
- Include all secret-bearing fields you intend to migrate (for example both `models.providers.*.apiKey` and `skills.entries.*.apiKey`) so audit can reach a clean state.
- `configure` targets secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for the selected agent scope.
- `configure` supports creating new `auth-profiles.json` mappings directly in the picker flow.
- Canonical supported surface: [SecretRef Credential Surface](/reference/secretref-credential-surface).
- It performs preflight resolution before apply.
- Generated plans default to scrub options (`scrubEnv`, `scrubAuthProfilesForProviderTargets`, `scrubLegacyAuthJson` all enabled).
- Apply path is one-way for migrated plaintext values.
- Apply path is one-way for scrubbed plaintext values.
- Without `--apply`, CLI still prompts `Apply this plan now?` after preflight.
- With `--apply` (and no `--yes`), CLI prompts an extra irreversible-migration confirmation.
- With `--apply` (and no `--yes`), CLI prompts an extra irreversible confirmation.
Exec provider safety note:
- Homebrew installs often expose symlinked binaries under `/opt/homebrew/bin/*`.
- Set `allowSymlinkCommand: true` only when needed for trusted package-manager paths, and pair it with `trustedDirs` (for example `["/opt/homebrew"]`).
- On Windows, if ACL verification is unavailable for a provider path, OpenClaw fails closed. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
## Apply a saved plan
@@ -154,10 +160,9 @@ Safety comes from strict preflight + atomic-ish apply with best-effort in-memory
## Example
```bash
# Audit first, then configure, then confirm clean:
openclaw secrets audit --check
openclaw secrets configure
openclaw secrets audit --check
```
If `audit --check` still reports plaintext findings after a partial migration, verify you also migrated skill keys (`skills.entries.*.apiKey`) and any other reported target paths.
If `audit --check` still reports plaintext findings, update the remaining reported target paths and rerun audit.

View File

@@ -109,6 +109,8 @@ Defaults:
6. Otherwise memory search stays disabled until configured.
- Local mode uses node-llama-cpp and may require `pnpm approve-builds`.
- Uses sqlite-vec (when available) to accelerate vector search inside SQLite.
- `memorySearch.provider = "ollama"` is also supported for local/self-hosted
Ollama embeddings (`/api/embeddings`), but it is not auto-selected.
Remote embeddings **require** an API key for the embedding provider. OpenClaw
resolves keys from auth profiles, `models.providers.*.apiKey`, or environment
@@ -116,7 +118,9 @@ variables. Codex OAuth only covers chat/completions and does **not** satisfy
embeddings for memory search. For Gemini, use `GEMINI_API_KEY` or
`models.providers.google.apiKey`. For Voyage, use `VOYAGE_API_KEY` or
`models.providers.voyage.apiKey`. For Mistral, use `MISTRAL_API_KEY` or
`models.providers.mistral.apiKey`.
`models.providers.mistral.apiKey`. Ollama typically does not require a real API
key (a placeholder like `OLLAMA_API_KEY=ollama-local` is enough when needed by
local policy).
When using a custom OpenAI-compatible endpoint,
set `memorySearch.remote.apiKey` (and optional `memorySearch.remote.headers`).
@@ -331,7 +335,7 @@ If you don't want to set an API key, use `memorySearch.provider = "local"` or se
Fallbacks:
- `memorySearch.fallback` can be `openai`, `gemini`, `voyage`, `mistral`, `local`, or `none`.
- `memorySearch.fallback` can be `openai`, `gemini`, `voyage`, `mistral`, `ollama`, `local`, or `none`.
- The fallback provider is only used when the primary embedding provider fails.
Batch indexing (OpenAI + Gemini + Voyage):

View File

@@ -83,6 +83,9 @@ When a profile fails due to auth/ratelimit errors (or a timeout that looks
like rate limiting), OpenClaw marks it in cooldown and moves to the next profile.
Format/invalidrequest errors (for example Cloud Code Assist tool call ID
validation failures) are treated as failoverworthy and use the same cooldowns.
OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`,
`stop reason: error`, and `reason: error` are classified as timeout/failover
signals.
Cooldowns use exponential backoff:

View File

@@ -60,6 +60,8 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Optional rotation: `ANTHROPIC_API_KEYS`, `ANTHROPIC_API_KEY_1`, `ANTHROPIC_API_KEY_2`, plus `OPENCLAW_LIVE_ANTHROPIC_KEY` (single override)
- Example model: `anthropic/claude-opus-4-6`
- CLI: `openclaw onboard --auth-choice token` (paste setup-token) or `openclaw models auth paste-token --provider anthropic`
- Policy note: setup-token support is technical compatibility; Anthropic has blocked some subscription usage outside Claude Code in the past. Verify current Anthropic terms and decide based on your risk tolerance.
- Recommendation: Anthropic API key auth is the safer, recommended path over subscription setup-token auth.
```json5
{
@@ -75,6 +77,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- CLI: `openclaw onboard --auth-choice openai-codex` or `openclaw models auth login --provider openai-codex`
- Default transport is `auto` (WebSocket-first, SSE fallback)
- Override per model via `agents.defaults.models["openai-codex/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
- Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw.
```json5
{
@@ -121,7 +124,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Provider: `zai`
- Auth: `ZAI_API_KEY`
- Example model: `zai/glm-4.7`
- Example model: `zai/glm-5`
- CLI: `openclaw onboard --auth-choice zai-api-key`
- Aliases: `z.ai/*` and `z-ai/*` normalize to `zai/*`
@@ -175,14 +178,20 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
Kimi K2 model IDs:
{/_moonshot-kimi-k2-model-refs:start_/ && null}
<!-- markdownlint-disable MD037 -->
{/_ moonshot-kimi-k2-model-refs:start _/ && null}
<!-- markdownlint-enable MD037 -->
- `moonshot/kimi-k2.5`
- `moonshot/kimi-k2-0905-preview`
- `moonshot/kimi-k2-turbo-preview`
- `moonshot/kimi-k2-thinking`
- `moonshot/kimi-k2-thinking-turbo`
{/_moonshot-kimi-k2-model-refs:end_/ && null}
<!-- markdownlint-disable MD037 -->
{/_ moonshot-kimi-k2-model-refs:end _/ && null}
<!-- markdownlint-enable MD037 -->
```json5
{
@@ -307,13 +316,13 @@ Synthetic provides Anthropic-compatible models behind the `synthetic` provider:
- Provider: `synthetic`
- Auth: `SYNTHETIC_API_KEY`
- Example model: `synthetic/hf:MiniMaxAI/MiniMax-M2.1`
- Example model: `synthetic/hf:MiniMaxAI/MiniMax-M2.5`
- CLI: `openclaw onboard --auth-choice synthetic-api-key`
```json5
{
agents: {
defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" } },
defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.5" } },
},
models: {
mode: "merge",
@@ -322,7 +331,7 @@ Synthetic provides Anthropic-compatible models behind the `synthetic` provider:
baseUrl: "https://api.synthetic.new/anthropic",
apiKey: "${SYNTHETIC_API_KEY}",
api: "anthropic-messages",
models: [{ id: "hf:MiniMaxAI/MiniMax-M2.1", name: "MiniMax M2.1" }],
models: [{ id: "hf:MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" }],
},
},
},
@@ -396,8 +405,8 @@ Example (OpenAIcompatible):
{
agents: {
defaults: {
model: { primary: "lmstudio/minimax-m2.1-gs32" },
models: { "lmstudio/minimax-m2.1-gs32": { alias: "Minimax" } },
model: { primary: "lmstudio/minimax-m2.5-gs32" },
models: { "lmstudio/minimax-m2.5-gs32": { alias: "Minimax" } },
},
},
models: {
@@ -408,8 +417,8 @@ Example (OpenAIcompatible):
api: "openai-completions",
models: [
{
id: "minimax-m2.1-gs32",
name: "MiniMax M2.1",
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -433,6 +442,9 @@ Notes:
- `contextWindow: 200000`
- `maxTokens: 8192`
- Recommended: set explicit values that match your proxy/model limits.
- For `api: "openai-completions"` on non-native endpoints (any non-empty `baseUrl` whose host is not `api.openai.com`), OpenClaw forces `compat.supportsDeveloperRole: false` to avoid provider 400 errors for unsupported `developer` roles.
- If `baseUrl` is empty/omitted, OpenClaw keeps the default OpenAI behavior (which resolves to `api.openai.com`).
- For safety, an explicit `compat.supportsDeveloperRole: true` is still overridden on non-native `openai-completions` endpoints.
## CLI examples

View File

@@ -28,10 +28,11 @@ Related:
- `agents.defaults.imageModel` is used **only when** the primary model cant accept images.
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
## Quick model picks (anecdotal)
## Quick model policy
- **GLM**: a bit better for coding/tool calling.
- **MiniMax**: better for writing and vibes.
- Set your primary to the strongest latest-generation model available to you.
- Use fallbacks for cost/latency-sensitive tasks and lower-stakes chat.
- For tool-enabled agents or untrusted inputs, avoid older/weaker model tiers.
## Setup wizard (recommended)
@@ -42,8 +43,7 @@ openclaw onboard
```
It can set up model + auth for common providers, including **OpenAI Code (Codex)
subscription** (OAuth) and **Anthropic** (API key recommended; `claude
setup-token` also supported).
subscription** (OAuth) and **Anthropic** (API key or `claude setup-token`).
## Config keys (overview)
@@ -160,7 +160,9 @@ JSON includes `auth.oauth` (warn window + profiles) and `auth.providers`
(effective auth per provider).
Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
Preferred Anthropic auth is the Claude Code CLI setup-token (run anywhere; paste on the gateway host if needed):
Auth choice is provider/account dependent. For always-on gateway hosts, API keys are usually the most predictable; subscription token flows are also supported.
Example (Anthropic setup-token):
```bash
claude setup-token

View File

@@ -10,7 +10,9 @@ title: "OAuth"
# OAuth
OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. This page explains:
OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. Anthropic subscription use outside Claude Code has been restricted for some users in the past, so treat it as a user-choice risk and verify current Anthropic policy yourself. OpenAI Codex OAuth is explicitly supported for use in external tools like OpenClaw. This page explains:
For Anthropic in production, API key auth is the safer recommended path over subscription setup-token auth.
- how the OAuth **token exchange** works (PKCE)
- where tokens are **stored** (and why)
@@ -54,6 +56,12 @@ For static secret refs and runtime snapshot activation behavior, see [Secrets Ma
## Anthropic setup-token (subscription auth)
<Warning>
Anthropic setup-token support is technical compatibility, not a policy guarantee.
Anthropic has blocked some subscription usage outside Claude Code in the past.
Decide for yourself whether to use subscription auth, and verify Anthropic's current terms.
</Warning>
Run `claude setup-token` on any machine, then paste it into OpenClaw:
```bash
@@ -76,7 +84,7 @@ openclaw models status
OpenClaws interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands.
### Anthropic (Claude Pro/Max) setup-token
### Anthropic setup-token
Flow shape:
@@ -88,6 +96,8 @@ The wizard path is `openclaw onboard` → auth choice `setup-token` (Anthropic).
### OpenAI Codex (ChatGPT OAuth)
OpenAI Codex OAuth is explicitly supported for use outside the Codex CLI, including OpenClaw workflows.
Flow shape (PKCE):
1. generate PKCE verifier/challenge + random `state`

View File

@@ -138,7 +138,7 @@ Legacy key migration:
Telegram:
- Uses Bot API `sendMessage` + `editMessageText`.
- Uses Bot API `sendMessageDraft` in DMs when available, and `sendMessage` + `editMessageText` for group/topic preview updates.
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
- `/reasoning stream` can write reasoning to preview.

View File

@@ -462,7 +462,7 @@ const needsNonImageSanitize =
"id": "anthropic/claude-opus-4.6",
"name": "Anthropic: Claude Opus 4.6"
},
{ "id": "minimax/minimax-m2.1:free", "name": "Minimax: Minimax M2.1" }
{ "id": "minimax/minimax-m2.5:free", "name": "Minimax: Minimax M2.5" }
]
}
}

View File

@@ -994,6 +994,7 @@
"brave-search",
"perplexity",
"tools/diffs",
"tools/pdf",
"tools/elevated",
"tools/exec",
"tools/exec-approvals",
@@ -1321,6 +1322,7 @@
"pages": [
"reference/wizard",
"reference/token-use",
"reference/secretref-credential-surface",
"reference/prompt-caching",
"reference/api-usage-costs",
"reference/transcript-hygiene",

View File

@@ -8,23 +8,26 @@ title: "Authentication"
# Authentication
OpenClaw supports OAuth and API keys for model providers. For Anthropic
accounts, we recommend using an **API key**. For Claude subscription access,
use the longlived token created by `claude setup-token`.
OpenClaw supports OAuth and API keys for model providers. For always-on gateway
hosts, API keys are usually the most predictable option. Subscription/OAuth
flows are also supported when they match your provider account model.
See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage
layout.
For SecretRef-based auth (`env`/`file`/`exec` providers), see [Secrets Management](/gateway/secrets).
## Recommended Anthropic setup (API key)
## Recommended setup (API key, any provider)
If youre using Anthropic directly, use an API key.
If youre running a long-lived gateway, start with an API key for your chosen
provider.
For Anthropic specifically, API key auth is the safe path and is recommended
over subscription setup-token auth.
1. Create an API key in the Anthropic Console.
1. Create an API key in your provider console.
2. Put it on the **gateway host** (the machine running `openclaw gateway`).
```bash
export ANTHROPIC_API_KEY="..."
export <PROVIDER>_API_KEY="..."
openclaw models status
```
@@ -33,7 +36,7 @@ openclaw models status
```bash
cat >> ~/.openclaw/.env <<'EOF'
ANTHROPIC_API_KEY=...
<PROVIDER>_API_KEY=...
EOF
```
@@ -52,8 +55,8 @@ See [Help](/help) for details on env inheritance (`env.shellEnv`,
## Anthropic: setup-token (subscription auth)
For Anthropic, the recommended path is an **API key**. If youre using a Claude
subscription, the setup-token flow is also supported. Run it on the **gateway host**:
If youre using a Claude subscription, the setup-token flow is supported. Run
it on the **gateway host**:
```bash
claude setup-token
@@ -79,6 +82,12 @@ This credential is only authorized for use with Claude Code and cannot be used f
…use an Anthropic API key instead.
<Warning>
Anthropic setup-token support is technical compatibility only. Anthropic has blocked
some subscription usage outside Claude Code in the past. Use it only if you decide
the policy risk is acceptable, and verify Anthropic's current terms yourself.
</Warning>
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
```bash
@@ -164,5 +173,5 @@ is missing, rerun `claude setup-token` and paste the token again.
## Requirements
- Claude Max or Pro subscription (for `claude setup-token`)
- Anthropic subscription account (for `claude setup-token`)
- Claude Code CLI installed (`claude` command available)

View File

@@ -527,7 +527,13 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
}
```
### Anthropic subscription + API key, MiniMax fallback
### Anthropic setup-token + API key, MiniMax fallback
<Warning>
Anthropic setup-token usage outside Claude Code has been restricted for some
users in the past. Treat this as user-choice risk and verify current Anthropic
terms before depending on subscription auth.
</Warning>
```json5
{
@@ -560,7 +566,7 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
workspace: "~/.openclaw/workspace",
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["minimax/MiniMax-M2.1"],
fallbacks: ["minimax/MiniMax-M2.5"],
},
},
}
@@ -597,7 +603,7 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
{
agent: {
workspace: "~/.openclaw/workspace",
model: { primary: "lmstudio/minimax-m2.1-gs32" },
model: { primary: "lmstudio/minimax-m2.5-gs32" },
},
models: {
mode: "merge",
@@ -608,8 +614,8 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
api: "openai-responses",
models: [
{
id: "minimax-m2.1-gs32",
name: "MiniMax M2.1 GS32",
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5 GS32",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },

View File

@@ -205,6 +205,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
- Bot token: `channels.telegram.botToken` or `channels.telegram.tokenFile`, with `TELEGRAM_BOT_TOKEN` as fallback for the default account.
- Optional `channels.telegram.defaultAccount` overrides default account selection when it matches a configured account id.
- In multi-account setups (2+ account ids), set an explicit default (`channels.telegram.defaultAccount` or `channels.telegram.accounts.default`) to avoid fallback routing; `openclaw doctor` warns when this is missing or invalid.
- `configWrites: false` blocks Telegram-initiated config writes (supergroup ID migrations, `/config set|unset`).
- Telegram stream previews use `sendMessage` + `editMessageText` (works in direct and group chats).
- Retry policy: see [Retry policy](/concepts/retry).
@@ -443,6 +444,13 @@ Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
commands: {
native: true, // opt-in
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Optional explicit URL for reverse-proxy/public deployments
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
textChunkLimit: 4000,
chunkMode: "length",
},
@@ -452,6 +460,13 @@ Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message), `onchar` (messages starting with trigger prefix).
When Mattermost native commands are enabled:
- `commands.callbackPath` must be a path (for example `/api/channels/mattermost/command`), not a full URL.
- `commands.callbackUrl` must resolve to the OpenClaw gateway endpoint and be reachable from the Mattermost server.
- For private/tailnet/internal callback hosts, Mattermost may require
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
Use host/domain values, not full URLs.
- `channels.mattermost.configWrites`: allow or deny Mattermost-initiated config writes.
- `channels.mattermost.requireMention`: require `@mention` before replying in channels.
- Optional `channels.mattermost.defaultAccount` overrides default account selection when it matches a configured account id.
@@ -825,11 +840,11 @@ Time format in system prompt. Default: `auto` (OS preference).
defaults: {
models: {
"anthropic/claude-opus-4-6": { alias: "opus" },
"minimax/MiniMax-M2.1": { alias: "minimax" },
"minimax/MiniMax-M2.5": { alias: "minimax" },
},
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["minimax/MiniMax-M2.1"],
fallbacks: ["minimax/MiniMax-M2.5"],
},
imageModel: {
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
@@ -1170,8 +1185,8 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway
**`docker.binds`** mounts additional host directories; global and per-agent binds are merged.
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in main config.
noVNC observer access uses VNC auth by default and OpenClaw emits a short-lived token URL that serves a local bootstrap page; noVNC password is passed via URL fragment (instead of URL query).
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in `openclaw.json`.
noVNC observer access uses VNC auth by default and OpenClaw emits a short-lived token URL (instead of exposing the password in the shared URL).
- `allowHostControl: false` (default) blocks sandboxed sessions from targeting the host browser.
- `network` defaults to `openclaw-sandbox-browser` (dedicated bridge network). Set to `bridge` only when you explicitly want global bridge connectivity.
@@ -1605,7 +1620,8 @@ Defaults for Talk mode (macOS/iOS/Android).
```
- Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID`.
- `apiKey` falls back to `ELEVENLABS_API_KEY`.
- `apiKey` and `providers.*.apiKey` accept plaintext strings or SecretRef objects.
- `ELEVENLABS_API_KEY` fallback applies only when no Talk API key is configured.
- `voiceAliases` lets Talk directives use friendly names.
---
@@ -1804,7 +1820,7 @@ Configures inbound media understanding (image/audio/video):
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc.)
- `model`: model id override
- `profile` / `preferredProfile`: auth profile selection
- `profile` / `preferredProfile`: `auth-profiles.json` profile selection
**CLI entry** (`type: "cli"`):
@@ -1817,7 +1833,7 @@ Configures inbound media understanding (image/audio/video):
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
- Failures fall back to the next entry.
Provider auth follows standard order: auth profiles → env vars → `models.providers.*.apiKey`.
Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`.
</Accordion>
@@ -1895,7 +1911,7 @@ Notes:
agents: {
defaults: {
subagents: {
model: "minimax/MiniMax-M2.1",
model: "minimax/MiniMax-M2.5",
maxConcurrent: 1,
runTimeoutSeconds: 900,
archiveAfterMinutes: 60,
@@ -1961,6 +1977,7 @@ OpenClaw uses the pi-coding-agent model catalog. Add custom providers via `model
- `models.providers.*.baseUrl`: upstream API base URL.
- `models.providers.*.headers`: extra static headers for proxy/tenant routing.
- `models.providers.*.models`: explicit provider model catalog entries.
- `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior.
- `models.bedrockDiscovery`: Bedrock auto-discovery settings root.
- `models.bedrockDiscovery.enabled`: turn discovery polling on/off.
- `models.bedrockDiscovery.region`: AWS region for discovery.
@@ -2111,8 +2128,8 @@ Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choi
env: { SYNTHETIC_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" },
models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax M2.1" } },
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.5" },
models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax M2.5" } },
},
},
models: {
@@ -2124,8 +2141,8 @@ Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choi
api: "anthropic-messages",
models: [
{
id: "hf:MiniMaxAI/MiniMax-M2.1",
name: "MiniMax M2.1",
id: "hf:MiniMaxAI/MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -2143,15 +2160,15 @@ Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw on
</Accordion>
<Accordion title="MiniMax M2.1 (direct)">
<Accordion title="MiniMax M2.5 (direct)">
```json5
{
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.1" },
model: { primary: "minimax/MiniMax-M2.5" },
models: {
"minimax/MiniMax-M2.1": { alias: "Minimax" },
"minimax/MiniMax-M2.5": { alias: "Minimax" },
},
},
},
@@ -2164,8 +2181,8 @@ Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw on
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.1",
name: "MiniMax M2.1",
id: "MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: false,
input: ["text"],
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
@@ -2185,7 +2202,7 @@ Set `MINIMAX_API_KEY`. Shortcut: `openclaw onboard --auth-choice minimax-api`.
<Accordion title="Local models (LM Studio)">
See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.1 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.5 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
</Accordion>
@@ -2637,14 +2654,11 @@ Validation:
- `source: "file"` id: absolute JSON pointer (for example `"/providers/openai/apiKey"`)
- `source: "exec"` id pattern: `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
### Supported fields in config
### Supported credential surface
- `models.providers.<provider>.apiKey`
- `skills.entries.<skillKey>.apiKey`
- `channels.googlechat.serviceAccount`
- `channels.googlechat.serviceAccountRef`
- `channels.googlechat.accounts.<accountId>.serviceAccount`
- `channels.googlechat.accounts.<accountId>.serviceAccountRef`
- Canonical matrix: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- `secrets apply` targets supported `openclaw.json` credential paths.
- `auth-profiles.json` refs are included in runtime resolution and audit coverage.
### Secret providers config
@@ -2682,6 +2696,7 @@ Notes:
- If `trustedDirs` is configured, the trusted-dir check applies to the resolved target path.
- `exec` child environment is minimal by default; pass required variables explicitly with `passEnv`.
- Secret refs are resolved at activation time into an in-memory snapshot, then request paths read the snapshot only.
- Active-surface filtering applies during activation: unresolved refs on enabled surfaces fail startup/reload, while inactive surfaces are skipped with diagnostics.
---
@@ -2701,8 +2716,8 @@ Notes:
}
```
- Per-agent auth profiles stored at `<agentDir>/auth-profiles.json`.
- Auth profiles support value-level refs (`keyRef` for `api_key`, `tokenRef` for `token`).
- Per-agent profiles are stored at `<agentDir>/auth-profiles.json`.
- `auth-profiles.json` supports value-level refs (`keyRef` for `api_key`, `tokenRef` for `token`).
- Static runtime credentials come from in-memory resolved snapshots; legacy static `auth.json` entries are scrubbed when discovered.
- Legacy OAuth imports from `~/.openclaw/credentials/oauth.json`.
- See [OAuth](/concepts/oauth).
@@ -2731,6 +2746,26 @@ Notes:
---
## CLI
```json5
{
cli: {
banner: {
taglineMode: "off", // random | default | off
},
},
}
```
- `cli.banner.taglineMode` controls banner tagline style:
- `"random"` (default): rotating funny/seasonal taglines.
- `"default"`: fixed neutral tagline (`All your chats, one OpenClaw.`).
- `"off"`: no tagline text (banner title/version still shown).
- To hide the entire banner (not just taglines), set env `OPENCLAW_HIDE_BANNER=1`.
---
## Wizard
Metadata written by CLI wizards (`onboard`, `configure`, `doctor`):
@@ -2879,7 +2914,7 @@ Split config into multiple files:
- Array of files: deep-merged in order (later overrides earlier).
- Sibling keys: merged after includes (override included values).
- Nested includes: up to 10 levels deep.
- Paths: resolved relative to the including file, but must stay inside the top-level config directory (`dirname` of the main config file). Absolute/`../` forms are allowed only when they still resolve inside that boundary.
- Paths: resolved relative to the including file, but must stay inside the top-level config directory (`dirname` of `openclaw.json`). Absolute/`../` forms are allowed only when they still resolve inside that boundary.
- Errors: clear messages for missing files, parse errors, and circular includes.
---

View File

@@ -532,6 +532,7 @@ Rules:
```
SecretRef details (including `secrets.providers` for `env`/`file`/`exec`) are in [Secrets Management](/gateway/secrets).
Supported credential paths are listed in [SecretRef Credential Surface](/reference/secretref-credential-surface).
</Accordion>
See [Environment](/help/environment) for full precedence and sources.

View File

@@ -128,6 +128,11 @@ Current migrations:
`agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
- `browser.ssrfPolicy.allowPrivateNetwork``browser.ssrfPolicy.dangerouslyAllowPrivateNetwork`
Doctor warnings also include account-default guidance for multi-account channels:
- If two or more `channels.<channel>.accounts` entries are configured without `channels.<channel>.defaultAccount` or `accounts.default`, doctor warns that fallback routing can pick an unexpected account.
- If `channels.<channel>.defaultAccount` is set to an unknown account ID, doctor warns and lists configured account IDs.
### 2b) OpenCode Zen provider overrides
If youve added `models.providers.opencode` (or `opencode-zen`) manually, it

View File

@@ -11,18 +11,18 @@ title: "Local Models"
Local is doable, but OpenClaw expects large context + strong defenses against prompt injection. Small cards truncate context and leak safety. Aim high: **≥2 maxed-out Mac Studios or equivalent GPU rig (~$30k+)**. A single **24 GB** GPU works only for lighter prompts with higher latency. Use the **largest / full-size model variant you can run**; aggressively quantized or “small” checkpoints raise prompt-injection risk (see [Security](/gateway/security)).
## Recommended: LM Studio + MiniMax M2.1 (Responses API, full-size)
## Recommended: LM Studio + MiniMax M2.5 (Responses API, full-size)
Best current local stack. Load MiniMax M2.1 in LM Studio, enable the local server (default `http://127.0.0.1:1234`), and use Responses API to keep reasoning separate from final text.
Best current local stack. Load MiniMax M2.5 in LM Studio, enable the local server (default `http://127.0.0.1:1234`), and use Responses API to keep reasoning separate from final text.
```json5
{
agents: {
defaults: {
model: { primary: "lmstudio/minimax-m2.1-gs32" },
model: { primary: "lmstudio/minimax-m2.5-gs32" },
models: {
"anthropic/claude-opus-4-6": { alias: "Opus" },
"lmstudio/minimax-m2.1-gs32": { alias: "Minimax" },
"lmstudio/minimax-m2.5-gs32": { alias: "Minimax" },
},
},
},
@@ -35,8 +35,8 @@ Best current local stack. Load MiniMax M2.1 in LM Studio, enable the local serve
api: "openai-responses",
models: [
{
id: "minimax-m2.1-gs32",
name: "MiniMax M2.1 GS32",
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5 GS32",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -53,7 +53,7 @@ Best current local stack. Load MiniMax M2.1 in LM Studio, enable the local serve
**Setup checklist**
- Install LM Studio: [https://lmstudio.ai](https://lmstudio.ai)
- In LM Studio, download the **largest MiniMax M2.1 build available** (avoid “small”/heavily quantized variants), start the server, confirm `http://127.0.0.1:1234/v1/models` lists it.
- In LM Studio, download the **largest MiniMax M2.5 build available** (avoid “small”/heavily quantized variants), start the server, confirm `http://127.0.0.1:1234/v1/models` lists it.
- Keep the model loaded; cold-load adds startup latency.
- Adjust `contextWindow`/`maxTokens` if your LM Studio build differs.
- For WhatsApp, stick to Responses API so only final text is sent.
@@ -68,11 +68,11 @@ Keep hosted models configured even when running local; use `models.mode: "merge"
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-5",
fallbacks: ["lmstudio/minimax-m2.1-gs32", "anthropic/claude-opus-4-6"],
fallbacks: ["lmstudio/minimax-m2.5-gs32", "anthropic/claude-opus-4-6"],
},
models: {
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
"lmstudio/minimax-m2.1-gs32": { alias: "MiniMax Local" },
"lmstudio/minimax-m2.5-gs32": { alias: "MiniMax Local" },
"anthropic/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -86,8 +86,8 @@ Keep hosted models configured even when running local; use `models.mode: "merge"
api: "openai-responses",
models: [
{
id: "minimax-m2.1-gs32",
name: "MiniMax M2.1 GS32",
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5 GS32",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },

View File

@@ -1,9 +1,9 @@
---
summary: "Contract for `secrets apply` plans: allowed target paths, validation, and ref-only auth-profile behavior"
summary: "Contract for `secrets apply` plans: target validation, path matching, and `auth-profiles.json` target scope"
read_when:
- Generating or reviewing `openclaw secrets apply` plan files
- Generating or reviewing `openclaw secrets apply` plans
- Debugging `Invalid plan target path` errors
- Understanding how `keyRef` and `tokenRef` influence implicit provider discovery
- Understanding target type and path validation behavior
title: "Secrets Apply Plan Contract"
---
@@ -11,7 +11,7 @@ title: "Secrets Apply Plan Contract"
This page defines the strict contract enforced by `openclaw secrets apply`.
If a target does not match these rules, apply fails before mutating config.
If a target does not match these rules, apply fails before mutating configuration.
## Plan file shape
@@ -29,29 +29,47 @@ If a target does not match these rules, apply fails before mutating config.
providerId: "openai",
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
{
type: "auth-profiles.api_key.key",
path: "profiles.openai:default.key",
pathSegments: ["profiles", "openai:default", "key"],
agentId: "main",
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
],
}
```
## Allowed target types and paths
## Supported target scope
| `target.type` | Allowed `target.path` shape | Optional id match rule |
| ------------------------------------ | --------------------------------------------------------- | --------------------------------------------------- |
| `models.providers.apiKey` | `models.providers.<providerId>.apiKey` | `providerId` must match `<providerId>` when present |
| `skills.entries.apiKey` | `skills.entries.<skillKey>.apiKey` | n/a |
| `channels.googlechat.serviceAccount` | `channels.googlechat.serviceAccount` | `accountId` must be empty/omitted |
| `channels.googlechat.serviceAccount` | `channels.googlechat.accounts.<accountId>.serviceAccount` | `accountId` must match `<accountId>` when present |
Plan targets are accepted for supported credential paths in:
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
## Target type behavior
General rule:
- `target.type` must be recognized and must match the normalized `target.path` shape.
Compatibility aliases remain accepted for existing plans:
- `models.providers.apiKey`
- `skills.entries.apiKey`
- `channels.googlechat.serviceAccount`
## Path validation rules
Each target is validated with all of the following:
- `type` must be one of the allowed target types above.
- `type` must be a recognized target type.
- `path` must be a non-empty dot path.
- `pathSegments` can be omitted. If provided, it must normalize to exactly the same path as `path`.
- Forbidden segments are rejected: `__proto__`, `prototype`, `constructor`.
- The normalized path must match one of the allowed path shapes for the target type.
- If `providerId` / `accountId` is set, it must match the id encoded in the path.
- The normalized path must match the registered path shape for the target type.
- If `providerId` or `accountId` is set, it must match the id encoded in the path.
- `auth-profiles.json` targets require `agentId`.
- When creating a new `auth-profiles.json` mapping, include `authProfileProvider`.
## Failure behavior
@@ -61,19 +79,12 @@ If a target fails validation, apply exits with an error like:
Invalid plan target path for models.providers.apiKey: models.providers.openai.baseUrl
```
No partial mutation is committed for that invalid target path.
No writes are committed for an invalid plan.
## Ref-only auth profiles and implicit providers
## Runtime and audit scope notes
Implicit provider discovery also considers auth profiles that store refs instead of plaintext credentials:
- `type: "api_key"` profiles can use `keyRef` (for example env-backed refs).
- `type: "token"` profiles can use `tokenRef`.
Behavior:
- For API-key providers (for example `volcengine`, `byteplus`), ref-only profiles can still activate implicit provider entries.
- For `github-copilot`, if the profile has no plaintext token, discovery will try `tokenRef` env resolution before token exchange.
- Ref-only `auth-profiles.json` entries (`keyRef`/`tokenRef`) are included in runtime resolution and audit coverage.
- `secrets apply` writes supported `openclaw.json` targets, supported `auth-profiles.json` targets, and optional scrub targets.
## Operator checks
@@ -85,10 +96,11 @@ openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
```
If apply fails with an invalid target path message, regenerate the plan with `openclaw secrets configure` or fix the target path to one of the allowed shapes above.
If apply fails with an invalid target path message, regenerate the plan with `openclaw secrets configure` or fix the target path to a supported shape above.
## Related docs
- [Secrets Management](/gateway/secrets)
- [CLI `secrets`](/cli/secrets)
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
- [Configuration Reference](/gateway/configuration-reference)

View File

@@ -1,35 +1,70 @@
---
summary: "Secrets management: SecretRef contract, runtime snapshot behavior, and safe one-way scrubbing"
read_when:
- Configuring SecretRefs for providers, auth profiles, skills, or Google Chat
- Operating secrets reload/audit/configure/apply safely in production
- Understanding fail-fast and last-known-good behavior
- Configuring SecretRefs for provider credentials and `auth-profiles.json` refs
- Operating secrets reload, audit, configure, and apply safely in production
- Understanding startup fail-fast, inactive-surface filtering, and last-known-good behavior
title: "Secrets Management"
---
# Secrets management
OpenClaw supports additive secret references so credentials do not need to be stored as plaintext in config files.
OpenClaw supports additive SecretRefs so supported credentials do not need to be stored as plaintext in configuration.
Plaintext still works. Secret refs are optional.
Plaintext still works. SecretRefs are opt-in per credential.
## Goals and runtime model
Secrets are resolved into an in-memory runtime snapshot.
- Resolution is eager during activation, not lazy on request paths.
- Startup fails fast if any referenced credential cannot be resolved.
- Reload uses atomic swap: full success or keep last-known-good.
- Runtime requests read from the active in-memory snapshot.
- Startup fails fast when an effectively active SecretRef cannot be resolved.
- Reload uses atomic swap: full success, or keep the last-known-good snapshot.
- Runtime requests read from the active in-memory snapshot only.
This keeps secret-provider outages off the hot request path.
This keeps secret-provider outages off hot request paths.
## Active-surface filtering
SecretRefs are validated only on effectively active surfaces.
- Enabled surfaces: unresolved refs block startup/reload.
- Inactive surfaces: unresolved refs do not block startup/reload.
- Inactive refs emit non-fatal diagnostics with code `SECRETS_REF_IGNORED_INACTIVE_SURFACE`.
Examples of inactive surfaces:
- Disabled channel/account entries.
- Top-level channel credentials that no enabled account inherits.
- Disabled tool/feature surfaces.
- Web search provider-specific keys that are not selected by `tools.web.search.provider`.
In auto mode (provider unset), provider-specific keys are also active for provider auto-detection.
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active (when `gateway.remote.enabled` is not `false`) if one of these is true:
- `gateway.mode=remote`
- `gateway.remote.url` is configured
- `gateway.tailscale.mode` is `serve` or `funnel`
In local mode without those remote surfaces:
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
## Gateway auth surface diagnostics
When a SecretRef is configured on `gateway.auth.password`, `gateway.remote.token`, or
`gateway.remote.password`, gateway startup/reload logs the surface state explicitly:
- `active`: the SecretRef is part of the effective auth surface and must resolve.
- `inactive`: the SecretRef is ignored for this runtime because another auth surface wins, or
because remote auth is disabled/not active.
These entries are logged with `SECRETS_GATEWAY_AUTH_SURFACE` and include the reason used by the
active-surface policy, so you can see why a credential was treated as active or inactive.
## Onboarding reference preflight
When onboarding runs in interactive mode and you choose secret reference storage, OpenClaw performs a fast preflight check before saving:
When onboarding runs in interactive mode and you choose SecretRef storage, OpenClaw runs preflight validation before saving:
- Env refs: validates env var name and confirms a non-empty value is visible during onboarding.
- Provider refs (`file` or `exec`): validates the selected provider, resolves the provided `id`, and checks value type.
- Provider refs (`file` or `exec`): validates provider selection, resolves `id`, and checks resolved value type.
If validation fails, onboarding shows the error and lets you retry.
@@ -122,22 +157,24 @@ Define providers under `secrets.providers`:
- `mode: "json"` expects JSON object payload and resolves `id` as pointer.
- `mode: "singleValue"` expects ref id `"value"` and returns file contents.
- Path must pass ownership/permission checks.
- Windows fail-closed note: if ACL verification is unavailable for a path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
### Exec provider
- Runs configured absolute binary path, no shell.
- By default, `command` must point to a regular file (not a symlink).
- Set `allowSymlinkCommand: true` to allow symlink command paths (for example Homebrew shims). OpenClaw validates the resolved target path.
- Enable `allowSymlinkCommand` only when required for trusted package-manager paths, and pair it with `trustedDirs` (for example `["/opt/homebrew"]`).
- When `trustedDirs` is set, checks apply to the resolved target path.
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
- Request payload (stdin):
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
Request payload (stdin):
```json
{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }
```
- Response payload (stdout):
Response payload (stdout):
```json
{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "sk-..." } }
@@ -242,37 +279,33 @@ Optional per-id errors:
}
```
## In-scope fields (v1)
## Supported credential surface
### `~/.openclaw/openclaw.json`
Canonical supported and unsupported credentials are listed in:
- `models.providers.<provider>.apiKey`
- `skills.entries.<skillKey>.apiKey`
- `channels.googlechat.serviceAccount`
- `channels.googlechat.serviceAccountRef`
- `channels.googlechat.accounts.<accountId>.serviceAccount`
- `channels.googlechat.accounts.<accountId>.serviceAccountRef`
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
### `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`
- `profiles.<profileId>.keyRef` for `type: "api_key"`
- `profiles.<profileId>.tokenRef` for `type: "token"`
OAuth credential storage changes are out of scope.
Runtime-minted or rotating credentials and OAuth refresh material are intentionally excluded from read-only SecretRef resolution.
## Required behavior and precedence
- Field without ref: unchanged.
- Field with ref: required at activation time.
- If plaintext and ref both exist, ref wins at runtime and plaintext is ignored.
- Field without a ref: unchanged.
- Field with a ref: required on active surfaces during activation.
- If both plaintext and ref are present, ref takes precedence on supported precedence paths.
Warning code:
Warning and audit signals:
- `SECRETS_REF_OVERRIDES_PLAINTEXT`
- `SECRETS_REF_OVERRIDES_PLAINTEXT` (runtime warning)
- `REF_SHADOWED` (audit finding when `auth-profiles.json` credentials take precedence over `openclaw.json` refs)
Google Chat compatibility behavior:
- `serviceAccountRef` takes precedence over plaintext `serviceAccount`.
- Plaintext value is ignored when sibling ref is set.
## Activation triggers
Secret activation is attempted on:
Secret activation runs on:
- Startup (preflight plus final activation)
- Config reload hot-apply path
@@ -283,9 +316,9 @@ Activation contract:
- Success swaps the snapshot atomically.
- Startup failure aborts gateway startup.
- Runtime reload failure keeps last-known-good snapshot.
- Runtime reload failure keeps the last-known-good snapshot.
## Degraded and recovered operator signals
## Degraded and recovered signals
When reload-time activation fails after a healthy state, OpenClaw enters degraded secrets state.
@@ -297,13 +330,22 @@ One-shot system event and log codes:
Behavior:
- Degraded: runtime keeps last-known-good snapshot.
- Recovered: emitted once after a successful activation.
- Recovered: emitted once after the next successful activation.
- Repeated failures while already degraded log warnings but do not spam events.
- Startup fail-fast does not emit degraded events because no runtime snapshot exists yet.
- Startup fail-fast does not emit degraded events because runtime never became active.
## Command-path resolution
Credential-sensitive command paths that opt in (for example `openclaw memory` remote-memory paths and `openclaw qr --remote`) can resolve supported SecretRefs via gateway snapshot RPC.
- When gateway is running, those command paths read from the active snapshot.
- If a configured SecretRef is required and gateway is unavailable, command resolution fails fast with actionable diagnostics.
- Snapshot refresh after backend secret rotation is handled by `openclaw secrets reload`.
- Gateway RPC method used by these command paths: `secrets.resolve`.
## Audit and configure workflow
Use this default operator flow:
Default operator flow:
```bash
openclaw secrets audit --check
@@ -311,26 +353,22 @@ openclaw secrets configure
openclaw secrets audit --check
```
Migration completeness:
- Include `skills.entries.<skillKey>.apiKey` targets when those skills use API keys.
- If `audit --check` still reports plaintext findings after a partial migration, migrate the remaining reported paths and rerun audit.
### `secrets audit`
Findings include:
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`)
- unresolved refs
- precedence shadowing (`auth-profiles` taking priority over config refs)
- legacy residues (`auth.json`, OAuth out-of-scope reminders)
- precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs)
- legacy residues (`auth.json`, OAuth reminders)
### `secrets configure`
Interactive helper that:
- configures `secrets.providers` first (`env`/`file`/`exec`, add/edit/remove)
- lets you select secret-bearing fields in `openclaw.json`
- lets you select supported secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for one agent scope
- can create a new `auth-profiles.json` mapping directly in the target picker
- captures SecretRef details (`source`, `provider`, `id`)
- runs preflight resolution
- can apply immediately
@@ -339,10 +377,11 @@ Helpful modes:
- `openclaw secrets configure --providers-only`
- `openclaw secrets configure --skip-provider-setup`
- `openclaw secrets configure --agent <id>`
`configure` apply defaults to:
`configure` apply defaults:
- scrub matching static creds from `auth-profiles.json` for targeted providers
- scrub matching static credentials from `auth-profiles.json` for targeted providers
- scrub legacy static `api_key` entries from `auth.json`
- scrub matching known secret lines from `<config-dir>/.env`
@@ -361,26 +400,31 @@ For strict target/path contract details and exact rejection rules, see:
## One-way safety policy
OpenClaw intentionally does **not** write rollback backups that contain pre-migration plaintext secret values.
OpenClaw intentionally does not write rollback backups containing historical plaintext secret values.
Safety model:
- preflight must succeed before write mode
- runtime activation is validated before commit
- apply updates files using atomic file replacement and best-effort in-memory restore on failure
- apply updates files using atomic file replacement and best-effort restore on failure
## `auth.json` compatibility notes
## Legacy auth compatibility notes
For static credentials, OpenClaw runtime no longer depends on plaintext `auth.json`.
For static credentials, runtime no longer depends on plaintext legacy auth storage.
- Runtime credential source is the resolved in-memory snapshot.
- Legacy `auth.json` static `api_key` entries are scrubbed when discovered.
- OAuth-related legacy compatibility behavior remains separate.
- Legacy static `api_key` entries are scrubbed when discovered.
- OAuth-related compatibility behavior remains separate.
## Web UI note
Some SecretInput unions are easier to configure in raw editor mode than in form mode.
## Related docs
- CLI commands: [secrets](/cli/secrets)
- Plan contract details: [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
- Credential surface: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- Auth setup: [Authentication](/gateway/authentication)
- Security posture: [Security](/gateway/security)
- Environment precedence: [Environment Variables](/help/environment)

View File

@@ -224,39 +224,40 @@ When the audit prints findings, treat this as a priority order:
High-signal `checkId` values you will most likely see in real deployments (not exhaustive):
| `checkId` | Severity | Why it matters | Primary fix key/path | Auto-fix |
| -------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------- |
| `fs.state_dir.perms_world_writable` | critical | Other users/processes can modify full OpenClaw state | filesystem perms on `~/.openclaw` | yes |
| `fs.config.perms_writable` | critical | Others can change auth/tool policy/config | filesystem perms on `~/.openclaw/openclaw.json` | yes |
| `fs.config.perms_world_readable` | critical | Config can expose tokens/settings | filesystem perms on config file | yes |
| `gateway.bind_no_auth` | critical | Remote bind without shared secret | `gateway.bind`, `gateway.auth.*` | no |
| `gateway.loopback_no_auth` | critical | Reverse-proxied loopback may become unauthenticated | `gateway.auth.*`, proxy setup | no |
| `gateway.http.no_auth` | warn/critical | Gateway HTTP APIs reachable with `auth.mode="none"` | `gateway.auth.mode`, `gateway.http.endpoints.*` | no |
| `gateway.tools_invoke_http.dangerous_allow` | warn/critical | Re-enables dangerous tools over HTTP API | `gateway.tools.allow` | no |
| `gateway.nodes.allow_commands_dangerous` | warn/critical | Enables high-impact node commands (camera/screen/contacts/calendar/SMS) | `gateway.nodes.allowCommands` | no |
| `gateway.tailscale_funnel` | critical | Public internet exposure | `gateway.tailscale.mode` | no |
| `gateway.control_ui.allowed_origins_required` | critical | Non-loopback Control UI without explicit browser-origin allowlist | `gateway.controlUi.allowedOrigins` | no |
| `gateway.control_ui.host_header_origin_fallback` | warn/critical | Enables Host-header origin fallback (DNS rebinding hardening downgrade) | `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` | no |
| `gateway.control_ui.insecure_auth` | warn | Insecure-auth compatibility toggle enabled | `gateway.controlUi.allowInsecureAuth` | no |
| `gateway.control_ui.device_auth_disabled` | critical | Disables device identity check | `gateway.controlUi.dangerouslyDisableDeviceAuth` | no |
| `gateway.real_ip_fallback_enabled` | warn/critical | Trusting `X-Real-IP` fallback can enable source-IP spoofing via proxy misconfig | `gateway.allowRealIpFallback`, `gateway.trustedProxies` | no |
| `discovery.mdns_full_mode` | warn/critical | mDNS full mode advertises `cliPath`/`sshPort` metadata on local network | `discovery.mdns.mode`, `gateway.bind` | no |
| `config.insecure_or_dangerous_flags` | warn | Any insecure/dangerous debug flags enabled | multiple keys (see finding detail) | no |
| `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no |
| `hooks.request_session_key_enabled` | warn/critical | External caller can choose sessionKey | `hooks.allowRequestSessionKey` | no |
| `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no |
| `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes |
| `sandbox.docker_config_mode_off` | warn | Sandbox Docker config present but inactive | `agents.*.sandbox.mode` | no |
| `sandbox.dangerous_network_mode` | critical | Sandbox Docker network uses `host` or `container:*` namespace-join mode | `agents.*.sandbox.docker.network` | no |
| `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` resolves to host exec when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no |
| `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` resolves to host exec when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no |
| `tools.exec.safe_bins_interpreter_unprofiled` | warn | Interpreter/runtime bins in `safeBins` without explicit profiles broaden exec risk | `tools.exec.safeBins`, `tools.exec.safeBinProfiles`, `agents.list[].tools.exec.*` | no |
| `security.exposure.open_groups_with_elevated` | critical | Open groups + elevated tools create high-impact prompt-injection paths | `channels.*.groupPolicy`, `tools.elevated.*` | no |
| `security.exposure.open_groups_with_runtime_or_fs` | critical/warn | Open groups can reach command/file tools without sandbox/workspace guards | `channels.*.groupPolicy`, `tools.profile/deny`, `tools.fs.workspaceOnly`, `agents.*.sandbox.mode` | no |
| `security.trust_model.multi_user_heuristic` | warn | Config looks multi-user while gateway trust model is personal-assistant | split trust boundaries, or shared-user hardening (`sandbox.mode`, tool deny/workspace scoping) | no |
| `tools.profile_minimal_overridden` | warn | Agent overrides bypass global minimal profile | `agents.list[].tools.profile` | no |
| `plugins.tools_reachable_permissive_policy` | warn | Extension tools reachable in permissive contexts | `tools.profile` + tool allow/deny | no |
| `models.small_params` | critical/info | Small models + unsafe tool surfaces raise injection risk | model choice + sandbox/tool policy | no |
| `checkId` | Severity | Why it matters | Primary fix key/path | Auto-fix |
| -------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -------- |
| `fs.state_dir.perms_world_writable` | critical | Other users/processes can modify full OpenClaw state | filesystem perms on `~/.openclaw` | yes |
| `fs.config.perms_writable` | critical | Others can change auth/tool policy/config | filesystem perms on `~/.openclaw/openclaw.json` | yes |
| `fs.config.perms_world_readable` | critical | Config can expose tokens/settings | filesystem perms on config file | yes |
| `gateway.bind_no_auth` | critical | Remote bind without shared secret | `gateway.bind`, `gateway.auth.*` | no |
| `gateway.loopback_no_auth` | critical | Reverse-proxied loopback may become unauthenticated | `gateway.auth.*`, proxy setup | no |
| `gateway.http.no_auth` | warn/critical | Gateway HTTP APIs reachable with `auth.mode="none"` | `gateway.auth.mode`, `gateway.http.endpoints.*` | no |
| `gateway.tools_invoke_http.dangerous_allow` | warn/critical | Re-enables dangerous tools over HTTP API | `gateway.tools.allow` | no |
| `gateway.nodes.allow_commands_dangerous` | warn/critical | Enables high-impact node commands (camera/screen/contacts/calendar/SMS) | `gateway.nodes.allowCommands` | no |
| `gateway.tailscale_funnel` | critical | Public internet exposure | `gateway.tailscale.mode` | no |
| `gateway.control_ui.allowed_origins_required` | critical | Non-loopback Control UI without explicit browser-origin allowlist | `gateway.controlUi.allowedOrigins` | no |
| `gateway.control_ui.host_header_origin_fallback` | warn/critical | Enables Host-header origin fallback (DNS rebinding hardening downgrade) | `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` | no |
| `gateway.control_ui.insecure_auth` | warn | Insecure-auth compatibility toggle enabled | `gateway.controlUi.allowInsecureAuth` | no |
| `gateway.control_ui.device_auth_disabled` | critical | Disables device identity check | `gateway.controlUi.dangerouslyDisableDeviceAuth` | no |
| `gateway.real_ip_fallback_enabled` | warn/critical | Trusting `X-Real-IP` fallback can enable source-IP spoofing via proxy misconfig | `gateway.allowRealIpFallback`, `gateway.trustedProxies` | no |
| `discovery.mdns_full_mode` | warn/critical | mDNS full mode advertises `cliPath`/`sshPort` metadata on local network | `discovery.mdns.mode`, `gateway.bind` | no |
| `config.insecure_or_dangerous_flags` | warn | Any insecure/dangerous debug flags enabled | multiple keys (see finding detail) | no |
| `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no |
| `hooks.request_session_key_enabled` | warn/critical | External caller can choose sessionKey | `hooks.allowRequestSessionKey` | no |
| `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no |
| `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes |
| `sandbox.docker_config_mode_off` | warn | Sandbox Docker config present but inactive | `agents.*.sandbox.mode` | no |
| `sandbox.dangerous_network_mode` | critical | Sandbox Docker network uses `host` or `container:*` namespace-join mode | `agents.*.sandbox.docker.network` | no |
| `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` resolves to host exec when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no |
| `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` resolves to host exec when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no |
| `tools.exec.safe_bins_interpreter_unprofiled` | warn | Interpreter/runtime bins in `safeBins` without explicit profiles broaden exec risk | `tools.exec.safeBins`, `tools.exec.safeBinProfiles`, `agents.list[].tools.exec.*` | no |
| `skills.workspace.symlink_escape` | warn | Workspace `skills/**/SKILL.md` resolves outside workspace root (symlink-chain drift) | workspace `skills/**` filesystem state | no |
| `security.exposure.open_groups_with_elevated` | critical | Open groups + elevated tools create high-impact prompt-injection paths | `channels.*.groupPolicy`, `tools.elevated.*` | no |
| `security.exposure.open_groups_with_runtime_or_fs` | critical/warn | Open groups can reach command/file tools without sandbox/workspace guards | `channels.*.groupPolicy`, `tools.profile/deny`, `tools.fs.workspaceOnly`, `agents.*.sandbox.mode` | no |
| `security.trust_model.multi_user_heuristic` | warn | Config looks multi-user while gateway trust model is personal-assistant | split trust boundaries, or shared-user hardening (`sandbox.mode`, tool deny/workspace scoping) | no |
| `tools.profile_minimal_overridden` | warn | Agent overrides bypass global minimal profile | `agents.list[].tools.profile` | no |
| `plugins.tools_reachable_permissive_policy` | warn | Extension tools reachable in permissive contexts | `tools.profile` + tool allow/deny | no |
| `models.small_params` | critical/info | Small models + unsafe tool surfaces raise injection risk | model choice + sandbox/tool policy | no |
## Control UI over HTTP
@@ -515,7 +516,7 @@ Even with strong system prompts, **prompt injection is not solved**. System prom
- Run sensitive tool execution in a sandbox; keep secrets out of the agents reachable filesystem.
- Note: sandboxing is opt-in. If sandbox mode is off, exec runs on the gateway host even though tools.exec.host defaults to sandbox, and host exec does not require approvals unless you set host=gateway and configure exec approvals.
- Limit high-risk tools (`exec`, `browser`, `web_fetch`, `web_search`) to trusted agents or explicit allowlists.
- **Model choice matters:** older/legacy models can be less robust against prompt injection and tool misuse. Prefer modern, instruction-hardened models for any bot with tools. We recommend Anthropic Opus 4.6 (or the latest Opus) because its strong at recognizing prompt injections (see [“A step forward on safety”](https://www.anthropic.com/news/claude-opus-4-5)).
- **Model choice matters:** older/smaller/legacy models are significantly less robust against prompt injection and tool misuse. For tool-enabled agents, use the strongest latest-generation, instruction-hardened model available.
Red flags to treat as untrusted:
@@ -566,10 +567,14 @@ tool calls. Reduce the blast radius by:
Prompt injection resistance is **not** uniform across model tiers. Smaller/cheaper models are generally more susceptible to tool misuse and instruction hijacking, especially under adversarial prompts.
<Warning>
For tool-enabled agents or agents that read untrusted content, prompt-injection risk with older/smaller models is often too high. Do not run those workloads on weak model tiers.
</Warning>
Recommendations:
- **Use the latest generation, best-tier model** for any bot that can run tools or touch files/networks.
- **Avoid weaker tiers** (for example, Sonnet or Haiku) for tool-enabled agents or untrusted inboxes.
- **Do not use older/weaker/smaller tiers** for tool-enabled agents or untrusted inboxes; the prompt-injection risk is too high.
- If you must use a smaller model, **reduce blast radius** (read-only tools, strong sandboxing, minimal filesystem access, strict allowlists).
- When running small models, **enable sandboxing for all sessions** and **disable web_search/web_fetch/browser** unless inputs are tightly controlled.
- For chat-only personal assistants with trusted input and no tools, smaller models are usually fine.

View File

@@ -101,6 +101,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [I set `gateway.bind: "lan"` (or `"tailnet"`) and now nothing listens / the UI says unauthorized](#i-set-gatewaybind-lan-or-tailnet-and-now-nothing-listens-the-ui-says-unauthorized)
- [Why do I need a token on localhost now?](#why-do-i-need-a-token-on-localhost-now)
- [Do I have to restart after changing config?](#do-i-have-to-restart-after-changing-config)
- [How do I disable funny CLI taglines?](#how-do-i-disable-funny-cli-taglines)
- [How do I enable web search (and web fetch)?](#how-do-i-enable-web-search-and-web-fetch)
- [config.apply wiped my config. How do I recover and avoid this?](#configapply-wiped-my-config-how-do-i-recover-and-avoid-this)
- [How do I run a central Gateway with specialized workers across devices?](#how-do-i-run-a-central-gateway-with-specialized-workers-across-devices)
@@ -147,7 +148,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
- [Can I use GPT 5.2 for daily tasks and Codex 5.3 for coding](#can-i-use-gpt-52-for-daily-tasks-and-codex-53-for-coding)
- [Why do I see "Model … is not allowed" and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
- [Why do I see "Unknown model: minimax/MiniMax-M2.1"?](#why-do-i-see-unknown-model-minimaxminimaxm21)
- [Why do I see "Unknown model: minimax/MiniMax-M2.5"?](#why-do-i-see-unknown-model-minimaxminimaxm25)
- [Can I use MiniMax as my default and OpenAI for complex tasks?](#can-i-use-minimax-as-my-default-and-openai-for-complex-tasks)
- [Are opus / sonnet / gpt built-in shortcuts?](#are-opus-sonnet-gpt-builtin-shortcuts)
- [How do I define/override model shortcuts (aliases)?](#how-do-i-defineoverride-model-shortcuts-aliases)
@@ -688,7 +689,7 @@ Docs: [Update](/cli/update), [Updating](/install/updating).
`openclaw onboard` is the recommended setup path. In **local mode** it walks you through:
- **Model/auth setup** (Anthropic **setup-token** recommended for Claude subscriptions, OpenAI Codex OAuth supported, API keys optional, LM Studio local models supported)
- **Model/auth setup** (provider OAuth/setup-token flows and API keys supported, plus local model options such as LM Studio)
- **Workspace** location + bootstrap files
- **Gateway settings** (bind/port/auth/tailscale)
- **Providers** (WhatsApp, Telegram, Discord, Mattermost (plugin), Signal, iMessage)
@@ -703,6 +704,10 @@ No. You can run OpenClaw with **API keys** (Anthropic/OpenAI/others) or with
**local-only models** so your data stays on your device. Subscriptions (Claude
Pro/Max or OpenAI Codex) are optional ways to authenticate those providers.
If you choose Anthropic subscription auth, decide for yourself whether to use it:
Anthropic has blocked some subscription usage outside Claude Code in the past.
OpenAI Codex OAuth is explicitly supported for external tools like OpenClaw.
Docs: [Anthropic](/providers/anthropic), [OpenAI](/providers/openai),
[Local models](/gateway/local-models), [Models](/concepts/models).
@@ -712,9 +717,9 @@ Yes. You can authenticate with a **setup-token**
instead of an API key. This is the subscription path.
Claude Pro/Max subscriptions **do not include an API key**, so this is the
correct approach for subscription accounts. Important: you must verify with
Anthropic that this usage is allowed under their subscription policy and terms.
If you want the most explicit, supported path, use an Anthropic API key.
technical path for subscription accounts. But this is your decision: Anthropic
has blocked some subscription usage outside Claude Code in the past.
If you want the clearest and safest supported path for production, use an Anthropic API key.
### How does Anthropic setuptoken auth work
@@ -734,12 +739,15 @@ Copy the token it prints, then choose **Anthropic token (paste setup-token)** in
Yes - via **setup-token**. OpenClaw no longer reuses Claude Code CLI OAuth tokens; use a setup-token or an Anthropic API key. Generate the token anywhere and paste it on the gateway host. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth).
Note: Claude subscription access is governed by Anthropic's terms. For production or multi-user workloads, API keys are usually the safer choice.
Important: this is technical compatibility, not a policy guarantee. Anthropic
has blocked some subscription usage outside Claude Code in the past.
You need to decide whether to use it and verify Anthropic's current terms.
For production or multi-user workloads, Anthropic API key auth is the safer, recommended choice.
### Why am I seeing HTTP 429 ratelimiterror from Anthropic
That means your **Anthropic quota/rate limit** is exhausted for the current window. If you
use a **Claude subscription** (setup-token or Claude Code OAuth), wait for the window to
use a **Claude subscription** (setup-token), wait for the window to
reset or upgrade your plan. If you use an **Anthropic API key**, check the Anthropic Console
for usage/billing and raise limits as needed.
@@ -763,8 +771,9 @@ OpenClaw supports **OpenAI Code (Codex)** via OAuth (ChatGPT sign-in). The wizar
### Do you support OpenAI subscription auth Codex OAuth
Yes. OpenClaw fully supports **OpenAI Code (Codex) subscription OAuth**. The onboarding wizard
can run the OAuth flow for you.
Yes. OpenClaw fully supports **OpenAI Code (Codex) subscription OAuth**.
OpenAI explicitly allows subscription OAuth usage in external tools/workflows
like OpenClaw. The onboarding wizard can run the OAuth flow for you.
See [OAuth](/concepts/oauth), [Model providers](/concepts/model-providers), and [Wizard](/start/wizard).
@@ -781,7 +790,7 @@ This stores OAuth tokens in auth profiles on the gateway host. Details: [Model p
### Is a local model OK for casual chats
Usually no. OpenClaw needs large context + strong safety; small cards truncate and leak. If you must, run the **largest** MiniMax M2.1 build you can locally (LM Studio) and see [/gateway/local-models](/gateway/local-models). Smaller/quantized models increase prompt-injection risk - see [Security](/gateway/security).
Usually no. OpenClaw needs large context + strong safety; small cards truncate and leak. If you must, run the **largest** MiniMax M2.5 build you can locally (LM Studio) and see [/gateway/local-models](/gateway/local-models). Smaller/quantized models increase prompt-injection risk - see [Security](/gateway/security).
### How do I keep hosted model traffic in a specific region
@@ -1290,12 +1299,13 @@ It prefers OpenAI if an OpenAI key resolves, otherwise Gemini if a Gemini key
resolves, then Voyage, then Mistral. If no remote key is available, memory
search stays disabled until you configure it. If you have a local model path
configured and present, OpenClaw
prefers `local`.
prefers `local`. Ollama is supported when you explicitly set
`memorySearch.provider = "ollama"`.
If you'd rather stay local, set `memorySearch.provider = "local"` (and optionally
`memorySearch.fallback = "none"`). If you want Gemini embeddings, set
`memorySearch.provider = "gemini"` and provide `GEMINI_API_KEY` (or
`memorySearch.remote.apiKey`). We support **OpenAI, Gemini, Voyage, Mistral, or local** embedding
`memorySearch.remote.apiKey`). We support **OpenAI, Gemini, Voyage, Mistral, Ollama, or local** embedding
models - see [Memory](/concepts/memory) for the setup details.
### Does memory persist forever What are the limits
@@ -1458,6 +1468,25 @@ The Gateway watches the config and supports hot-reload:
- `gateway.reload.mode: "hybrid"` (default): hot-apply safe changes, restart for critical ones
- `hot`, `restart`, `off` are also supported
### How do I disable funny CLI taglines
Set `cli.banner.taglineMode` in config:
```json5
{
cli: {
banner: {
taglineMode: "off", // random | default | off
},
},
}
```
- `off`: hides tagline text but keeps the banner title/version line.
- `default`: uses `All your chats, one OpenClaw.` every time.
- `random`: rotating funny/seasonal taglines (default behavior).
- If you want no banner at all, set env `OPENCLAW_HIDE_BANNER=1`.
### How do I enable web search and web fetch
`web_fetch` works without an API key. `web_search` requires a Brave Search API
@@ -2028,12 +2057,11 @@ Models are referenced as `provider/model` (example: `anthropic/claude-opus-4-6`)
### What model do you recommend
**Recommended default:** `anthropic/claude-opus-4-6`.
**Good alternative:** `anthropic/claude-sonnet-4-5`.
**Reliable (less character):** `openai/gpt-5.2` - nearly as good as Opus, just less personality.
**Budget:** `zai/glm-4.7`.
**Recommended default:** use the strongest latest-generation model available in your provider stack.
**For tool-enabled or untrusted-input agents:** prioritize model strength over cost.
**For routine/low-stakes chat:** use cheaper fallback models and route by agent role.
MiniMax M2.1 has its own docs: [MiniMax](/providers/minimax) and
MiniMax M2.5 has its own docs: [MiniMax](/providers/minimax) and
[Local models](/gateway/local-models).
Rule of thumb: use the **best model you can afford** for high-stakes work, and a cheaper
@@ -2077,8 +2105,9 @@ Docs: [Models](/concepts/models), [Configure](/cli/configure), [Config](/cli/con
### What do OpenClaw, Flawd, and Krill use for models
- **OpenClaw + Flawd:** Anthropic Opus (`anthropic/claude-opus-4-6`) - see [Anthropic](/providers/anthropic).
- **Krill:** MiniMax M2.1 (`minimax/MiniMax-M2.1`) - see [MiniMax](/providers/minimax).
- These deployments can differ and may change over time; there is no fixed provider recommendation.
- Check the current runtime setting on each gateway with `openclaw models status`.
- For security-sensitive/tool-enabled agents, use the strongest latest-generation model available.
### How do I switch models on the fly without restarting
@@ -2145,7 +2174,7 @@ Model "provider/model" is not allowed. Use /model to list available models.
That error is returned **instead of** a normal reply. Fix: add the model to
`agents.defaults.models`, remove the allowlist, or pick a model from `/model list`.
### Why do I see Unknown model minimaxMiniMaxM21
### Why do I see Unknown model minimaxMiniMaxM25
This means the **provider isn't configured** (no MiniMax provider config or auth
profile was found), so the model can't be resolved. A fix for this detection is
@@ -2156,8 +2185,8 @@ Fix checklist:
1. Upgrade to **2026.1.12** (or run from source `main`), then restart the gateway.
2. Make sure MiniMax is configured (wizard or JSON), or that a MiniMax API key
exists in env/auth profiles so the provider can be injected.
3. Use the exact model id (case-sensitive): `minimax/MiniMax-M2.1` or
`minimax/MiniMax-M2.1-lightning`.
3. Use the exact model id (case-sensitive): `minimax/MiniMax-M2.5` or
`minimax/MiniMax-M2.5-highspeed` (legacy: `minimax/MiniMax-M2.5-Lightning`).
4. Run:
```bash
@@ -2180,9 +2209,9 @@ Fallbacks are for **errors**, not "hard tasks," so use `/model` or a separate ag
env: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.1" },
model: { primary: "minimax/MiniMax-M2.5" },
models: {
"minimax/MiniMax-M2.1": { alias: "minimax" },
"minimax/MiniMax-M2.5": { alias: "minimax" },
"openai/gpt-5.2": { alias: "gpt" },
},
},
@@ -2260,8 +2289,8 @@ Z.AI (GLM models):
{
agents: {
defaults: {
model: { primary: "zai/glm-4.7" },
models: { "zai/glm-4.7": {} },
model: { primary: "zai/glm-5" },
models: { "zai/glm-5": {} },
},
},
env: { ZAI_API_KEY: "..." },

View File

@@ -136,7 +136,7 @@ Live tests are split into two layers so we can isolate failures:
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
- Set `OPENCLAW_LIVE_MODELS=modern` (or `all`, alias for modern) to actually run this suite; otherwise it skips to keep `pnpm test:live` focused on gateway smoke
- How to select models:
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet/Haiku 4.5, GPT-5.x + Codex, Gemini 3, GLM 4.7, MiniMax M2.1, Grok 4)
- `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet/Haiku 4.5, GPT-5.x + Codex, Gemini 3, GLM 4.7, MiniMax M2.5, Grok 4)
- `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist
- or `OPENCLAW_LIVE_MODELS="openai/gpt-5.2,anthropic/claude-opus-4-6,..."` (comma allowlist)
- How to select providers:
@@ -167,7 +167,7 @@ Live tests are split into two layers so we can isolate failures:
- How to enable:
- `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly)
- How to select models:
- Default: modern allowlist (Opus/Sonnet/Haiku 4.5, GPT-5.x + Codex, Gemini 3, GLM 4.7, MiniMax M2.1, Grok 4)
- Default: modern allowlist (Opus/Sonnet/Haiku 4.5, GPT-5.x + Codex, Gemini 3, GLM 4.7, MiniMax M2.5, Grok 4)
- `OPENCLAW_LIVE_GATEWAY_MODELS=all` is an alias for the modern allowlist
- Or set `OPENCLAW_LIVE_GATEWAY_MODELS="provider/model"` (or comma list) to narrow
- How to select providers (avoid “OpenRouter everything”):
@@ -251,7 +251,7 @@ Narrow, explicit allowlists are fastest and least flaky:
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.2" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- Tool calling across several providers:
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.2,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.2,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,zai/glm-4.7,minimax/minimax-m2.5" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- Google focus (Gemini API key + Antigravity):
- Gemini (API key): `OPENCLAW_LIVE_GATEWAY_MODELS="google/gemini-3-flash-preview" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
@@ -280,10 +280,10 @@ This is the “common models” run we expect to keep working:
- Google (Gemini API): `google/gemini-3-pro-preview` and `google/gemini-3-flash-preview` (avoid older Gemini 2.x models)
- Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash`
- Z.AI (GLM): `zai/glm-4.7`
- MiniMax: `minimax/minimax-m2.1`
- MiniMax: `minimax/minimax-m2.5`
Run gateway smoke with tools + image:
`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.2,openai-codex/gpt-5.3-codex,anthropic/claude-opus-4-6,google/gemini-3-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.2,openai-codex/gpt-5.3-codex,anthropic/claude-opus-4-6,google/gemini-3-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.5" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
### Baseline: tool calling (Read + optional Exec)
@@ -293,7 +293,7 @@ Pick at least one per provider family:
- Anthropic: `anthropic/claude-opus-4-6` (or `anthropic/claude-sonnet-4-5`)
- Google: `google/gemini-3-flash-preview` (or `google/gemini-3-pro-preview`)
- Z.AI (GLM): `zai/glm-4.7`
- MiniMax: `minimax/minimax-m2.1`
- MiniMax: `minimax/minimax-m2.5`
Optional additional coverage (nice to have):

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -54,7 +54,7 @@ OpenClaw is a **self-hosted gateway** that connects your favorite chat apps —
- **Agent-native**: built for coding agents with tool use, sessions, memory, and multi-agent routing
- **Open source**: MIT licensed, community-driven
**What do you need?** Node 22+, an API key (Anthropic recommended), and 5 minutes.
**What do you need?** Node 22+, an API key from your chosen provider, and 5 minutes. For best quality and security, use the strongest latest-generation model available.
## How it works

View File

@@ -15,7 +15,7 @@ read_when:
- [flyctl CLI](https://fly.io/docs/hands-on/install-flyctl/) installed
- Fly.io account (free tier works)
- Model auth: Anthropic API key (or other provider keys)
- Model auth: API key for your chosen model provider
- Channel credentials: Discord bot token, Telegram token, etc.
## Beginner quick path

View File

@@ -23,7 +23,7 @@ What I need you to do:
1. Check if Determinate Nix is installed (if not, install it)
2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix
3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot)
4. Set up secrets (bot token, Anthropic key) - plain files at ~/.secrets/ is fine
4. Set up secrets (bot token, model provider API key) - plain files at ~/.secrets/ is fine
5. Fill in the template placeholders and run home-manager switch
6. Verify: launchd running, bot responds to messages

Some files were not shown because too many files have changed in this diff Show More