Compare commits

..

2062 Commits

Author SHA1 Message Date
Peter Steinberger
7ec621ab58 chore: polish qa lab follow-ups 2026-04-05 09:13:50 +01:00
Peter Steinberger
8762df9bb4 feat: add qa lab extension 2026-04-05 09:13:49 +01:00
Peter Steinberger
dce0467826 refactor: hide qa channels with exposure metadata 2026-04-05 09:12:00 +01:00
Peter Steinberger
258484854b feat: add qa channel foundation 2026-04-05 09:12:00 +01:00
Vincent Koc
4c11a520a8 fix(plugins): carry workspaceDir in runtime state 2026-04-05 09:10:09 +01:00
Peter Steinberger
3038079c2f fix: clamp pi embedded fenced chunk splits 2026-04-05 09:09:26 +01:00
Peter Steinberger
b57372d665 refactor: route capability runtime through channel stores 2026-04-05 09:07:33 +01:00
Peter Steinberger
1903be5401 refactor: remove generated plugin sdk facades 2026-04-05 09:07:33 +01:00
Peter Steinberger
fd968bfb2d fix: recover unloaded macOS launch agents (#43766) 2026-04-05 17:06:22 +09:00
Peter Steinberger
07e7b7177f test: stabilize pi embedded text-end hooks 2026-04-05 09:06:15 +01:00
Jamil Zakirov
ffb5b99114 fix: propagate workspaceDir to snapshot plugin loads (#61138)
* plugins: include resolved workspaceDir in provider hook cache keys

resolveProviderPluginsForHooks, resolveProviderPluginsForCatalogHooks, and
resolveProviderRuntimePlugin used the raw params.workspaceDir for cache keys
and plugin-id discovery while resolvePluginProviders already fell back to
the active registry workspace. Resolve workspaceDir once at the top of each
function so cache keys, candidate filtering, and loading all use the same
workspace root.

* fix(plugins): inherit runtime workspace for snapshot loads

* test(gateway): stub runtime registry seam

* fix(plugins): restore workspace fallback after rebase

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 09:03:54 +01:00
Peter Steinberger
e28e83f4e4 test: load browser fixture for sandbox policy 2026-04-05 09:00:42 +01:00
Peter Steinberger
3728cfbe29 test: use stable sandbox denied tool assertions 2026-04-05 08:58:55 +01:00
Peter Steinberger
70b8ce72df test: refresh simple completion provider fallback 2026-04-05 08:57:04 +01:00
Vincent Koc
69b74476d7 fix(contracts): lock runtime seam regressions 2026-04-05 08:52:20 +01:00
Peter Steinberger
23275edef1 refactor: simplify web provider plugin discovery 2026-04-05 08:50:01 +01:00
Vincent Koc
c863ee1b86 fix(config): migrate bundled private-network aliases (#60862)
* refactor(plugin-sdk): centralize private-network opt-in semantics

* fix(config): migrate bundled private-network aliases

* fix(config): add bundled private-network doctor adapters

* fix(config): expose bundled channel migration hooks

* fix(config): prefer canonical private-network key

* test(config): refresh rebased private-network outputs
2026-04-05 08:49:44 +01:00
Vincent Koc
87b8680ded fix(cache): order stable project context before heartbeat (#61236)
* fix(cache): order stable project context before heartbeat

* docs(changelog): note project context cache ordering

* Update CHANGELOG.md
2026-04-05 08:49:20 +01:00
Vincent Koc
2d2824874e fix(contracts): align provider and sdk inventories 2026-04-05 08:44:35 +01:00
Peter Steinberger
07c2f81392 fix: preserve explicit Ollama apiKey during discovery 2026-04-05 08:43:50 +01:00
Peter Steinberger
377ccbcf1d test: stabilize gateway chat and method suites 2026-04-05 08:43:21 +01:00
Peter Steinberger
3635b2b8d6 test: split gateway session utils coverage 2026-04-05 08:43:21 +01:00
Vincent Koc
49f52ddf36 fix(fetch): honor mocked global fetch with dispatchers 2026-04-05 08:42:37 +01:00
wzfmini01
ef5f47bd39 fix(google-gemini-cli-auth): detect bundled npm installs (#60486) (#60486)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:41:43 +01:00
Peter Steinberger
acd8966ff0 test: refresh agent model expectation fixtures 2026-04-05 08:33:54 +01:00
Peter Steinberger
31d8b022eb fix: treat inline buttons as native approval ui 2026-04-05 08:33:54 +01:00
Peter Steinberger
d91d3cc0f0 fix: respect custom env snapshots for vertex auth 2026-04-05 08:33:54 +01:00
Peter Steinberger
c9c7271f4f test: keep mocked fetch active with guarded dispatchers 2026-04-05 08:33:54 +01:00
Ted Li
b474e098d1 docs: correct overstated prompt-cache comments from #58036 #58037 #58038 (#60633)
* docs: correct overstated prompt-cache comments from #58036 #58037 #58038

* docs: restore purpose context in MCP tool sort comment

* docs: drop misleading 'legacy' framing from image-prune comments

* docs: restore useful context stripped from image-prune comments

* docs: restore 'deterministically' in MCP tool sort comment

* docs: restore 'idempotent' at attempt.ts callsite

* docs: restore 'provider prompt cache' in context-guard comment
2026-04-05 08:32:51 +01:00
Peter Steinberger
c2bf2cc2b7 test: stabilize gateway config.apply cases 2026-04-05 08:31:08 +01:00
wirjo
019a25e35c Fix/bedrock aws sdk apikey injection (#61194)
* fix(bedrock): stop injecting fake apiKey marker for aws-sdk auth when no env vars exist

When the Bedrock provider uses auth: "aws-sdk" and no AWS environment
variables are set (EC2 instance roles, ECS task roles, etc.),
resolveAwsSdkApiKeyVarName() fell back to "AWS_PROFILE" unconditionally.
This string was injected as apiKey in the provider config during
normalisation, which poisoned the downstream auth resolver — it treated
the marker as a literal key and failed with "No API key found".

The fix:
- resolveAwsSdkApiKeyVarName() now returns undefined (not "AWS_PROFILE")
  when no AWS env vars are present
- resolveBedrockConfigApiKey() (extension) gets the same fix
- resolveMissingProviderApiKey() guards both the providerApiKeyResolver
  and direct aws-sdk branches: if the resolver returns nothing, the
  provider config is returned unchanged (no apiKey injected)
- The aws-sdk credential chain then resolves credentials at request time
  via IMDS/ECS task role/etc. as intended

When AWS env vars ARE present (AWS_ACCESS_KEY_ID, AWS_PROFILE,
AWS_BEARER_TOKEN_BEDROCK), the marker is still injected correctly.

Closes #49891
Closes #50699
Fixes #54274

* test(bedrock): update resolveBedrockConfigApiKey test for undefined return on empty env

The test previously expected "AWS_PROFILE" when no env vars are set.
Now expects undefined (matching the fix), and adds a separate assertion
that AWS_PROFILE is returned when the env var is actually present.

* fix(bedrock): lock aws-sdk env marker behavior

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:24:05 +01:00
狼哥
eb130aa4e9 fix(google): disable pinned dns for image generation (#59873)
* fix(google): restore proxy-safe image generation (#59873)

* fix(ssrf): preserve transport policy without pinned dns

* fix(ssrf): use undici fetch for dispatcher requests

* fix(ssrf): type dispatcher fetch path

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:23:22 +01:00
Peter Steinberger
9238b98a7a fix: fall back to resolved agent dir for btw command 2026-04-05 08:21:52 +01:00
Peter Steinberger
2aafa8fb7d refactor: remove ollama sdk facades 2026-04-05 08:15:39 +01:00
Vincent Koc
155f4300ba fix(voice-call): use full config for realtime transcription (#61224)
* fix(voice-call): use full config for realtime transcription

* fix(changelog): note voice-call transcription regression

* Update CHANGELOG.md
2026-04-05 08:14:41 +01:00
Vincent Koc
42bc411c46 fix(gateway): catch invalid cron session targets 2026-04-05 08:10:29 +01:00
André Santos
eb0f367e00 fix(cache): enable prompt cache retention for Anthropic Vertex AI (#60888)
* fix(cache): enable prompt cache retention for Anthropic Vertex AI

* fix(cache): add anthropic-vertex to isAnthropicFamilyCacheTtlEligible

* fix(cache): use hostname parsing for long-TTL endpoint eligibility

* docs(changelog): note anthropic vertex cache ttl fix

---------

Co-authored-by: affsantos <andreffsantos91@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-05 08:07:02 +01:00
Peter Steinberger
a6894a5238 test: harden live model skip handling 2026-04-05 08:04:56 +01:00
Peter Steinberger
68851f2e97 fix(config): cap generated schema export types 2026-04-05 07:58:02 +01:00
Peter Steinberger
20803dac14 fix: fail closed for invalid persisted cron targets 2026-04-05 07:57:16 +01:00
Peter Steinberger
b7a08c6bad fix: preserve catalog metadata for allowlisted models 2026-04-05 07:56:31 +01:00
Peter Steinberger
20b08f1a85 fix: enforce paired scope baselines on reconnect 2026-04-05 07:53:57 +01:00
Vincent Koc
19b7fbaa73 fix(memory): honor mocked batch fetch clients 2026-04-05 07:48:03 +01:00
Peter Steinberger
a65ab607c7 fix(gateway): use launchd KeepAlive restarts 2026-04-05 07:43:37 +01:00
Peter Steinberger
d655a8bc76 feat: add Fireworks provider and simplify plugin setup loading 2026-04-05 07:43:14 +01:00
Ayaan Zaidi
f842f518cd fix: update embedded runner transport override (#61214)
* fix: update embedded runner transport override

* fix: update embedded runner transport override (#61214)

* fix: update embedded runner transport override (#61214)

* fix: update embedded runner transport override (#61214)
2026-04-05 12:12:50 +05:30
Peter Steinberger
bf226be64a test: keep cli backend coverage on core seams 2026-04-05 07:40:46 +01:00
Peter Steinberger
c9029503fd fix: honor mocked guarded fetch implementations 2026-04-05 07:39:43 +01:00
Vincent Koc
c09bf9812a fix(build): restore main build on current agent api 2026-04-05 07:38:09 +01:00
Vincent Koc
005766671e fix(ci): use agent transport property 2026-04-05 07:34:45 +01:00
Vincent Koc
cb1bf28526 build(a2ui): allow sparse core builds 2026-04-05 07:34:33 +01:00
Vincent Koc
2a999bf9c9 refactor(memory): invert memory host sdk dependency 2026-04-05 07:34:33 +01:00
Peter Steinberger
f59da4557c test: refresh gateway talk and scope fixtures 2026-04-05 07:31:30 +01:00
Peter Steinberger
332afa2fda refactor: narrow claude cli fallback seams 2026-04-05 07:29:32 +01:00
Vincent Koc
3da235bf39 fix(telegram): force paginated commands callbacks 2026-04-05 07:28:47 +01:00
Vincent Koc
61fc4a16b7 docs(changelog): remove duplicate Unreleased entries 2026-04-05 07:23:04 +01:00
Vincent Koc
db1d62b784 test(ci): cover bare default provider inference 2026-04-05 07:19:52 +01:00
Peter Steinberger
a084e46536 fix: use undici runtime fetch for dispatcher flows 2026-04-05 07:18:33 +01:00
Peter Steinberger
757fe86309 test: lock whatsapp session migration keys 2026-04-05 07:18:15 +01:00
Peter Steinberger
657c6f6788 fix: stabilize docker e2e lanes 2026-04-05 07:15:24 +01:00
Peter Steinberger
e5023cc141 fix(agents): invalidate stale cli sessions on auth changes 2026-04-05 07:14:52 +01:00
Peter Steinberger
903cb3c48c test: align bash exec mocks with reset modules 2026-04-05 07:10:49 +01:00
Peter Steinberger
37cc06f1fd fix: normalize claude cli fallback config 2026-04-05 07:09:13 +01:00
Ayaan Zaidi
f039bbf2aa fix: resolve acpx plugin root from shared chunks 2026-04-05 11:37:05 +05:30
Peter Steinberger
e25693315e fix: stabilize embedded runner transport and channel state 2026-04-05 07:04:18 +01:00
Peter Steinberger
749ed86fe3 test: stabilize gateway canvas and session cleanup 2026-04-05 07:04:18 +01:00
Peter Steinberger
5e0e50b12e test: stabilize gateway wizard e2e flow 2026-04-05 07:04:18 +01:00
Ayaan Zaidi
4cfb990382 fix: restore whatsapp doctor contract surface 2026-04-05 11:31:12 +05:30
Peter Steinberger
e9fa9f7822 test: reload transcript policy smoke module 2026-04-05 06:59:55 +01:00
Peter Steinberger
cb31c4813b test: mock models config planner in write serialization 2026-04-05 06:54:40 +01:00
Peter Steinberger
f5da2360a2 test: scope models config write serialization spy 2026-04-05 06:51:08 +01:00
Peter Steinberger
7f6e8c0645 test: reload gateway status command under mocks 2026-04-05 06:46:47 +01:00
Peter Steinberger
055428019e test: harden bash tool async exec coverage 2026-04-05 06:42:26 +01:00
Peter Steinberger
b63557679e test: harden models-config write serialization timing 2026-04-05 06:10:30 +01:00
Peter Steinberger
058fde2d88 test: reload runtime plugins module per test 2026-04-05 06:06:12 +01:00
Peter Steinberger
74416c5b33 test: force real timers for exec foreground timeout 2026-04-05 06:01:21 +01:00
Peter Steinberger
f7a32cd25e test: reset imessage facade runtime before each test 2026-04-05 05:58:02 +01:00
Peter Steinberger
15d5878d91 test: update telegram paginated commands expectations 2026-04-05 05:53:42 +01:00
Peter Steinberger
50b5c483ee fix: canonicalize legacy whatsapp group sessions 2026-04-05 05:47:04 +01:00
tarouca
bf0f4d93f0 fix: restore Telegram DM voice-note transcription (#61008) (thanks @manueltarouca)
* fix(telegram): enable voice-note transcription in DMs

The preflight transcription condition only triggered for group chats
(isGroup && requireMention), so voice notes sent in direct messages
were never transcribed -- they arrived as raw <media:audio> placeholders.

This regression was introduced when the Telegram channel was moved from
src/telegram/ to extensions/telegram/, losing the fix from c15385fc94.

Widen the condition to fire whenever there is audio and no accompanying
text, regardless of chat type. Group-specific guards (requireMention,
disableAudioPreflight, senderAllowedForAudioPreflight) still apply
only in group contexts.

* fix: restore Telegram DM voice-note transcription (#61008) (thanks @manueltarouca)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 09:49:44 +05:30
Peter Steinberger
0a71ac5d3c fix: keep discord open-policy allowlist nested 2026-04-05 05:04:10 +01:00
Peter Steinberger
1392a78c75 fix: infer configured provider for bare default models 2026-04-05 05:04:10 +01:00
Ayaan Zaidi
87a0390666 fix: write nested plugin wizard config paths (#61159) 2026-04-05 08:59:12 +05:30
Ayaan Zaidi
69be9c4a6f fix: widen path utils root contract 2026-04-05 08:59:12 +05:30
Ayaan Zaidi
9af48d9c10 fix: write nested plugin wizard config paths 2026-04-05 08:59:12 +05:30
Ayaan Zaidi
11e6c9de2e fix: cancel in-flight Android talk playback on stop (#61164) 2026-04-05 08:53:36 +05:30
Ayaan Zaidi
a746ba2dcb test(android): cover playback disable idempotency 2026-04-05 08:53:36 +05:30
Ayaan Zaidi
8d1f9ab5b8 fix(android): cancel in-flight talk playback on stop 2026-04-05 08:53:36 +05:30
OfflynAI
f0c970fb43 fix: skip sandbox skill copy junk (#61090) (thanks @joelnishanth)
* fix(skills): exclude .git and node_modules when copying skills to workspace (#60879)

* fix(skills): cover sync copy exclusions

* fix: skip sandbox skill copy junk (#61090) (thanks @joelnishanth)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 08:42:58 +05:30
Peter Steinberger
a235f5ed64 test: stabilize gateway control ui auth suites 2026-04-05 12:11:29 +09:00
Peter Steinberger
2636cc261c fix: scope discord doctor allowFrom alias migration 2026-04-05 04:06:31 +01:00
Ayaan Zaidi
8355f24652 test: fix talk config gate regression 2026-04-05 08:34:33 +05:30
Peter Steinberger
54a360a33e fix: stabilize shared auth and sessions send tests 2026-04-05 12:03:30 +09:00
Peter Steinberger
cad1b89b26 fix: keep core gateway tool invokes on shipped tools 2026-04-05 12:03:30 +09:00
Peter Steinberger
740d096009 test: stabilize config apply gateway suite 2026-04-05 12:03:30 +09:00
Peter Steinberger
1811e54920 test: fix plugin stream typing assertions 2026-04-05 12:03:30 +09:00
Peter Steinberger
6596e64a68 fix: stabilize gateway auth fallback tests 2026-04-05 12:03:30 +09:00
Vincent Koc
2246e8f0a9 fix(ci): sanitize providerless model warning 2026-04-05 12:02:05 +09:00
Vincent Koc
d23a81baa1 fix(ci): add no-wait completion reply option 2026-04-05 12:00:41 +09:00
Vincent Koc
19ef298678 fix(ci): skip reply wait for non-message subagents 2026-04-05 11:59:16 +09:00
Vincent Koc
7d34c1dc4c test(ci): cover non-waiting subagent completion 2026-04-05 11:58:47 +09:00
Cathryn Lavery
7587e4cac3 fix: ensure bypassPermissions when custom CLI backend args override defaults (#61114)
* fix: ensure bypassPermissions on custom CLI backend args

When users override cliBackends.claude-cli.args (e.g. to add --verbose
or change --output-format), the override array replaces the default
entirely. The normalization step only re-added --permission-mode
bypassPermissions when the legacy --dangerously-skip-permissions flag
was present — if neither flag existed, it did nothing.

This causes cron and heartbeat runs to silently fail with "exec denied:
Cron runs cannot wait for interactive exec approval" because the CLI
subprocess launches in interactive permission mode.

Fix: always inject --permission-mode bypassPermissions when no explicit
permission-mode flag is found in the resolved args, regardless of
whether the legacy flag was present.

* test(anthropic): add claude-cli permission normalization coverage

* fix(test-utils): include video generation providers

* fix: preserve claude-cli bypassPermissions on custom args (#61114) (thanks @cathrynlavery)

---------

Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-04 21:55:14 -05:00
Ayaan Zaidi
91ddf3857a fix: use talk.speak for Android replies (#60954) 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
e4fe853439 fix(android): fall back on legacy talk errors 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
dd6b160707 fix(android): tighten compressed talk playback 2026-04-05 08:20:47 +05:30
Michael Faath
628fc21192 Android: stop reply speaker on voice teardown 2026-04-05 08:20:47 +05:30
Michael Faath
5942b1062e Android: route voice replies through reply speaker 2026-04-05 08:20:47 +05:30
Michael Faath
b4f0e5ae2c Android: fix mic capture queue race 2026-04-05 08:20:47 +05:30
Michael Faath
a4ada035d8 Gateway: use runtime config for talk.speak 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
3f67a52d52 docs(talk): update android playback docs 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
aae3ab152a chore(protocol): regenerate swift talk models 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
db13a29bbf test(android): cover talk.speak playback helpers 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
98d5939564 feat(android): add talk.speak playback path 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
b558610ef3 fix(elevenlabs): pass talk latency override 2026-04-05 08:20:47 +05:30
Ayaan Zaidi
823ce7957d fix(gateway): harden talk.speak responses 2026-04-05 08:20:47 +05:30
Peter Steinberger
fb580b551e fix: restore provider and config compatibility checks 2026-04-05 03:47:57 +01:00
Neerav Makwana
22175faaec fix: trim menu descriptions before dropping commands (#61129) (thanks @neeravmakwana)
* fix(telegram): trim menu descriptions before dropping commands

* fix: note Telegram command menu trimming (#61129) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-05 08:05:16 +05:30
Vincent Koc
f217e6b72d fix(test-utils): include video generation providers 2026-04-05 11:09:51 +09:00
Vincent Koc
b56517b0ee refactor(providers): tighten family outlier contracts 2026-04-05 11:09:26 +09:00
scoootscooob
6ab1b43081 fix(dotenv): load gateway.env compatibility fallback (#61084)
* fix(dotenv): load gateway env fallback

* fix(dotenv): preserve legacy cli env loading

* fix(dotenv): keep gateway fallback scoped to default profile
2026-04-04 18:24:29 -07:00
scoootscooob
9860db5cea fix(memory): allow Gemini multimodal fallback before registry hydration (#61085)
* fix(memory): allow Gemini multimodal fallback

* docs(memory): clarify multimodal fallback
2026-04-04 18:24:20 -07:00
Andy Tien
dca21563c6 fix(cli): set non-zero exit code on argument errors (#60923)
Merged via squash.

Prepared head SHA: 0de0c43111
Co-authored-by: Linux2010 <35169750+Linux2010@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 03:17:51 +03:00
Altay
f299bb812b test(agents): stabilize announce cleanup assertions (#61088)
* test(plugin-sdk): use telegram public config seam

* test(agents): stabilize announce cleanup assertions
2026-04-05 03:09:28 +03:00
Altay
04b64e40d4 test(plugin-sdk): type telegram command config mock 2026-04-05 02:37:16 +03:00
Altay
2ba3484d10 fix(plugin-sdk): avoid telegram config import side effects (#61061)
* fix(plugin-sdk): avoid telegram config import side effects

* fix(plugin-sdk): address telegram contract review

* test(plugin-sdk): tighten telegram contract guards
2026-04-05 02:32:04 +03:00
Altay
d37e4a6c3a fix(contracts): align Teams guard and MiniMax loader (#61068) 2026-04-05 02:13:46 +03:00
Peter Steinberger
2781897d2c fix: restore provider policy fallbacks 2026-04-04 23:43:47 +01:00
Altay
2b3e89c6d4 fix(ci): remove anthropic auth parse error 2026-04-05 01:40:25 +03:00
Altay
ccc7549afe fix(ci): break facade runtime init cycle (#61053)
* fix(ci): break facade runtime init cycle

* style(config): normalize provider schema imports
2026-04-05 01:31:59 +03:00
Kim
fd71bc04ec fix(skills): unify runtime inclusion and available_skills exposure policy (#60852)
Merged via squash.

Prepared head SHA: 2b48b3a455
Co-authored-by: KimGLee <150593189+KimGLee@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 01:30:22 +03:00
coolramukaka-sys
70015be8b5 fix(msteams): replace deprecated HttpPlugin with httpServerAdapter (#60939)
Merged via squash.

Prepared head SHA: 7fe7f3c6bb
Co-authored-by: coolramukaka-sys <271658891+coolramukaka-sys@users.noreply.github.com>
Co-authored-by: BradGroux <3053586+BradGroux@users.noreply.github.com>
Reviewed-by: @BradGroux
2026-04-04 17:21:45 -05:00
Peter Steinberger
37301cbc3b docs: clarify anthropic extra usage billing 2026-04-05 07:14:35 +09:00
Peter Steinberger
b4216d197d fix: restore anthropic setup-token auth flow 2026-04-05 07:14:35 +09:00
Xi Qi
334c4be73e style(UI): improve mobile chat layout (#60220)
* UI: improve mobile chat layout

* change .chat-group-messages min-width: from 604 to 602

* UI: fix chat-group-messages overflow in split-view and mobile layouts

* UI: revert chat.css import order in styles.css and components.css

* UI: simplify mobile chat layout overrides in grouped.css

* ui: move .chat and .chat-thread styles to chat/layout.css

* fix: document mobile chat layout improvements

* fix: improve narrow mobile chat width

---------

Co-authored-by: Altay <altay@uinaf.dev>
2026-04-05 01:05:48 +03:00
zhuzm
9d7fe7cdd2 Control UI: use a dedicated loading style for the Cron refresh button (#60394)
Merged via squash.

Prepared head SHA: f7757b9e34
Co-authored-by: coder-zhuzm <63866641+coder-zhuzm@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 00:59:07 +03:00
bbddbb
96aea0a6d6 fix(ui): prevent overview access grid layout overlap on resize (#56924)
Merged via squash.

Prepared head SHA: ab327093fc
Co-authored-by: bbddbb1 <75060417+bbddbb1@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 00:39:25 +03:00
Hanna
8b06ca205a fix(avatar): check ui.assistant.avatar in resolveAvatarSource (#60778)
Merged via squash.

Prepared head SHA: df8d953a14
Co-authored-by: hannasdev <4538260+hannasdev@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-05 00:36:02 +03:00
Peter Steinberger
63cabcb524 test: stabilize forked gateway suites 2026-04-05 06:33:07 +09:00
Peter Steinberger
801b5d4afa fix: stabilize isolated gateway test runtime 2026-04-05 06:33:07 +09:00
Peter Steinberger
329fbc3f89 docs: refresh claude-cli migration cli mirror 2026-04-04 22:22:04 +01:00
Peter Steinberger
bc910942e2 docs: refresh history sanitization tag mirrors 2026-04-04 22:21:26 +01:00
Peter Steinberger
eee868452f docs: refresh claude-cli model ref mirrors 2026-04-04 22:19:07 +01:00
Peter Steinberger
896928d8c0 docs: refresh slack secretref status mirrors 2026-04-04 22:14:15 +01:00
Peter Steinberger
6de100d4e2 docs: refresh claude-cli naming mirrors 2026-04-04 22:11:45 +01:00
Peter Steinberger
8ea5b1ddc0 docs: refresh anthropic token compatibility mirrors 2026-04-04 22:09:21 +01:00
@zimeg
13b6a48991 fix(slack): import plugin secret input config 2026-04-04 14:09:00 -07:00
Peter Steinberger
66a0ab3752 docs: refresh anthropic auth mirror refs 2026-04-04 22:07:08 +01:00
Peter Steinberger
9eb3718438 docs: refresh token auth command mirrors 2026-04-04 22:05:05 +01:00
Peter Steinberger
5c5c82dfaa docs: refresh anthropic oauth defaults refs 2026-04-04 22:01:16 +01:00
Peter Steinberger
f14f7b9fde docs: refresh silent token guidance mirrors 2026-04-04 21:58:12 +01:00
Altay
0089eb28fa fix(pnpm-workspace): add acpx to minimumReleaseAgeExclude (#61032) 2026-04-04 23:57:34 +03:00
Peter Steinberger
102f7f34e1 docs: refresh silent token semantics mirrors 2026-04-04 21:56:30 +01:00
Peter Steinberger
3d65b14019 docs: refresh NO_REPLY history mirrors 2026-04-04 21:55:11 +01:00
Peter Steinberger
adfdde5cb3 docs: refresh chat history sanitization mirrors 2026-04-04 21:53:25 +01:00
Peter Steinberger
291afbbb95 docs: refresh transcript sanitization mirrors 2026-04-04 21:52:15 +01:00
Mulualem
de918c282c fix(ui): tighten cron row overflow constraints 2026-04-04 15:50:03 -05:00
Mulualem
69cc35c9bd fix(ui): constrain cron job entries within list boundary 2026-04-04 15:50:03 -05:00
Peter Steinberger
b83c5fb8e0 docs: refresh provider runtime fallback refs 2026-04-04 21:48:41 +01:00
Peter Steinberger
38e54f488a docs: refresh native approval ui mirrors 2026-04-04 21:44:30 +01:00
Peter Steinberger
4f9804ec24 docs: refresh config schema and gateway tool mirrors 2026-04-04 21:43:09 +01:00
Peter Steinberger
746a57a2af docs: refresh sdk provider hook inventory refs 2026-04-04 21:39:18 +01:00
Peter Steinberger
93f11ff9f7 docs: refresh provider hook inventory coverage refs 2026-04-04 21:38:01 +01:00
Peter Steinberger
73d50fba28 docs: refresh provider hook inventory refs 2026-04-04 21:33:31 +01:00
Peter Steinberger
0fb53f1b90 docs: refresh provider transport and synthetic auth refs 2026-04-04 21:31:50 +01:00
Peter Steinberger
dd5439dd5b docs: refresh tool catalog mirrors 2026-04-04 21:28:05 +01:00
Peter Steinberger
4324eac5e9 docs: refresh config schema metadata mirrors 2026-04-04 21:26:51 +01:00
Peter Steinberger
849dbc58b1 docs: refresh gateway overview mirrors 2026-04-04 21:25:40 +01:00
Peter Steinberger
4b5146921c docs: refresh bridge removal mirrors 2026-04-04 21:24:09 +01:00
Peter Steinberger
79e8edc7bd docs: expand gateway protocol method and event refs 2026-04-04 21:20:11 +01:00
Peter Steinberger
8bc59eceb7 docs: refresh gateway feature registry mirrors 2026-04-04 21:18:02 +01:00
Chinar Amrutkar
e419989c34 docs: add PR limits to contribution guide (#60910)
Add PR limits section explaining:
- 10 open PRs per author cap
- r: too-many-prs label auto-close mechanism
- How to get exception via #clawtributors Discord

Fixes: #38283
2026-04-05 05:17:10 +09:00
@zimeg
28e1142a24 revert(slack): use packaged thread status method 2026-04-04 13:15:57 -07:00
@zimeg
68b84980cc refactor(slack): use packaged thread status method 2026-04-04 13:14:06 -07:00
Peter Steinberger
b60eee6017 docs: refresh memory status summary refs 2026-04-04 21:13:49 +01:00
Peter Steinberger
e2b841d7d0 docs: refresh shared-secret default mirrors 2026-04-04 21:11:16 +01:00
Peter Steinberger
0738ed8d19 docs: refresh control-ui shared-secret mirrors 2026-04-04 21:05:12 +01:00
Peter Steinberger
90387d4a88 docs: refresh hosted shared-secret auth mirrors 2026-04-04 21:04:17 +01:00
Peter Steinberger
7678917c49 docs: refresh exposed bind auth mirrors 2026-04-04 21:01:34 +01:00
Peter Steinberger
1ae356c40c docs: refresh sandbox bind security refs 2026-04-04 20:57:37 +01:00
@zimeg
86aa24b7a5 docs(slack): move typing status indicator to reaction fallback 2026-04-04 12:56:54 -07:00
Peter Steinberger
3b4bed7c38 docs: refresh model-scoped cooldown mirrors 2026-04-04 20:54:05 +01:00
Peter Steinberger
59a6bf7569 docs: refresh auth probe reason-code refs 2026-04-04 20:51:43 +01:00
Peter Steinberger
136a5ad2eb docs: refresh background process tool refs 2026-04-04 20:49:15 +01:00
Peter Steinberger
4b993ba6e4 docs: refresh config schema mirror refs 2026-04-04 20:45:46 +01:00
Peter Steinberger
e336300e60 docs: refresh failover and compaction pattern refs 2026-04-04 20:43:58 +01:00
Peter Steinberger
97a587ddca docs: refresh qwen auth-choice mirrors 2026-04-04 20:40:31 +01:00
Peter Steinberger
0ef29325ed docs: refresh config schema mirror refs 2026-04-04 20:38:15 +01:00
Peter Steinberger
420f2191f5 docs: refresh whatsapp helper sdk refs 2026-04-04 20:36:30 +01:00
Peter Steinberger
ba8eb4af38 docs: refresh config schema output refs 2026-04-04 20:35:01 +01:00
Peter Steinberger
976bc47458 docs: refresh gateway rpc safe-flow mirrors 2026-04-04 20:32:28 +01:00
Peter Steinberger
13396fa99e docs: refresh gateway tool safe-edit mirrors 2026-04-04 20:31:34 +01:00
Peter Steinberger
2bd91a0f02 docs: refresh browser existing-session mirror refs 2026-04-04 20:29:31 +01:00
Peter Steinberger
8efb0801a0 docs: refresh browser existing-session limit refs 2026-04-04 20:26:30 +01:00
Peter Steinberger
bbdc429dcb docs: refresh voice-call streaming transcription refs 2026-04-04 20:23:28 +01:00
Peter Steinberger
46cb292c2a docs: refresh Firecrawl and web_fetch config refs 2026-04-04 20:21:16 +01:00
Peter Steinberger
33f8ca6cb0 docs: refresh reserved sdk helper refs 2026-04-04 20:17:52 +01:00
Peter Steinberger
8eb1ea5b2e docs: refresh config schema mirrors 2026-04-04 20:14:33 +01:00
@zimeg
c2027d9de2 docs(slack): remove text streaming scope requirements 2026-04-04 12:13:25 -07:00
Peter Steinberger
4453b51e2c docs: refresh session tool inventory refs 2026-04-04 20:10:44 +01:00
Hiroshi Tanaka
3f1b369f4a feat(config): add rich description fields to JSON Schema output [AI-assisted] (#60067)
Merged via squash.

Prepared head SHA: a98b971924
Co-authored-by: solavrc <145330217+solavrc@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 22:10:08 +03:00
Peter Steinberger
92aed3168a docs: refresh core tool catalog mirrors 2026-04-04 20:09:24 +01:00
Peter Steinberger
da8a4131fe docs: refresh manifest contract examples 2026-04-04 20:07:01 +01:00
Peter Steinberger
ccd45bd9f0 fix(agents): refresh runtime tool and subagent coverage 2026-04-04 20:06:32 +01:00
Peter Steinberger
496df07804 fix(extensions): align provider helper surfaces 2026-04-04 20:06:32 +01:00
Peter Steinberger
6d5e2c7e6b docs: refresh legacy manifest contract refs 2026-04-04 20:04:11 +01:00
Peter Steinberger
c488becf43 docs: refresh plugin overview mirrors 2026-04-04 20:03:17 +01:00
Peter Steinberger
67d6fc8847 chore(plugins): sync versions to 2026.4.4 2026-04-04 20:03:01 +01:00
Peter Steinberger
7c1c4daa4e docs: refresh realtime transcription capability refs 2026-04-04 20:02:14 +01:00
Peter Steinberger
7988b5962a docs: refresh plugin capability inventory refs 2026-04-04 20:01:19 +01:00
Peter Steinberger
43acbcd283 docs: refresh plugin capability registration refs 2026-04-04 19:59:50 +01:00
Peter Steinberger
36b64969bc docs: refresh qwen sdk helper refs 2026-04-04 19:58:29 +01:00
Peter Steinberger
e0ef3855ca docs: refresh video generation config refs 2026-04-04 19:56:54 +01:00
Peter Steinberger
62dd299af1 docs: refresh qwen video generation refs 2026-04-04 19:55:39 +01:00
Peter Steinberger
28946635aa docs: refresh provider runtime hook refs 2026-04-04 19:52:56 +01:00
Peter Steinberger
4650b972b9 docs: refresh provider scoped failover refs 2026-04-04 19:49:54 +01:00
Peter Steinberger
a29755615e docs: refresh configured provider fallback refs 2026-04-04 19:48:46 +01:00
Peter Steinberger
3b47c0af28 docs: refresh video generation plugin refs 2026-04-04 19:47:30 +01:00
Peter Steinberger
c3ee8c611d docs: refresh video generation sdk refs 2026-04-04 19:45:59 +01:00
Peter Steinberger
879d45a56c docs: refresh qwen media and config refs 2026-04-04 19:42:13 +01:00
Peter Steinberger
b1279b0db3 docs: refresh untrusted file wrapper refs 2026-04-04 19:39:09 +01:00
Peter Steinberger
eaef4ee1b1 docs: refresh heartbeat skip-reason refs 2026-04-04 19:36:34 +01:00
Peter Steinberger
ca200eb480 fix(providers): stabilize runtime normalization hooks 2026-04-04 19:34:56 +01:00
Peter Steinberger
e06e36d41a docs(qwen): fix front matter formatting 2026-04-04 19:34:56 +01:00
Peter Steinberger
889ddb5edf docs(qwen): refresh provider and endpoint guides 2026-04-04 19:34:56 +01:00
Peter Steinberger
df7693027c style(qwen): apply formatter follow-ups 2026-04-04 19:34:56 +01:00
Peter Steinberger
e3ac0f43df feat(qwen): add qwen provider and video generation 2026-04-04 19:34:56 +01:00
Peter Steinberger
759373e887 docs: refresh multi-agent sandbox recall mirror 2026-04-04 19:33:48 +01:00
Peter Steinberger
d0d57ea435 docs: refresh session recall sanitization mirrors 2026-04-04 19:33:13 +01:00
Peter Steinberger
9aec55f0a2 docs: refresh text runtime sdk refs 2026-04-04 19:31:32 +01:00
Peter Steinberger
c329dd8250 docs: refresh subagent completion fallback refs 2026-04-04 19:29:58 +01:00
Peter Steinberger
f94645dfe5 docs: refresh session recall sanitization refs 2026-04-04 19:26:37 +01:00
Peter Steinberger
fd222d3f07 docs: refresh chat history scaffolding refs 2026-04-04 19:23:55 +01:00
Peter Steinberger
3b109c3419 docs: refresh push-based orchestration refs 2026-04-04 19:22:39 +01:00
Peter Steinberger
e42deea653 docs: refresh sessions_history safety refs 2026-04-04 19:20:34 +01:00
Peter Steinberger
39d9ded2e5 docs: refresh chat history display mirrors 2026-04-04 19:17:58 +01:00
Vincent Koc
6f2e804182 fix(agents): prefer background completion wake over polling (#60877)
* fix(agents): prefer completion wake over polling

* fix(changelog): note completion wake guidance

* fix(agents): qualify quiet exec completion wake

* fix(agents): qualify disabled exec completion wake

* fix(agents): split process polling from control actions
2026-04-05 03:17:10 +09:00
Peter Steinberger
0c3ec064f1 docs: refresh OpenResponses file input refs 2026-04-04 19:13:44 +01:00
Peter Steinberger
852d3a742c docs: refresh gateway probe warning mirrors 2026-04-04 19:10:31 +01:00
Peter Steinberger
3bf538d720 docs: refresh gateway status deep mirrors 2026-04-04 19:06:30 +01:00
Peter Steinberger
f3ce1bdb4f docs: refresh platform discovery mirrors 2026-04-04 19:03:20 +01:00
Peter Steinberger
40f958a953 fix(ci): narrow runtime seams and partial mocks 2026-04-04 19:03:00 +01:00
Peter Steinberger
83fe8efe3d fix(test): isolate ollama runtime test seams 2026-04-04 19:03:00 +01:00
oliviareid-svg
7ff90c516a fix: strip leaked outbound tool-call scaffolding (#60619)
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-04-05 02:02:36 +08:00
Peter Steinberger
0cf9c6ec95 docs: refresh discovery TXT mode refs 2026-04-04 19:01:18 +01:00
Peter Steinberger
e6f054ac76 docs: refresh gateway probe and discovery refs 2026-04-04 19:00:09 +01:00
Peter Steinberger
ac5d1de13a docs: refresh status deep health mirrors 2026-04-04 18:56:46 +01:00
Peter Steinberger
65bb1e772b docs: refresh remote gateway ssh mirrors 2026-04-04 18:56:08 +01:00
Mason
09016db731 fix: wrap untrusted file inputs (#60277)
Merged via squash.

Prepared head SHA: 56ce545786
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-05 01:54:48 +08:00
Peter Steinberger
9d45f4b4e9 docs: refresh gateway health snapshot refs 2026-04-04 18:54:04 +01:00
Peter Steinberger
72b59231a3 docs: refresh channels status probe mirrors 2026-04-04 18:52:01 +01:00
Peter Steinberger
6d89b363a2 docs: refresh setup-code bootstrap scope mirrors 2026-04-04 18:48:26 +01:00
Peter Steinberger
a10ba044bc docs: refresh approval error helper refs 2026-04-04 18:47:15 +01:00
Peter Steinberger
8fd53cdf86 docs: refresh bootstrap scope role-prefix refs 2026-04-04 18:46:30 +01:00
Peter Steinberger
131a78d3f3 docs: refresh setup runtime helper refs 2026-04-04 18:45:12 +01:00
Peter Steinberger
2b548aa2b1 docs: refresh elevated config mirror refs 2026-04-04 18:40:14 +01:00
Peter Steinberger
4db910698a docs: refresh sandbox and security elevated refs 2026-04-04 18:39:12 +01:00
Peter Steinberger
f1d8786a96 docs: refresh exec host and elevated refs 2026-04-04 18:38:10 +01:00
Peter Steinberger
9fbbdc62c8 docs: refresh shared native approval auto-enable refs 2026-04-04 18:34:29 +01:00
Peter Steinberger
4154aa8b0f docs: refresh discord native approval approver refs 2026-04-04 18:33:39 +01:00
Peter Steinberger
414e834c26 docs: refresh matrix and slack native approval refs 2026-04-04 18:31:47 +01:00
Peter Steinberger
f81d55d7ea docs: refresh native approval routing refs 2026-04-04 18:28:23 +01:00
Tak Hoffman
3bf1b69ece CI: make bad-barnacle bypass PR auto-response 2026-04-04 12:28:03 -05:00
Peter Steinberger
a08449b83f docs: refresh approval fallback refs 2026-04-04 18:27:27 +01:00
Peter Steinberger
2a80b7f30b docs: refresh silent cron delivery refs 2026-04-04 18:26:28 +01:00
Peter Steinberger
2ab8acb2c9 docs: refresh chat thinking and compaction refs 2026-04-04 18:25:13 +01:00
Gustavo Madeira Santana
e627f53d24 core: dedupe approval not-found handling (#60932)
Merged via squash.

Prepared head SHA: 108221fdfe
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-04 13:23:58 -04:00
Ayaan Zaidi
ef7c84ae92 style: trim live model switch comment noise 2026-04-04 22:42:30 +05:30
Ayaan Zaidi
e4bd4b8b49 style(agents): trim exec routing comments 2026-04-04 22:41:22 +05:30
Ayaan Zaidi
0817bf446f fix: keep NO_REPLY detection case-insensitive 2026-04-04 22:38:59 +05:30
Ayaan Zaidi
cde1e2d3a1 fix: preserve compaction split after trailing tool results 2026-04-04 22:34:05 +05:30
Ayaan Zaidi
3f7bd3bd7b fix: split before unfinished compaction tool turns 2026-04-04 22:30:27 +05:30
Tak Hoffman
3017a71bb7 ui: add chat thinking selector 2026-04-04 11:51:45 -05:00
wangchunyue
f463256660 fix: suppress NO_REPLY direct cron leaks (#45737) (thanks @openperf)
* fix(cron): suppress NO_REPLY sentinel in direct delivery path

* fix: set deliveryAttempted on filtered NO_REPLY to prevent timer fallback

* fix: mark silent NO_REPLY direct deliveries as delivered

* fix(cron): unify silent direct delivery handling

* fix: suppress NO_REPLY direct cron leaks (#45737) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 22:16:20 +05:30
wangchunyue
08992e1dbc fix: keep tool calls paired during compaction (#58849) (thanks @openperf)
* fix(compaction): keep tool_use and toolResult together when splitting messages

* fix: keep displaced tool results in compaction chunks

* fix: keep tool calls paired during compaction (#58849) (thanks @openperf)

* fix: avoid stalled compaction splits on aborted tool calls (#58849) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 22:12:43 +05:30
Ayaan Zaidi
77509024b8 fix: restore exec host=node routing (#60788) (thanks @openperf) 2026-04-04 21:59:57 +05:30
openperf
d98eaba4c3 fix(agents): resolve exec host=node routing regression and elevated gateway override 2026-04-04 21:59:57 +05:30
wangchunyue
17f086c021 fix: handle subagent live model switches (#58178) (thanks @openperf)
* fix(agents): handle LiveSessionModelSwitchError in subagent execution

Add retry loop for cross-provider model switches in the subagent
command path, mirroring the existing logic in agent-runner-execution.ts.

- Wrap runWithModelFallback in a while(true) loop inside agentCommandInternal
- Catch LiveSessionModelSwitchError and update provider, model,
  fallbackProvider, fallbackModel, providerForAuthProfileValidation,
  sessionEntry.authProfileOverride, and storedModelOverride before retrying
- Guard storedModelOverride update: only set when the model genuinely
  changed (compared before mutation) or a session override already existed
- Reset lifecycleEnded flag so the retried iteration can emit lifecycle events
- Add comprehensive tests covering retry success, error propagation,
  lifecycle reset, auth-profile forwarding, and fallback override state

Fixes #57998

* fix(agents): include provider change in storedModelOverride guard

* fix(agents): validate allowlist and clear stale compaction count on live model switch

* fix(agents): remove broken allowlist guard on live model switch

* fix(agents): address security review — bound retry loop, validate allowlist, redact error in lifecycle events

* fix(agents): restore error observability in lifecycle events using err.message

* fix(agents): sanitize log inputs and shallow-copy sessionEntry on live model switch

* fix(agents): enforce allowlist on empty set and sanitize error message

* fix: handle subagent live model switches (#58178) (thanks @openperf)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 21:56:11 +05:30
@zimeg
beee44ba47 docs(slack): reorder sections of introduced concepts 2026-04-04 08:54:31 -07:00
Onur
7de3a16ab4 ACPX: bump pinned version to 0.4.1 (#60918)
* ACPX: bump pinned version to 0.4.1

* ACPX: refresh lockfile for 0.4.1
2026-04-04 17:37:17 +02:00
Altay
ae460eff84 fix(failover): scope openrouter-specific matchers (#60909) 2026-04-04 18:24:03 +03:00
Peter Steinberger
fba6e194bd docs: refresh provider stream export refs 2026-04-04 16:23:00 +01:00
Peter Steinberger
c4205c7aae docs: refresh provider stream family refs 2026-04-04 16:21:21 +01:00
Peter Steinberger
a7b1a3140f docs: refresh skills cli stream refs 2026-04-04 16:19:34 +01:00
Peter Steinberger
bcaff8c208 docs: refresh failover generic error refs 2026-04-04 16:18:07 +01:00
Peter Steinberger
6067fe59d8 docs: refresh mcp config refs 2026-04-04 16:15:11 +01:00
Peter Steinberger
89535f9313 docs: refresh pairing locality refs 2026-04-04 16:13:04 +01:00
Aaron Zhu
983909f826 fix(agents): classify generic provider errors for failover (#59325)
* fix(agents): classify generic provider errors for failover

Anthropic returns bare 'An unknown error occurred' during API instability
and OpenRouter wraps upstream failures as 'Provider returned error'. Neither
message was recognized by the failover classifier, so the error surfaced
directly to users instead of triggering the configured fallback chain.

Add both patterns to the serverError classifier so they are classified as
transient server errors (timeout) and trigger model failover.

Closes #49706
Closes #45834

* fix(agents): scope unknown-error failover by provider

* docs(changelog): note provider-scoped unknown-error failover

---------

Co-authored-by: Aaron Zhu <aaron@Aarons-MacBook-Air.local>
Co-authored-by: Altay <altay@uinaf.dev>
2026-04-04 18:11:46 +03:00
Peter Steinberger
8a6da9d488 docs: refresh gateway auth handshake refs 2026-04-04 16:09:53 +01:00
Altay
5012b52780 fix(cli): route skills list output to stdout when --json is active (#60914)
* fix(cli): route skills list output to stdout when --json is active

runSkillsAction used defaultRuntime.log() which goes through console.log.
The --json preAction hook calls routeLogsToStderr(), redirecting console.log
to stderr. Switch to defaultRuntime.writeStdout() which writes directly to
process.stdout, consistent with how other --json commands (e.g. skills search)
already emit their output.

Fixes #57599

* test(cli): add skills JSON stdout regression coverage

* test(cli): refine skills CLI stream coverage

* fix(cli): add changelog entry for skills JSON stdout fix

---------

Co-authored-by: Aftabbs <aftabbs.wwe@gmail.com>
2026-04-04 18:09:44 +03:00
Peter Steinberger
db0b514e45 docs: refresh typebox protocol samples 2026-04-04 16:06:40 +01:00
Peter Steinberger
bc21e3c83d docs: refresh typebox protocol registry refs 2026-04-04 16:04:25 +01:00
Peter Steinberger
3470a80b36 docs: expand gateway protocol method inventory 2026-04-04 16:02:21 +01:00
Peter Steinberger
beb3740bb7 docs: expand gateway protocol rpc refs 2026-04-04 16:01:15 +01:00
Peter Steinberger
b944da561c docs: refresh sdk inventory refs 2026-04-04 15:57:06 +01:00
Peter Steinberger
5633495c19 docs: refresh provider sdk family refs 2026-04-04 15:53:24 +01:00
Peter Steinberger
b3cfedf312 docs: refresh registration mode mirror refs 2026-04-04 15:48:27 +01:00
Peter Steinberger
4c6b7a3a77 docs: refresh setup entrypoint import refs 2026-04-04 15:47:26 +01:00
Peter Steinberger
eb4c5890ab docs: refresh optional setup helper refs 2026-04-04 15:45:28 +01:00
Peter Steinberger
3b502882b9 docs: refresh setup runtime and promotion refs 2026-04-04 15:43:34 +01:00
Peter Steinberger
226b12d7b5 docs: refresh provider tool compat refs 2026-04-04 15:39:17 +01:00
Peter Steinberger
4dbc66b1ed fix: remove bundled channel startup reentry 2026-04-04 15:39:12 +01:00
Peter Steinberger
b9201e8333 refactor: share announce test runtime seams 2026-04-04 23:38:36 +09:00
Peter Steinberger
5584af7ac3 docs: refresh proxy provider runtime refs 2026-04-04 15:37:20 +01:00
Peter Steinberger
f5cc6a101b style: reflow system prompt tool summary 2026-04-04 23:36:46 +09:00
Peter Steinberger
a4fc1200de style: normalize provider formatting 2026-04-04 23:36:46 +09:00
Peter Steinberger
1ca1ce85ee docs: refresh xai and zai provider refs 2026-04-04 15:34:57 +01:00
Peter Steinberger
de001d0e07 docs: refresh subagent completion delivery refs 2026-04-04 15:32:45 +01:00
Vincent Koc
1f2e068e6b test(providers): require plugin-boundary family coverage 2026-04-04 23:30:28 +09:00
Peter Steinberger
d06633c618 docs: refresh device management authz refs 2026-04-04 15:28:36 +01:00
Peter Steinberger
3dda70a578 docs: refresh gemini cli oauth setup refs 2026-04-04 15:27:42 +01:00
Peter Steinberger
fb8e20ddb6 fix: harden paired-device management authz (#50627) (thanks @coygeek) 2026-04-04 23:27:05 +09:00
Peter Steinberger
9ac9edff43 docs: refresh gateway operator scope refs 2026-04-04 15:25:57 +01:00
Vincent Koc
cb1c2e8f86 test(providers): cover xai and zai stream hooks 2026-04-04 23:24:18 +09:00
Vincent Koc
e277c01953 test(providers): cover openrouter replay family 2026-04-04 23:23:02 +09:00
hugh.li
9dd449045a fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows (#40729)
* fix(google-gemini-cli-auth): fix Gemini CLI OAuth failures on Windows

Two issues prevented Gemini CLI OAuth from working on Windows:

1. resolveGeminiCliDirs: the first candidate `dirname(dirname(resolvedPath))`
   can resolve to an unrelated ancestor directory (e.g. the nvm root
   `C:\Users\<user>\AppData\Local\nvm`) when gemini is installed via nvm.
   The subsequent `findFile` recursive search (depth 10) then picks up an
   `oauth2.js` from a completely different package (e.g.
   `discord-api-types/payloads/v10/oauth2.js`), which naturally does not
   contain Google OAuth credentials, causing silent extraction failure.

   Fix: validate candidate directories before including them — only keep
   candidates that contain a `package.json` or a `node_modules/@google/
   gemini-cli-core` subdirectory.

2. resolvePlatform: returns "WINDOWS" on win32, but Google's loadCodeAssist
   API rejects it as an invalid Platform enum value (400 INVALID_ARGUMENT),
   just like it rejects "LINUX".

   Fix: use "PLATFORM_UNSPECIFIED" for all non-macOS platforms.

* test(google-gemini-cli-auth): keep oauth regressions portable

* chore(changelog): add google gemini cli auth fix note

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-04 23:22:36 +09:00
Peter Steinberger
eddb94555a docs: refresh heartbeat task batching refs 2026-04-04 15:22:01 +01:00
Vincent Koc
3f9e93fd28 test(providers): cover opencode replay family hooks 2026-04-04 23:21:41 +09:00
Joe LaPenna
bb82fe8f19 fix: constrain device bootstrap scope checks by role prefix (#57258) (thanks @jlapenna) (#57258)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-04 23:21:01 +09:00
Vincent Koc
a2e0a094c1 test(providers): cover stream family plugin hooks 2026-04-04 23:20:28 +09:00
Vincent Koc
fa34f3a9d5 fix(ci): restore provider runtime seams 2026-04-04 23:19:23 +09:00
Peter Steinberger
c09e128587 fix(gateway): include talk secrets in CLI pairing defaults (#56481) (thanks @maxpetrusenko) 2026-04-04 23:18:54 +09:00
Max P
8262078ee5 fix(agents): inherit completion announce delivery target (#56481) 2026-04-04 23:18:54 +09:00
Vincent Koc
4fe21de3ce test(providers): cover xai tool compat seam 2026-04-04 23:18:31 +09:00
Vincent Koc
20d14745cf refactor(providers): flatten passthrough provider hooks 2026-04-04 23:16:53 +09:00
Peter Steinberger
ea2f56b4e8 docs: refresh bundled channel naming mirrors 2026-04-04 15:16:11 +01:00
Vincent Koc
1e7f9e8746 test(providers): cover transport family matrix 2026-04-04 23:14:02 +09:00
Peter Steinberger
4be01a5cd5 docs: refresh onboarding channel mirrors 2026-04-04 15:13:14 +01:00
Peter Steinberger
772ee1f81f docs: refresh bundled channel ownership refs 2026-04-04 15:11:20 +01:00
Peter Steinberger
b7e6a3bc9e ci: retrigger workflow shell retry 34 2026-04-04 15:09:50 +01:00
Peter Steinberger
edc51b1fa4 ci: retrigger workflow shell retry 33 2026-04-04 15:09:50 +01:00
Peter Steinberger
6ce96c273f ci: retrigger workflow shell retry 32 2026-04-04 15:09:50 +01:00
Peter Steinberger
37a3b6b25f ci: retrigger workflow shell retry 31 2026-04-04 15:09:50 +01:00
Peter Steinberger
b7296fe5dd ci: retrigger workflow shell retry 30 2026-04-04 15:09:50 +01:00
Peter Steinberger
e191bf36a5 ci: retrigger workflow shell retry 29 2026-04-04 15:09:50 +01:00
Peter Steinberger
9766da7f00 ci: retrigger workflow shell retry 28 2026-04-04 15:09:50 +01:00
Peter Steinberger
e509c5c3ea fix(ci): avoid readonly embedded session mutation 2026-04-04 15:09:50 +01:00
Peter Steinberger
93fe3c5442 ci: retrigger workflow shell retry 27 2026-04-04 15:09:50 +01:00
Peter Steinberger
e949bd7d04 ci: retrigger workflow shell retry 26 2026-04-04 15:09:50 +01:00
Peter Steinberger
a29abebee0 ci: retrigger workflow shell retry 25 2026-04-04 15:09:50 +01:00
Peter Steinberger
dbeab5e60f ci: retrigger workflow shell retry 24 2026-04-04 15:09:50 +01:00
Peter Steinberger
99ebb7a248 ci: retrigger workflow shell retry 23 2026-04-04 15:09:50 +01:00
Peter Steinberger
788cff6759 ci: retrigger workflow shell retry 22 2026-04-04 15:09:50 +01:00
Peter Steinberger
0a69b3558a fix(build): stabilize lazy runtime entrypoints 2026-04-04 15:09:50 +01:00
Peter Steinberger
e5b9e32979 ci: retrigger workflow shell retry 21 2026-04-04 15:09:49 +01:00
Peter Steinberger
fe0b209850 ci: retrigger workflow shell retry 20 2026-04-04 15:09:49 +01:00
Peter Steinberger
8dc049abc5 ci: retrigger workflow shell retry 19 2026-04-04 15:09:49 +01:00
Peter Steinberger
6b265ce415 ci: retrigger workflow shell retry 18 2026-04-04 15:09:49 +01:00
Peter Steinberger
470b4452ce fix(ci): drop stale browser runtime imports 2026-04-04 15:09:49 +01:00
Peter Steinberger
5ef3bdb5f4 ci: retrigger workflow shell retry 17 2026-04-04 15:09:49 +01:00
Peter Steinberger
fb59b5c461 fix(ci): sync openrouter stream hook seams 2026-04-04 15:09:49 +01:00
Peter Steinberger
b575dc704c ci: retrigger workflow shell retry 16 2026-04-04 15:09:49 +01:00
Peter Steinberger
a0dbdbd8d4 ci: retrigger workflow shell retry 15 2026-04-04 15:09:49 +01:00
Peter Steinberger
571cd92b22 ci: retrigger workflow shell retry 14 2026-04-04 15:09:49 +01:00
Peter Steinberger
5a6a2bb861 ci: retrigger workflow shell retry 13 2026-04-04 15:09:49 +01:00
Peter Steinberger
5a3062ffb9 ci: retrigger workflow shell retry 12 2026-04-04 15:09:49 +01:00
Peter Steinberger
e0e6eaa03c ci: retrigger workflow shell retry 11 2026-04-04 15:09:49 +01:00
Peter Steinberger
867402449f ci: retrigger workflow shell retry 10 2026-04-04 15:09:49 +01:00
Peter Steinberger
e1ea02e556 ci: retrigger workflow shell retry 9 2026-04-04 15:09:49 +01:00
Peter Steinberger
d2ff8e28dd ci: retrigger workflow shell retry 8 2026-04-04 15:09:49 +01:00
Peter Steinberger
671c724626 ci: retrigger workflow shell retry 7 2026-04-04 15:09:49 +01:00
Peter Steinberger
cad662196f ci: retrigger workflow shell retry 6 2026-04-04 15:09:49 +01:00
Peter Steinberger
35260d3443 ci: retrigger workflow shell retry 5 2026-04-04 15:09:49 +01:00
Peter Steinberger
a1b794a12c fix(ci): repair node test regressions 2026-04-04 15:09:49 +01:00
Peter Steinberger
41513eaf2b ci: retrigger workflow shell retry 4 2026-04-04 15:09:49 +01:00
Peter Steinberger
7b8c4335b3 ci: retrigger workflow shell retry 3 2026-04-04 15:09:49 +01:00
Peter Steinberger
95480863f3 ci: retrigger workflow shell retry 2 2026-04-04 15:09:49 +01:00
Peter Steinberger
d0e041ad5c ci: retrigger workflow shell retry 2026-04-04 15:09:49 +01:00
Peter Steinberger
2ea583496d ci: retrigger workflow shell another time 2026-04-04 15:09:49 +01:00
Peter Steinberger
9e596e383d ci: retrigger workflow shell again again 2026-04-04 15:09:49 +01:00
Peter Steinberger
f81e31b23e ci: retrigger workflow shell once more 2026-04-04 15:09:49 +01:00
Peter Steinberger
5f8ae068dc ci: retrigger workflow shell again 2026-04-04 15:09:49 +01:00
Peter Steinberger
cad18b5ec2 ci: retrigger workflow shell 2026-04-04 15:09:48 +01:00
Peter Steinberger
dd771f1dc6 fix(ci): repair plugin boundary and bootstrap regressions 2026-04-04 15:09:48 +01:00
Peter Steinberger
a5836343df fix(ci): guard anthropic cli backend registration 2026-04-04 15:09:48 +01:00
Peter Steinberger
73f0b11a88 ci: retrigger workflow shell again 2026-04-04 15:09:48 +01:00
Peter Steinberger
daf4eea943 ci: retrigger stuck workflow shell 2026-04-04 15:09:48 +01:00
Peter Steinberger
2c6c2d4907 ci: retrigger stuck workflow 2026-04-04 15:09:48 +01:00
Peter Steinberger
2a0d5f9094 fix(ci): remove duplicated heartbeat prompt setup 2026-04-04 15:09:48 +01:00
Peter Steinberger
c5c5c77ebb fix(ci): restore contract-safe core imports 2026-04-04 15:09:48 +01:00
Chinar Amrutkar
8cf20a0c59 fix(heartbeat): address review comments 3035416659, 3035425446, 3035425447
- sessionId: derive valid ID from sessionKey (replace : with _)
- Move prompt null check before isolated session setup to avoid churn
- Improve tasks block stripping regex to handle blank lines

Fixes: #3035416659, #3035425446, #3035425447
2026-04-04 15:09:48 +01:00
Peter Steinberger
5c32dddb1c fix(ci): restore heartbeat task batching checks 2026-04-04 15:09:48 +01:00
Chinar Amrutkar
e0634aab66 fix(heartbeat): update task timestamps on alerts-disabled exit
Fixes: #3034825973
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
dbfb0b5618 fix(heartbeat): prevent outer loop from exiting on task field lines
The YAML parser's outer loop was exiting the tasks block when it
encountered 'interval:' or 'prompt:' lines, causing only the first
task to be parsed. Added isTaskField check to skip those lines.

Fixes: #3034790131
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
05c948e4de fix(heartbeat): preserve HEARTBEAT.md directives in task-mode prompt
Pass heartbeatFileContent to resolveHeartbeatRunPrompt and append
non-task directives from HEARTBEAT.md to the task-mode prompt.

Fixes: #3033850983
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
cebea1bf95 fix(heartbeat): remove dead helpers, persist timestamps on all exits
- Remove unused getTaskLastRunMs/updateTaskLastRunMs functions
- Add timestamp updates to all successful exit paths

Fixes: #3030557564, #3034645588
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
5fffdc478e fix(heartbeat): add startedAt param, null prompt handling, timestamp updates
- Fix: Pass startedAt into resolveHeartbeatRunPrompt
- Fix: Return proper object instead of null for no-tasks-due
- Fix: Add early return when prompt is null
- Fix: Persist timestamps on successful exits
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
ba09426707 fix(heartbeat): address review comments - parsing, timing, state, skips
- Fix YAML parsing to capture interval:/prompt: before breaking
- Record task timestamps AFTER successful execution (not before)
- Initialize task state on first run (handle undefined session)
- Skip API call when no tasks due (return null)
- Use startedAt consistently for due-task filtering

Fixes: #3030568439, #3033833124, #3030570872, #3030568408, #3030570872, #3035434022, #3035434368
2026-04-04 15:09:48 +01:00
Chinar Amrutkar
728d14e918 fix: add heartbeatTaskState to SessionEntry type
The heartbeat task batching feature uses heartbeatTaskState to track
last run times for periodic tasks, but this property was missing
from the SessionEntry type, causing TypeScript compilation errors.
2026-04-04 15:09:47 +01:00
Chinar Amrutkar
103bebd651 feat(heartbeat): add task batching support via HEARTBEAT.md
- Add parseHeartbeatTasks() to parse YAML-like task definitions
- Add isTaskDue() to check if task interval has elapsed
- Add heartbeatTaskState to session store for tracking last run times
- Modify resolveHeartbeatRunPrompt to build batched prompts for due tasks
- Update task last run times after successful heartbeat execution

Implements openclaw#29570
2026-04-04 15:09:47 +01:00
Peter Steinberger
890de57036 docs: refresh failover billing refs 2026-04-04 15:09:05 +01:00
Peter Steinberger
5fa60e6535 docs: refresh channel overview mirrors 2026-04-04 15:07:32 +01:00
Peter Steinberger
fde6e07f2a docs: refresh bundled channel setup refs 2026-04-04 15:06:39 +01:00
Peter Steinberger
1a431a532b docs: refresh bundled channel mirrors 2026-04-04 15:05:02 +01:00
Rockcent
b2f972e364 fix(failover): OpenRouter 403 Key limit exceeded triggers billing fallback (#59892)
Merged via squash.

Prepared head SHA: 7f8265231c
Co-authored-by: rockcent <128210877+rockcent@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 17:03:21 +03:00
Peter Steinberger
11542e9310 docs: refresh bundled channel plugin refs 2026-04-04 15:02:08 +01:00
Peter Steinberger
f02af9bb41 docs: refresh onboarding channel setup refs 2026-04-04 15:00:41 +01:00
Peter Steinberger
9dea255ee2 docs: refresh bundled channel overview refs 2026-04-04 14:58:17 +01:00
Peter Steinberger
756cb22f15 docs: refresh model selection fallback refs 2026-04-04 14:55:44 +01:00
Peter Steinberger
3e5bcc8cb2 docs: refresh isolated cron model switch refs 2026-04-04 14:53:45 +01:00
Vincent Koc
9cc300be78 fix(ci): restore main follow-up checks 2026-04-04 22:51:31 +09:00
Peter Steinberger
aa32f74fe6 docs: refresh cron delivery ownership refs 2026-04-04 14:51:08 +01:00
Peter Steinberger
981737035d docs: refresh isolated cron delivery refs 2026-04-04 14:48:51 +01:00
Peter Steinberger
3bc2e47966 docs: clarify failover 402 handling 2026-04-04 14:46:32 +01:00
Peter Steinberger
73584b1d33 docs: refresh failover and compaction refs 2026-04-04 14:44:51 +01:00
Peter Steinberger
bbb73d3171 refactor: split isolated cron runner phases 2026-04-04 14:42:35 +01:00
Peter Steinberger
9698ba7215 test: split isolated cron harness resets 2026-04-04 14:42:35 +01:00
Peter Steinberger
91d20781ed refactor: extract isolated cron execution seams 2026-04-04 14:42:35 +01:00
Peter Steinberger
083b882052 style(plugin-sdk): format provider stream helpers 2026-04-04 22:40:08 +09:00
Peter Steinberger
f9717f2eae fix(agents): align runtime with updated deps 2026-04-04 22:40:08 +09:00
Peter Steinberger
76d1f26782 chore(deps): update workspace dependencies 2026-04-04 22:40:08 +09:00
Peter Steinberger
70b39f4893 docs: refresh mattermost group config refs 2026-04-04 14:39:38 +01:00
Peter Steinberger
60206817b3 docs: refresh telegram command sdk refs 2026-04-04 14:38:33 +01:00
ToToKr
3b80f42152 fix(mattermost): add groups property to config schema (#57618) (#58271)
Merged via squash.

Prepared head SHA: 8d478fc092
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 16:37:53 +03:00
Peter Steinberger
8ca5a9174a docs: refresh gateway auth precedence refs 2026-04-04 14:36:52 +01:00
Peter Steinberger
882654d9ae docs: refresh talk config and doctor refs 2026-04-04 14:35:03 +01:00
Peter Steinberger
13f9475f6c docs: refresh bootstrap handoff token refs 2026-04-04 14:32:40 +01:00
Peter Steinberger
93ab8dd531 test: add CLI handshake regression coverage (#50240) (thanks @xiwuqi) 2026-04-04 22:32:15 +09:00
Peter Steinberger
114496871d docs: refresh tailscale auth rate limit refs 2026-04-04 14:30:13 +01:00
Peter Steinberger
7d22a16adb fix: bound bootstrap handoff token scopes 2026-04-04 22:29:52 +09:00
Peter Steinberger
7c0752f834 docs: refresh cron model override refs 2026-04-04 14:26:46 +01:00
Peter Steinberger
f502b023d9 docs: refresh device token scope mirrors 2026-04-04 14:25:47 +01:00
Peter Steinberger
ebe0a27b4d docs: refresh device token scope refs 2026-04-04 14:23:41 +01:00
Peter Steinberger
3758a0ce5b refactor(gateway): simplify connect auth parsing 2026-04-04 22:23:09 +09:00
Peter Steinberger
68ec7c9bbf docs: refresh plugin config schema refs 2026-04-04 14:21:00 +01:00
AARON AGENT
16e7e2551b fix(cron): prevent agent default model from overriding cron payload model (#58294)
* fix(cron): prevent agent default model from overriding cron payload model (#58065)

When a cron job specifies a model override via the Advanced settings,
runWithModelFallback could silently fall back to the agent's configured
primary model. This happened because fallbacksOverride was undefined
when neither payload.fallbacks nor per-agent fallbacks were configured,
causing resolveFallbackCandidates to append the agent primary as a
last-resort candidate. A transient failure on the cron-selected model
(rate limit, model-not-found, etc.) would then succeed on the agent
default, making it appear as if the override was ignored entirely.

Fix: when the cron payload carries an explicit model override, ensure
fallbacksOverride is always a defined array (empty when no fallbacks
are configured) so the agent primary is never silently appended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: use stricter toEqual([]) assertion for fallbacksOverride

Replace toBeDefined() + toBeInstanceOf(Array) with toEqual([])
to catch regressions where the array unexpectedly gains entries.
Addresses review feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve cron override fallback semantics (#58294)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-04 22:18:38 +09:00
Peter Steinberger
79be1e126a fix: harden parallels smoke harness 2026-04-04 14:18:18 +01:00
Peter Steinberger
99e45eb3ba docs: refresh remote bootstrap refs 2026-04-04 14:17:59 +01:00
Peter Steinberger
3f1b2703b7 fix: preserve cached device token scopes safely (#46032) (thanks @caicongyang) 2026-04-04 22:17:38 +09:00
Assistant
056c0870a9 fix(gateway): preserve stored scopes when reconnecting with device token
When the gateway client reconnects using a stored device token, it was
defaulting to ["operator.admin"] scopes instead of preserving the
previously authorized scopes from the stored token. This caused the
operator device token to be regenerated without operator.read scope,
breaking status/probe/health commands.

This fix:
1. Loads the stored scopes along with the stored token in selectConnectAuth
2. Uses the stored scopes when reconnecting with a valid device token
3. Falls back to explicitly requested scopes or default admin-only scope
   when no stored scopes exist

Fixes #46000
2026-04-04 22:17:38 +09:00
Peter Steinberger
2ecb8ca352 docs: refresh control ui auth ux refs 2026-04-04 14:14:54 +01:00
Peter Steinberger
07c7c4b9ec docs: refresh tailscale http auth refs 2026-04-04 14:13:36 +01:00
Peter Steinberger
11b8a025a4 docs: refresh gateway auth overview refs 2026-04-04 14:12:38 +01:00
Sebastian B Otaegui
33e6a7a28e feat(plugin-sdk): export OpenClawSchema via plugin-sdk/config-schema (#60557)
Merged via squash.

Prepared head SHA: 637ff7d3c8
Co-authored-by: feniix <91633+feniix@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 16:10:43 +03:00
Evan Newman
a26b844b88 fix(doctor): avoid repeat talk normalization changes from key order (#59911)
Merged via squash.

Prepared head SHA: a67bcaa11b
Co-authored-by: ejames-dev <180847219+ejames-dev@users.noreply.github.com>
Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
Reviewed-by: @hxy91819
2026-04-04 21:07:10 +08:00
Peter Steinberger
022618e887 docs: refresh browser auth refs 2026-04-04 14:04:24 +01:00
Peter Steinberger
0afd30d325 docs: refresh shared-secret auth mirrors 2026-04-04 14:02:29 +01:00
Peter Steinberger
f8dcd3ed83 docs: refresh tailscale auth mirrors 2026-04-04 14:00:36 +01:00
Peter Steinberger
b0025b1921 docs: refresh hook ingress security refs 2026-04-04 13:59:09 +01:00
Vincent Koc
0d47106b98 fix(tests): restore stream wrapper type coverage 2026-04-04 21:56:48 +09:00
Vincent Koc
71ea82a4f4 fix(build): restore portable provider runtime types 2026-04-04 21:56:48 +09:00
Vincent Koc
2a03326925 fix(plugin-sdk): keep telegram command config available 2026-04-04 21:56:48 +09:00
Vincent Koc
b3faf20d91 perf(agents): avoid repeated subagent registry rescans 2026-04-04 21:56:48 +09:00
Peter Steinberger
6cff644dc9 docs: refresh http endpoint auth refs 2026-04-04 13:56:08 +01:00
Peter Steinberger
032dbf0ec6 fix: serialize async auth rate-limit attempts 2026-04-04 21:55:09 +09:00
Peter Steinberger
c63a32661a docs: refresh gateway auth overview mirrors 2026-04-04 13:54:15 +01:00
Peter Steinberger
11d17b3c38 docs: refresh control ui device identity refs 2026-04-04 13:52:23 +01:00
Vincent Koc
a6707c2e1f refactor(providers): flatten shared stream hooks 2026-04-04 21:51:58 +09:00
Peter Steinberger
8f473023e4 docs: refresh web surface auth mirrors 2026-04-04 13:50:47 +01:00
Hsiao A
ae16452a69 fix(slack): pre-set shuttingDown before app.stop() to prevent orphaned ping intervals (#56646)
Merged via squash with admin override.

Prepared head SHA: f1c91d50b0
Note: required red lanes are currently inherited from latest origin/main, not introduced by this PR.
Co-authored-by: hsiaoa <70124331+hsiaoa@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-04 20:49:23 +08:00
Peter Steinberger
16346d6784 docs: clarify trusted proxy mirror refs 2026-04-04 13:49:05 +01:00
Peter Steinberger
4991cd66ef docs: refresh reverse proxy hardening refs 2026-04-04 13:47:59 +01:00
Peter Steinberger
7985cf5531 docs: refresh trusted proxy auth guidance 2026-04-04 13:44:34 +01:00
Peter Steinberger
62babffc40 docs: refresh security audit reference docs 2026-04-04 13:42:47 +01:00
jason
6e28bd2eb6 feishu: fix schema 2.0 card config in interactive card UX functions (#53395)
Merged via squash.

Prepared head SHA: 31f2396404
Co-authored-by: drvoss <3031622+drvoss@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-04-04 15:38:37 +03:00
Peter Steinberger
375bd73ce1 docs: refresh security fix refs 2026-04-04 13:35:42 +01:00
Peter Steinberger
f2b3b3d912 docs: clarify setup node-manager refs 2026-04-04 13:34:02 +01:00
Peter Steinberger
6ee905c7bd docs: refresh gateway skills rpc refs 2026-04-04 13:32:32 +01:00
Peter Steinberger
b05761aae0 docs: refresh skills cli and install refs 2026-04-04 13:31:13 +01:00
Peter Steinberger
db2cc5c28a docs: refresh clawhub mirror refs 2026-04-04 13:29:07 +01:00
Peter Steinberger
f16566d30e docs: refresh cron failure delivery refs 2026-04-04 13:25:44 +01:00
XING
587f19967c fix(cron): notify user via primary delivery channel on job failure (#60622)
Merged via squash.

Prepared head SHA: bee4dfca06
Co-authored-by: artwalker <44759507+artwalker@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-04 20:24:16 +08:00
Peter Steinberger
c89d4857e4 docs: clarify bundled plugin recovery refs 2026-04-04 13:24:04 +01:00
Peter Steinberger
56960e33e6 docs: refresh plugin install and marketplace refs 2026-04-04 13:22:46 +01:00
Peter Steinberger
3607962a44 docs: refresh plugin channel metadata refs 2026-04-04 13:18:34 +01:00
Peter Steinberger
86c799f4e1 docs: refresh plugin cli and inspect refs 2026-04-04 13:16:39 +01:00
Peter Steinberger
20f9f99db6 docs: refresh plugin manifest and bundle refs 2026-04-04 13:15:25 +01:00
Vincent Koc
9b82692425 refactor(providers): drop trivial stream lambdas 2026-04-04 21:14:00 +09:00
Vincent Koc
b742909dca fix(agents): prefer cron for deferred follow-ups (#60811)
* fix(agents): prefer cron for deferred follow-ups

* fix(agents): gate cron scheduling guidance

* fix(changelog): add scheduling guidance note

* fix(agents): restore exec approval agent hint
2026-04-04 21:11:27 +09:00
Peter Steinberger
d46eabb010 docs: complete sdk export coverage docs 2026-04-04 13:10:46 +01:00
Peter Steinberger
6b991b2afa docs: clarify reserved bundled sdk families 2026-04-04 13:09:17 +01:00
Peter Steinberger
b424a7a3a4 docs: refresh sdk memory import refs 2026-04-04 13:07:52 +01:00
Peter Steinberger
e91b52f396 docs: refresh sdk helper import refs 2026-04-04 13:06:57 +01:00
Peter Steinberger
363c666201 docs: refresh sdk capability import refs 2026-04-04 13:05:49 +01:00
Vincent Koc
486505a54e refactor(providers): share kilocode stream family 2026-04-04 21:05:42 +09:00
Peter Steinberger
dd030fb761 docs: refresh sdk core runtime refs 2026-04-04 13:04:01 +01:00
Peter Steinberger
f9f9462c79 docs: refresh channel helper import refs 2026-04-04 13:02:43 +01:00
Peter Steinberger
8cf6e4b5df fix(plugin-sdk): unblock gateway test surfaces 2026-04-04 21:02:04 +09:00
Peter Steinberger
27972489d3 docs: refresh sdk runtime import refs 2026-04-04 13:01:15 +01:00
Peter Steinberger
cec15e08d1 docs: clarify bundled helper sdk seams 2026-04-04 12:59:26 +01:00
Vincent Koc
8059942216 refactor(providers): share xai stream helper 2026-04-04 20:56:34 +09:00
Peter Steinberger
72f54059c4 docs: refresh setup helper import refs 2026-04-04 12:56:02 +01:00
Peter Steinberger
1c5c15b1d4 docs: refresh sdk entrypoint wording 2026-04-04 12:55:05 +01:00
Peter Steinberger
940bf899f0 docs: refresh provider entry import refs 2026-04-04 12:54:15 +01:00
Peter Steinberger
502b024523 docs: refresh bundled provider package examples 2026-04-04 12:52:55 +01:00
Peter Steinberger
120b1d2ed2 docs: refresh provider package barrel refs 2026-04-04 12:51:31 +01:00
Peter Steinberger
e5b48ea2b4 docs: refresh anthropic stream helper refs 2026-04-04 12:49:53 +01:00
Peter Steinberger
0166fd426e docs: refresh minimax auth path refs 2026-04-04 12:47:07 +01:00
Peter Steinberger
9da0feeecf docs: fix minimax usage docs merge markers 2026-04-04 12:43:44 +01:00
Peter Steinberger
a375635a9a docs: refresh status token fallback refs 2026-04-04 12:42:50 +01:00
Peter Steinberger
fb0d60d7f3 fix: resolve MiniMax portal usage auth 2026-04-04 12:42:30 +01:00
Peter Steinberger
9d684e1040 docs: refresh provider usage auth refs 2026-04-04 12:40:55 +01:00
Peter Steinberger
c0d509e794 docs: refresh status cache fallback refs 2026-04-04 12:39:02 +01:00
Peter Steinberger
ac254f50e8 docs: refresh minimax usage refs 2026-04-04 12:36:18 +01:00
Vincent Koc
83c10350c6 refactor(providers): share anthropic stream helper 2026-04-04 20:35:30 +09:00
Stuart Sy
3f457cabf7 fix(status): hydrate cache usage in transcript fallback (#59247)
* fix(status): hydrate cache usage in transcript fallback

* docs(changelog): note status cache fallback fix

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-04 20:34:41 +09:00
Peter Steinberger
3100984a33 docs: refresh browser origin auth refs 2026-04-04 12:34:11 +01:00
Peter Steinberger
72847db28b test: cover android canvas a2ui trust gate 2026-04-04 20:33:24 +09:00
Peter Steinberger
1efce6f23c docs: refresh provider stream family docs 2026-04-04 12:32:43 +01:00
Peter Steinberger
9eb8184f36 fix: improve MiniMax coding-plan parsing (#52349) (thanks @IVY-AI-gif) 2026-04-04 20:32:15 +09:00
IVY
dd9c9dac53 style: format with oxfmt 2026-04-04 20:32:15 +09:00
IVY
30de4337bf fix: address review feedback and formatting
- Remove redundant name === 'MiniMax-M*' condition (already matched by startsWith)
- Use !== undefined guard instead of falsy check in deriveWindowLabelFromTimestamps
- Pass chatRemains directly to deriveWindowLabel when available
- Remove JSDoc comment style to match codebase conventions
2026-04-04 20:32:15 +09:00
IVY
efd5d5eb20 fix(usage): improve MiniMax coding-plan usage parsing for model_remains array
- Pick the chat model entry (MiniMax-M*) from model_remains instead of using the first BFS candidate, which could be a speech/video/image model with total_count=0.
- Derive window label from start_time/end_time timestamps when window_hours/window_minutes fields are absent; fixes the hardcoded 5h default for 4h windows.
- Include model name in plan label so users can distinguish free-tier coding-plan quota from paid API balance.

Closes #52335
2026-04-04 20:32:15 +09:00
Peter Steinberger
90af255a91 docs: refresh gemini cli usage refs 2026-04-04 12:30:55 +01:00
Peter Steinberger
65fcf7e104 fix(gateway): scope browser-origin auth throttling 2026-04-04 20:30:39 +09:00
Vincent Koc
8f7b02e567 refactor(providers): share openai stream families 2026-04-04 20:29:11 +09:00
Peter Steinberger
035a754f0f fix: harden android a2ui trust matching 2026-04-04 20:28:08 +09:00
Peter Steinberger
1cfc10e836 docs: refresh minimax multimodal refs 2026-04-04 12:27:47 +01:00
Vincent Koc
c75f82448f fix(google-cli): parse gemini json response and stats (#60801)
* fix(google-cli): restore gemini json reporting

* fix(google-cli): fall back to stats when usage is empty

* fix(changelog): note gemini cli cache reporting
2026-04-04 20:27:22 +09:00
Peter Steinberger
46cb493ac8 fix(sandbox): cover home credential bind audit 2026-04-04 20:27:10 +09:00
Peter Steinberger
3ec0463da9 docs: refresh minimax thinking refs 2026-04-04 12:23:33 +01:00
Peter Steinberger
3dda75894b refactor(agents): centralize run wait helpers 2026-04-04 20:22:16 +09:00
Peter Steinberger
42778ccd46 docs: refresh provider stream family refs 2026-04-04 12:21:37 +01:00
Peter Steinberger
9615488855 fix: disable MiniMax reasoning leak (#55809) (thanks @moktamd) 2026-04-04 20:21:37 +09:00
moktamd
2701e75f40 fix: disable thinking for MiniMax anthropic-messages streaming
MiniMax M2.7 returns reasoning_content in OpenAI-style delta chunks
({delta: {content: "", reasoning_content: "..."}}) when thinking is
active, rather than native Anthropic thinking block SSE events. Pi-ai's
Anthropic provider does not handle this format, causing the model's
internal reasoning to appear as visible chat output.

Add createMinimaxThinkingDisabledWrapper that injects
thinking: {type: "disabled"} into the outgoing payload for any MiniMax
anthropic-messages request where thinking is not already explicitly
configured, preventing the provider from generating reasoning_content
deltas during streaming.

Fixes #55739
2026-04-04 20:21:37 +09:00
Peter Steinberger
561bacd06a fix: harden synology chat TLS helper defaults 2026-04-04 20:21:13 +09:00
Peter Steinberger
b473816afb docs: refresh native streaming compat refs 2026-04-04 12:20:31 +01:00
Vincent Koc
bc648ac8e6 refactor(providers): add stream family hooks 2026-04-04 20:19:53 +09:00
Peter Steinberger
1037af01ad style(agents): normalize runtime prompt formatting 2026-04-04 12:19:08 +01:00
Peter Steinberger
c70b10460c style(auth): normalize auth choice formatting 2026-04-04 12:19:08 +01:00
Peter Steinberger
f3aad63f4e style(providers): normalize import and wrap formatting 2026-04-04 12:19:08 +01:00
Peter Steinberger
3207c5326a refactor: share native streaming compat helpers 2026-04-04 12:18:45 +01:00
Peter Steinberger
aaa173a4a7 docs: clarify node exec approval binding 2026-04-04 12:18:32 +01:00
Peter Steinberger
9ddfaff45f docs: clarify node exec approval plan forwarding 2026-04-04 12:18:04 +01:00
Peter Steinberger
605f48556b refactor(browser): share lifecycle cleanup helpers 2026-04-04 12:17:46 +01:00
Peter Steinberger
c3f415ad6e fix: preserve node system.run approval plans 2026-04-04 20:16:53 +09:00
Peter Steinberger
f832699fd7 docs: refresh provider hook overview refs 2026-04-04 12:16:29 +01:00
Peter Steinberger
53c33f8207 fix: forward node exec approval plans 2026-04-04 20:16:19 +09:00
Peter Steinberger
62c54fdc16 docs: refresh provider replay family refs 2026-04-04 12:15:31 +01:00
Jasmine Zhang
b838ecf885 fix: add 60s timeout to MiniMax VLM fetch call
The VLM image analysis fetch had no timeout, causing sessions to hang
indefinitely when the MiniMax API is slow or unresponsive. Other
vision/model API calls in the codebase already use timeouts. Adds
AbortSignal.timeout(60_000) consistent with image upload workloads.

Fixes #54139
2026-04-04 20:15:13 +09:00
Peter Steinberger
39bcf695dc fix(cron): reject unsafe custom session targets earlier 2026-04-04 20:13:39 +09:00
Peter Steinberger
00337cdde1 docs: refresh codex auth and ws refs 2026-04-04 12:11:45 +01:00
Vincent Koc
c29d4bbb86 test(providers): add family capability matrix coverage 2026-04-04 20:11:25 +09:00
Peter Steinberger
91bac7cb83 fix(usage): restore provider auth fallback 2026-04-04 12:10:45 +01:00
Peter Steinberger
6bbccb087a docs: refresh google cached content refs 2026-04-04 12:10:29 +01:00
Peter Steinberger
49bf527fd4 docs: clarify reserved gateway method namespaces 2026-04-04 12:08:41 +01:00
Peter Steinberger
9b352ab5b0 test: isolate session status from provider runtime leak 2026-04-04 12:08:05 +01:00
Peter Steinberger
b7411ad594 refactor(cron): share descendant run quiescence wait 2026-04-04 20:07:33 +09:00
Peter Steinberger
7b6334b0f4 refactor(agents): share run wait reply helpers 2026-04-04 20:07:33 +09:00
Peter Steinberger
bbb0b574c4 refactor: centralize gateway method policy helpers 2026-04-04 20:07:18 +09:00
Vincent Koc
d766465e38 fix(google): add direct cachedContent support (#60757)
* fix(google): restore gemini cache reporting

* fix(google): split cli parsing into separate PR

* fix(google): drop remaining cli overlap

* fix(google): honor cachedContent alias precedence
2026-04-04 20:07:13 +09:00
Peter Steinberger
b9e3c1a02e docs: refresh cron and subagent browser cleanup refs 2026-04-04 12:06:39 +01:00
Peter Steinberger
7ffbbd8586 fix: reserve admin gateway method prefixes 2026-04-04 20:04:48 +09:00
Peter Steinberger
86ee50b968 docs: refresh web search overview mirrors 2026-04-04 12:04:28 +01:00
Peter Steinberger
3b09b58c5d test: cover browser cleanup for cron and subagents (#60146) (thanks @BrianWang1990) 2026-04-04 20:03:57 +09:00
BrianWang1990
e697838899 style: fix import order in server-cron.ts
Move plugin-sdk import after cron/* imports per alphabetical convention.
2026-04-04 20:03:57 +09:00
BrianWang1990
72b2e413d6 fix(browser): clean up browser tabs/processes when cron tasks and subagents complete
When cron tasks or subagents use browser automation, the browser
processes were not cleaned up after the task completed. This caused
orphaned Chrome processes (PPID=1) to accumulate over time.

Root cause: closeTrackedBrowserTabsForSessions was only called during
session-reset/session-delete (via ensureSessionRuntimeCleanup), but
isolated cron runs and subagent completions never triggered these paths.

Fix: Add browser tab cleanup in two places:
1. server-cron.ts: wrap runCronIsolatedAgentTurn in try/finally to
   ensure browser tabs are cleaned up after every cron run.
2. subagent-registry-lifecycle.ts: call closeTrackedBrowserTabsForSessions
   when a subagent run completes, before the announce cleanup flow.

Both cleanup calls are best-effort (caught errors) so they never mask
the actual task result or break the completion flow.

Fixes #60104
2026-04-04 20:03:57 +09:00
Peter Steinberger
0b1c9c7057 fix: stabilize codex auth ownership and ws fallback cache 2026-04-04 20:03:15 +09:00
Peter Steinberger
fca889eea3 docs: refresh browser troubleshooting mirrors 2026-04-04 12:02:48 +01:00
Peter Steinberger
29f062770d docs: refresh browser stop cleanup refs 2026-04-04 12:02:10 +01:00
Peter Steinberger
c524d6c76c docs: refresh shared minimax web search refs 2026-04-04 12:00:58 +01:00
Peter Steinberger
bec891b2e2 test: cover attach-only browser stop cleanup (#60097) (thanks @pedh) 2026-04-04 19:59:59 +09:00
pedh
2c9723afd5 fix(browser): disconnect Playwright CDP session on stop for attachOnly/remote profiles
When `browser stop` is called for an `attachOnly` or remote CDP
profile, `profileState.running` is null (no process was launched), so
`stopRunningBrowser()` returned early without closing the Playwright
CDP connection. This left emulation overrides (prefers-color-scheme,
viewport, etc.) permanently applied until a full gateway restart.

Now call `closePlaywrightBrowserConnectionForProfile()` before
returning for attachOnly and remote CDP profiles, matching the cleanup
behavior already present in `resetProfile()`. Regular profiles that
were never started still return `{ stopped: false }`.

Fixes #60095
2026-04-04 19:59:59 +09:00
Peter Steinberger
0bc9f0b5ba docs: refresh browser screenshot route refs 2026-04-04 11:58:46 +01:00
Jithendra
d204be80af feat(tools): add MiniMax as bundled web search provider
Add native MiniMax Search integration via their Coding Plan search API
(POST /v1/coding_plan/search). This brings MiniMax in line with Brave,
Kimi, Grok, Gemini, and other providers that already have bundled web
search support.

- Implement WebSearchProviderPlugin with caching, credential resolution,
  and trusted endpoint wrapping
- Support both global (api.minimax.io) and CN (api.minimaxi.com)
  endpoints, inferred from explicit region config, model provider base
  URL, or minimax-portal OAuth base URL
- Prefer MINIMAX_CODE_PLAN_KEY over MINIMAX_API_KEY in credential
  fallback, matching existing repo precedence
- Accept SecretRef objects for webSearch.apiKey (type: [string, object])
- Register in bundled registry, provider-id compat map, and fast-path
  plugin id list with full alignment test coverage
- Add unit tests for endpoint/region resolution and edge cases

Closes #47927
Related #11399

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 19:56:04 +09:00
Peter Steinberger
a722719720 docs: refresh synology webhook auth refs 2026-04-04 11:55:57 +01:00
Peter Steinberger
7d16359aae docs: note Chrome 146 screenshot compat fix (#60682) (thanks @mvanhorn) 2026-04-04 19:55:37 +09:00
Matt Van Horn
b22f6257f0 fix(browser): remove fromSurface: false for Chrome 146+ screenshot compat 2026-04-04 19:55:37 +09:00
Peter Steinberger
05da802e1c refactor: split device-pair command helpers 2026-04-04 19:55:04 +09:00
Peter Steinberger
fdb1be0079 docs: refresh mattermost slash auth refs 2026-04-04 11:54:52 +01:00
Peter Steinberger
8a532dead2 docs: refresh browser cdp validation refs 2026-04-04 11:53:36 +01:00
Peter Steinberger
2a65bfee96 fix(mattermost): harden slash command token validation 2026-04-04 19:51:41 +09:00
Peter Steinberger
53d3fbcef6 docs: refresh browser existing session docs 2026-04-04 11:51:07 +01:00
Peter Steinberger
5583bda61d docs: note browser profile CDP validation fix (#60477) (thanks @eleqtrizit) 2026-04-04 19:51:02 +09:00
Agustin Rivera
5da360cada fix(browser): trim validation error prefix 2026-04-04 19:51:02 +09:00
Agustin Rivera
aefc6fc161 fix(browser): validate profile cdp urls 2026-04-04 19:51:02 +09:00
Peter Steinberger
36cc397548 fix: reuse shared Synology Chat secret compare 2026-04-04 19:49:35 +09:00
Peter Steinberger
c5b2b69f94 docs: refresh live model switch docs 2026-04-04 11:49:23 +01:00
Peter Steinberger
bc356cc8c2 fix: harden direct CDP websocket validation (#60469) (thanks @eleqtrizit) 2026-04-04 19:48:01 +09:00
Agustin Rivera
c3f8427973 fix(browser): validate initial cdp endpoints 2026-04-04 19:48:01 +09:00
Agustin Rivera
80720b4994 fix(browser): validate cdp websocket pivots 2026-04-04 19:48:01 +09:00
Peter Steinberger
e4ea3c03cf fix: scope live model switch pending state (#60266) (thanks @kiranvk-2011) 2026-04-04 19:45:53 +09:00
kiranvk2011
b36a3a3295 fix: add .catch() to fire-and-forget stale-flag clear to prevent unhandled rejection 2026-04-04 19:45:53 +09:00
kiranvk2011
e8f6ceedd4 fix: clear stale liveModelSwitchPending flag when model already matches
When the liveModelSwitchPending flag is set but the current model already
matches the persisted selection (e.g. the switch was applied as an override
and the current attempt is already using the new model), the flag is now
consumed eagerly via a fire-and-forget clearLiveModelSwitchPending() call.

Without this, the stale flag could persist across fallback iterations and
later cause a spurious LiveSessionModelSwitchError when the model rotates
to a fallback candidate that differs from the persisted selection.

Also expands JSDoc on shouldSwitchToLiveModel to document the stale-flag
clearing and deferral semantics.
2026-04-04 19:45:53 +09:00
kiranvk2011
251e086eac fix: use explicit flag for live model switch detection in fallback chain
Replace the ambiguous comparison-based approach (hasDifferentLiveSessionModelSelection
+ in-memory map EMBEDDED_RUN_MODEL_SWITCH_REQUESTS) with a persisted
`liveModelSwitchPending` flag on SessionEntry.

The root cause: the in-memory map was never populated in production because
requestLiveSessionModelSwitch() was removed in commit 622b91d04e and replaced
with refreshQueuedFollowupSession(). This left the comparison-based detection
as the only path, which could not distinguish user-initiated model switches
(via /model command) from system-initiated fallback rotations.

The fix:
- Add `liveModelSwitchPending?: boolean` to SessionEntry (persisted)
- Set the flag to true ONLY when /model command applies a model override
- New `shouldSwitchToLiveModel()` checks the flag + model mismatch together
- New `clearLiveModelSwitchPending()` resets the flag after consumption
- Replace throw-site logic in run.ts to use the new flag-based functions
- Remove orphaned resolveCurrentLiveSelection helper

Only the /model command sets this flag, so system-initiated fallback rotations
are never mistaken for user-initiated model switches. This restores the
live-switch-during-active-run feature that was accidentally broken.

Fixes #57857, #57760, #58137
2026-04-04 19:45:53 +09:00
Peter Steinberger
678e9e6078 docs: refresh gemini cli oauth references 2026-04-04 11:45:37 +01:00
Peter Steinberger
20a7b1a9dc fix: finalize device-pair scope hardening (#55996) (thanks @coygeek) 2026-04-04 19:44:43 +09:00
Coy Geek
9dcef6df02 fix: scope pairing guard to internal gateway callers 2026-04-04 19:44:43 +09:00
Coy Geek
05ca581ed0 fix: fail closed when pairing scopes are missing 2026-04-04 19:44:43 +09:00
Coy Geek
353d93613c fix: enforce pairing approval scopes 2026-04-04 19:44:43 +09:00
Peter Steinberger
5d0562badf docs: clarify cli backend mcp overlays 2026-04-04 11:43:29 +01:00
Peter Steinberger
cc602fe9d4 docs: refresh anthropic cli backend docs 2026-04-04 11:40:58 +01:00
Peter Steinberger
3f042ed002 fix: stabilize async provider test types 2026-04-04 19:39:22 +09:00
Peter Steinberger
87d840e9ee fix: tighten Teams and device typing 2026-04-04 19:39:22 +09:00
Peter Steinberger
75fb29ffe6 docs: refresh provider sdk hook docs 2026-04-04 11:38:25 +01:00
Peter Steinberger
d1bf2c6de1 docs: clarify device token role bounds 2026-04-04 11:36:02 +01:00
Peter Steinberger
e675634eb3 fix: preserve streamed Kimi tool args on repair fallback 2026-04-04 11:35:49 +01:00
Peter Steinberger
5bef64bc31 test: harden media provider auto-registration (#56279) (thanks @Ezio0) 2026-04-04 19:35:28 +09:00
Peter Steinberger
277df463d6 docs: clarify openrouter cache markers 2026-04-04 11:34:17 +01:00
Vincent Koc
39d2a719c9 refactor(providers): add family replay and tool hooks 2026-04-04 19:33:31 +09:00
Peter Steinberger
4e099689c0 feat: stream Claude CLI JSONL output 2026-04-04 19:33:08 +09:00
Peter Steinberger
2ab1f1c054 docs: clarify openai usage normalization 2026-04-04 11:32:58 +01:00
Peter Steinberger
10e0592ed0 refactor: extract device token rotate target guard 2026-04-04 19:32:25 +09:00
Vincent Koc
0a3211df2d fix(openrouter): gate prompt cache markers by endpoint (#60761)
* fix(openrouter): gate prompt cache markers by endpoint

* test(openrouter): use claude sonnet 4.6 cache model
2026-04-04 19:32:13 +09:00
Peter Steinberger
ee742cec40 fix: fallback ws usage totals (#54940) (thanks @lyfuci) 2026-04-04 19:32:05 +09:00
Peter Steinberger
4ee648c508 docs: refresh model picker provider filtering 2026-04-04 11:30:18 +01:00
复试资料
e955cffd32 Agents: widen WS usage aliases 2026-04-04 19:28:54 +09:00
复试资料
d166f2648e Agents: normalize WS usage aliases 2026-04-04 19:28:54 +09:00
Peter Steinberger
9367379771 docs: clarify prompt cache stability 2026-04-04 11:28:19 +01:00
Peter Steinberger
f0d3e231ef fix: cover bundled provider picker aliases (#58819) (thanks @Luckymingxuan) 2026-04-04 19:27:26 +09:00
Mingxuan
c4a903319e fix(model-picker): fallback to unfiltered list when provider filter yields empty results 2026-04-04 19:27:26 +09:00
Mingxuan
360fdaa4f2 fix(model-picker): use matchesPreferredProvider for plan variant matching 2026-04-04 19:27:26 +09:00
Mingxuan
fd3b7b5ae7 fix: add augmentModelCatalog hooks to bundled providers for proper filtering 2026-04-04 19:27:26 +09:00
Mingxuan
792558de01 fix(model-picker): use preferredProvider presence for filtering instead of catalog check
When auth choice explicitly sets a preferred provider (e.g., volcengine-api-key or byteplus-api-key), the model picker should always filter by that provider. Previously, it relied on providerIds.includes(preferredProvider), which could be false if the catalog hadn't loaded that provider's models yet due to a race condition between auth choice setup and catalog loading.

This ensures that selecting a provider via auth choice consistently filters the model list to only that provider's models, rather than showing all providers.
2026-04-04 19:27:26 +09:00
Peter Steinberger
6b82140336 fix: land device token role guard follow-up (#60462) (thanks @eleqtrizit) 2026-04-04 19:27:10 +09:00
Agustin Rivera
7cda9df4cb fix(device): reject unapproved token roles 2026-04-04 19:27:10 +09:00
Peter Steinberger
d58b4d7425 fix: respect MINIMAX_API_HOST in bundled minimax catalogs (#34524) (thanks @caiqinghua) 2026-04-04 19:26:12 +09:00
Peter Steinberger
2c36ca562d docs: clarify minimax usage window semantics 2026-04-04 11:25:51 +01:00
Peter Steinberger
01a24c20bf refactor: expose node pairing approval scopes 2026-04-04 19:23:33 +09:00
Peter Steinberger
848e7abb57 docs: refresh node pairing scope references 2026-04-04 11:22:02 +01:00
0912078
28021a0325 fix(minimax): invert usage_percent when deriving usedPercent from remaining-only fields
MiniMax's usage_percent / usagePercent fields report the *remaining* quota
as a percentage, not the consumed quota. When count fields (prompt_limit /
prompt_remain) are also present, fromCounts already computed the correct
usedPercent and the inverted value was silently ignored. But when only
usage_percent is returned (no count fields), the code treated it as a
used-percent and passed it through unchanged, causing the menu bar to show
"2% left" instead of "98% left".

Move usage_percent and usagePercent from PERCENT_KEYS to a new
REMAINING_PERCENT_KEYS array. deriveUsedPercent now inverts remaining-percent
values to obtain usedPercent, matching the behaviour already validated by the
existing "prefers count-based usage when percent looks inverted" test. Count-
based fromCounts still takes priority over both key groups.

Fixes #60193

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 19:20:50 +09:00
Peter Steinberger
1222961a77 docs: clarify macos cli install fallbacks 2026-04-04 11:20:23 +01:00
Peter Steinberger
7807e1ef05 docs: refresh bun install and onboarding references 2026-04-04 11:19:13 +01:00
Vincent Koc
5779831723 fix(agents): stabilize prompt cache followups 2026-04-04 19:17:59 +09:00
Peter Steinberger
a631270f01 docs: refresh package-manager update references 2026-04-04 11:17:14 +01:00
Peter Steinberger
c441db7e13 docs: refresh update channel references 2026-04-04 11:14:51 +01:00
Peter Steinberger
ca2fdcc45f fix: enforce node pairing approval scopes end-to-end (#60461) (thanks @eleqtrizit) 2026-04-04 19:13:48 +09:00
Agustin Rivera
0089d0e2e6 fix(pairing): require pairing scope for node approvals 2026-04-04 19:13:48 +09:00
Peter Steinberger
a90f3ffdac docs: clarify installer service refresh behavior 2026-04-04 10:52:02 +01:00
Peter Steinberger
93d8a8602b docs: refresh local installer references 2026-04-04 10:51:22 +01:00
Peter Steinberger
790a24002e docs: refresh daemon overview references 2026-04-04 10:49:13 +01:00
Peter Steinberger
f39b5e86e5 docs: refresh persistence guidance 2026-04-04 10:44:55 +01:00
Peter Steinberger
a2fa6e8b90 docs: refresh cloud persistence wording 2026-04-04 10:44:08 +01:00
Peter Steinberger
508ca72fc7 docs: refresh hosted backup guidance 2026-04-04 10:42:02 +01:00
Peter Steinberger
559e42b60c docs: fix hosted auth profile paths 2026-04-04 10:40:40 +01:00
Peter Steinberger
d7e288bee9 docs: refresh backup and migration storage refs 2026-04-04 10:39:42 +01:00
Peter Steinberger
f7c5988334 docs: refresh docker hosting auth storage refs 2026-04-04 10:36:35 +01:00
Peter Steinberger
0ed7662365 docs: refresh container auth and runtime refs 2026-04-04 10:35:35 +01:00
Brad Groux
fce81fccd8 msteams: add typingIndicator config and prevent duplicate DM typing indicator (#60771)
* msteams: add typingIndicator config and avoid duplicate DM typing

* fix(msteams): validate typingIndicator config

* fix(msteams): stop streaming before Teams timeout

* fix(msteams): classify expired streams correctly

* fix(msteams): handle link text from html attachments

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 04:34:24 -05:00
Peter Steinberger
af4e9d19cf docs: refresh linux gateway service guidance 2026-04-04 10:32:33 +01:00
Peter Steinberger
2d0ca75282 docs: refresh systemd service refs 2026-04-04 10:29:00 +01:00
Peter Steinberger
0182dd1694 docs: refresh linux service docs 2026-04-04 10:27:09 +01:00
Peter Steinberger
eb932d59e0 docs: refresh ci pipeline docs 2026-04-04 10:24:24 +01:00
Peter Steinberger
36fe4800d2 docs: refresh pi development docs 2026-04-04 10:21:30 +01:00
Peter Steinberger
cfcdf002c8 docs: refresh legacy tts and logging docs 2026-04-04 10:19:38 +01:00
Peter Steinberger
de63a646d6 docs: refresh shared web search references 2026-04-04 10:16:02 +01:00
Peter Steinberger
6b7d0deaf6 docs: refresh image generation shared references 2026-04-04 10:13:04 +01:00
Peter Steinberger
d24b9088fd docs: refresh image generation fallback refs 2026-04-04 10:10:32 +01:00
Peter Steinberger
c06248aee7 docs: refresh pdf tool model fallback refs 2026-04-04 10:07:16 +01:00
Peter Steinberger
2a5da613f4 docs: refresh media auto-detect refs 2026-04-04 10:05:30 +01:00
Peter Steinberger
459ede5a7e docs: refresh groq audio docs 2026-04-04 10:01:12 +01:00
Peter Steinberger
ac8d91edff docs: refresh bedrock discovery docs 2026-04-04 09:57:13 +01:00
Peter Steinberger
29033400eb docs: refresh zai glm refs 2026-04-04 09:54:52 +01:00
Peter Steinberger
74d39e9efe fix(ci): type zai dynamic model test callbacks 2026-04-04 09:52:34 +01:00
Peter Steinberger
c26ab4649d docs: refresh xai model ids 2026-04-04 09:52:02 +01:00
Peter Steinberger
7c43dfe28f fix(ci): isolate discord think autocomplete runtime 2026-04-04 09:49:35 +01:00
Peter Steinberger
05baeb2ada docs: refresh moonshot catalog refs 2026-04-04 09:49:20 +01:00
Peter Steinberger
7f5cf1a837 style: format explicit session-id resume helpers 2026-04-04 17:48:43 +09:00
Peter Steinberger
cd36ff7483 fix: resume explicit session-id agent runs 2026-04-04 17:48:43 +09:00
Peter Steinberger
87f512f80d docs: refresh minimax auth choice refs 2026-04-04 09:47:01 +01:00
Peter Steinberger
b5608397d0 docs: refresh minimax and kilocode refs 2026-04-04 09:45:18 +01:00
Peter Steinberger
323415204e fix: preserve registered glm-5 variants (#48185) (thanks @haoyu-haoyu) 2026-04-04 17:42:20 +09:00
Peter Steinberger
6b100e4dcf docs: expand static provider catalogs 2026-04-04 09:42:02 +01:00
ximi
9e0cf17d0c fix(minimax): correct model pricing per official docs 2026-04-04 17:40:57 +09:00
Peter Steinberger
7207a36d40 docs: refresh bundled provider overview refs 2026-04-04 09:39:56 +01:00
Peter Steinberger
1d5c57bad9 fix(ci): align browser and signal test expectations 2026-04-04 09:38:53 +01:00
Peter Steinberger
238fac6636 fix: cover status transcript fallback (#55041) (thanks @jjjojoj) 2026-04-04 17:38:44 +09:00
jjjojoj
97a8ba89fd fix: use transcript usage as fallback for /status token display
When using custom providers like LM Studio, Ollama, or DashScope,
token counts in /status show as 0 because the agent meta store
does not always have usage data populated for these providers.

Fix: set includeTranscriptUsage: true in both /status command and
the session_status tool. This enables the existing fallback path
that reads usage from the session transcript JSONL file when the
meta store has zero/missing token counts.

The merge logic already guards against overwriting valid data:
- totalTokens: only updated when zero or transcript value is larger
- inputTokens/outputTokens: only filled when zero/missing
- model/contextTokens: only filled when missing

Fixes #54995
2026-04-04 17:38:44 +09:00
Peter Steinberger
b601c7cb8f docs: refresh modelstudio catalog refs 2026-04-04 09:37:58 +01:00
Peter Steinberger
6a1ed07b33 docs: refresh router provider catalogs 2026-04-04 09:37:20 +01:00
Peter Steinberger
b1e3e59429 fix(ci): align stale provider and channel tests 2026-04-04 09:35:14 +01:00
Peter Steinberger
44762c0c80 docs: refresh bundled provider defaults 2026-04-04 09:32:58 +01:00
潘晓波0668000512
cca35404ea 修复:MiniMax coding_plan 将 interval/weekly usage_count 按剩余配额解析 2026-04-04 17:32:00 +09:00
Peter Steinberger
edc470f6b0 docs: refresh openai compatible proxy guides 2026-04-04 09:30:57 +01:00
Peter Steinberger
69980e8bf4 fix: resolve bare model ids via allowlist (#51580) (thanks @honwee) 2026-04-04 17:30:54 +09:00
陈大虾🦞
1ffbe09a6a fix(model): infer provider from allowlist for bare model IDs to prevent prefix drift (#48369) 2026-04-04 17:30:54 +09:00
Peter Steinberger
2906cfd6d7 fix: auto-register image-capable config providers (#51418) (thanks @xydt-610) 2026-04-04 17:29:54 +09:00
xydt-610
1d8bba7e39 fix(media-understanding): auto-register image capability for config providers with image input (#51392) 2026-04-04 17:29:54 +09:00
Peter Steinberger
3da187156f docs: clarify native and proxy request shaping 2026-04-04 09:29:09 +01:00
Peter Steinberger
f4855baf35 fix(ci): await async provider test registration 2026-04-04 09:28:43 +01:00
Peter Steinberger
4812b9d2e2 fix: preserve qualified chat model refs (#49874) (thanks @ShionEria) 2026-04-04 17:28:28 +09:00
ShionElia
683c028553 fix: preserve qualified provider prefix in Control UI model selector
When sessions report an already-qualified model id (e.g. ollama/qwen3:30b),
resolveServerChatModelValue was re-qualifying it using modelProvider,
producing incorrect values like openai-codex/qwen3:30b.

Preserve already-qualified model refs as-is before applying provider prefix.
Adds test coverage for qualified model preservation.

Fixes #49839
2026-04-04 17:28:28 +09:00
Peter Steinberger
1fcb2cfeb5 docs: clarify provider attribution behavior 2026-04-04 09:27:31 +01:00
Peter Steinberger
73572e04c1 fix: preserve generic DashScope streaming usage (#52395) (thanks @IVY-AI-gif) 2026-04-04 17:25:33 +09:00
Peter Steinberger
a192f345d4 docs: refresh key-free web search ordering 2026-04-04 09:25:20 +01:00
Peter Steinberger
54cfd746de docs: polish moonshot kimi docs (#57883) (thanks @chenxin-yan) 2026-04-04 17:23:29 +09:00
Chenxin Yan
8347022b50 remove redundency 2026-04-04 17:23:29 +09:00
Chenxin Yan
6615c5788b docs: fix incorrect Kimi Coding provider ID and model refs
The Kimi Coding plugin registers with provider ID `kimi` and default
model ID `kimi-code`, making the correct model ref `kimi/kimi-code`.

The docs incorrectly showed `kimi-coding/k2p5` as the provider/model
ref. This is confusing because `kimi-coding` is only a plugin alias,
not the actual provider ID used in config.

Updated all references in:
- docs/concepts/model-providers.md
- docs/providers/moonshot.md
- docs/zh-CN/concepts/model-providers.md
- docs/zh-CN/providers/moonshot.md
2026-04-04 17:23:29 +09:00
Peter Steinberger
df4b5d2137 docs: refresh self-hosted web search references 2026-04-04 09:22:30 +01:00
Vincent Koc
cdccbf2c1c fix(github-copilot): send IDE auth headers on runtime requests (#60755)
* Fix Copilot IDE auth headers

* fix(github-copilot): align tests and changelog

* fix(changelog): scope copilot replacement entry

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
2026-04-04 17:22:19 +09:00
Peter Steinberger
38ed8c355a docs: refresh perplexity web search references 2026-04-04 09:21:06 +01:00
Vincent Koc
e4c3df2fb6 docs(changelog): note cache boundary fix 2026-04-04 17:20:24 +09:00
Vincent Koc
a50b838dc2 test(agents): annotate cache trace wrapper params 2026-04-04 17:20:23 +09:00
Vincent Koc
1a13c34f5b fix(agents): close cache boundary transport gaps 2026-04-04 17:20:23 +09:00
Peter Steinberger
58a56d9a82 feat: add MiniMax TTS provider (#55921) (thanks @duncanita) 2026-04-04 09:19:45 +01:00
Peter Steinberger
a746f0e8c3 style: normalize telegram fetch test formatting 2026-04-04 09:19:45 +01:00
Vincent Koc
7ad43f21d3 style(msteams): format split graph message import 2026-04-04 09:19:45 +01:00
gnuduncan
e934211170 fix(minimax): use global TTS endpoint default and add missing Talk Mode overrides
Switch DEFAULT_MINIMAX_TTS_BASE_URL from api.minimaxi.com (CN) to
api.minimax.io (global) so international API keys work out of the box.
Add vol and pitch to resolveTalkOverrides for parity with resolveTalkConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:19:45 +01:00
gnuduncan
7d7f5d85b4 feat(minimax): add native TTS speech provider (T2A v2)
Add MiniMax as a fourth TTS provider alongside OpenAI, ElevenLabs, and
Microsoft. Registers a SpeechProviderPlugin in the existing minimax
extension with config resolution, directive parsing, and Talk Mode
support. Hex-encoded audio response from the T2A v2 API is decoded to
MP3.

Closes #52720

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:19:45 +01:00
Peter Steinberger
49d962a82f docs: refresh brave web search references 2026-04-04 09:19:11 +01:00
Peter Steinberger
d1a4363783 fix(runtime): restore gateway watch on legacy state 2026-04-04 09:18:28 +01:00
Peter Steinberger
0051a86b8f docs: clarify synthesized web search count behavior 2026-04-04 09:17:49 +01:00
Gaston Rodriguez
b6b1d5dd6c Moonshot: reuse native base URL for Kimi web search 2026-04-04 17:16:29 +09:00
Peter Steinberger
e5d03f734a docs: refresh kimi web search setup 2026-04-04 09:15:01 +01:00
Peter Steinberger
21ca006eca fix(infra): restore approval account binding compatibility 2026-04-04 09:13:11 +01:00
Peter Steinberger
af7c6f4c68 fix: harden kimi web search setup (#59356) (thanks @Innocent-children) 2026-04-04 17:11:47 +09:00
innocent-children
216294765b fix(kimi): unify runtime model fallback to kimi-k2.5
Remove DEFAULT_KIMI_MODEL (moonshot-v1-128k) and align resolveKimiModel
fallback to DEFAULT_KIMI_SEARCH_MODEL (kimi-k2.5). The legacy model
does not support the $web_search builtin_function tool, so env-var-only
users without a configured model would hit the original bug.
2026-04-04 17:11:47 +09:00
innocent-children
111495d3ca 修复kimi web_search错误 2026-04-04 17:11:47 +09:00
Peter Steinberger
11a87b4b7a docs: clarify plugin facade runtime snapshots 2026-04-04 09:11:25 +01:00
Peter Steinberger
85ade25003 docs: refresh minimax multimodal references 2026-04-04 09:09:06 +01:00
Peter Steinberger
daac149744 fix(ci): honor runtime config snapshots for facades 2026-04-04 09:08:25 +01:00
Peter Steinberger
42f6de16b2 fix: advertise MiniMax M2.7 image input (#54843) (thanks @MerlinMiao88888888) 2026-04-04 17:07:35 +09:00
王淼0668000666
51d998d828 minimax: add image capability to MiniMax-M2.7 model 2026-04-04 17:07:35 +09:00
王淼0668000666
87b41ca693 minimax: add image capability to MiniMax-M2.7 model 2026-04-04 17:07:35 +09:00
Peter Steinberger
ed866020df docs: refresh task reconciliation references 2026-04-04 09:07:08 +01:00
Peter Steinberger
7d1575b5df fix: reconcile stale cron and chat-backed tasks (#60310) (thanks @lml2468) 2026-04-04 17:05:57 +09:00
Peter Steinberger
7036e5afbf fix: honor exec approval security from approvals (#60310) (thanks @lml2468) 2026-04-04 17:05:57 +09:00
Peter Steinberger
8cec7c68b9 fix(ci): restore typecheck on main 2026-04-04 09:05:17 +01:00
Peter Steinberger
da50b492c8 docs: refresh gateway status diagnostics refs 2026-04-04 09:05:08 +01:00
Peter Steinberger
dc2575f6c4 docs: clarify local agent plugin preload 2026-04-04 09:04:11 +01:00
Peter Steinberger
7671f4f1e3 docs: clarify gateway and plugin http auth scopes 2026-04-04 09:01:05 +01:00
Peter Steinberger
bc75968074 perf(cli): trim gateway status startup imports 2026-04-04 08:59:56 +01:00
Peter Steinberger
be15805a84 refactor(runtime): lazy-load control-ui and channel-config surfaces 2026-04-04 08:59:56 +01:00
Peter Steinberger
f9e9d4e357 fix(cli): preload plugins for local agent runs 2026-04-04 08:59:37 +01:00
Vincent Koc
12be79ac48 docs(agents): clarify mobile pairing ws scope 2026-04-04 16:57:58 +09:00
Peter Steinberger
7286a10679 fix: resolve rebase gate drift (#59815) 2026-04-04 16:57:44 +09:00
Peter Steinberger
36987831ce fix: restore current-main gate (#59815) 2026-04-04 16:57:44 +09:00
Peter Steinberger
926c107fe5 fix: narrow plugin route runtime scope fallback (#59815) (thanks @pgondhi987) 2026-04-04 16:57:44 +09:00
Pavan Kumar Gondhi
0e04ca36b9 fix: finalize issue changes 2026-04-04 16:57:44 +09:00
Pavan Kumar Gondhi
74270762ff fix: address review feedback 2026-04-04 16:57:44 +09:00
Pavan Kumar Gondhi
b02b2c3a0b fix: address issue 2026-04-04 16:57:44 +09:00
Peter Steinberger
4f3ad7c6fc docs(changelog): dedupe prompt cache entry 2026-04-04 16:57:30 +09:00
Peter Steinberger
8f85c7386b docs: close remaining cli index coverage gaps 2026-04-04 08:57:20 +01:00
Peter Steinberger
5a13756ca3 docs: expand cli index coverage refs 2026-04-04 08:56:23 +01:00
Peter Steinberger
a81cf1da1f refactor: share sdk lazy config and cli test helpers 2026-04-04 16:55:04 +09:00
Peter Steinberger
6a55556b83 docs: expand sandbox and daemon index refs 2026-04-04 08:54:21 +01:00
Peter Steinberger
edfaa01d1d refactor(plugin-sdk): split runtime helper seams 2026-04-04 08:53:19 +01:00
Peter Steinberger
470898b5e1 docs: refresh gateway update and memory refs 2026-04-04 08:52:43 +01:00
Peter Steinberger
4c450ede65 fix(feishu): narrow channel sdk seams 2026-04-04 08:50:28 +01:00
Peter Steinberger
19036ef394 fix: unblock current main checks 2026-04-04 16:50:25 +09:00
Peter Steinberger
cbc6a1ddb8 fix: restore main type surfaces 2026-04-04 16:50:25 +09:00
Peter Steinberger
04b539e98c fix: restore channel sdk schema typing 2026-04-04 16:50:25 +09:00
Peter Steinberger
f6df3ed70c fix: clean up stale cron and chat-backed tasks (#60310) 2026-04-04 16:50:25 +09:00
Peter Steinberger
6afdf10266 fix: honor exec approval security from approvals (#60310) 2026-04-04 16:50:25 +09:00
Peter Steinberger
b5265a07d7 refactor: replace 156k-line generated baselines with SHA-256 hash files
Config and Plugin SDK drift detection now compares SHA-256 hashes instead
of full JSON content. The .sha256 files (6 lines total) are tracked in git;
the full JSON baselines are gitignored and generated locally for inspection.

Same CI guarantee, zero repo churn on schema changes.
2026-04-04 16:49:21 +09:00
Peter Steinberger
b4e9802ef3 test: tidy gateway scope forwarding coverage 2026-04-04 16:48:26 +09:00
Peter Steinberger
22dad753a5 docs: refresh setup and config refs 2026-04-04 08:48:15 +01:00
Peter Steinberger
1d1c52e6e6 docs: refresh mcp approvals and hooks refs 2026-04-04 08:46:37 +01:00
sudie-codes
928a5128f4 msteams: add channel-list and channel-info actions (#57529)
* msteams: add channel-list and channel-info actions via Graph API

* msteams: use action helpers, add channel-list pagination

* msteams: address PR #57529 review feedback
2026-04-04 02:43:08 -05:00
Peter Steinberger
3967ffec22 docs: refresh agent and agents refs 2026-04-04 08:42:55 +01:00
Peter Steinberger
9bbedf3caa test: replace hanging pair approve poc coverage 2026-04-04 16:42:46 +09:00
Peter Steinberger
2c0f096688 docs: refresh channel support messaging 2026-04-04 16:41:56 +09:00
Peter Steinberger
138ef136ee docs: refresh message and channels refs 2026-04-04 08:39:04 +01:00
Brad Groux
c88d6d67c8 feat(msteams): add OpenClaw User-Agent header to Microsoft HTTP calls (#51568) (#60433)
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 02:38:57 -05:00
Brad Groux
dd2faa3764 fix(msteams): persist conversation reference during DM pairing (#60432)
* fix(msteams): persist conversation reference during DM pairing (#43323)

* ci: retrigger checks

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 02:38:54 -05:00
Brad Groux
06c6ff6670 fix(msteams): handle Adaptive Card Action.Submit invoke activities (#60431)
* fix(msteams): handle Adaptive Card Action.Submit invoke activities (#55384)

* ci: retrigger checks

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-04 02:38:51 -05:00
Brad Groux
1b2fb6b98b feat: add bundled StepFun provider plugin (#60032) (#60430)
Co-authored-by: hengm3467 <100685635+hengm3467@users.noreply.github.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-04 02:38:49 -05:00
Peter Steinberger
7a03027e7f docs: refresh pairing devices and dns refs 2026-04-04 08:36:27 +01:00
Peter Steinberger
545ecc63bd docs: refresh docs search and tui refs 2026-04-04 08:34:43 +01:00
Peter Steinberger
4b490d90ec docs: expand cli security and webhook refs 2026-04-04 08:33:50 +01:00
Vincent Koc
74f60dfd0b test(agents): extend live cache runner scenarios 2026-04-04 16:33:14 +09:00
Peter Steinberger
f79c00b972 docs: expand cli maintenance summaries 2026-04-04 08:31:36 +01:00
Peter Steinberger
5d7979c5c7 docs: refresh reset and uninstall refs 2026-04-04 08:30:25 +01:00
Vincent Koc
c76646adb1 feat(agents): add prompt cache break diagnostics (#60707)
* feat(agents): add prompt cache break diagnostics

* test(agents): wire cache trace into live cache suite

* fix(agents): always record cache trace result stage

* feat(status): show cache reuse in verbose output

* fix(agents): ignore missing prompt cache usage

* chore(changelog): note prompt cache diagnostics

* fix(agents): harden prompt cache diagnostics
2026-04-04 16:29:32 +09:00
Peter Steinberger
df09fe9adf docs: refresh system health and sessions refs 2026-04-04 08:28:41 +01:00
Peter Steinberger
d584ccfc77 docs: expand logs cli reference 2026-04-04 08:27:14 +01:00
Vincent Koc
9ea37202a8 fix(config): strip legacy googlechat streamMode on load 2026-04-04 16:26:35 +09:00
Peter Steinberger
09997f032f docs: refresh tasks and status references 2026-04-04 08:24:24 +01:00
Peter Steinberger
0a5bce21a6 fix: tighten pairing guard and unblock landing gate (#60491) (thanks @eleqtrizit) 2026-04-04 16:24:10 +09:00
Agustin Rivera
cb0b15a195 fix(pair): guard setup fallback subcommands 2026-04-04 16:24:10 +09:00
Agustin Rivera
9bb97b54fe fix(pair): fail fast before qr setup lookup 2026-04-04 16:24:10 +09:00
Agustin Rivera
83e5fe5e8b fix(pair): enforce pairing scope for setup commands 2026-04-04 16:24:10 +09:00
Peter Steinberger
c3a2701c45 fix(android): delay operator bootstrap reconnect until stored auth 2026-04-04 16:23:37 +09:00
Peter Steinberger
4f95822aa8 docs: refresh cron cli references 2026-04-04 08:22:24 +01:00
Vincent Koc
d75a8933e7 fix(agents): stabilize prompt cache fingerprints (#60731)
* fix(agents): stabilize prompt cache fingerprints

* chore(changelog): note prompt cache fingerprint stability

* refactor(agents): simplify capability normalization

* refactor(agents): simplify prompt capability normalization helper
2026-04-04 16:20:36 +09:00
Peter Steinberger
0660bef81e docs: refresh cli acp and approvals summaries 2026-04-04 08:20:20 +01:00
Peter Steinberger
3e5c571e57 docs: sync browser cli summary 2026-04-04 08:18:14 +01:00
Peter Steinberger
53e2554281 docs: expand browser cli reference 2026-04-04 08:17:19 +01:00
Vincent Koc
5c685eee9c fix(config): remove lingering channel streamMode leaks (#60733) 2026-04-04 16:14:38 +09:00
Peter Steinberger
644ed24ed8 docs(changelog): clarify breaking config aliases 2026-04-04 16:14:28 +09:00
Vincent Koc
65842aabad refactor(providers): share google and xai provider helpers (#60722)
* refactor(google): share oauth token helpers

* refactor(xai): share tool auth fallback helpers

* refactor(xai): share tool auth resolution

* refactor(xai): share tool config helpers

* refactor(xai): share fallback auth helpers

* refactor(xai): share responses tool helpers

* refactor(google): share http request config helper

* fix(xai): re-export shared web search extractor

* fix(xai): import plugin config type

* fix(providers): preserve default google network guard
2026-04-04 16:14:15 +09:00
Peter Steinberger
c87903a4c6 fix(ci): restore build and typecheck on main 2026-04-04 08:13:16 +01:00
Peter Steinberger
d2bace59d1 docs: refresh live testing auth storage 2026-04-04 08:12:52 +01:00
Peter Steinberger
66b1520d92 docs: refresh auth command references 2026-04-04 08:10:34 +01:00
Peter Steinberger
32d2654340 build: bump version to 2026.4.4 2026-04-04 16:09:42 +09:00
Peter Steinberger
95a6d386c0 docs: expand provider overview coverage 2026-04-04 08:07:36 +01:00
Peter Steinberger
14cfcdba1a docs(test): refresh stale model refs 2026-04-04 08:05:49 +01:00
Peter Steinberger
f38a3ae996 docs(changelog): reorder unreleased notes 2026-04-04 16:04:40 +09:00
Peter Steinberger
9195cf839b docs: refresh provider overview references 2026-04-04 08:03:56 +01:00
Peter Steinberger
1738900a9a docs: refresh moonshot kimi coding refs 2026-04-04 08:01:41 +01:00
Peter Steinberger
0013568500 docs: refresh google and openrouter onboarding docs 2026-04-04 07:59:52 +01:00
Peter Steinberger
406a47284a fix(ci): restore channel typing and root-help metadata build 2026-04-04 07:59:32 +01:00
Peter Steinberger
7b4e20fc8c docs: sync cloudflare and synthetic provider docs 2026-04-04 07:57:43 +01:00
Peter Steinberger
20266ff7dd fix: preserve mobile bootstrap auth fallback (#60238) (thanks @ngutman) 2026-04-04 15:57:38 +09:00
Nimrod Gutman
226ca1f324 fix(auth): address qr bootstrap review feedback 2026-04-04 15:57:38 +09:00
Nimrod Gutman
a9140abea6 fix(auth): hand off qr bootstrap to bounded device tokens 2026-04-04 15:57:38 +09:00
Vincent Koc
c4597992ca fix(config): remove remaining legacy surface leaks (#60726) 2026-04-04 15:55:31 +09:00
Peter Steinberger
1c42f0e866 docs: refresh auth storage reference examples 2026-04-04 07:52:22 +01:00
Peter Steinberger
ad7461b639 docs: align auth storage and token auth guidance 2026-04-04 07:50:26 +01:00
Peter Steinberger
da3f5e9bca docs(providers): refresh model examples and env defaults 2026-04-04 07:49:22 +01:00
Vincent Koc
0609bf8581 feat(memory): harden dreaming and multilingual memory promotion (#60697)
* feat(memory): add recall audit and doctor repair flow

* refactor(memory): rename symbolic scoring and harden dreaming

* feat(memory): add multilingual concept vocabulary

* docs(changelog): note dreaming memory follow-up

* docs(changelog): shorten dreaming follow-up entry

* fix(memory): address review follow-ups

* chore(skills): tighten security triage trust model

* Update CHANGELOG.md
2026-04-04 15:48:13 +09:00
Peter Steinberger
0ab160cda9 docs(anthropic): remove setup-token setup docs 2026-04-04 15:46:25 +09:00
Peter Steinberger
1b4bb5be19 fix(anthropic): remove setup-token onboarding path 2026-04-04 15:46:25 +09:00
Peter Steinberger
15bee338e9 docs: refresh provider hook docs 2026-04-04 07:46:15 +01:00
Peter Steinberger
359c6dedbe docs: prefer channel-core in channel sdk docs 2026-04-04 07:46:15 +01:00
Peter Steinberger
6e6b4f6004 ci: gate releases on live cache floors 2026-04-04 15:44:34 +09:00
Peter Steinberger
be4eb269fc refactor: tighten ACP spawn failure typing 2026-04-04 15:43:23 +09:00
Peter Steinberger
b167ad052c refactor(providers): move defaults and error policy into plugins 2026-04-04 07:43:14 +01:00
Peter Steinberger
e34f42559f docs: refresh plugin sdk import reference 2026-04-04 07:41:44 +01:00
Peter Steinberger
27aa659498 docs: clarify plugin entry export contract 2026-04-04 07:40:24 +01:00
Peter Steinberger
d5cb8cebcd refactor(extensions): split channel runtime helper seams 2026-04-04 07:39:53 +01:00
Peter Steinberger
667a54a4b7 refactor(plugins): narrow bundled channel core seams 2026-04-04 07:39:53 +01:00
Peter Steinberger
381ee4d218 docs: align bundled plugin defaults in docs 2026-04-04 07:38:55 +01:00
Peter Steinberger
50a1fac1c5 docs: remove stale plugins status command 2026-04-04 07:37:25 +01:00
Peter Steinberger
c8be1ca6ae docs: note sdk config schema memoization 2026-04-04 07:35:04 +01:00
Peter Steinberger
25b069a6f3 refactor(gateway): split MCP loopback transport helpers 2026-04-04 15:34:13 +09:00
Peter Steinberger
f856aaea40 fix(ci): pick the real root-help bundle 2026-04-04 07:33:48 +01:00
Vincent Koc
26f0c7ee90 refactor(plugin-sdk): lazily resolve plugin config schemas 2026-04-04 15:32:33 +09:00
Peter Steinberger
85c5d90c11 docs: sync acp spawn workspace behavior 2026-04-04 07:32:09 +01:00
Peter Steinberger
71c0c2cc06 fix: harden ACP spawn workspace resolution 2026-04-04 15:29:56 +09:00
zssggle-rgb
d718d17b5b fix(acp): fall back when inherited target workspace is missing 2026-04-04 15:29:56 +09:00
Peter Steinberger
6507f54965 docs: refresh generic model examples 2026-04-04 07:27:32 +01:00
Vincent Koc
bcd11176ef refactor(amazon-bedrock): lazy-load provider registration 2026-04-04 15:26:37 +09:00
Peter Steinberger
195e380e05 docs: remove legacy cache retention notes 2026-04-04 15:26:19 +09:00
Peter Steinberger
cb6d0576be docs: refresh media understanding examples 2026-04-04 07:25:52 +01:00
Peter Steinberger
332caa4cb1 style: normalize embedded runner imports 2026-04-04 15:24:50 +09:00
Peter Steinberger
3d55b28853 style: wrap long runtime and test lines 2026-04-04 15:24:50 +09:00
Peter Steinberger
b379dac798 chore: ignore dist-runtime artifacts 2026-04-04 15:24:50 +09:00
Peter Steinberger
1809da659e docs: refresh cli and node pairing references 2026-04-04 07:23:11 +01:00
Vincent Koc
6fc69f5d33 fix(secrets): drop legacy talk apiKey target surface (#60717) 2026-04-04 15:22:41 +09:00
Peter Steinberger
e7e1707277 fix(ci): restore build and typecheck on main 2026-04-04 07:22:16 +01:00
Vincent Koc
7e7460c2f9 refactor(anthropic): lazy-load provider registration 2026-04-04 15:20:28 +09:00
Peter Steinberger
666f1f4db0 refactor(providers): remove core default and usage bias 2026-04-04 07:19:29 +01:00
Peter Steinberger
9e4cf3996e test: add gateway durable allow-always coverage (#59880) (thanks @luoyanglang) 2026-04-04 15:18:24 +09:00
Peter Steinberger
cdb572d703 test: tune live cache assertions 2026-04-04 15:18:09 +09:00
Vincent Koc
c4d2c4899d refactor(browser): lazy-load plugin registration 2026-04-04 15:17:44 +09:00
Peter Steinberger
3de09fbe74 fix: restore claude cli loopback mcp bridge (#35676) (thanks @mylukin) 2026-04-04 15:16:20 +09:00
Vincent Koc
c2435306a7 refactor(acpx): lazy-load runtime service entry 2026-04-04 15:14:51 +09:00
Peter Steinberger
e2454d4b8a docs: align provider and onboarding references 2026-04-04 07:14:28 +01:00
Peter Steinberger
dd16080af7 refactor(exec): dedupe durable approval checks 2026-04-04 07:12:26 +01:00
Peter Steinberger
b32a2cadc2 docs(acp): clarify default startup and runtime paths 2026-04-04 15:10:26 +09:00
Vincent Koc
e56ffd48df refactor(github-copilot): lazy-load provider registration 2026-04-04 15:06:02 +09:00
Ayaan Zaidi
1c1f32e756 fix: trust local bot api media roots (#60705) 2026-04-04 11:35:36 +05:30
Ayaan Zaidi
cfc52fcf2b fix(telegram): trust local bot api media roots 2026-04-04 11:35:36 +05:30
Peter Steinberger
c91b6bf322 fix(ci): unblock agent typing and cache startup metadata 2026-04-04 07:04:17 +01:00
Peter Steinberger
3a3f88a80a refactor(media): move provider defaults into media metadata 2026-04-04 07:00:47 +01:00
Peter Steinberger
fca80d2ee2 refactor(thinking): move provider thinking fallback out of core 2026-04-04 07:00:47 +01:00
Peter Steinberger
b59ce0903c docs: add SOUL personality guide 2026-04-04 14:59:35 +09:00
Vincent Koc
3437818b91 refactor(vllm): lazy-load provider registration 2026-04-04 14:56:04 +09:00
Peter Steinberger
0587fb3fc8 fix: note gateway allow-always reuse (#59880) (thanks @luoyanglang) 2026-04-04 14:55:26 +09:00
luoyanglang
b54acd97b3 fix(exec): reuse gateway allow-always approvals 2026-04-04 14:55:26 +09:00
Vincent Koc
ede6d03850 refactor(openrouter): lazy-load provider registration 2026-04-04 14:54:59 +09:00
Vincent Koc
0099c309c9 refactor(openai): lazy-load provider registration 2026-04-04 14:53:02 +09:00
Vincent Koc
fcf1aee2b4 refactor(microsoft): lazy-load speech provider 2026-04-04 14:51:55 +09:00
Vincent Koc
73115b5480 fix(zalouser): migrate legacy group allow aliases (#60702)
* fix(channels): prefer source contract surfaces in source checkouts

* fix(zalouser): migrate legacy group allow aliases
2026-04-04 14:50:15 +09:00
Peter Steinberger
ae7942bf5e fix: prefer Claude CLI in Anthropic onboarding 2026-04-04 14:49:55 +09:00
Peter Steinberger
1ab37d7a12 refactor(gateway): classify pairing locality 2026-04-04 06:47:14 +01:00
Vincent Koc
b3186aeef9 test(agents): expand live cache runner scenarios 2026-04-04 14:46:56 +09:00
Vincent Koc
32dd0aa7e7 fix(plugin-sdk): lazy acp runtime testing merge 2026-04-04 14:43:53 +09:00
Vincent Koc
fd01561327 fix(agents): close remaining prompt cache boundary gaps (#60691)
* fix(agents): route default stream fallbacks through boundary shapers

* fix(agents): close remaining cache boundary gaps

* chore(changelog): note cache prefix follow-up rollout

* fix(agents): preserve cache-safe fallback stream bases
2026-04-04 14:41:47 +09:00
Peter Steinberger
30ba837a7b test: isolate MCP live cache probe 2026-04-04 14:39:51 +09:00
Peter Steinberger
0ebc7b6077 docs: clarify anthropic claude cli migration 2026-04-04 14:38:42 +09:00
Peter Steinberger
40da986b21 fix: preserve docker cli pairing locality (#55113) (thanks @sar618) 2026-04-04 14:36:30 +09:00
sar618
224fceee1a fix(gateway): skip device pairing for authenticated CLI connections in Docker
CLI connections with valid shared auth (token/password) now bypass device
pairing, fixing the chicken-and-egg problem where Docker CLI commands fail
with 'pairing required' (1008) despite sharing the gateway's network
namespace and auth token.

The existing shouldSkipBackendSelfPairing only matched gateway-client/backend
mode. CLI connections use cli/cli mode and were excluded. Additionally,
isLocalDirectRequest produces false negatives in Docker (host networking,
network_mode sharing) even when remoteAddress is 127.0.0.1, so CLI connections
with valid shared auth skip the locality check entirely — the token is the
trust anchor.

Closes #55067
Related: #12210, #23471, #30740
2026-04-04 14:36:30 +09:00
Peter Steinberger
2b538464e1 fix(docs): format dreaming memory tables 2026-04-04 06:31:40 +01:00
Vincent Koc
71562cc570 docs(changelog): add config surface breaking note 2026-04-04 14:30:46 +09:00
Vincent Koc
b390591779 fix(matrix): migrate room allow aliases to enabled (#60690)
* fix(matrix): migrate room allow aliases to enabled

* test(matrix): keep migration coverage on the channel seam

* chore(config): refresh baselines after matrix alias cleanup
2026-04-04 14:27:50 +09:00
Vincent Koc
6e0fe1b91e docs: expand dreaming memory documentation 2026-04-04 14:25:29 +09:00
Vignesh Natarajan
10d5b8813d Agents/logging: reduce orphaned-user warning noise for background runs 2026-04-03 22:24:02 -07:00
Peter Steinberger
e4dc03f108 refactor(acpx): split Windows command parsing 2026-04-04 14:19:20 +09:00
Peter Steinberger
41243529fb refactor(providers): centralize provider model policy 2026-04-04 06:16:48 +01:00
Vincent Koc
e07d8fd20b docs(agents): tighten provider boundary guidance 2026-04-04 14:13:46 +09:00
Peter Steinberger
026ca40be9 fix(ci): repair voice-call provider resolution typing 2026-04-04 06:11:30 +01:00
Vignesh Natarajan
18016e7546 Docs/memory: add Dreaming concept page and overview links 2026-04-03 22:10:32 -07:00
Peter Steinberger
db177ab2ac docs: add changelog for #60689 2026-04-04 14:10:20 +09:00
Peter Steinberger
e985324d87 fix(acpx): preserve Windows Claude CLI paths 2026-04-04 14:10:20 +09:00
Vignesh Natarajan
9802c060bf Dreaming UI: explain modes on hover in header controls 2026-04-03 22:08:49 -07:00
Peter Steinberger
b392c78bab fix(ci): align settings host test fixtures 2026-04-04 06:08:26 +01:00
Peter Steinberger
cff8b5bebd fix(agents): preserve acp and openai wrapper defaults 2026-04-04 14:07:19 +09:00
Peter Steinberger
bc8048250e fix(agents): harden claude cli parsing and queueing 2026-04-04 14:07:19 +09:00
Peter Steinberger
4ed17fd987 refactor(voice-call): migrate legacy config via doctor 2026-04-04 14:06:52 +09:00
Vincent Koc
561db47566 docs(boundaries): add import-topology guardrails 2026-04-04 14:06:18 +09:00
Peter Steinberger
0777ddace8 perf: split more targeted test lanes 2026-04-04 06:05:24 +01:00
Peter Steinberger
5ddc57aa22 style(ui): format chat view templates 2026-04-04 06:02:57 +01:00
Peter Steinberger
64d9b65b56 style(core): format reply and infra helpers 2026-04-04 06:02:47 +01:00
Peter Steinberger
fd75d214f2 style(extensions): format channel integration updates 2026-04-04 06:02:37 +01:00
Peter Steinberger
8b5672bda4 test: align ui vitest configs with thread policy 2026-04-04 06:00:15 +01:00
Vignesh Natarajan
f8c4777515 Dreaming: move setup controls to header and tighten status plumbing 2026-04-03 21:58:46 -07:00
Vignesh Natarajan
a5f66b5c48 fix(plugins): constrain workspace discovery to .openclaw/extensions 2026-04-03 21:57:58 -07:00
Peter Steinberger
02cc09dafe test: refresh vitest config assertions 2026-04-04 05:57:27 +01:00
Peter Steinberger
ca9d2f3b41 ci: align vitest entrypoints with root config 2026-04-04 05:57:27 +01:00
Peter Steinberger
757a20b656 test: enforce thread-first vitest configs 2026-04-04 05:57:26 +01:00
Peter Steinberger
33e10c4772 fix(ci): repair bundled test selection and compat typing 2026-04-04 05:56:55 +01:00
Vincent Koc
230a39797a fix(infra): break exec safe-bin import cycle 2026-04-04 13:53:32 +09:00
Peter Steinberger
8a3d946f4a test: cover vitest contention scheduling 2026-04-04 05:51:27 +01:00
Peter Steinberger
55812eaf14 fix: throttle vitest under local contention 2026-04-04 05:50:46 +01:00
Vincent Koc
9afaec1b0c docs(changelog): add cache-prefix attribution 2026-04-04 13:47:43 +09:00
Peter Steinberger
53fd262173 ci: align pnpm pins and vitest config 2026-04-04 05:44:29 +01:00
Peter Steinberger
22e6225dd0 perf: split hooks, tui, and extension lanes 2026-04-04 05:38:47 +01:00
Peter Steinberger
af102907c5 docs: add GitHub sponsor to README 2026-04-04 13:36:58 +09:00
Peter Steinberger
39135ca3a4 refactor(voice-call): isolate config compatibility 2026-04-04 13:34:05 +09:00
Vincent Koc
64f28906de fix(agents): split system prompt cache prefix by transport (#59054)
* fix(agents): restore Anthropic prompt cache seam

* fix(agents): strip cache boundary for completions

* fix(agents): strip cache boundary for cli backends

* chore(changelog): note cross-transport cache boundary rollout

* fix(agents): route default stream fallbacks through boundary shapers

* fix(agents): strip cache boundary for provider streams
2026-04-04 13:32:32 +09:00
Peter Steinberger
b0e1551eb8 refactor(extensions): add channel-owned config schema seams 2026-04-04 05:31:11 +01:00
Peter Steinberger
c17985aa9f test: align hook install unsafe flag assertion 2026-04-04 05:27:57 +01:00
Peter Steinberger
e95b723b82 fix: load telegram command config from contract surfaces 2026-04-04 05:26:54 +01:00
Peter Steinberger
c7cb43cac9 perf: split more scoped vitest lanes 2026-04-04 05:26:32 +01:00
Peter Steinberger
64b971b2b0 fix: resolve config write test drift 2026-04-04 05:25:57 +01:00
Peter Steinberger
3a62b0e75b fix(ci): remove invalid live cache reasoning flag 2026-04-04 05:24:29 +01:00
Peter Steinberger
b16e70e37f refactor(plugins): route bundled channel config runtime through metadata 2026-04-04 05:20:43 +01:00
Peter Steinberger
5b294b7fbd test: keep vitest thread workers conservative 2026-04-04 05:20:19 +01:00
Peter Steinberger
943da1864a test: add tool-turn cache coverage 2026-04-04 13:19:00 +09:00
Peter Steinberger
53b5b1b32d fix(ci): repair redundant channel union types 2026-04-04 05:08:02 +01:00
Peter Steinberger
1246e2b03a refactor(extensions): move channel-specific config surfaces out of core 2026-04-04 05:06:32 +01:00
Peter Steinberger
0f544fa1ca fix(ci): repair bluebubbles status test import 2026-04-04 05:03:19 +01:00
Peter Steinberger
39d3cad479 fix(ci): repair check lane type drift 2026-04-04 04:59:18 +01:00
Peter Steinberger
e277ac0838 fix: defer command secret target registry loading 2026-04-04 04:58:09 +01:00
Peter Steinberger
f84486157e refactor(channels): remove bluebubbles core status collector 2026-04-04 04:53:38 +01:00
Peter Steinberger
bc457fd1b8 refactor(channels): move bootstrap channel logic behind extension seams 2026-04-04 04:53:38 +01:00
Peter Steinberger
fff7e610df feat(plugins): auto-load provider plugins from model support 2026-04-04 04:52:25 +01:00
Peter Steinberger
5b144655f2 test(ci): align channel defaults and clean stale hook tests 2026-04-04 04:51:33 +01:00
Peter Steinberger
f4fa53de3f fix(ci): repair zalouser sdk path and exec timeout kill 2026-04-04 04:51:33 +01:00
Peter Steinberger
ca99ad0af8 test: add live cache provider probes 2026-04-04 12:46:10 +09:00
Peter Steinberger
efefa5560d perf: optimize vitest jsdom and isolated lanes 2026-04-04 04:45:01 +01:00
Marcus Castro
9d1a58f551 fix(auto-reply): preserve reasoning markers during block coalescing (#60655)
* fix: preserve reasoning markers during block coalescing

* docs(changelog): add auto-reply reasoning fix entry
2026-04-04 00:44:11 -03:00
Peter Steinberger
ed0cbcba2f refactor(voice-call): use config for realtime tuning 2026-04-04 12:43:23 +09:00
@zimeg
e636ba6ab0 docs(slack): move slash command settings to matching section 2026-04-03 20:42:23 -07:00
Peter Steinberger
32ba917079 perf: split infra, tooling, and provider test lanes 2026-04-04 04:39:47 +01:00
Vignesh Natarajan
f62db7950a fix(control-ui): keep session key helpers browser-safe 2026-04-03 20:39:36 -07:00
Vincent Koc
b7ec90258b fix(plugins): preserve bundled origin when workspace matches bundled root 2026-04-04 12:38:43 +09:00
Peter Steinberger
0ad75cffe3 test: restore native root vitest entrypoint 2026-04-04 04:37:08 +01:00
Peter Steinberger
bb1cc84d50 test: default vitest root projects to threads 2026-04-04 04:37:08 +01:00
Vincent Koc
fb5066dfb1 refactor(zalouser): lazy-load account runtimes 2026-04-04 12:36:39 +09:00
Peter Steinberger
6b003a7f2b refactor(cli): reuse install safety overrides 2026-04-04 12:35:58 +09:00
Peter Steinberger
406f06dcc5 fix: preserve linked install unsafe flag and baseline regressions 2026-04-04 12:34:55 +09:00
JD Davis
8a8ea94228 CLI: forward unsafe flag to linked hook-pack probes 2026-04-04 12:34:55 +09:00
JD Davis
bac15a7313 CLI: pass unsafe flag through linked plugin probes 2026-04-04 12:34:55 +09:00
Peter Steinberger
7cd40ad565 refactor(voice-call): clean provider boundaries 2026-04-04 12:33:47 +09:00
Vincent Koc
6964e4acf7 refactor(discord): lazy-load action and audit runtimes 2026-04-04 12:32:21 +09:00
Peter Steinberger
a82bc7d887 fix(ci): align contract expectations 2026-04-04 12:29:11 +09:00
Peter Steinberger
df48a7bfc0 fix: resolve stale plugin-sdk and test type regressions 2026-04-04 04:28:59 +01:00
Peter Steinberger
eb9051cc7c refactor(openai): move native transport policy into extension 2026-04-04 04:27:14 +01:00
Peter Steinberger
585b1c9413 fix(ci): repair openai codex provider test syntax 2026-04-04 04:27:02 +01:00
Vignesh
4c1022c73b feat(memory-core): add dreaming promotion with weighted recall thresholds (#60569)
* memory-core: add dreaming promotion flow with weighted thresholds

* docs(memory): mark dreaming as experimental

* memory-core: address dreaming promotion review feedback

* memory-core: harden short-term promotion concurrency

* acpx: make abort-process test timer-independent

* memory-core: simplify dreaming config with mode presets

* memory-core: add /dreaming command and tighten recall tracking

* ui: add Dreams tab with sleeping lobster animation

Adds a new Dreams tab to the gateway UI under the Agent group.
The tab is gated behind the memory-core dreaming config — it only
appears in the sidebar when dreaming.mode is not 'off'.

Features:
- Sleeping vector lobster with breathing animation
- Floating Z's, twinkling starfield, moon glow
- Rotating dream phrase bubble (17 whimsical phrases)
- Memory stats bar (short-term, long-term, promoted)
- Active/idle visual states
- 14 unit tests

* plugins: fix --json stdout pollution from hook runner log

The hook runner initialization message was using log.info() which
writes to stdout via console.log, breaking JSON.parse() in the
Docker smoke test for 'openclaw plugins list --json'. Downgrade to
log.debug() so it only appears when debugging is enabled.

* ui: keep Dreams tab visible when dreaming is off

* tests: fix contracts and stabilize extension shards

* memory-core: harden dreaming recall persistence and locking

* fix: stabilize dreaming PR gates (#60569) (thanks @vignesh07)

* test: fix rebase drift in telegram and plugin guards
2026-04-03 20:26:53 -07:00
Vincent Koc
2687a49575 refactor(line): lazy-load channel runtime seams 2026-04-04 12:26:20 +09:00
Peter Steinberger
eeb2888f6e fix(ci): sync openai provider lockfile 2026-04-04 04:24:31 +01:00
Ayaan Zaidi
d7b8faa7bf fix: keep Kimi anthropic tool payloads native (#60391) (thanks @Eric-Guo) 2026-04-04 08:53:57 +05:30
Peter Steinberger
41e16a883b fix(cli): honor unsafe override for linked installs 2026-04-04 12:22:49 +09:00
Peter Steinberger
2416e2d51d fix(ci): repair seam drift and matrix test timing 2026-04-04 04:22:17 +01:00
Peter Steinberger
d7ba6d3e68 test: move vitest config regression under active unit surface 2026-04-04 04:19:08 +01:00
Peter Steinberger
33453838da perf: route test commands through scoped lanes 2026-04-04 04:18:10 +01:00
tmimmanuel
0fef95b17d fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel)
* fix(daemon): preserve Windows Task Scheduler settings on reinstall and exit early on failed restart

* fix(daemon): add test coverage for Create/Change paths, fix early exit grace period

* fix(daemon): fix startup-fallback tests for new isRegisteredScheduledTask call

* fix(daemon): report early restart failure accurately

* fix: preserve Windows scheduled task restart/install behavior (#59335) (thanks @tmimmanuel)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 08:46:00 +05:30
Peter Steinberger
ff0c1b57a7 fix(auth): respect externally managed codex refresh tokens 2026-04-04 04:12:05 +01:00
Vincent Koc
26c9a4ce63 fix(contracts): align runtime seams and codex expectations 2026-04-04 12:11:07 +09:00
Vincent Koc
fc79ebe098 refactor(zalouser): narrow channel runtime imports 2026-04-04 12:09:58 +09:00
Vincent Koc
20937422ca refactor(mattermost): narrow channel runtime imports 2026-04-04 12:09:54 +09:00
Vincent Koc
e750c10577 refactor(nostr): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Vincent Koc
ba20e6cd98 refactor(nextcloud-talk): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Vincent Koc
6349e6aa3e refactor(irc): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Vincent Koc
c4bae0f7bf refactor(msteams): narrow channel runtime imports 2026-04-04 12:08:38 +09:00
Peter Steinberger
a23ab9b906 refactor: move voice-call realtime providers into extensions 2026-04-04 12:07:23 +09:00
Vincent Koc
61f93540b2 refactor(discord): narrow channel runtime imports 2026-04-04 12:06:00 +09:00
Vincent Koc
9bfaf7b681 refactor(slack): narrow channel runtime imports 2026-04-04 12:06:00 +09:00
Peter Steinberger
7e69c2f6a7 test: trim remaining mock drift 2026-04-04 04:04:12 +01:00
Vincent Koc
2f5509e36d refactor(slack): lazy-load directory config seam 2026-04-04 12:03:14 +09:00
Vincent Koc
f9cf868553 refactor(slack): lazy-load target resolution seams 2026-04-04 12:02:58 +09:00
Vincent Koc
bc6b20e542 refactor(discord): lazy-load directory and resolver seams 2026-04-04 12:02:58 +09:00
Peter Steinberger
af94a3a89b test: use native vitest root projects 2026-04-04 04:01:32 +01:00
Vincent Koc
2050ef2740 refactor(whatsapp): lazy-load channel directory and action seams 2026-04-04 12:01:30 +09:00
Peter Steinberger
df86f4dc00 docs(changelog): reorder unreleased highlights 2026-04-04 12:00:10 +09:00
Vincent Koc
6c31b2fbc5 refactor(imessage): narrow channel runtime imports 2026-04-04 11:59:38 +09:00
Vincent Koc
0737816010 refactor(line): narrow channel runtime imports 2026-04-04 11:59:38 +09:00
Vincent Koc
e9d802c32b fix(openai): align gpt-5.4 codex context test 2026-04-04 11:59:05 +09:00
Peter Steinberger
94b0062e90 fix: keep local marketplace paths stable (#60556) (thanks @eleqtrizit) 2026-04-04 11:58:52 +09:00
Agustin Rivera
e8ebd6ab8c fix(marketplace): narrow canonical path checks 2026-04-04 11:58:52 +09:00
Agustin Rivera
750d963cb9 fix(marketplace): preserve local symlink installs 2026-04-04 11:58:52 +09:00
Agustin Rivera
b1dd3ded35 fix(marketplace): canonicalize remote plugin paths 2026-04-04 11:58:52 +09:00
Peter Steinberger
f25f147fc3 refactor(whatsapp): move legacy group session detection into contract surface 2026-04-04 03:57:56 +01:00
Vincent Koc
098abd484d fix(channels): keep feishu override parent fallbacks 2026-04-04 11:57:27 +09:00
Vincent Koc
5eb32f24ea refactor(discord): normalize lazy loader formatting 2026-04-04 11:53:21 +09:00
Vincent Koc
bf1b1d63bd refactor(discord): lazy-load channel send seams 2026-04-04 11:53:21 +09:00
Vincent Koc
e249a852ae refactor(slack): lazy-load async channel seams 2026-04-04 11:53:21 +09:00
Peter Steinberger
a3a06524f2 fix(ci): restore session and setup fallbacks 2026-04-04 03:52:37 +01:00
Peter Steinberger
3c23126980 fix(ci): tolerate missing contract surface roots 2026-04-04 03:52:37 +01:00
Peter Steinberger
6b3ff0dd4f feat(openai): add codex gpt-5.4-mini support 2026-04-04 11:51:57 +09:00
Vincent Koc
7df763b04d refactor(providers): share xai compat helper 2026-04-04 11:45:13 +09:00
Karl Yang
6d33c67c01 fix: enable groq and deepgram bundled media providers by default (#59982) (thanks @yxjsxy)
* fix: add enabledByDefault to groq and deepgram media plugin manifests

The groq and deepgram plugin manifests were missing the
enabledByDefault: true flag. Without this flag, both plugins are
treated as bundled-but-disabled-by-default, so resolveRuntimePluginRegistry
loads without them. When buildProviderRegistry later needs to resolve
audio providers, the active registry is used first (short-circuits
the compat path in resolvePluginCapabilityProviders), leaving groq
and deepgram absent from the registry.

This caused 'Media provider not available: groq' errors when users
configured tools.media.audio.models with groq or deepgram, even
with GROQ_API_KEY / DEEPGRAM_API_KEY set correctly.

The fix mirrors the pattern used by other audio/media-only providers
such as mistral, which already has enabledByDefault: true.

Fixes #59875

* fix: enable groq and deepgram bundled media providers by default (#59982) (thanks @yxjsxy)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 08:11:34 +05:30
Monty Taylor
d605cb08c5 matrix: force SSSS recreation on backup reset when SSSS key is broken (bad MAC) (#60599)
Merged via squash.

Prepared head SHA: 3b0a623407
Co-authored-by: emonty <95156+emonty@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 22:34:23 -04:00
Vincent Koc
fb1cb99c88 fix(xai): narrow stream wrapper params 2026-04-04 11:31:27 +09:00
Vincent Koc
b5a849801c chore(plugin-sdk): refresh api baseline 2026-04-04 11:30:30 +09:00
Vincent Koc
e273753d45 refactor(providers): share anthropic tool payload helper 2026-04-04 11:30:30 +09:00
George Zhang
87885b948a fix: handle sensitive, number-clear, and array-clear edge cases in plugin config TUI (#60640) (#60640)
- Skip sensitive fields with a note directing users to openclaw config set
  or the Web UI (WizardPrompter has no masked input)
- Clear number fields to undefined when input is empty instead of storing 0
- Allow clearing array fields to undefined via empty input
2026-04-03 19:27:26 -07:00
Vincent Koc
761bd3bbd0 refactor(providers): share passthrough replay helpers 2026-04-04 11:22:41 +09:00
Ayaan Zaidi
6a3a0c405f fix: replay interrupted recurring jobs on first restart (#60583) (thanks @joelnishanth) 2026-04-04 07:51:04 +05:30
joelnishanth
7a16e14301 fix(cron): resume interrupted recurring jobs on first restart (#60495) 2026-04-04 07:51:04 +05:30
Vincent Koc
9e389cff3d fix(config): migrate legacy group allow aliases (#60597)
* fix(config): migrate legacy group allow aliases

* fix(config): inline legacy streaming migration helpers

* refactor(config): rename legacy account matcher helper

* chore(agents): codify config contract boundaries

* fix(config): keep legacy allow aliases writable

* Update AGENTS.md
2026-04-04 11:15:32 +09:00
Ayaan Zaidi
945b198c76 fix(android): allow cleartext LAN gateways 2026-04-04 07:36:18 +05:30
Vincent Koc
94adc24393 chore(plugin-sdk): refresh api baseline 2026-04-04 11:03:28 +09:00
Vincent Koc
30479b4ee0 refactor(providers): compose provider stream wrappers 2026-04-04 11:03:28 +09:00
Michael Faath
85c76e83b7 fix: restore android talk mode reply tts (#60306) (thanks @MKV21)
* Android: keep talk-mode session key synced for TTS replies

* Android: harden talk-mode reply playback state

* Android: harden talk-mode playback cancellation

* Android: avoid stale talk-mode playback preemption

* Android: tighten talk-mode playback claiming

* fix: distill android talk-mode playback ownership

* fix: restore android talk mode reply tts (#60306) (thanks @MKV21)

---------

Co-authored-by: Michael Faath <michaelfaath@macbookpro.speedport.ip>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-04 07:28:56 +05:30
Vincent Koc
858bf405f4 refactor(providers): share replay and tool compat helpers (#60637)
* refactor(providers): share replay and tool compat helpers

* chore(plugin-sdk): refresh api baseline
2026-04-04 10:55:36 +09:00
Vincent Koc
dd31ee1139 fix(cli): log pending control ui build 2026-04-04 10:47:38 +09:00
Peter Steinberger
b76ed0fadf fix: harden OpenAI websocket transport 2026-04-04 02:38:36 +01:00
Peter Steinberger
1e6e685347 fix: unblock cli startup metadata 2026-04-04 02:35:36 +01:00
Peter Steinberger
143d377c5a fix(cli): keep status json startup lean 2026-04-04 02:16:56 +01:00
Gustavo Madeira Santana
3713b0e506 vertex: read ADC files without exists preflight (#60592)
Merged via squash.

Prepared head SHA: 72f7372e97
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 21:13:34 -04:00
Peter Steinberger
34cd49faa6 perf: route browser and line extension tests 2026-04-04 02:08:45 +01:00
Peter Steinberger
1e90b3afcd perf: split extension channel vitest lane 2026-04-04 02:08:45 +01:00
Peter Steinberger
e941d425ac perf: split acp and ui vitest lanes 2026-04-04 02:08:45 +01:00
Peter Steinberger
fb0ff6896a perf: route contract test targets 2026-04-04 02:08:45 +01:00
Peter Steinberger
b04c4e599c perf: route bundled and extension helper tests 2026-04-04 02:08:44 +01:00
Peter Steinberger
ac11e02518 perf: route bundled and extension helper tests 2026-04-04 02:08:44 +01:00
Peter Steinberger
269771a4b6 perf: route targeted tests to scoped vitest configs 2026-04-04 02:08:44 +01:00
Peter Steinberger
37ee19521f fix(status): keep empty status path lightweight 2026-04-04 10:02:42 +09:00
Peter Steinberger
f8a3840a42 fix(ci): restore contextTokens runtime typing 2026-04-04 02:00:19 +01:00
Gustavo Madeira Santana
931ddd96f0 fix(cache): preserve full 3-turn history image cache window (#60603)
Merged via squash.

Prepared head SHA: 58d06ea372
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 20:48:56 -04:00
Peter Steinberger
b8021d6709 docs: add prompt cache stability rules 2026-04-04 01:47:00 +01:00
Peter Steinberger
58d2b9dd46 fix: add runtime model contextTokens caps 2026-04-04 09:36:53 +09:00
Peter Steinberger
45675c1698 docs: update Anthropic subscription billing guidance 2026-04-04 09:32:13 +09:00
Peter Steinberger
b2fb1210e1 fix: normalize openai websocket errors 2026-04-04 01:31:49 +01:00
Peter Steinberger
a38cb20177 feat(openai): add default prompt overlay 2026-04-04 09:27:07 +09:00
Gustavo Madeira Santana
f6f7609b66 matrix: retry credentials after legacy migration race (#60591)
Merged via squash.

Prepared head SHA: e050b39de0
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 20:25:49 -04:00
Boris Cherny
af81c437fa fix(cache): delay history image pruning to preserve prompt cache prefix (#58038)
pruneProcessedHistoryImages was stripping image blocks from every
already-answered user turn on each run. Turn N sends image bytes → provider
caches the prefix. Turn N+1 replaces image with text marker → bytes diverge
at that message → cache miss from there onward.

Now only prune images older than 3 assistant turns. Recent history stays
byte-identical so the cached prefix survives, while legacy sessions with
persisted image payloads still get cleaned up.
2026-04-03 17:22:58 -07:00
Gustavo Madeira Santana
300fb36879 infra: atomically replace sync JSON writes (#60589)
Merged via squash.

Prepared head SHA: cb8ed77049
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 20:21:44 -04:00
Peter Steinberger
628c71103e fix: align native openai transport defaults 2026-04-04 01:20:34 +01:00
Boris Cherny
bc16b9dccf fix(cache): sort MCP tools deterministically to stabilize prompt cache (#58037)
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-03 17:19:53 -07:00
George Zhang
881f7dc82f Plugin SDK: add plugin config TUI prompts to onboard and configure wizards (#60590) (#60590)
Wire uiHints from plugin manifests into the TUI wizard so sandbox/tool
plugins get interactive config prompts during openclaw onboard (manual
flow) and openclaw configure --section plugins.

- Add setup.plugin-config.ts: discovers plugins with non-advanced uiHints,
  generates type-aware prompts (enum→select, boolean→confirm, array→csv,
  string/number→text) from jsonSchema + uiHints metadata.
- Onboard: new step after Skills, before Hooks (skipped in QuickStart).
  Only shows plugins with unconfigured fields.
- Configure: new 'plugins' section in the section menu. Shows all
  configurable plugins with configured/total field counts.

Closes #60030
2026-04-03 17:19:19 -07:00
Boris Cherny
f6380ae4b7 fix(cache): compact newest tool results first to preserve prompt cache prefix (#58036)
* fix(cache): compact newest tool results first to preserve prompt cache prefix

compactExistingToolResultsInPlace iterated front-to-back, replacing the
oldest tool results with placeholders when context exceeded 75%. This
rewrote messages[k] for small k, invalidating the provider prompt cache
from that point onward on every subsequent turn.

Reverse the loop to compact newest-first. The cached prefix stays intact;
the tradeoff is the model loses recent tool output instead of old, which
is acceptable since this guard only fires as an emergency measure past
the 75% threshold.

* fix(cache): compact newest tool results first to preserve prompt cache prefix (#58036) Thanks @bcherny

---------

Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-03 17:19:15 -07:00
Peter Steinberger
d01cb5ecc6 docs: expand model fallback guide 2026-04-04 09:13:43 +09:00
Peter Steinberger
5bea93fd63 fix: restore gateway watch boot path 2026-04-04 01:10:49 +01:00
Peter Steinberger
fe72474153 fix: persist fallback overrides safely 2026-04-04 09:00:16 +09:00
Peter Steinberger
411282c36d docs(changelog): note Telegram picker fix (#60384) (thanks @sfuminya) 2026-04-04 08:58:50 +09:00
fumin
43272d27f8 fix(telegram): compare full provider/model in models picker 2026-04-04 08:58:50 +09:00
Vincent Koc
bb4e54ccf7 fix(ci): restore plugin sdk boundary seams 2026-04-04 08:51:57 +09:00
Vincent Koc
df83374a54 docs(changelog): summarize provider transport rollout 2026-04-04 08:34:23 +09:00
Peter Steinberger
236a9003b6 test(ci): fix logs cli gateway mock typing 2026-04-04 00:28:25 +01:00
Peter Steinberger
3a3fdf1920 fix(ci): restore plugin contract surfaces 2026-04-04 00:24:57 +01:00
Ted Li
ff62705206 fix(whatsapp): reset watchdog timeout after reconnect (#60007)
Merged via squash.

Prepared head SHA: 64223e5dd4
Co-authored-by: MonkeyLeeT <6754057+MonkeyLeeT@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-04-03 20:23:36 -03:00
Peter Steinberger
306fe841f5 fix(cli): add local logs fallback 2026-04-04 08:17:11 +09:00
Peter Steinberger
de2eaccfce fix: restore discord startup logging and boundary bootstrap 2026-04-04 00:16:10 +01:00
Peter Steinberger
0f18e44538 test: trim onboarding helper partial mock 2026-04-04 00:13:45 +01:00
Peter Steinberger
d02fc365b4 test(plugins): drop stale core test files 2026-04-04 00:11:54 +01:00
Peter Steinberger
ab318de8b7 test(plugins): finish moving contract coverage 2026-04-04 00:11:39 +01:00
Peter Steinberger
e4b5027c5e refactor(plugins): move extension seams into extensions 2026-04-04 00:10:16 +01:00
Tak Hoffman
c19321ed9e docs: trim PR template root-cause boilerplate 2026-04-03 18:08:55 -05:00
Agustin Rivera
ff607adc69 fix(sandbox): block home credential binds (#59157)
* fix(sandbox): block home credential binds
* fix(sandbox): harden blocked credential bind checks
2026-04-03 16:06:22 -07:00
Peter Steinberger
4540effd6c test(ci): make mattermost client retry deterministic 2026-04-03 23:55:39 +01:00
Efe Büken
5fc9918a20 fix(matrix): surface user-visible 'too large' marker when Matrix media exceeds size limit (#60289)
Merged via squash.

Prepared head SHA: f33dd49946
Co-authored-by: efe-arv <259833796+efe-arv@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 18:53:51 -04:00
Peter Steinberger
94fee8486f test: make mattermost reconnect tests deterministic 2026-04-03 23:46:19 +01:00
pgondhi987
e19dce0aed fix(hooks): harden before_tool_call hook runner to fail-closed on error [AI] (#59822)
* fix: address issue

* fix: address PR review feedback

* docs: add changelog entry for PR merge

* docs: normalize changelog entry placement

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-03 16:44:35 -06:00
Peter Steinberger
1322aa2ba2 test(ci): make mattermost reconnect deterministic 2026-04-03 23:30:33 +01:00
Peter Steinberger
2af05ac558 test: restore leaked timer spies in mattermost reconnect tests 2026-04-03 23:27:29 +01:00
Peter Steinberger
3d2734185b test: stabilize rebased auto-reply command checks 2026-04-03 23:11:34 +01:00
Gustavo Madeira Santana
9004ef65df Plugins: add install --force overwrite flag (#60544)
Merged via squash.

Prepared head SHA: 28ae50b615
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 18:09:14 -04:00
Tak Hoffman
3fd29e549d fix: honor tools default account 2026-04-03 17:06:50 -05:00
Peter Steinberger
ccf16a25d2 test: tolerate plain baileys exports in whatsapp hooks 2026-04-03 23:06:43 +01:00
Tak Hoffman
cc0987f7b1 fix: honor allowlist default account 2026-04-03 17:04:11 -05:00
Agustin Rivera
42ffdf882f fix(fetch): normalize guarded redirect handling (#59121)
* fix(fetch): align guarded redirect rewrites

* fix(fetch): tighten redirect coverage

* fix(fetch): add changelog entry
2026-04-03 15:03:18 -07:00
Tak Hoffman
393d8c7606 fix: honor approve default account 2026-04-03 17:01:58 -05:00
Tak Hoffman
5f1f43af4d fix: honor config write default account 2026-04-03 16:59:58 -05:00
Tak Hoffman
4d60d61dec fix: honor reset notice default account 2026-04-03 16:57:15 -05:00
Tak Hoffman
da68fa4079 fix: honor followup default account 2026-04-03 16:53:38 -05:00
Tak Hoffman
6ddc86a3d1 fix: honor session default delivery account 2026-04-03 16:51:34 -05:00
Peter Steinberger
30fd4c6cdb test: add default channel account stubs in subagent focus tests 2026-04-03 22:51:17 +01:00
Tak Hoffman
01534d9bd5 fix: honor acp projector default account 2026-04-03 16:49:16 -05:00
Peter Steinberger
381a865822 test: force real timers in mattermost reconnect tests 2026-04-03 22:47:19 +01:00
Tak Hoffman
2b54ce30ae fix: honor acp delivery default account 2026-04-03 16:46:25 -05:00
Tak Hoffman
bb649de1ad fix: honor subagent default account 2026-04-03 16:43:26 -05:00
Tak Hoffman
4518b9ea7a fix: honor acp reset default account 2026-04-03 16:39:50 -05:00
Peter Steinberger
32f9a7c7bc test(ci): stabilize discord model picker timers 2026-04-03 22:35:42 +01:00
Tak Hoffman
10062e8111 fix: honor plugin binding default account 2026-04-03 16:31:24 -05:00
Peter Steinberger
4fb0837220 test: relax qr dashboard cli exit assertion 2026-04-03 22:30:46 +01:00
Agustin Rivera
e8e7d1fab3 Keep non-interactive auth choices on trusted plugins (#59120)
* fix(onboard): ignore untrusted workspace auth choices

* fix(onboard): scope auth-choice inference to trusted plugins (#59120) (thanks @eleqtrizit)
2026-04-03 14:28:01 -07:00
Tak Hoffman
037da3ce34 fix: honor acp dispatch default account 2026-04-03 16:26:41 -05:00
Peter Steinberger
ee45a59b4e test: normalize owning npm path assertions 2026-04-03 22:25:34 +01:00
Tak Hoffman
932379b19f fix: honor acp spawn default account 2026-04-03 16:23:29 -05:00
Peter Steinberger
be1d31fa8a test(ci): fix windows update and task cleanup cases 2026-04-03 22:22:51 +01:00
Peter Steinberger
9f132fc1b0 test: stabilize qr dashboard ci assertion 2026-04-03 22:17:01 +01:00
Nyx
dc21e3bb1e fix(plugins): reuse active registry during tool resolution (#52262)
Merged via squash.

Prepared head SHA: 55982a6be6
Co-authored-by: PerfectPan <24316656+PerfectPan@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 17:16:33 -04:00
Peter Steinberger
5b29483ab1 test(ci): type-safe exec timeout stub 2026-04-03 22:14:59 +01:00
Tak Hoffman
001e0c1f65 fix: honor default account in plugin commands 2026-04-03 16:13:31 -05:00
Peter Steinberger
5a94909654 test(ci): stabilize exec timeout tests 2026-04-03 22:12:08 +01:00
solodmd
8ae8a5c174 config: skip empty string in raw redaction to avoid corrupting snapshot (#28214)
Merged via squash.

Prepared head SHA: 07ec5b77b1
Co-authored-by: solodmd <51304754+solodmd@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 17:11:06 -04:00
Tak Hoffman
759598f737 fix: honor default account in conversation bindings 2026-04-03 16:09:48 -05:00
Peter Steinberger
267b6f595c test: harden windows ci coverage 2026-04-03 22:09:34 +01:00
Peter Steinberger
361efd28c9 test(ci): preserve runtime module shape in qr tests 2026-04-03 22:05:25 +01:00
Peter Steinberger
66dfe18c36 fix(ci): avoid duplicate accountId spread 2026-04-03 21:59:44 +01:00
Nimrod Gutman
9b667fc534 fix(agents): cover default workspace fallback (#59858) (thanks @joelnishanth) 2026-04-03 23:57:58 +03:00
joelnishanth
94e170763e fix: respect agents.defaults.workspace for non-default agents (#59789) 2026-04-03 23:57:58 +03:00
Peter Steinberger
8343a11a6b fix(ci): type qr dashboard runtime mocks 2026-04-03 21:57:39 +01:00
Tak Hoffman
1c5a4d01c9 fix: preserve channel status account ids 2026-04-03 15:56:54 -05:00
Peter Steinberger
eb6698002c fix(ci): repair qr test typing and mattermost setup status 2026-04-03 21:55:33 +01:00
Peter Steinberger
51eb877a15 test(ci): stabilize qr cli runtime mocks 2026-04-03 21:53:02 +01:00
Tak Hoffman
db4d0c0abc fix: honor mattermost default setup status 2026-04-03 15:51:49 -05:00
Tak Hoffman
8e023ffd06 fix: honor imessage default setup status 2026-04-03 15:49:56 -05:00
Tak Hoffman
2e9cad224d fix: honor irc default setup status 2026-04-03 15:47:49 -05:00
Tak Hoffman
cfef9bf856 fix: honor feishu default setup status 2026-04-03 15:44:49 -05:00
Peter Steinberger
0204b8dd28 fix: stabilize live and docker test lanes 2026-04-03 21:43:36 +01:00
Tak Hoffman
5d3edb1d40 fix: honor nextcloud default setup status 2026-04-03 15:41:44 -05:00
Bruce MacDonald
5ec53fff0c feat(ollama): add bundled web search provider (#59318)
Merged via squash.

Prepared head SHA: 1ec105f356
Co-authored-by: BruceMacD <5853428+BruceMacD@users.noreply.github.com>
Co-authored-by: BruceMacD <5853428+BruceMacD@users.noreply.github.com>
Reviewed-by: @BruceMacD
2026-04-03 13:41:24 -07:00
Tak Hoffman
dbb0164934 fix: honor signal default setup status 2026-04-03 15:39:50 -05:00
Peter Steinberger
40ae49effa fix(ci): normalize line default-account setup checks 2026-04-03 21:39:01 +01:00
Peter Steinberger
f336f0b83c test: trim more helper partial mocks 2026-04-03 21:38:01 +01:00
mappel-nv
21e53aea9e Gateway: refresh websocket auth after secrets reload (#60323)
* Gateway: refresh websocket auth after secrets reload

* Gateway: always restore auth reload test globals

* chore: add changelog for websocket auth reload

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-03 14:35:31 -06:00
Peter Steinberger
f3a6d13965 test: trim helper partial mocks 2026-04-03 21:34:42 +01:00
Tak Hoffman
183601c347 fix: honor line default setup status 2026-04-03 15:32:44 -05:00
Peter Steinberger
fa6e6603fa test(ci): harden cli and exec tests for shared workers 2026-04-03 21:30:47 +01:00
Tak Hoffman
5361b5cf04 fix: honor zalo default setup status 2026-04-03 15:27:59 -05:00
Tak Hoffman
8cb3316afb fix: honor bluebubbles default setup status 2026-04-03 15:24:29 -05:00
Peter Steinberger
63603876bf test: trim fs bridge partial mocks 2026-04-03 21:24:00 +01:00
Tak Hoffman
6317fce9cb fix: honor discord default setup status 2026-04-03 15:22:44 -05:00
Peter Steinberger
8158597f84 test(ci): make mattermost retry test deterministic 2026-04-03 21:22:16 +01:00
@zimeg
8d557c19d5 docs(slack): set always online to true in example app manifest 2026-04-03 13:21:15 -07:00
Tak Hoffman
f59c52bc16 fix: honor slack default setup status 2026-04-03 15:21:02 -05:00
Peter Steinberger
faff198777 test: trim control ui assets partial mocks 2026-04-03 21:19:14 +01:00
Peter Steinberger
fde573bab2 test(ci): reset mattermost websocket timers per test 2026-04-03 21:17:55 +01:00
Tak Hoffman
de6997a203 fix: honor googlechat default setup status 2026-04-03 15:15:01 -05:00
Peter Steinberger
ee63fdb056 test(ci): harden context engine runtime bridge test 2026-04-03 21:12:56 +01:00
Peter Steinberger
93e716e775 test: trim model startup retry partial mocks 2026-04-03 21:10:17 +01:00
Tak Hoffman
756597e6ad fix: honor discord default directory cache account 2026-04-03 15:09:18 -05:00
Peter Steinberger
328b7bee75 test(ci): fix slack pairing notify typing 2026-04-03 21:07:06 +01:00
Tak Hoffman
78022740fc fix: honor twitch default send account 2026-04-03 15:06:56 -05:00
Peter Steinberger
4c0f51df81 test: trim gateway and infra partial mocks 2026-04-03 21:03:54 +01:00
Peter Steinberger
b57922552e fix(ci): restore command auth sdk export 2026-04-03 21:02:59 +01:00
Vincent Koc
58ee283658 test(providers): fix anthropic payload test typing 2026-04-04 05:02:22 +09:00
Tak Hoffman
299ed8cb39 fix: honor slack default pairing account 2026-04-03 15:01:55 -05:00
@zimeg
2a13508379 docs(slack): expand app manifest example and scope checklist 2026-04-03 12:58:47 -07:00
Vincent Koc
067496b129 refactor(providers): share anthropic payload policy 2026-04-04 04:57:47 +09:00
Peter Steinberger
3e0ddaf5bc test(ci): stabilize mattermost websocket retry test 2026-04-03 20:56:09 +01:00
Tak Hoffman
d44af743db fix: honor whatsapp default setup finalize account 2026-04-03 14:55:27 -05:00
Tak Hoffman
f8a0f9ffd3 fix: honor twitch default setup account 2026-04-03 14:52:31 -05:00
Peter Steinberger
d007559c38 test: trim more agent e2e partial mocks 2026-04-03 20:50:57 +01:00
Tak Hoffman
84db697cd6 fix: honor twitch default outbound account 2026-04-03 14:49:55 -05:00
Peter Steinberger
2a5fbf0fd6 fix(ci): align discord dm auth account expectation 2026-04-03 20:48:05 +01:00
Peter Steinberger
7db148706a test: trim more runtime partial mocks 2026-04-03 20:46:57 +01:00
Tak Hoffman
f56a9f3b3b fix: honor twitch default runtime account 2026-04-03 14:46:23 -05:00
Peter Steinberger
1ff586cda1 fix(ci): repair discord preflight test types 2026-04-03 20:44:03 +01:00
Tak Hoffman
314512ae14 fix: honor whatsapp default heartbeat account 2026-04-03 14:42:38 -05:00
Peter Steinberger
2247089381 test: trim command install partial mocks 2026-04-03 20:42:29 +01:00
Peter Steinberger
92409aa4d6 test: trim agent e2e partial mocks 2026-04-03 20:42:29 +01:00
Peter Steinberger
52fb51db77 fix(ci): update whatsapp setup status mock signature 2026-04-03 20:40:54 +01:00
Peter Steinberger
3f86972e46 test: trim sandbox and gateway partial mocks 2026-04-03 20:40:27 +01:00
Tak Hoffman
5942726d25 fix: honor discord default dm preflight account 2026-04-03 14:37:56 -05:00
Peter Steinberger
ae976a90a5 test: trim more command partial mocks 2026-04-03 20:37:14 +01:00
Peter Steinberger
4481c41368 fix(ci): repair slack feishu and telegram regressions 2026-04-03 20:36:40 +01:00
Tak Hoffman
aa983566c4 fix: honor whatsapp default setup status account 2026-04-03 14:34:40 -05:00
Peter Steinberger
6f8f2a012b test: trim commands and cli partial mocks 2026-04-03 20:34:23 +01:00
Tak Hoffman
f7f467b042 fix: honor telegram default debounce account 2026-04-03 14:30:34 -05:00
Peter Steinberger
a715b83e67 test: trim auto-reply and cli partial mocks 2026-04-03 20:30:10 +01:00
Tak Hoffman
6c5064b437 fix: honor slack default hook account 2026-04-03 14:26:57 -05:00
Vincent Koc
30e43550bb test(cli): make plugin install recovery requests explicit 2026-04-04 04:26:51 +09:00
Vincent Koc
4265a59892 fix(config): hide legacy internal hook handlers 2026-04-04 04:26:51 +09:00
Peter Steinberger
d9af49a7af test: trim reply label generator mock 2026-04-03 20:25:49 +01:00
Tak Hoffman
58d6c16d12 fix: honor discord default mention account 2026-04-03 14:24:07 -05:00
Peter Steinberger
6068497409 test: trim auto-reply tools mock 2026-04-03 20:23:34 +01:00
Peter Steinberger
c8c0aeda76 test: trim more auto-reply partial mocks 2026-04-03 20:22:24 +01:00
Tak Hoffman
489a62e788 fix: honor whatsapp default outbound account 2026-04-03 14:22:08 -05:00
Peter Steinberger
63443acc2b fix(ci): repair telegram test harness config 2026-04-03 20:21:50 +01:00
Peter Steinberger
0805add3a4 test: trim more reply and agent mocks 2026-04-03 20:20:51 +01:00
Tak Hoffman
a18167a2cb fix: honor feishu tool account context 2026-04-03 14:19:15 -05:00
Peter Steinberger
f5ec0e429f test: trim more agent tool partial mocks 2026-04-03 20:18:56 +01:00
Peter Steinberger
1fbf863f53 test: trim more agent tool mocks 2026-04-03 20:16:50 +01:00
Peter Steinberger
e286ba2bab test: trim more agent partial mocks 2026-04-03 20:15:55 +01:00
Peter Steinberger
ee5113b1ae test: trim sandbox and transcript partial mocks 2026-04-03 20:14:39 +01:00
Peter Steinberger
6a465611d8 test: trim openclaw tools partial mocks 2026-04-03 20:14:39 +01:00
Tak Hoffman
6286ef55da fix: honor discord default guild action account 2026-04-03 14:13:00 -05:00
Vincent Koc
9224afca3d refactor(providers): share xai and replay helpers 2026-04-04 04:11:57 +09:00
Vincent Koc
cc1881a838 refactor(providers): share payload patch helpers 2026-04-04 04:11:56 +09:00
Vincent Koc
0273062dfd refactor(signal): lazy-load send runtime 2026-04-04 04:11:45 +09:00
Vincent Koc
b361667f98 test(contracts): split config write lanes 2026-04-04 04:11:00 +09:00
Vincent Koc
24afd52fcd test(contracts): remove old group policy runner 2026-04-04 04:10:15 +09:00
Vincent Koc
1d4fcb6a01 test(contracts): split group policy lanes 2026-04-04 04:10:15 +09:00
Vincent Koc
724dd5ca3d refactor(slack): lazy-load action and send runtimes 2026-04-04 04:08:41 +09:00
Tak Hoffman
c7554d3072 fix: honor discord default action runtime account 2026-04-03 14:08:32 -05:00
Vincent Koc
0bbacca828 test(contracts): split channel catalog lanes 2026-04-04 04:08:24 +09:00
lurebat
37de88181b fix(whatsapp): ignore self-chat quoted replies in groups (#60148)
Merged via squash.

Prepared head SHA: c51b55e0ba
Co-authored-by: lurebat <154669821+lurebat@users.noreply.github.com>
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Reviewed-by: @mcaxtr
2026-04-03 16:07:46 -03:00
Peter Steinberger
2d2fe2bf47 test: trim more agent partial mocks 2026-04-03 20:06:42 +01:00
Tak Hoffman
5f17362667 fix: honor slack default channel-type account 2026-04-03 14:05:35 -05:00
Peter Steinberger
4578351488 test: trim agent and discord harness partial mocks 2026-04-03 20:04:52 +01:00
Peter Steinberger
811efa2db0 fix(ci): honor bluebubbles account action gates 2026-04-03 20:03:27 +01:00
Peter Steinberger
35a9eeb857 test: trim sandbox and pi runner partial mocks 2026-04-03 20:02:57 +01:00
Peter Steinberger
4e27e22663 test: trim agents importActual mocks 2026-04-03 20:00:35 +01:00
Peter Steinberger
ffba320a2c fix(ci): align test mock typings 2026-04-03 19:59:55 +01:00
Vincent Koc
0464435777 fix(ci): align windows builtin mock types 2026-04-04 03:57:48 +09:00
Vincent Koc
fa5ea4529a fix(types): align rebased channel setup flows 2026-04-04 03:57:47 +09:00
Vincent Koc
e57b6be85f fix(types): align setup helper contracts 2026-04-04 03:57:47 +09:00
Vincent Koc
516e9054de fix(types): align portable runtime helpers 2026-04-04 03:57:47 +09:00
Vincent Koc
5e204df0bf fix(types): align rebased main helper contracts 2026-04-04 03:57:47 +09:00
Vincent Koc
88d3b73c6d fix(types): annotate portable exported helper types 2026-04-04 03:57:47 +09:00
Peter Steinberger
4b71a94450 fix(ci): repair contract and interaction drift 2026-04-03 19:57:35 +01:00
Peter Steinberger
c9dfc35dfd test: fix discord runtime mock typing and lock UX 2026-04-03 19:56:37 +01:00
Peter Steinberger
9215ff0615 test: route pure infra tests through boundary lane 2026-04-03 19:56:12 +01:00
Tak Hoffman
7c25af83e4 fix: honor line monitor default account 2026-04-03 13:55:44 -05:00
Peter Steinberger
03aea06321 test: trim gateway importActual mocks 2026-04-03 19:54:37 +01:00
Peter Steinberger
a301e2ef87 test: trim cli and infra importActual mocks 2026-04-03 19:54:37 +01:00
Tak Hoffman
961d8eb095 fix: honor line default runtime account 2026-04-03 13:54:01 -05:00
Peter Steinberger
5e9ae0bfd4 test(node): fix execFile mock typing 2026-04-03 19:53:38 +01:00
Peter Steinberger
4948760c65 test(plugins): genericize core helper contracts 2026-04-03 19:53:38 +01:00
Peter Steinberger
1c66a050c2 refactor(plugins): move outbound dep aliases into extensions 2026-04-03 19:53:38 +01:00
Tak Hoffman
f007082e06 fix: honor signal setup default account 2026-04-03 13:52:28 -05:00
Vincent Koc
eecb36eff4 fix(ci): stabilize zero-delay retry and slack interaction tests 2026-04-04 03:52:07 +09:00
Gustavo Madeira Santana
1420b3bad7 docs: tighten skills and Matrix wording 2026-04-03 14:51:37 -04:00
Vincent Koc
1c470c2736 test(contracts): split tts lanes 2026-04-04 03:51:10 +09:00
Tak Hoffman
d305a80acd fix: honor imessage setup default account 2026-04-03 13:50:52 -05:00
Peter Steinberger
bc23db501b test: trim more core importOriginal usage 2026-04-03 19:49:43 +01:00
Peter Steinberger
6115a9498c test: trim config importOriginal usage 2026-04-03 19:49:43 +01:00
Peter Steinberger
8e8f8d0745 test: trim more extension importOriginal usage 2026-04-03 19:49:43 +01:00
Vincent Koc
d8458a1481 refactor(providers): share transport stream helpers 2026-04-04 03:49:09 +09:00
Vincent Koc
fcec417d7d fix(ci): preserve conversation runtime mock signatures 2026-04-04 03:48:58 +09:00
Tak Hoffman
d2ca915a7f fix: honor telegram default action account 2026-04-03 13:48:45 -05:00
Peter Steinberger
88ab29f492 fix(ci): relax discord runtime mock module constraint 2026-04-03 19:46:59 +01:00
Tak Hoffman
534b0c663e fix: honor zalouser default runtime account 2026-04-03 13:46:36 -05:00
Tak Hoffman
f66c9b829e fix: honor slack default runtime account 2026-04-03 13:45:28 -05:00
Tak Hoffman
4f5f1fa724 fix: honor imessage default runtime account 2026-04-03 13:44:15 -05:00
Peter Steinberger
b8af2c65e5 fix(ci): bind full discord conversation runtime mock type 2026-04-03 19:44:05 +01:00
Tak Hoffman
4ca1ae8046 fix: honor signal default runtime account 2026-04-03 13:43:09 -05:00
Tak Hoffman
c7875f193b fix: honor discord default runtime account 2026-04-03 13:41:55 -05:00
Peter Steinberger
e3f410efb5 fix(ci): widen discord binding runtime mock type 2026-04-03 19:40:47 +01:00
Peter Steinberger
3fb6e3e91f test: trim more extension importOriginal usage 2026-04-03 19:40:20 +01:00
Tak Hoffman
17c0026c04 fix: honor bluebubbles default runtime account 2026-04-03 13:39:22 -05:00
Tak Hoffman
9289f967df fix: honor mattermost default runtime account 2026-04-03 13:38:03 -05:00
Peter Steinberger
e76a16dfa5 fix(ci): preserve omitted feishu finalize account context 2026-04-03 19:38:00 +01:00
Vincent Koc
e697fa5e75 feat(providers): add google transport runtime 2026-04-04 03:35:58 +09:00
Peter Steinberger
2156bf0210 test: fix setup wizard and execFile test drift 2026-04-03 19:35:38 +01:00
Peter Steinberger
0ad2da060e test: route openclaw root through boundary config 2026-04-03 19:35:27 +01:00
Peter Steinberger
cc62fd38f6 test: trim more extension mock imports 2026-04-03 19:34:55 +01:00
Tak Hoffman
a8302e8eab fix: honor mattermost default reply account 2026-04-03 13:34:53 -05:00
Peter Steinberger
323ad51eb8 fix(ci): align execFile mock typings 2026-04-03 19:31:41 +01:00
Peter Steinberger
8be2dea382 test: trim more extension partial mocks 2026-04-03 19:31:32 +01:00
Peter Steinberger
b27fd7cc49 fix(setup): narrow default account id typing 2026-04-03 19:30:35 +01:00
Peter Steinberger
0c95e3f073 refactor(plugins): move command ui policy into extensions 2026-04-03 19:30:35 +01:00
Peter Steinberger
e5d2181403 fix(ci): repair discord interactive test seams 2026-04-03 19:29:14 +01:00
Peter Steinberger
45a6f769bb test: trim core partial mocks 2026-04-03 19:28:19 +01:00
Peter Steinberger
6eca4e0136 test: trim extension partial mocks 2026-04-03 19:28:19 +01:00
Tak Hoffman
24a4ed1013 fix: honor matrix default runtime account 2026-04-03 13:26:34 -05:00
Peter Steinberger
eea069bdc3 fix(ci): repair bundled and extension test drift 2026-04-03 19:25:23 +01:00
Tak Hoffman
e063f67ac0 fix: honor nextcloud default runtime account 2026-04-03 13:24:58 -05:00
Vincent Koc
26b7260bf4 refactor(signal): narrow channel runtime imports 2026-04-04 03:24:21 +09:00
Vincent Koc
e9cbdc7439 fix(plugins): narrow top-level allowFrom account resolver 2026-04-04 03:24:21 +09:00
Tak Hoffman
d5c6e7af0f fix: honor whatsapp default heartbeat account 2026-04-03 13:23:29 -05:00
Gustavo Madeira Santana
1f660bf930 Docs: document agent skill allowlists 2026-04-03 14:23:05 -04:00
Tak Hoffman
5c3dc40794 fix: honor googlechat default runtime account 2026-04-03 13:22:11 -05:00
Peter Steinberger
28b8e019f7 test(setup): fix latest type regressions 2026-04-03 19:21:46 +01:00
Peter Steinberger
df18f4c517 refactor(matrix): move legacy migrations behind doctor 2026-04-03 19:21:24 +01:00
Tak Hoffman
5eb3341db1 fix: honor zalo default runtime account 2026-04-03 13:19:50 -05:00
Gustavo Madeira Santana
5e365a8ec4 agents: preserve remote skill sync eligibility 2026-04-03 14:19:43 -04:00
Tak Hoffman
045010a2a5 fix: honor zalouser default runtime account 2026-04-03 13:18:11 -05:00
Peter Steinberger
72b8025107 fix: align feishu and matrix type guards 2026-04-03 19:17:14 +01:00
Peter Steinberger
4c5c361db7 test: stub gateway speech providers 2026-04-03 19:16:56 +01:00
Vincent Koc
956e746da1 fix(plugins): narrow nested allowFrom account resolver 2026-04-04 03:16:14 +09:00
Vincent Koc
7d691a3ce3 refactor(whatsapp): narrow channel runtime imports 2026-04-04 03:16:14 +09:00
Peter Steinberger
5c6dca78d9 fix(discord): avoid bundled sibling requires 2026-04-03 19:15:21 +01:00
Peter Steinberger
53f8c2047a fix(ci): restore channel approval and lifecycle harnesses 2026-04-03 19:14:42 +01:00
Tak Hoffman
d20e3d5691 fix: honor feishu setup adapter default 2026-04-03 13:14:10 -05:00
Tak Hoffman
a89cb679a2 fix: honor nostr setup default account 2026-04-03 13:12:49 -05:00
Peter Steinberger
13bc70397a test: trim test partial mocks 2026-04-03 19:10:56 +01:00
Tak Hoffman
5c4551458f fix: honor qqbot setup default account 2026-04-03 13:10:49 -05:00
Peter Steinberger
181bd6327f test(plugins): fix rebase fallout 2026-04-03 19:10:00 +01:00
Peter Steinberger
42ffe86fc7 test(cron): stabilize regression harness 2026-04-03 19:09:21 +01:00
Peter Steinberger
03a43fe231 refactor(plugins): genericize core channel seams 2026-04-03 19:09:21 +01:00
Peter Steinberger
856592cf00 fix(outbound): restore generic delivery and security seams 2026-04-03 19:09:20 +01:00
Peter Steinberger
ab96520bba refactor(plugins): move channel behavior into plugins 2026-04-03 19:09:20 +01:00
Josh Lehman
c52df32878 refactor: move bundled replay policy ownership into plugins (#60452)
* refactor: move bundled replay policy ownership into plugins

* test: preserve replay fallback until providers adopt hooks

* test: cover response replay branches for ollama and zai

---------

Co-authored-by: Shakker <shakkerdroid@gmail.com>
2026-04-03 19:08:10 +01:00
Tak Hoffman
7fb58afb41 fix: honor googlechat default allowFrom account 2026-04-03 13:07:07 -05:00
Tak Hoffman
7be2d361de fix: honor feishu finalize default account 2026-04-03 13:04:12 -05:00
Vincent Koc
abc3f27ba9 refactor(zalo): narrow action runtime imports 2026-04-04 03:03:15 +09:00
Vincent Koc
0ba93afda9 fix(feishu): guard scoped setup config access 2026-04-04 03:03:15 +09:00
Tak Hoffman
b7b53b29e8 fix: honor discord setup default account 2026-04-03 13:01:28 -05:00
Peter Steinberger
d9e59f7329 fix(ci): align loader and channel test expectations 2026-04-03 19:00:23 +01:00
Vincent Koc
54479220f5 refactor(xai): share model hint helper 2026-04-04 02:58:58 +09:00
Vincent Koc
ea4265a820 feat(providers): add anthropic transport runtime 2026-04-04 02:58:58 +09:00
Tak Hoffman
8fc684cb55 fix: honor feishu default account setup policy 2026-04-03 12:58:50 -05:00
Peter Steinberger
5d20c73e05 fix: route Copilot Claude through Anthropic 2026-04-04 02:57:59 +09:00
Gustavo Madeira Santana
e588a363f9 fix: respect approval request filters in ambiguity checks 2026-04-03 13:57:18 -04:00
Tak Hoffman
4bbd67c21e fix: honor imessage default account setup policy 2026-04-03 12:56:42 -05:00
Peter Steinberger
de49b26bb1 test: trim acp spawn parent stream resets 2026-04-03 18:56:17 +01:00
Peter Steinberger
91a3554cd7 test: trim session status module resets 2026-04-03 18:55:23 +01:00
Peter Steinberger
3fd27211b1 fix(ci): stabilize channel approval and monitor tests 2026-04-03 18:54:48 +01:00
Peter Steinberger
7eed2e2911 fix: align cron and bluebubbles test drift 2026-04-03 18:53:58 +01:00
Peter Steinberger
7439479047 test: tighten runtime setup and boundary guards 2026-04-03 18:53:34 +01:00
Peter Steinberger
3edfc494df test: expand builtin mock helper usage 2026-04-03 18:53:34 +01:00
Peter Steinberger
613393621c test: reduce discord monitor partial mocks 2026-04-03 18:53:03 +01:00
Tak Hoffman
5888e44745 fix: honor signal default account setup policy 2026-04-03 12:52:14 -05:00
Peter Steinberger
6739c28718 refactor: clarify auth failover policy 2026-04-04 02:49:18 +09:00
Tak Hoffman
1d1a8264ec fix: honor zalo default account setup policy 2026-04-03 12:49:09 -05:00
Vincent Koc
50e1eb56d7 fix(security): harden discord proxy and bundled channel activation (#60455)
* fix(security): tighten discord proxy and mobile tls guards

* fix(plugins): enforce allowlists for bundled channels

* fix(types): align callers with removed legacy config aliases

* fix(security): preserve bundled channel opt-in and ipv6 proxies
2026-04-04 02:48:52 +09:00
Peter Steinberger
3ddf745f97 fix(ci): restore account setup typings 2026-04-03 18:48:44 +01:00
Gustavo Madeira Santana
dc306013e1 Approvals: scope foreign-channel account routing (#60417)
Merged via squash.

Prepared head SHA: 3ad6cae91f
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 13:48:00 -04:00
Tak Hoffman
0769590f03 fix: honor zalouser default account setup policy 2026-04-03 12:47:22 -05:00
Tak Hoffman
f115aba582 fix: honor bluebubbles default account setup policy 2026-04-03 12:45:42 -05:00
Tak Hoffman
638e831bca fix: honor telegram default account setup policy 2026-04-03 12:43:51 -05:00
Peter Steinberger
379c329f81 test: trim dispatch and command partial mocks 2026-04-03 18:42:52 +01:00
Tak Hoffman
4edf6a2c7d fix: honor line default account setup policy 2026-04-03 12:42:18 -05:00
Tak Hoffman
4afa720a0c fix: honor nextcloud default account setup policy 2026-04-03 12:42:18 -05:00
Tak Hoffman
d0a43cf8c0 fix: honor googlechat default account setup policy 2026-04-03 12:42:18 -05:00
Tak Hoffman
acfa09679c fix: honor qqbot default account config 2026-04-03 12:42:18 -05:00
Tak Hoffman
b929a4c27d fix: honor imessage setup account status 2026-04-03 12:42:18 -05:00
Tak Hoffman
9f049cb1d8 fix: honor nostr default account routing 2026-04-03 12:42:18 -05:00
Tak Hoffman
f77054eaee fix: honor feishu setup account id 2026-04-03 12:42:18 -05:00
Tak Hoffman
35256d6f1d fix: honor nostr setup account label 2026-04-03 12:42:18 -05:00
Tak Hoffman
17060ca124 fix: honor feishu setup account writes 2026-04-03 12:42:17 -05:00
Tak Hoffman
53612fa128 fix: label whatsapp setup account status 2026-04-03 12:42:17 -05:00
Tak Hoffman
fc61e8d280 fix: honor feishu setup account status 2026-04-03 12:42:17 -05:00
Tak Hoffman
e37c0da23a fix: honor synology setup account status 2026-04-03 12:42:17 -05:00
Tak Hoffman
42ebc8c170 fix: honor irc setup account status 2026-04-03 12:42:17 -05:00
Tak Hoffman
e6a9408c3b fix: honor qqbot setup account status 2026-04-03 12:42:17 -05:00
Gustavo Madeira Santana
ddd250d130 feat(skills): add inherited agent skill allowlists (#59992)
Merged via squash.

Prepared head SHA: 6f60779a57
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-03 13:41:28 -04:00
Peter Steinberger
04f59a7227 fix: remove duplicate bluebubbles actions field 2026-04-03 18:40:28 +01:00
Peter Steinberger
636a23b73e test: extract node builtin mock helpers 2026-04-03 18:40:28 +01:00
Vincent Koc
47b8be7116 refactor(discord): lazy-load message actions 2026-04-04 02:38:43 +09:00
Vincent Koc
7e3b48c254 test(ci): align memory fallback reindex assertion 2026-04-04 02:37:38 +09:00
Peter Steinberger
1bee69f79b refactor: route direct extension test targets 2026-04-04 02:36:48 +09:00
Peter Steinberger
d0d5b34b44 fix(ci): repair extension test regressions 2026-04-03 18:36:35 +01:00
Peter Steinberger
2c5f244554 chore: update changelog for #60404 (thanks @extrasmall0) 2026-04-04 02:35:27 +09:00
Peter Steinberger
865fa2ba72 fix: narrow auth permanent lockouts 2026-04-04 02:35:27 +09:00
Extra Small
42e1d489fd fix(auth): use shorter backoff for auth_permanent failures
auth_permanent errors (e.g. API_KEY_INVALID) can be caused by transient
provider outages rather than genuinely revoked credentials. Previously
these used the same 5h-24h billing backoff, which left providers disabled
long after the upstream issue resolved.

Introduce separate authPermanentBackoffMinutes (default: 10) and
authPermanentMaxMinutes (default: 60) config options so auth_permanent
failures recover in minutes rather than hours.

Fixes #56838
2026-04-04 02:35:27 +09:00
Vincent Koc
022a24ec48 refactor(signal): split uuid helper from monitor 2026-04-04 02:34:14 +09:00
Vincent Koc
c4eaa30ee7 fix(bluebubbles): remove duplicate action config field 2026-04-04 02:34:14 +09:00
Vincent Koc
e88f4fe5f4 fix(ci): narrow matrix action test discovery 2026-04-04 02:33:59 +09:00
Peter Steinberger
a7dd6036a0 style: format doctor and gateway harness mocks 2026-04-03 18:33:47 +01:00
Peter Steinberger
68edc53090 test: trim doctor and gateway partial mocks 2026-04-03 18:33:47 +01:00
Peter Steinberger
f36ed7105f test: reduce extension runtime partial mocks 2026-04-03 18:33:47 +01:00
Peter Steinberger
14c863dc4a test: reduce telegram media harness imports 2026-04-03 18:33:47 +01:00
Peter Steinberger
fb9be1fcb6 test: trim models and cron partial mocks 2026-04-03 18:33:47 +01:00
Peter Steinberger
1c16c6a94a test: split inbound contract helpers 2026-04-03 18:33:46 +01:00
Peter Steinberger
d6e89f96d6 refactor: share gateway config auth helpers 2026-04-04 02:29:29 +09:00
Vincent Koc
646e271c72 test(contracts): split provider wizard lanes 2026-04-04 02:29:00 +09:00
Vincent Koc
71de4adcce test(contracts): split bundled web search lanes 2026-04-04 02:28:15 +09:00
Vincent Koc
f93f76dcc4 fix(ci): dedupe bluebubbles actions config type 2026-04-04 02:26:42 +09:00
Vincent Koc
22fd61e483 test(contracts): split plugin registration lanes 2026-04-04 02:26:39 +09:00
Peter Steinberger
ebdade0efc ci: shard extension fast checks 2026-04-03 18:26:26 +01:00
Vincent Koc
230c61885d refactor(slack): lazy-load webhook handler 2026-04-04 02:26:19 +09:00
Vincent Koc
7ad72281f7 refactor(providers): share pi openai reasoning compat gate 2026-04-04 02:25:10 +09:00
Vincent Koc
bee60a479b test(contracts): fix tts provider fixtures 2026-04-04 02:24:28 +09:00
Peter Steinberger
5fbef0f914 fix(ci): resolve tracked merge markers 2026-04-03 18:22:03 +01:00
Peter Steinberger
be9db66533 fix: split discord voice timeouts and restore gate on main (#60345) (thanks @geekhuashan) 2026-04-04 02:21:43 +09:00
geekhuashan
0c575f37fd fix(discord): add DiscordVoiceReadyListener fire-and-forget error-path test
Add test covering the DiscordVoiceReadyListener.handle() path where
autoJoin() rejects, confirming the error is caught and does not propagate.
2026-04-04 02:21:43 +09:00
geekhuashan
db593440c4 fix(discord voice): fire-and-forget autoJoin and increase playback timeout to 60s 2026-04-04 02:21:43 +09:00
Vincent Koc
136f177cb3 test(contracts): split provider contract lanes 2026-04-04 02:20:35 +09:00
Peter Steinberger
7fd9e40960 fix: tighten gateway shared-auth disconnects (#60387) (thanks @mappel-nv) 2026-04-04 02:20:22 +09:00
Michael Appel
c742963fd9 Gateway: avoid secret-ref auth disconnect churn 2026-04-04 02:20:22 +09:00
Michael Appel
97558f2325 Gateway: expand shared-auth rotation coverage 2026-04-04 02:20:22 +09:00
Michael Appel
54b269b2cb Gateway: disconnect shared-auth sessions on auth change 2026-04-04 02:20:22 +09:00
Peter Steinberger
e0580e6863 test: harden shared-worker runtime setup 2026-04-03 18:18:56 +01:00
Peter Steinberger
2981cce130 fix: align config and plugin test types 2026-04-03 18:18:56 +01:00
Vincent Koc
ff68fd3060 refactor(providers): share completions format defaults 2026-04-04 02:18:12 +09:00
Vincent Koc
39a16c600f test(contracts): localize provider contract suites 2026-04-04 02:17:15 +09:00
Vincent Koc
f881eb066c test(contracts): remove dead session binding helper 2026-04-04 02:16:04 +09:00
Vincent Koc
514b37e185 fix(providers): keep native modelstudio streaming usage compat 2026-04-04 02:15:46 +09:00
Vincent Koc
feed4007fe test(contracts): localize surface registry helpers 2026-04-04 02:15:01 +09:00
Vincent Koc
dd42154e45 fix(providers): stop forcing reasoning effort on proxy completions 2026-04-04 02:14:10 +09:00
Vincent Koc
dd35b97398 test(contracts): narrow session binding registry seeding 2026-04-04 02:13:31 +09:00
Vincent Koc
7836c9a6c2 fix(providers): stop forcing store on proxy completions 2026-04-04 02:12:59 +09:00
Peter Steinberger
ae359c0c8b fix: remove changelog conflict marker 2026-04-04 02:12:10 +09:00
Peter Steinberger
5e69d7e75b fix: land discord everyone mention gating 2026-04-04 02:12:10 +09:00
geekhuashan
6ba490ec7b fix(discord): guard @everyone shortcut against bot-authored messages
Preserve the !author.bot || sender.isPluralKit guard when short-circuiting
wasMentioned on mentionedEveryone, so bot relay messages don't spuriously
trigger mention-gate logic. Add test coverage for the wasMentioned path.
2026-04-04 02:12:10 +09:00
geekhuashan
0b69119f1b fix(discord): detect @everyone mentions in message preflight 2026-04-04 02:12:10 +09:00
Vincent Koc
f575bc2bfe test(ci): harden proxy-sensitive and timeout unit tests 2026-04-04 02:12:00 +09:00
Vincent Koc
b871707628 test(contracts): localize remaining suite helpers 2026-04-04 02:11:44 +09:00
Vincent Koc
50d85dcd59 refactor(providers): share openai compat defaults 2026-04-04 02:10:24 +09:00
Vincent Koc
c5a45eb274 test(contracts): localize registry-backed contract helpers 2026-04-04 02:09:25 +09:00
Peter Steinberger
3c07b126ed fix(ci): restore discord action loader 2026-04-03 18:07:31 +01:00
Vincent Koc
f1911274aa test(contracts): localize surface and session binding helpers 2026-04-04 02:06:38 +09:00
chziyue
d5c42a07ef fix(ui): Stop button shows Send during tool execution (#54528)
Merged via squash.

Prepared head SHA: f2d65a5c3d
Co-authored-by: chziyue <62380760+chziyue@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
2026-04-03 18:59:47 +02:00
Peter Steinberger
54cd0859d3 test(ci): align discord ack removal expectation 2026-04-03 17:58:33 +01:00
Peter Steinberger
911e3974f7 refactor: clarify Discord classic fallback 2026-04-04 01:58:06 +09:00
Tak Hoffman
9e6de5ece4 fix: honor tlon setup account status 2026-04-03 11:57:50 -05:00
Peter Steinberger
f076e97a3c fix(ci): restore discord ack cleanup 2026-04-03 17:54:44 +01:00
Tak Hoffman
b2a9e0d7a7 fix: honor setup binary path account overrides 2026-04-03 11:54:21 -05:00
Shakker
2c0967150d test: preserve whatsapp account helpers in setup mock 2026-04-03 17:51:01 +01:00
Vincent Koc
eec6f59a77 fix(providers): disable z.ai strict tool shaping 2026-04-04 01:50:55 +09:00
Vincent Koc
745f1c9812 fix(types): align callers with removed legacy config aliases 2026-04-04 01:50:44 +09:00
Vincent Koc
ddaef48421 fix(ci): restore discord reaction account context 2026-04-04 01:50:34 +09:00
Tak Hoffman
51f6bc4940 fix: honor selected account in setup status 2026-04-03 11:50:09 -05:00
Peter Steinberger
5edefc4d5b fix: remove changelog conflict marker (#60066) (thanks @huntharo) 2026-04-04 01:49:35 +09:00
Peter Steinberger
ec01cd0ceb fix: tidy plugin update override docs (#60066) (thanks @huntharo) 2026-04-04 01:49:35 +09:00
huntharo
c4f40c3f7d Plugins: allow unsafe-force override on update 2026-04-04 01:49:35 +09:00
Vincent Koc
824ff335c6 fix(providers): align custom transport compat defaults 2026-04-04 01:48:00 +09:00
Peter Steinberger
958cebcc87 fix: preserve Discord component-only media behavior (#60361) (thanks @geekhuashan) 2026-04-04 01:46:32 +09:00
geekhuashan
0e07c8973e fix(discord): forward mediaReadFile and mediaAccess in component classic message path
Forward mediaReadFile and mediaAccess through the sendMessageDiscord shortcut
in sendDiscordComponentMessage, so local-file media works correctly when
falling back to classic Discord messages. Add test coverage.
2026-04-04 01:46:32 +09:00
geekhuashan
efbb9a1296 fix(discord): downgrade text-only component+media to classic message and auto-append file block 2026-04-04 01:46:32 +09:00
Vincent Koc
36ed66cce2 fix(providers): honor moonshot transport compat (#60411) 2026-04-04 01:45:45 +09:00
Vincent Koc
54e8790ad7 fix(providers): honor moonshot transport compat 2026-04-04 01:45:19 +09:00
Shakker
33d54e93b8 test: keep legacy tts contract fixtures typed 2026-04-03 17:43:55 +01:00
Shakker
21002a60be test: align legacy streaming config coverage 2026-04-03 17:43:55 +01:00
Shakker
21a87eaf2b fix: use safeExtend for bluebubbles config 2026-04-03 17:43:17 +01:00
Shakker
06b2e8b79a test: satisfy xai transport model typing 2026-04-03 17:43:17 +01:00
Shakker
91572df932 fix: restore bluebubbles action config typing 2026-04-03 17:43:17 +01:00
Peter Steinberger
a5e725a3b8 test: align vitest defaults with migrated config 2026-04-03 17:42:48 +01:00
Tak Hoffman
b63a175d3d fix: honor imessage probe account config 2026-04-03 11:40:50 -05:00
Marcus Castro
b473e056a0 fix(whatsapp): restore source login tool import 2026-04-03 13:40:20 -03:00
Shakker
408dac8d21 docs(changelog): note secrets runtime isolation cleanup 2026-04-03 17:38:05 +01:00
Shakker
bc46c1dacc test: restore secrets suite isolation cleanup 2026-04-03 17:38:05 +01:00
Tak Hoffman
7f4dd21227 fix: honor zalo action discovery account config 2026-04-03 11:37:22 -05:00
Tak Hoffman
eb497a89cd fix: honor zalouser action discovery account config 2026-04-03 11:35:55 -05:00
Peter Steinberger
b118efea80 test: split reply flow coverage by owner surface 2026-04-03 17:35:01 +01:00
Peter Steinberger
d74d47443e test: trim extension setup startup 2026-04-03 17:33:45 +01:00
Peter Steinberger
2e041c8b66 test: split slack monitor coverage 2026-04-03 17:33:45 +01:00
Peter Steinberger
1512fab782 test: remove discord channel partial mocks 2026-04-03 17:33:45 +01:00
Peter Steinberger
e263b5d7b6 test: split telegram channel coverage 2026-04-03 17:33:45 +01:00
Peter Steinberger
6686ef0b3a test: split whatsapp channel coverage 2026-04-03 17:33:45 +01:00
Tak Hoffman
d21ae7173f fix: honor slack action discovery account config 2026-04-03 11:33:23 -05:00
Vincent Koc
2a19771c3c test(contracts): split plugins-core lanes 2026-04-04 01:30:48 +09:00
Peter Steinberger
a1cad12139 fix(ci): restore compat config types 2026-04-03 17:29:43 +01:00
Tak Hoffman
c22f2a0cab fix: honor feishu action discovery account config 2026-04-03 11:28:51 -05:00
Peter Steinberger
570ed4285e refactor: extract Discord ack reaction helpers 2026-04-04 01:28:04 +09:00
Tak Hoffman
226e2389f6 fix: honor mattermost action discovery account config 2026-04-03 11:27:07 -05:00
Vincent Koc
09f66b0073 fix(build): align bluebubbles action gating 2026-04-04 01:25:02 +09:00
Vincent Koc
c7a947dc0a fix(config): remove legacy config aliases from public schema 2026-04-04 01:24:14 +09:00
Tak Hoffman
d59d236a90 fix: honor matrix action discovery account config 2026-04-03 11:23:13 -05:00
Vincent Koc
6ac5806a39 fix(providers): honor mistral transport compat (#60405) 2026-04-04 01:21:41 +09:00
Tak Hoffman
fb8048a188 fix: honor telegram action discovery account config 2026-04-03 11:20:49 -05:00
Peter Steinberger
2b900b576c refactor: modernize vitest projects config 2026-04-03 17:20:30 +01:00
Vincent Koc
9dba944c42 fix(build): restore current main type gates 2026-04-04 01:20:25 +09:00
Peter Steinberger
cf4d3c4daf refactor: share Discord ack reaction runtime context 2026-04-04 01:19:57 +09:00
Vincent Koc
4c969391fe test(contracts): split plugins-core extension lanes 2026-04-04 01:18:54 +09:00
Tak Hoffman
832810a5bb fix: honor discord action discovery account config 2026-04-03 11:18:26 -05:00
Vincent Koc
8ffd1c0973 test(contracts): split outbound payload contract lanes 2026-04-04 01:17:58 +09:00
Vincent Koc
1f509e0e04 test(contracts): split setup and status contract lanes 2026-04-04 01:16:38 +09:00
Vincent Koc
56f8de0cf9 test(contracts): split plugin and action contract lanes 2026-04-04 01:15:39 +09:00
Vincent Koc
6389498a7c test(contracts): split surface contract lanes 2026-04-04 01:13:53 +09:00
Tak Hoffman
632a10cddc fix: honor googlechat action discovery account config 2026-04-03 11:13:33 -05:00
Vincent Koc
a2836e6db6 fix(ci): narrow openai responses input literals 2026-04-04 01:13:09 +09:00
Tak Hoffman
da5c6ac2b6 fix: honor bluebubbles action discovery account config 2026-04-03 11:11:58 -05:00
Vincent Koc
0b4cdfc53e docs: fix changelog attribution gaps and remove duplicates 2026-04-04 01:11:03 +09:00
Vincent Koc
7ed789d67d fix(providers): centralize compat endpoint detection (#60399) 2026-04-04 01:10:50 +09:00
Tak Hoffman
fb4127082a fix: honor signal action discovery account config 2026-04-03 11:09:39 -05:00
Vincent Koc
9fbf501d5a fix(ci): align whatsapp and responses typing 2026-04-04 01:09:28 +09:00
Peter Steinberger
2766a3409c fix: resolve rebase type drift (#60249) (thanks @shakkernerd) 2026-04-04 01:07:28 +09:00
Peter Steinberger
664265fc66 test(heartbeat): pass reply spy into seeded override case 2026-04-04 01:07:28 +09:00
Peter Steinberger
2a1a7ea6f9 fix(browser): route test support through sdk testing 2026-04-04 01:07:28 +09:00
Peter Steinberger
7e2b26f77b fix(plugins): restore activation state wrapper 2026-04-04 01:07:28 +09:00
Peter Steinberger
0b74755894 test(utils): drop moved provider setup 2026-04-04 01:07:28 +09:00
Peter Steinberger
8541a5c3fa docs(changelog): note test-runtime perf work 2026-04-04 01:07:28 +09:00
Peter Steinberger
88dac32623 fix(reply-threading): align fallback test coverage 2026-04-04 01:07:28 +09:00
Peter Steinberger
0324055d09 test: align latest main runtime harnesses 2026-04-04 01:07:28 +09:00
Peter Steinberger
5bafa6edcf fix(auto-reply): align fallback model runtime state 2026-04-04 01:07:28 +09:00
Peter Steinberger
c563cdc901 fix(telegram): allow target approvals fallback 2026-04-04 01:07:28 +09:00
Peter Steinberger
6f5e71fdbc test: fix talk voice runtime type import 2026-04-04 01:07:28 +09:00
Shakker
2fa3a09137 test: harden command queue timer cleanup 2026-04-04 01:07:28 +09:00
Shakker
1877a2ea26 fix: stabilize rebased test surfaces 2026-04-04 01:07:28 +09:00
Shakker
d75fa152b9 test: trim secrets runtime snapshot setup 2026-04-04 01:07:28 +09:00
Shakker
38f76a1f8f perf: trim media secret setup and isolate heavy tests 2026-04-04 01:07:28 +09:00
Shakker
20250653ce fix: resolve rebased planner and loader regressions 2026-04-04 01:07:28 +09:00
Shakker
192f880a0b refactor: trim cron session cleanup imports 2026-04-04 01:07:28 +09:00
Shakker
998810a6a3 test: localize imessage alias channel coverage 2026-04-04 01:07:28 +09:00
Shakker
ce784b62f5 refactor: trim outbound direct-send runtimes 2026-04-04 01:07:28 +09:00
Shakker
5dd6189a2a refactor: split plugin interactive registration 2026-04-04 01:07:28 +09:00
Shakker
5b176c8cc5 test: split plugin install source coverage 2026-04-04 01:07:28 +09:00
Shakker
4919a8871b refactor: lazy load compaction store updates 2026-04-04 01:07:28 +09:00
Shakker
a3227e58d2 test: split secrets runtime integration coverage 2026-04-04 01:07:28 +09:00
Shakker
db76dbc546 test: split plugin loader coverage by concern 2026-04-04 01:07:28 +09:00
Shakker
b53dcb9380 test: fix bluebubbles monitor route typing 2026-04-04 01:07:28 +09:00
Shakker
da120962b9 test: fix image generation runtime test types 2026-04-04 01:07:28 +09:00
Shakker
383eea86dc test: move image generation runtime coverage to owner 2026-04-04 01:07:28 +09:00
Shakker
3c50139285 refactor: narrow heartbeat session imports 2026-04-04 01:07:28 +09:00
Shakker
590655472b perf: fast-path built-in reasoning provider checks 2026-04-04 01:07:28 +09:00
Shakker
9c4ea016d9 fix: use pnpm exec for scripted vitest runs 2026-04-04 01:07:28 +09:00
Shakker
e1143fb95f test: tighten bluebubbles monitor runtime typing 2026-04-04 01:07:28 +09:00
Shakker
24da2c39f3 refactor: isolate session transcript coverage 2026-04-04 01:07:28 +09:00
Shakker
27a8ef1284 refactor: narrow telegram message context runtime imports 2026-04-04 01:07:28 +09:00
Shakker
71b5a7c35b test: slim shared plugin runtime mock 2026-04-04 01:07:28 +09:00
Shakker
e9e7033ea1 test: trim embeddings provider import cost 2026-04-04 01:07:28 +09:00
Shakker
0af1d0ddb2 test: split security audit code safety coverage 2026-04-04 01:07:28 +09:00
Shakker
eb2cd09b72 test: trim facade runtime circular harness cost 2026-04-04 01:07:28 +09:00
Shakker
3cce39cae2 test: shrink media runtime facade coverage 2026-04-04 01:07:28 +09:00
Shakker
a65ff4de9f test: drain cron regression queue work before cleanup 2026-04-04 01:07:28 +09:00
Shakker
6299a5fbfe test: merge cron delivery-target thread coverage 2026-04-04 01:07:28 +09:00
Shakker
5ff72867bf test: type heartbeat reply mocks 2026-04-04 01:07:28 +09:00
Shakker
dcc3467a2b test: reset cron regression command queue state 2026-04-04 01:07:28 +09:00
Shakker
8bd3067e69 refactor: move built-in channel normalization to ids 2026-04-04 01:07:28 +09:00
Shakker
57ee3d9673 test: route config artifact checks through contracts 2026-04-04 01:07:28 +09:00
Shakker
33248980d9 test: split cron service regression ownership 2026-04-04 01:07:28 +09:00
Shakker
deb70e7e25 test: split cron isolated-agent turn coverage 2026-04-04 01:07:28 +09:00
Shakker
ebb4e9f0a6 test: route repo contract tests through contracts surface 2026-04-04 01:07:28 +09:00
Shakker
75b66403be test: route script suites through contracts surface 2026-04-04 01:07:28 +09:00
Shakker
335b472c37 test: merge subagent context-engine coverage into registry suite 2026-04-04 01:07:28 +09:00
Shakker
a78dba4396 refactor: lazy load heartbeat reply runtime 2026-04-04 01:07:28 +09:00
Shakker
bf3d1f85b8 test: avoid resetting cron issue regression modules 2026-04-04 01:07:28 +09:00
Shakker
5ae346427f test: fix stale typing in active suites 2026-04-04 01:07:28 +09:00
Shakker
ccdd33545b test: narrow qr dashboard integration surfaces 2026-04-04 01:07:28 +09:00
Shakker
652f273a0f refactor: lazy load approval forwarder defaults 2026-04-04 01:07:28 +09:00
Shakker
48fe2fd8be test: trim subagent context-engine harness cost 2026-04-04 01:07:28 +09:00
Shakker
9cf7b92e0d refactor: trim bundled capability metadata imports 2026-04-04 01:07:28 +09:00
Shakker
ac7e1f7c6c test: fix branch regression coverage 2026-04-04 01:07:28 +09:00
Shakker
6be5d34f2f test: avoid rebuilding openclaw tools in camera tests 2026-04-04 01:07:28 +09:00
Shakker
18891b1806 refactor: lazy load subagent registry runtime hooks 2026-04-04 01:07:28 +09:00
Shakker
08560c1f48 refactor: lazy load task cancellation control runtime 2026-04-04 01:07:28 +09:00
Shakker
192c02cd92 test: reuse subagent registry loop guard harness 2026-04-04 01:07:28 +09:00
Shakker
2d4428bcbb test: isolate outbound target registry boundaries 2026-04-04 01:07:28 +09:00
Shakker
2afa169250 test: isolate plugin dispatch runner boundaries 2026-04-04 01:07:28 +09:00
Shakker
0126653783 test: isolate message action runner test boundaries 2026-04-04 01:07:28 +09:00
Shakker
36c8282795 refactor: lazy load cli gateway helper runtimes 2026-04-04 01:07:28 +09:00
Shakker
58f1044ec0 test: drop duplicate outbound target coverage 2026-04-04 01:07:28 +09:00
Shakker
23422ccb68 refactor: lazy load cli gateway rpc runtime 2026-04-04 01:07:28 +09:00
Shakker
94340fdbae test: split message action runner boundaries 2026-04-04 01:07:28 +09:00
Shakker
42786afc64 refactor: trim image generation runtime imports 2026-04-04 01:07:28 +09:00
Shakker
768ec2a712 test: trim message action poll runner setup 2026-04-04 01:07:28 +09:00
Shakker
0875c2e370 test: trim message action media runner setup 2026-04-04 01:07:28 +09:00
Shakker
50069bcb59 fix: guard media image auto model resolution 2026-04-04 01:07:28 +09:00
Shakker
4b79ae7ad8 test: trim provider usage auth normalization setup 2026-04-04 01:07:28 +09:00
Shakker
14ff2c30d1 perf: prefer configured media auth providers 2026-04-04 01:07:28 +09:00
Shakker
25e9ff01cf refactor: trim media understanding runner test imports 2026-04-04 01:07:28 +09:00
Shakker
19493b681d test: harden channel metadata validation harness 2026-04-04 01:07:28 +09:00
Shakker
8d5c11d31b refactor: trim thinking helper import graph 2026-04-04 01:07:28 +09:00
Shakker
54af005f59 refactor: lazy load cron delivery outbound runtime 2026-04-04 01:07:28 +09:00
Shakker
9951f22766 refactor: split lightweight provider model id helpers 2026-04-04 01:07:28 +09:00
Shakker
9a6dda1b66 refactor: localize workspace skill prompt contract 2026-04-04 01:07:28 +09:00
Shakker
cc57bcfe2f refactor: lazy load cron subagent followup runtime 2026-04-04 01:07:28 +09:00
Shakker
9919e978ca refactor: lazy load cron auth and model runtime 2026-04-04 01:07:28 +09:00
Shakker
49563843dc refactor: trim remote skills startup imports 2026-04-04 01:07:28 +09:00
Shakker
c593ed0055 refactor: split lightweight plugin config policy 2026-04-04 01:07:28 +09:00
Shakker
4499d572fa refactor: split skill command specs from workspace snapshot 2026-04-04 01:07:28 +09:00
Shakker
24edb82ece refactor: split delivery target runtime seams 2026-04-04 01:07:28 +09:00
Shakker
d6ad92c1a0 fix: trim non-live test setup work 2026-04-04 01:07:28 +09:00
chi
33e6a6724d fix(telegram): enable HTML formatting for model switch messages (#60042)
* fix(telegram): enable HTML formatting for model switch messages

The model switch confirmation message was displaying raw Markdown
(**text**) instead of bold formatting because parse_mode was not set.

Changes:
- Add optional extra parameter to editMessageWithButtons for parse_mode
- Change format from Markdown ** to HTML <b> tags
- Pass parse_mode: 'HTML' when editing model switch message

Fixes the issue where model names appeared as **provider/model**
instead of bold text in Telegram.

* fix(telegram): escape HTML entities in model switch confirmation

Add defensive `escapeHtml` helper to sanitize `selection.provider`
and `selection.model` before interpolating them into the HTML
callback message. This prevents potential API rejection (HTTP 400)
if future provider or model names contain `<`, `>`, or `&`.

Addresses review feedback on unescaped HTML interpolation.

* test(telegram): cover HTML model switch confirmation

---------

Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-04-04 00:05:09 +08:00
Tak Hoffman
7c738ad036 fix: honor whatsapp heartbeat account allowFrom 2026-04-03 11:04:00 -05:00
Vincent Koc
3d799ba004 fix(ci): tighten whatsapp and openai transport types 2026-04-04 01:02:41 +09:00
Vincent Koc
f49d8f665c test(providers): use preferred gpt-5.4 constant 2026-04-04 00:59:50 +09:00
Tak Hoffman
8805c7b55b fix: honor imessage setup dm policy accounts 2026-04-03 10:56:55 -05:00
Tak Hoffman
d114b4e033 fix: honor signal setup dm policy accounts 2026-04-03 10:54:40 -05:00
Peter Steinberger
b7f524abaa fix: resolve post-rebase gate follow-ups for #60081 2026-04-04 00:53:45 +09:00
Peter Steinberger
bf6bd7432a fix: harden discord ack auth and gate fallout (#60081) (thanks @FunJim) 2026-04-04 00:53:45 +09:00
FunJim
c1741abc3c test(discord): update ack reaction assertions to expect propagated cfg
The implementation fix propagates the hydrated cfg to reactMessageDiscord
and removeReactionDiscord. Update test assertions to expect the cfg
property in the options argument using expect.objectContaining to handle
the dynamic session store path.
2026-04-04 00:53:45 +09:00
FunJim
b51214ec3e fix(discord): pass hydrated config to ack reactions to fix SecretRef resolution
When extracting `reactMessageDiscord`, it defaulted to reading the raw config (which contains `SecretRef`s) if a hydrated `cfg` was omitted. We now pass the pre-resolved `cfg` context into the reaction options so the plugin SDK resolves the token via memory rather than the raw file.
2026-04-04 00:53:45 +09:00
Tak Hoffman
27ced5c1d3 fix: honor line setup dm policy accounts 2026-04-03 10:52:03 -05:00
Vincent Koc
f02f3b925d refactor(zalouser): lazy-load async runtime surfaces 2026-04-04 00:51:22 +09:00
Tak Hoffman
d1883470e7 fix: honor whatsapp setup dm policy accounts 2026-04-03 10:49:39 -05:00
Vincent Koc
bd4f745833 fix(providers): respect responses developer-role compat (#60385) 2026-04-04 00:49:16 +09:00
Vincent Koc
62b736a8c2 refactor(whatsapp): lazy-load login tool 2026-04-04 00:47:33 +09:00
Shakker
d9dbce4093 fix: restore missing contract registry config import 2026-04-03 16:45:32 +01:00
Tak Hoffman
69d018ce4f fix: honor zalouser setup dm policy accounts 2026-04-03 10:44:46 -05:00
Tak Hoffman
4107a5a4f0 fix: honor zalo setup dm policy accounts 2026-04-03 10:42:23 -05:00
Peter Steinberger
41ce3269f4 refactor(plugins): split activation snapshot and compat flow 2026-04-04 00:42:11 +09:00
Vincent Koc
eb3481fca9 refactor(discord): lazy-load actions and audit 2026-04-04 00:40:30 +09:00
Shakker
a6a200ebc2 docs(changelog): note browser and whatsapp seam split 2026-04-03 16:39:47 +01:00
Shakker
b1747d8b1c fix: remove unused sandbox browser type import 2026-04-03 16:39:47 +01:00
Shakker
846bfaa045 fix: align plugin sdk subpath expectations 2026-04-03 16:39:47 +01:00
Peter Steinberger
3aac90fc85 fix: restore browser-config sdk compatibility 2026-04-03 16:39:47 +01:00
Shakker
9a88a933cf refactor: narrow audit browser enablement check 2026-04-03 16:39:47 +01:00
Shakker
35541377d1 test: split whatsapp setup surface coverage 2026-04-03 16:39:47 +01:00
Shakker
9b8c892ff4 perf: direct export whatsapp target helpers 2026-04-03 16:39:47 +01:00
Shakker
5e7ebd098e fix: remove duplicate sandbox browser import 2026-04-03 16:39:47 +01:00
Shakker
e7cb9dec43 refactor: add approval auth runtime subpath 2026-04-03 16:39:47 +01:00
Shakker
6e3203a728 refactor: narrow whatsapp chunking imports 2026-04-03 16:39:47 +01:00
Shakker
4615ddf89b test: trim whatsapp channel test barrels 2026-04-03 16:39:47 +01:00
Shakker
6d6060d3ec perf: split whatsapp targets facade 2026-04-03 16:39:47 +01:00
Shakker
4528f8779e test: localize browser config env helper 2026-04-03 16:39:47 +01:00
Shakker
a5b23f17fb perf: split browser config sdk support 2026-04-03 16:39:47 +01:00
Shakker
557a07bd5b perf: skip browser runtime lookup for empty tab cleanup 2026-04-03 16:39:47 +01:00
Shakker
f41a67b118 fix: restore browser and whatsapp boundary contracts 2026-04-03 16:39:47 +01:00
Shakker
2e520d112d refactor: split browser sdk imports for sandbox and audit 2026-04-03 16:39:47 +01:00
Tak Hoffman
625201bddc fix: honor bluebubbles setup dm policy accounts 2026-04-03 10:39:32 -05:00
Vincent Koc
4b93000d11 test(contracts): isolate plugin registry 2026-04-04 00:38:27 +09:00
Tak Hoffman
6a28756e98 fix: honor nextcloud setup dm policy accounts 2026-04-03 10:37:07 -05:00
Vincent Koc
e67be773d6 test(contracts): isolate action registry 2026-04-04 00:35:35 +09:00
Tak Hoffman
3d8a039149 fix: honor legacy setup dm policy accounts 2026-04-03 10:34:18 -05:00
Peter Steinberger
904d9db132 fix(ci): repair whatsapp harness mocking 2026-04-03 16:32:47 +01:00
Vincent Koc
f2204cb35a test(contracts): isolate setup and status registries 2026-04-04 00:32:20 +09:00
Vincent Koc
d755709ddd refactor(discord): lazy-load cross-context ui 2026-04-04 00:31:29 +09:00
Tak Hoffman
b1026a0b28 fix: honor account-scoped setup dm policy 2026-04-03 10:31:00 -05:00
Peter Steinberger
c6b8109bd8 fix(ci): use sdk seams in whatsapp test harnesses 2026-04-03 16:29:53 +01:00
Peter Steinberger
8b6c224554 refactor(plugins): share activation context for provider runtimes 2026-04-04 00:29:09 +09:00
Vincent Koc
c8318754b5 test(contracts): lazily resolve session binding registry 2026-04-04 00:28:22 +09:00
Vincent Koc
7c6eba4634 test(config): cover thread binding legacy doctor paths 2026-04-04 00:26:41 +09:00
Vincent Koc
f1f6b98639 test(contracts): isolate slack outbound harness 2026-04-04 00:26:16 +09:00
Vincent Koc
79aa212789 refactor(whatsapp): lazy-load send and action runtimes 2026-04-04 00:25:02 +09:00
Tak Hoffman
dae0400a8f fix: honor discord account guild policy config 2026-04-03 10:24:42 -05:00
Vincent Koc
745aa26420 fix(ci): remove duplicate migrated test imports 2026-04-04 00:24:20 +09:00
Vincent Koc
ed297eb8b9 fix(providers): align cache-ttl anthropic semantics (#60375) 2026-04-04 00:22:32 +09:00
Vincent Koc
af835acd00 test(config): cover legacy tts doctor paths 2026-04-04 00:21:45 +09:00
Vincent Koc
ade6b61358 test(contracts): split registry-backed channel contract lanes 2026-04-04 00:21:30 +09:00
Vincent Koc
f71ef47288 fix(ci): disable automatic clawhub release workflow 2026-04-04 00:20:28 +09:00
Vincent Koc
a592cd67cb fix(ci): bump clawhub plugin versions for release gate 2026-04-04 00:20:27 +09:00
Peter Steinberger
1dfcdbdf91 fix(testing): repair bundled plugin helper imports 2026-04-03 16:19:39 +01:00
Tak Hoffman
e3fea41b59 fix: honor telegram account topic mention config 2026-04-03 10:19:11 -05:00
Vincent Koc
c71df2f4b0 test(commands): allow scoped channel test registries 2026-04-04 00:18:34 +09:00
Peter Steinberger
2e779a1b20 refactor(discord): share thread starter snapshot parsing 2026-04-04 00:17:57 +09:00
Vincent Koc
0f129c87ba test(config): cover telegram and x_search legacy doctor paths 2026-04-04 00:16:57 +09:00
Tak Hoffman
a3541a1cce fix: honor telegram account replyToMode 2026-04-03 10:16:05 -05:00
Tak Hoffman
30c0dc3d47 fix: honor discord account replyToMode 2026-04-03 10:14:42 -05:00
Vincent Koc
4e3b2781fb test(contracts): split session binding registry seams 2026-04-04 00:13:40 +09:00
Peter Steinberger
93f136cbed test: split inbound contract suites by channel 2026-04-03 16:13:09 +01:00
Peter Steinberger
2da3b45ce7 test: reduce discord component partial mocks 2026-04-03 16:13:09 +01:00
Tak Hoffman
8e9607c064 fix: honor googlechat account replyToMode 2026-04-03 10:12:47 -05:00
Vincent Koc
f08a1c34dd fix(providers): scope anthropic-family cache semantics (#60370) 2026-04-04 00:11:57 +09:00
Vincent Koc
b50b85a5db refactor(zalo): lazy-load setup wizard surface 2026-04-04 00:11:07 +09:00
Vincent Koc
702a200844 fix(ci): guard optional discord reaction cleanup 2026-04-04 00:09:32 +09:00
Tak Hoffman
78c390ea86 docs: align messages config support notes 2026-04-03 10:08:34 -05:00
Vincent Koc
5ad2c61c9a test(config): cover gateway bind legacy doctor flow 2026-04-04 00:07:19 +09:00
Peter Steinberger
cee5f960b5 fix(gateway): preserve raw activation source for startup plugin loads 2026-04-04 00:06:57 +09:00
Vincent Koc
93d514f816 fix(ci): correct zalo status helper imports 2026-04-04 00:06:47 +09:00
Vincent Koc
0eb9416d9c refactor(telegram): lazy-load send and action runtimes 2026-04-04 00:06:38 +09:00
Tak Hoffman
30fc29c9b0 fix: honor discord status reactions toggle 2026-04-03 10:05:17 -05:00
Vincent Koc
ca68d57dc2 test(config): cover legacy heartbeat and memorySearch doctor paths 2026-04-04 00:04:25 +09:00
Vincent Koc
9e6da1e70a fix(providers): pass anthropic cache retention through custom apis (#60359) 2026-04-04 00:04:09 +09:00
Vincent Koc
6366010884 fix(ci): route extension test helpers through public sdk seams 2026-04-04 00:03:48 +09:00
Peter Steinberger
ad8870ae28 test: narrow gateway and model status runtime seams 2026-04-03 16:03:32 +01:00
Peter Steinberger
a6816cb59c test: reduce subagent announce import overhead 2026-04-03 16:03:32 +01:00
Peter Steinberger
25a187568f test: trim whatsapp monitor import overhead 2026-04-03 16:03:32 +01:00
Shakker
b98ee01814 fix: restore cron context window priming 2026-04-03 16:03:10 +01:00
Shakker
f5276ed38b test: preserve cron model-selection helper exports 2026-04-03 16:03:10 +01:00
Shakker
de952c036a refactor: split cron delivery planning from sending 2026-04-03 16:03:10 +01:00
Shakker
bd8d29c2b1 fix: align cron test delivery result types 2026-04-03 16:03:10 +01:00
Shakker
6363094e93 refactor: trim cron session store startup imports 2026-04-03 16:03:10 +01:00
Shakker
1f0c4a624b refactor: route cron subagent reads through registry seam 2026-04-03 16:03:10 +01:00
Shakker
11dbcdc46d refactor: narrow model fallback auth imports 2026-04-03 16:03:10 +01:00
Shakker
b721f5e48a refactor: lazy load cron gateway cleanup 2026-04-03 16:03:10 +01:00
Shakker
a4efe7c028 refactor: narrow cron delivery session imports 2026-04-03 16:03:10 +01:00
Shakker
12fa700579 refactor: lazy load cron usage formatting 2026-04-03 16:03:10 +01:00
Shakker
fc8ab82aab refactor: trim cron session startup imports 2026-04-03 16:03:10 +01:00
Shakker
88b1c00b39 refactor: lazy load cron cli runtime 2026-04-03 16:03:10 +01:00
Shakker
7a9ad3820e refactor: localize cron channel test outbounds 2026-04-03 16:03:10 +01:00
Hiroshi Tanaka
e9a1f7818c fix(discord): extract forwarded message text in thread starter resolution (#60139)
resolveDiscordThreadStarter only checked content and embeds, returning
null for forwarded messages where the text lives in message_snapshots.

Add a local resolveStarterForwardedText helper that extracts text
directly from the message_snapshots array on the REST response object.
This avoids fragile type casts and keeps the change self-contained
within threading.ts.

Fixes #60129
2026-04-04 00:02:42 +09:00
Vincent Koc
fbd361d338 fix(config): surface legacy channel streaming aliases (#60358) 2026-04-04 00:00:38 +09:00
Vincent Koc
3257136160 refactor(zalo): narrow channel sdk imports 2026-04-04 00:00:34 +09:00
Vincent Koc
5ccf6d229b test(channels): remove unused group policy helper 2026-04-03 23:57:43 +09:00
Tak Hoffman
759c81ceb8 test: audit heartbeat config honor 2026-04-03 09:55:51 -05:00
Vincent Koc
35cf7d0340 fix(config): migrate legacy sandbox perSession alias (#60346)
* fix(config): migrate legacy sandbox perSession alias

* fix(config): preserve invalid sandbox persession values
2026-04-03 23:55:47 +09:00
Vincent Koc
b6dd7ac232 test(channels): remove unused dm policy helper 2026-04-03 23:54:07 +09:00
Vincent Koc
f9f0a593e4 test(helpers): remove unused plugin helper wrappers 2026-04-03 23:52:58 +09:00
Vincent Koc
a028e16eaa test(helpers): remove unused bundled plugin surface wrapper 2026-04-03 23:50:55 +09:00
Vincent Koc
35aa6c6126 test(channel): remove unused outbound helper 2026-04-03 23:50:06 +09:00
Vincent Koc
756cf847e0 refactor(telegram): lazy-load audit and monitor surfaces 2026-04-03 23:49:53 +09:00
Vincent Koc
279ee5e842 test(commands): lazy-load default channel registry plugins 2026-04-03 23:48:38 +09:00
Frank Yang
e9f82ac752 fix: clear stale ClawHub query results on input change (#60267)
* fix: clear stale ClawHub query results on input change

* docs: move ClawHub follow-up changelog entry to section tail
2026-04-03 22:48:14 +08:00
Vincent Koc
76ff144037 test(outbound): remove unused runner helper 2026-04-03 23:46:24 +09:00
Vincent Koc
66825c0969 refactor(providers): centralize native provider detection (#60341)
* refactor(providers): centralize native provider detection

* fix(providers): preserve openrouter thinking format

* fix(providers): preserve openrouter host thinking format
2026-04-03 23:46:21 +09:00
Vincent Koc
24e10e6e45 test(contracts): lazy-load slack outbound contract surface 2026-04-03 23:45:01 +09:00
Vincent Koc
38a4b2b14c refactor(signal): route target normalization through channel-targets 2026-04-03 23:44:50 +09:00
Vincent Koc
5ce7aee33b test(cron): localize core channel outbound test loads 2026-04-03 23:41:54 +09:00
Vincent Koc
e9acbdb111 fix(ci): share heavy check lock across test and lint 2026-04-03 23:41:34 +09:00
Vincent Koc
8ffeadd8f9 test(contracts): lazy-load outbound contract plugin helpers 2026-04-03 23:40:19 +09:00
Peter Steinberger
cd38eba316 refactor: unify plugin activation source plumbing 2026-04-03 23:39:36 +09:00
Vincent Koc
975d2ddce2 test(contracts): lazy-load inbound contract plugin helpers 2026-04-03 23:39:26 +09:00
Vincent Koc
3b69b8e3c4 fix(ci): route extension test helpers through sdk testing 2026-04-03 23:39:06 +09:00
Vincent Koc
c013b9cdf3 test(contracts): lazy-load session binding test facades 2026-04-03 23:37:59 +09:00
Vincent Koc
316978700e test(gateway): lazy-load speech provider test surfaces 2026-04-03 23:33:16 +09:00
Vincent Koc
316da43dd7 fix(config): migrate legacy talk config via doctor (#60333)
* fix(config): migrate legacy talk config via doctor

* fix(config): harden legacy talk provider migration
2026-04-03 23:32:36 +09:00
Vincent Koc
23d8a979b3 test(contracts): lazy-load discord thread binding test surface 2026-04-03 23:31:47 +09:00
Vincent Koc
1556490ee7 fix(ci): collapse duplicate provider request union 2026-04-03 23:30:25 +09:00
Vincent Koc
cddb34ba6a refactor(line): lazy-load card command 2026-04-03 23:29:34 +09:00
Vincent Koc
0d7d573cd6 test(commands): split default channel test registry helper 2026-04-03 23:29:24 +09:00
Vincent Koc
06ed0eaad5 fix(ci): narrow discord subagent hook types 2026-04-03 23:28:03 +09:00
Peter Steinberger
b40d4b63f6 refactor: centralize update targets and extension guardrails 2026-04-03 23:26:31 +09:00
Vincent Koc
8f5f78bbe8 feat(providers): reopen model request transport config (#60327)
* feat(providers): reopen model request transport config

* chore(config): refresh request override baselines
2026-04-03 23:25:11 +09:00
Vincent Koc
cedc8bdebb refactor(signal): lazy-load monitor surfaces 2026-04-03 23:24:42 +09:00
Vincent Koc
4e9629d60c fix(ci): route bluebubbles helper through local barrel 2026-04-03 23:24:17 +09:00
Peter Steinberger
d375cd727e fix: migrate legacy web search config on startup 2026-04-03 23:24:02 +09:00
Shakker
549e0bb268 test: keep imessage test plugin facade-free by default 2026-04-03 15:23:50 +01:00
Vincent Koc
690c58baa2 refactor(discord): lazy-load subagent hooks 2026-04-03 23:22:34 +09:00
Vincent Koc
0ecf84524d fix(ci): restore line runtime seams 2026-04-03 23:19:39 +09:00
Vincent Koc
78fb352506 refactor(feishu): lazy-load subagent hooks 2026-04-03 23:19:11 +09:00
Vincent Koc
52008e2e60 fix(doctor): clarify legacy config migration guidance (#60326) 2026-04-03 23:16:11 +09:00
Vincent Koc
a9a057d1eb fix(ci): normalize extension harness imports 2026-04-03 23:15:57 +09:00
Vincent Koc
ac20eed335 fix(ci): route extension tests through sdk seams 2026-04-03 23:15:57 +09:00
Vincent Koc
ed166ba338 test(contracts): extract narrow channel contract helpers 2026-04-03 23:14:45 +09:00
Josh Lehman
799c6f40aa refactor: move provider replay runtime ownership into plugins (#60126)
* refactor: move provider replay runtime ownership into plugins

* fix(provider-runtime): address review followups

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-04-03 23:14:37 +09:00
Shakker
f328e7f4a6 docs: add changelog for outbound seam refactor 2026-04-03 15:10:48 +01:00
Shakker
6395336454 fix: resolve outbound seam follow-ups 2026-04-03 15:10:48 +01:00
Shakker
1a23627e32 refactor: split delivery target runtime seams 2026-04-03 15:10:48 +01:00
Shakker
c2e93c76bd refactor: split session store loader from maintenance 2026-04-03 15:10:48 +01:00
Shakker
883a35a38c refactor: narrow cron delivery target session imports 2026-04-03 15:10:48 +01:00
Shakker
cef0f36931 refactor: split chat history text helpers 2026-04-03 15:10:48 +01:00
Shakker
4a0905b94b refactor: lazy load outbound channel bootstrap 2026-04-03 15:10:48 +01:00
Shakker
4a81771290 refactor: lazy load outbound transcript mirroring 2026-04-03 15:10:48 +01:00
Shakker
e26a590f7a refactor: drop heavy channel outbound test imports 2026-04-03 15:10:48 +01:00
Shakker
a61408737f refactor: localize deliver test outbounds 2026-04-03 15:10:48 +01:00
Shakker
d338299dc7 refactor: keep deliver tests channel-generic 2026-04-03 15:10:48 +01:00
Shakker
909895c471 refactor: narrow deliver test channel boundaries 2026-04-03 15:10:48 +01:00
Shakker
d7e4fad872 refactor: trim outbound delivery test imports 2026-04-03 15:10:48 +01:00
Shakker
52717ee399 refactor: trim outbound target test imports 2026-04-03 15:10:48 +01:00
Agustin Rivera
3cd9aac6bb Require owner access for /allowlist writes (#59836)
* fix(allowlist): require owner access for writes

* docs(changelog): note allowlist owner gate fix

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
2026-04-03 07:07:36 -07:00
Vincent Koc
62b1fe0b85 fix(ci): correct browser live test export 2026-04-03 23:07:20 +09:00
Peter Steinberger
8d0bed458e refactor: simplify reply-threading and test helpers 2026-04-03 23:06:22 +09:00
Vincent Koc
1125cd3b97 refactor(xai): lazy-load heavy tool modules 2026-04-03 23:03:33 +09:00
Vincent Koc
efeee6f921 fix(ci): use plugin registry test bridges 2026-04-03 23:03:15 +09:00
Vincent Koc
4b2c7404e5 test(types): remove remaining testing barrel references 2026-04-03 23:03:02 +09:00
Vincent Koc
5341d483d1 test(acpx): use direct ACP adapter contract seam 2026-04-03 23:01:51 +09:00
Peter Steinberger
0dad4072b4 fix: keep extension helper imports behind local runtime barrels (#60153) 2026-04-03 23:01:43 +09:00
Peter Steinberger
96e8352bda fix: align extension boundary guardrails for landing (#60153) 2026-04-03 23:01:43 +09:00
Peter Steinberger
0c0d84fbd9 fix: route pnpm test wrappers through the active runner (#60153) 2026-04-03 23:01:43 +09:00
Peter Steinberger
ab57d24f79 fix: stabilize npm owner update test on Windows (#60153) (thanks @jayeshp19) 2026-04-03 23:01:43 +09:00
jayeshp19
b9ede82cc2 greptile fix 2026-04-03 23:01:43 +09:00
jayeshp19
eb4b6f7024 Use owning npm prefix for global updates 2026-04-03 23:01:43 +09:00
Vincent Koc
c1d68f213d test(helpers): use direct internal seams 2026-04-03 23:00:28 +09:00
Vincent Koc
d2427c19e0 fix(ci): restore extension runtime seams 2026-04-03 22:57:28 +09:00
Vincent Koc
9b83e462cf test(channels): use narrow active registries in sticky tests 2026-04-03 22:57:16 +09:00
Vincent Koc
9aab672a69 refactor(bluebubbles): narrow monitor processing exports 2026-04-03 22:56:26 +09:00
Peter Steinberger
bc137951e9 fix: preserve allowlist guard for auto-enabled bundled channels (#60233) (thanks @dorukardahan) 2026-04-03 22:55:31 +09:00
Doruk Ardahan
cd08facd7a fix(plugins): keep auto-enabled channels behind allowlists 2026-04-03 22:55:30 +09:00
Doruk Ardahan
f7d24c1ed5 fix(plugins): allow configured bundled channels past allowlists 2026-04-03 22:55:30 +09:00
Vincent Koc
2ad69083d2 refactor(feishu): narrow reply dispatcher exports 2026-04-03 22:54:29 +09:00
Vincent Koc
1ad9fe0b52 test(discord): use direct channel test helper 2026-04-03 22:51:37 +09:00
Vincent Koc
f6e99bd514 refactor(msteams): narrow messenger sdk imports 2026-04-03 22:50:54 +09:00
pgondhi987
b48b528b02 fix(skills): block UV_PYTHON in workspace dotenv and harden uv installer env [AI] (#59178)
* fix: address issue

* fix: finalize issue changes

* fix: address PR review feedback

* Changelog: note uv installer env hardening

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
2026-04-03 06:50:43 -07:00
Vincent Koc
8b5e80fcaa refactor(msteams): narrow store sdk imports 2026-04-03 22:49:27 +09:00
Vincent Koc
6f9b4b52f8 refactor(msteams): narrow send sdk imports 2026-04-03 22:47:07 +09:00
Vincent Koc
3b358414d3 test(channels): use direct contract helper imports 2026-04-03 22:46:58 +09:00
Vincent Koc
4798e125f4 feat(providers): add llm transport adapter seam (#60264)
* feat(providers): add llm transport adapter seam

* fix(providers): harden openai transport adapter

* fix(providers): correct transport usage accounting
2026-04-03 22:45:47 +09:00
Vincent Koc
875c3813aa refactor(msteams): narrow outbound sdk imports 2026-04-03 22:45:09 +09:00
Vincent Koc
f904a49568 refactor(feishu): narrow channel runtime exports 2026-04-03 22:43:09 +09:00
Vincent Koc
020093bf4b test(slack): use direct outbound payload harness 2026-04-03 22:43:06 +09:00
Vincent Koc
597416fdd9 refactor(feishu): narrow outbound runtime exports 2026-04-03 22:41:46 +09:00
Peter Steinberger
173bb0aea0 test: remove update-cli shared partial mock 2026-04-03 14:40:11 +01:00
Onur
fa9e1e3d8e CI: add ClawHub plugin release workflow (#59179)
* CI: add ClawHub plugin release workflow

* CI: harden ClawHub plugin release workflow

* CI: finish ClawHub plugin release hardening

* CI: watch shared ClawHub release inputs

* CI: harden ClawHub publish workflow

* CI: watch more ClawHub release deps

* CI: match shared release inputs by prefix

* CI: pin ClawHub publish source commit

* CI: refresh pinned ClawHub release commit

* CI: rename ClawHub plugin release environment

---------

Co-authored-by: Onur Solmaz <onur@solmaz.io>
2026-04-03 15:40:07 +02:00
Shakker
e0e5df25e6 test: unstick context lookup fake-timer warmup 2026-04-03 14:38:30 +01:00
Peter Steinberger
8962a8fb73 test: reduce status and update-cli partial mocks 2026-04-03 14:37:47 +01:00
Peter Steinberger
bab14fb8f1 test: lazy-load config doc baseline runtime 2026-04-03 14:29:17 +01:00
Peter Steinberger
bb25a8050c test: baseline bundled runtime sidecar paths 2026-04-03 14:26:01 +01:00
Shakker
8fabfa5d1c fix: rehydrate context token cache after module reload 2026-04-03 14:23:44 +01:00
Peter Steinberger
ba7297ff21 test: narrow media fs-safe test seams 2026-04-03 14:22:12 +01:00
Shakker
3b727d2433 test: harden package-root mocks after setup slimming 2026-04-03 14:17:16 +01:00
Shakker
a898cd464a fix: use runtime plugin state in test setup 2026-04-03 14:17:16 +01:00
Shakker
a764b8f8cd refactor: trim test setup agent imports 2026-04-03 14:17:16 +01:00
Shakker
73073a91bb refactor: isolate session store test cleanup state 2026-04-03 14:17:16 +01:00
Shakker
7bd6bb91c4 fix: trim non-live test setup work 2026-04-03 14:17:16 +01:00
Peter Steinberger
6510ecafb0 test: narrow bluebubbles reply-cache imports 2026-04-03 14:16:29 +01:00
Peter Steinberger
85bd5b3ce7 fix(ci): refresh protocol models and align channel tests 2026-04-03 14:13:32 +01:00
Peter Steinberger
e43a362ad9 test: trim update-cli metadata fixture import 2026-04-03 14:09:28 +01:00
Peter Steinberger
3a2667b5fc test: fix status and update-cli mock drift 2026-04-03 14:06:32 +01:00
Peter Steinberger
bbbfbd2db4 test: import slack monitor helpers directly 2026-04-03 14:03:05 +01:00
Peter Steinberger
91618438bc test: narrow googlechat channel test deps 2026-04-03 14:03:05 +01:00
Peter Steinberger
6d05607cd9 test: trim nextcloud send config import cost 2026-04-03 14:03:05 +01:00
Peter Steinberger
a884ad3cf2 fix(ci): route extension test helpers through sdk seams 2026-04-03 13:58:21 +01:00
Peter Steinberger
35d890b5ef test: narrow subagent spawn test seams 2026-04-03 13:53:11 +01:00
Peter Steinberger
88bc6d852f fix(ci): route remaining feishu runtime seams locally 2026-04-03 13:52:40 +01:00
Peter Steinberger
1a75fc9e05 fix: align latest-main gate drift on #60221 2026-04-03 21:52:35 +09:00
Ayaan Zaidi
39361d13be fix: restore bootstrap tokens after send failure (#60221) 2026-04-03 21:52:35 +09:00
Ayaan Zaidi
5e3a3c42ca fix(gateway): revoke bootstrap tokens after handshake commit 2026-04-03 21:52:35 +09:00
Ayaan Zaidi
b08d58c917 fix(gateway): track bootstrap profile redemption 2026-04-03 21:52:35 +09:00
Ayaan Zaidi
0891253012 fix(pairing): preserve mixed-role node scopes 2026-04-03 21:52:35 +09:00
Ayaan Zaidi
a42f000b53 fix(gateway): defer bootstrap token revocation 2026-04-03 21:52:35 +09:00
Peter Steinberger
df115822b9 test: reduce non-telegram import overhead 2026-04-03 13:49:51 +01:00
Peter Steinberger
4f4aa46d00 test: split telegram bot command menu coverage 2026-04-03 13:49:51 +01:00
Peter Steinberger
da1980b923 fix(ci): route final feishu helper barrels locally 2026-04-03 13:46:06 +01:00
Vincent Koc
a3b9ae8fab refactor(feishu): narrow bot runtime exports 2026-04-03 21:44:51 +09:00
Vincent Koc
8735dd7d19 fix(ci): restore channel helper seams 2026-04-03 21:43:32 +09:00
Vincent Koc
7ede711cae fix(test): use shell spawn for windows test runner 2026-04-03 21:43:32 +09:00
Vincent Koc
01a163c7f3 fix(discord): restore runtime action seam 2026-04-03 21:43:32 +09:00
Vincent Koc
851de3554e refactor(feishu): split comment dispatcher seam 2026-04-03 21:43:29 +09:00
Vincent Koc
349d3e8289 test(plugin-sdk): extract direct helper seams 2026-04-03 21:42:04 +09:00
Vincent Koc
2fff6be4c3 refactor(feishu): split monitor state seam 2026-04-03 21:41:47 +09:00
Peter Steinberger
2a54a7f7cd fix(ci): route remaining feishu helper barrels locally 2026-04-03 13:41:32 +01:00
Vincent Koc
77c04ec29c test(whatsapp): use direct outbound helper fixtures 2026-04-03 21:40:12 +09:00
Vincent Koc
56ead96f48 test(discord): use direct system event helpers 2026-04-03 21:39:30 +09:00
Vincent Koc
027a544d8f refactor(feishu): split dedup runtime seam 2026-04-03 21:38:51 +09:00
Vincent Koc
710c63edad test(extensions): use direct runtime capture helpers 2026-04-03 21:37:41 +09:00
Vincent Koc
319aa2f1fe refactor(feishu): split runtime helper seams 2026-04-03 21:37:32 +09:00
Peter Steinberger
899fe6ffa5 fix(ci): route feishu bot helpers through local barrel 2026-04-03 13:36:55 +01:00
Vincent Koc
0ca2a91213 test(extensions): use direct helper seams in provider tests 2026-04-03 21:36:13 +09:00
Vincent Koc
a05daaa832 refactor(feishu): split channel runtime seam 2026-04-03 21:34:08 +09:00
Vincent Koc
0a61138e77 test(deepgram): use direct audio test helpers 2026-04-03 21:32:47 +09:00
Vincent Koc
e4cc8cd975 refactor(feishu): split outbound runtime seam 2026-04-03 21:32:25 +09:00
Peter Steinberger
ee39ec29d1 fix(ci): restore talk-voice plugin runtime export 2026-04-03 13:32:16 +01:00
Vincent Koc
344717a2d5 refactor(feishu): split comment handler seam 2026-04-03 21:30:16 +09:00
Peter Steinberger
65cddd79eb fix(ci): align discord actions contract with config discovery 2026-04-03 13:29:17 +01:00
Vincent Koc
beb108cfaa refactor(feishu): split bot runtime seam 2026-04-03 21:28:15 +09:00
Peter Steinberger
49936f6066 refactor: move ollama synthetic auth precedence into extension 2026-04-03 21:25:02 +09:00
Vincent Koc
a0dbba1626 test(extensions): narrow provider registration test helpers 2026-04-03 21:24:43 +09:00
Vincent Koc
568859e1fb test(extensions): avoid barrel testing helpers in media tests 2026-04-03 21:23:47 +09:00
Vincent Koc
2a04d5c16f test(extensions): narrow utility test helper imports 2026-04-03 21:23:47 +09:00
Vincent Koc
a3cadfd51d test(talk-voice): slim command runtime fixture 2026-04-03 21:22:00 +09:00
Peter Steinberger
7c41b9fca9 fix(ci): route telegram test harness through reply runtime 2026-04-03 13:21:38 +01:00
Vincent Koc
7fa0c76ffc test(mattermost): slim leaf runtime fixtures 2026-04-03 21:21:24 +09:00
Vincent Koc
f0a4423271 fix(tui): tolerate clock skew in pending-history reconciliation 2026-04-03 21:21:09 +09:00
Peter Steinberger
a3f34a8f77 test: reduce telegram context partial mocks 2026-04-03 13:19:50 +01:00
Vincent Koc
c186644662 test(googlechat): use narrow registry helpers in webhook routing tests 2026-04-03 21:18:51 +09:00
Vincent Koc
e4abd34466 test(feishu): drop unused client runtime imports 2026-04-03 21:18:17 +09:00
Vincent Koc
4e60653959 fix(test): default local Vitest to one worker (#60281) 2026-04-03 21:18:12 +09:00
Vincent Koc
69da71c0ce test(feishu): slim tool runtime fixtures 2026-04-03 21:17:11 +09:00
Vincent Koc
cd81e0a07b test(zalo): replace heavy testing helpers in monitor tests 2026-04-03 21:17:02 +09:00
Peter Steinberger
5184522f2f refactor: trim extension test runner surface 2026-04-03 13:15:43 +01:00
Vincent Koc
3a68414569 test(feishu): slim bot runtime fixtures 2026-04-03 21:14:08 +09:00
Peter Steinberger
99397254a1 fix(ci): relax feishu runtime test casts 2026-04-03 13:12:23 +01:00
Peter Steinberger
d2dae50a75 test: trim telegram bot import graph 2026-04-03 13:10:43 +01:00
Vincent Koc
9245b9e2f4 test(zalo): use narrow registry helpers in lifecycle tests 2026-04-03 21:10:33 +09:00
Peter Steinberger
f59d0eac68 refactor(plugin-runtime): remove plugin-specific core seams 2026-04-03 13:08:39 +01:00
Vincent Koc
4846ebce12 fix(test): serialize local heavy checks (#60273) 2026-04-03 21:07:56 +09:00
Vincent Koc
feca4aa49e test(feishu): replace heavy runtime mock helper in bot tests 2026-04-03 21:07:43 +09:00
Peter Steinberger
fbb2537774 refactor: clarify tool schema normalization 2026-04-03 21:07:19 +09:00
Peter Steinberger
1118d032ca refactor: split extension test helpers 2026-04-03 13:06:11 +01:00
Peter Steinberger
87abcfd6a6 fix: harden ollama tool-call replay (#52253) (thanks @Adam-Researchh) 2026-04-03 21:06:06 +09:00
Vincent Koc
e116e7d584 test(feishu): slim comment monitor runtime fixtures 2026-04-03 21:05:54 +09:00
Vincent Koc
dc312a4c76 test(feishu): slim reaction monitor runtime fixtures 2026-04-03 21:02:26 +09:00
Peter Steinberger
685ef52284 refactor: simplify test workflow helpers 2026-04-03 13:00:00 +01:00
Peter Steinberger
71a54d0c95 fix(ci): forward bluebubbles barrel and node env fixes 2026-04-03 12:58:10 +01:00
Vincent Koc
688eb8435b test(bluebubbles): split webhook ingress seam 2026-04-03 20:58:03 +09:00
Vincent Koc
2734d06ade test(matrix): avoid loading send module in thread binding tests 2026-04-03 20:56:58 +09:00
Vincent Koc
a8040ab9d9 test(matrix): avoid loading action modules in tool tests 2026-04-03 20:55:47 +09:00
Vincent Koc
b925f6d46c test(zalo): avoid loading monitor and probe modules in startup test 2026-04-03 20:55:09 +09:00
Vincent Koc
fbc4fa6ac3 test(feishu): avoid loading streaming card module in dispatcher tests 2026-04-03 20:54:24 +09:00
Vincent Koc
d888ce242b test(bluebubbles): split monitor processing seam 2026-04-03 20:54:08 +09:00
Peter Steinberger
0d938748a5 refactor(sessions): clarify duplicate session resolution 2026-04-03 20:53:17 +09:00
Vincent Koc
a0c6ea5aba test(feishu): avoid loading bot and send modules in menu tests 2026-04-03 20:52:26 +09:00
Peter Steinberger
a2077b28ef refactor: trim vitest wrapper layers 2026-04-03 12:52:14 +01:00
Vincent Koc
bd1e78ea34 test(msteams): avoid loading graph upload module in messenger tests 2026-04-03 20:50:00 +09:00
Vincent Koc
82fca281b6 test(msteams): avoid loading graph module in message tests 2026-04-03 20:50:00 +09:00
Vincent Koc
b410c5434c test(msteams): avoid loading graph module in member tests 2026-04-03 20:50:00 +09:00
Vincent Koc
d9aa88dd6c test(bluebubbles): split channel status seam 2026-04-03 20:46:42 +09:00
Vincent Koc
9bd05d3841 test(browser): stop reloading auth server module 2026-04-03 20:45:45 +09:00
Peter Steinberger
57999f9965 fix: narrow empty MCP tool schema normalization (#60176) (thanks @Bartok9) 2026-04-03 20:45:33 +09:00
Bartok Moltbot
19dbe00763 fix(tools): normalize truly empty MCP tool schemas for OpenAI
Fixes #60158

MCP tools with parameter-free schemas may return truly empty objects
`{}` without a `type` field. The existing normalization handled
`{ type: "object" }` → `{ type: "object", properties: {} }` but
missed the truly empty case.

OpenAI gpt-5.4 rejects tool schemas without `type: "object"` and
`properties`, causing HTTP 400 errors:

```
Invalid schema for function 'flux-mcp__get_flux_instance':
In context=(), object schema missing properties.
```

This change catches empty schemas (no type, no properties, no unions)
before the final pass-through and converts them to the required format.

Added test case for parameter-free MCP tool schemas.
2026-04-03 20:45:33 +09:00
Peter Steinberger
6845b8061c docs: simplify vitest workflow guidance 2026-04-03 12:45:13 +01:00
Peter Steinberger
9ef5d85e40 refactor: remove custom test planner runtime 2026-04-03 12:45:13 +01:00
Peter Steinberger
c80c1cf56f test: drop planner fixtures and coverage 2026-04-03 12:45:13 +01:00
Vincent Koc
d21d859ded test(browser): stop reloading cdp screenshot module 2026-04-03 20:44:53 +09:00
Vincent Koc
11c6202ec0 test(bluebubbles): split action metadata seam 2026-04-03 20:44:23 +09:00
Vincent Koc
9a53c3d772 test(browser): drop redundant module resets 2026-04-03 20:43:49 +09:00
Vincent Koc
6e3eb34a90 test(bluebubbles): narrow action helper imports 2026-04-03 20:42:29 +09:00
Peter Steinberger
36a233ff98 fix(config): honor isolated state-dir config writes 2026-04-03 12:42:08 +01:00
Vincent Koc
51d6d7013f fix(tui): preserve pending sends and busy-state visibility (#59800)
* fix(tui): preserve pending messages across refreshes

* fix(tui): keep fallback runs visibly active

* fix(tui): expose full verbose mode and reclaim width

* refactor(tui): drop stale optimistic-send state

* test(tui): drop unused state binding

* docs(changelog): add tui beta note

* fix(tui): bound fallback wait and dedupe pending restore

* fix(tui): preserve queued sends and busy-state visibility

* chore(changelog): align tui pending-send note

* chore(changelog): refine tui release note
2026-04-03 20:39:55 +09:00
Vincent Koc
79da4a46b4 fix(feishu): annotate send target return 2026-04-03 20:36:24 +09:00
Vincent Koc
dc6e041cfe test(bluebubbles): narrow monitor normalize number parsing 2026-04-03 20:36:24 +09:00
Vincent Koc
2ec9d3d58b test(bluebubbles): narrow request-url export 2026-04-03 20:36:24 +09:00
Vincent Koc
69e0fbd95e test(bluebubbles): narrow media-send limit import 2026-04-03 20:36:24 +09:00
Vincent Koc
13412c0c50 test(bluebubbles): narrow send markdown import 2026-04-03 20:36:24 +09:00
Peter Steinberger
afa78a5b13 test: trim telegram testing barrel imports 2026-04-03 12:36:07 +01:00
Peter Steinberger
de1d0f4fae fix(ci): restore telegram real registry test support 2026-04-03 12:31:28 +01:00
samzong
37ab4b7fdc [Feat] Add ClawHub skill search and detail in Control UI (#60134)
* feat(gateway): add skills.search and skills.detail RPC methods

Expose ClawHub search and detail capabilities through the Gateway protocol,
enabling desktop/web clients to browse and inspect skills from the registry.

New RPCs:
- skills.search: search ClawHub skills by query with optional limit
- skills.detail: fetch full detail for a single skill by slug

Both methods delegate to existing agent-layer functions
(searchSkillsFromClawHub, fetchSkillDetailFromClawHub) which wrap
the ClawHub HTTP client. No new external dependencies.

Signed-off-by: samzong <samzong.lu@gmail.com>

* feat(skills): add ClawHub skill search and detail in Control UI

Add skills.search and skills.detail Gateway RPC methods with typed
protocol schemas, AJV validators, and handler implementations. Wire
the new RPCs into the Control UI Skills panel with a debounced search
input, results list, detail dialog, and one-click install from ClawHub.

Gateway:
- SkillsSearchParams/ResultSchema and SkillsDetailParams/ResultSchema
- Handler calls searchClawHubSkills and fetchClawHubSkillDetail directly
- Remove zero-logic fetchSkillDetailFromClawHub wrapper
- 9 handler tests including boundary validation

Control UI:
- searchClawHub, loadClawHubDetail, installFromClawHub controllers
- 300ms debounced search input to avoid 429 rate limits
- Dedicated install busy state (clawhubInstallSlug) with success/error feedback
- Install buttons disabled during install with progress text
- Detail dialog with owner, version, changelog, platform metadata

Part of #43301

Signed-off-by: samzong <samzong.lu@gmail.com>

* fix(skills): guard search and detail responses against stale writes

Signed-off-by: samzong <samzong.lu@gmail.com>

* fix(skills): reset loading flags on query clear and detail close

Signed-off-by: samzong <samzong.lu@gmail.com>

* fix(gateway): register skills.search/detail in read scope and method list

Add skills.search and skills.detail to the operator READ scope group
and the server methods list. Without this, unclassified methods default
to operator.admin, blocking read-only operator sessions.

Also guard the detail loading reset in the finally block by the active
slug to prevent a transient flash when rapidly switching skills.

Signed-off-by: samzong <samzong.lu@gmail.com>

* fix(skills): guard search loading reset by active query

Signed-off-by: samzong <samzong.lu@gmail.com>

* test: cover ClawHub skills UI flow

* fix: clear stale ClawHub search results

---------

Signed-off-by: samzong <samzong.lu@gmail.com>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-04-03 19:30:44 +08:00
Peter Steinberger
d39e4dff6a test: make planner lanes explicit 2026-04-03 12:29:29 +01:00
Peter Steinberger
f4393791eb test: split vitest setup for projects 2026-04-03 12:29:29 +01:00
Peter Steinberger
1dd88c6288 fix(sessions): harden session id resolution 2026-04-03 20:29:20 +09:00
Peter Steinberger
1337be3063 refactor: narrow telegram native command test seams 2026-04-03 12:25:47 +01:00
Peter Steinberger
db6d149f75 test: route telegram plugin tests through extensions 2026-04-03 12:25:47 +01:00
Peter Steinberger
0a2a1ff778 fix(ci): make gateway audit path test platform-safe 2026-04-03 12:22:29 +01:00
Vincent Koc
5021b12ac1 perf(browser): trim invoke-browser test imports 2026-04-03 20:12:40 +09:00
Vincent Koc
045d590542 perf(matrix): isolate probe runtime deps 2026-04-03 20:05:50 +09:00
Vincent Koc
fac89d403b perf(browser): split remote profile tab op tests 2026-04-03 20:03:48 +09:00
Peter Steinberger
d3310f4837 fix(ci): resolve changelog conflict markers 2026-04-03 12:03:22 +01:00
Peter Steinberger
e2e1197fa9 refactor(gateway): clarify local mode guardrails 2026-04-03 20:02:32 +09:00
Peter Steinberger
4e22e75697 test: reduce telegram broad partial mocks 2026-04-03 12:01:10 +01:00
Peter Steinberger
225431665a test: trim telegram media retry import cost 2026-04-03 12:01:10 +01:00
Peter Steinberger
05df7f802b docs: add test cost guardrails 2026-04-03 12:01:10 +01:00
Peter Steinberger
566fc72106 refactor(discord): share proxy resolution helpers 2026-04-03 19:58:23 +09:00
Vincent Koc
53504b3662 fix(agents): suppress profile allowlist warnings 2026-04-03 19:55:05 +09:00
Peter Steinberger
2c7eea8f10 fix(gateway): fail closed on missing mode 2026-04-03 19:50:45 +09:00
Peter Steinberger
a6649201b7 docs: clarify default subagent allowlists 2026-04-03 19:45:05 +09:00
Peter Steinberger
d921784718 fix: support default subagent allowlists (#59944) (thanks @hclsys) 2026-04-03 19:43:17 +09:00
HCL
a57766bad0 fix(agents): fall back to defaults for subagents.allowAgents
resolveAgentConfig().subagents.allowAgents reads only the per-agent
entry, never falling back to agents.defaults.subagents.allowAgents.
Other subagent defaults like runTimeoutSeconds correctly read from
cfg.agents.defaults.subagents — allowAgents was missed.

Root cause: subagent-spawn.ts:463 and agents-list-tool.ts:49 both
use resolveAgentConfig() which returns only per-agent config without
defaults merging. The same pattern is already established at
subagent-spawn.ts:403 for runTimeoutSeconds.

Fix: add cfg.agents.defaults.subagents.allowAgents as fallback when
per-agent entry doesn't specify allowAgents. Both call sites fixed.

Closes #59938

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: HCL <chenglunhu@gmail.com>
2026-04-03 19:42:24 +09:00
Peter Steinberger
50f4bffbb6 fix: unblock Discord land by breaking import cycles (#57465) 2026-04-03 19:38:59 +09:00
Peter Steinberger
32ebaa3757 refactor: share session model resolution helpers 2026-04-03 19:37:56 +09:00
Peter Steinberger
67d87abf7c style: normalize telegram fetch test formatting 2026-04-03 11:37:41 +01:00
Peter Steinberger
bb3ea2137b test: move telegram fetch coverage into extensions 2026-04-03 11:37:41 +01:00
Peter Steinberger
e0c4458a2f test: add lighter extensions vitest setup 2026-04-03 11:37:41 +01:00
Peter Steinberger
52225db134 fix(ci): reuse sdk tool auth error for whatsapp 2026-04-03 11:35:45 +01:00
Peter Steinberger
5400980305 test(plugin-sdk): tighten boundary guardrails 2026-04-03 11:35:37 +01:00
Peter Steinberger
1c26e806ff refactor: simplify gateway startup logs 2026-04-03 11:31:34 +01:00
Peter Steinberger
16ca1f4d74 test: route extension boundary inventory off unit 2026-04-03 11:30:45 +01:00
Peter Steinberger
b406b7d2e4 refactor: extract embedded runner failover helpers 2026-04-03 19:28:39 +09:00
Peter Steinberger
71f8c0344a fix(ci): refresh schema and boundary expectations 2026-04-03 11:26:06 +01:00
Vincent Koc
1a5787137f style(msteams): format split graph message import 2026-04-03 19:23:26 +09:00
Vincent Koc
b53ab34d04 perf(msteams): split graph message tests 2026-04-03 19:23:26 +09:00
Peter Steinberger
a5eb8e08ad test: reroute telegram fetch network policy suite 2026-04-03 11:19:29 +01:00
Vincent Koc
c0a8d07fce test(browser): collapse wrapper suite files 2026-04-03 19:18:49 +09:00
Peter Steinberger
fb0d82ba9f fix(ci): refresh guardrails and config baselines 2026-04-03 11:18:40 +01:00
Peter Steinberger
2766c27b2a refactor(plugin-sdk): genericize web channel runtime seams 2026-04-03 11:17:28 +01:00
Peter Steinberger
182bec5091 test: run boundary inventory suites without global setup 2026-04-03 11:17:00 +01:00
Vincent Koc
d55e580307 perf(acpx): clean up runtime fixtures per test 2026-04-03 19:15:26 +09:00
Vincent Koc
e18611188d test(discord): defer provider runtime mocks 2026-04-03 19:13:10 +09:00
Peter Steinberger
d68840ef40 fix(ci): handle bundled fast-check task 2026-04-03 11:10:50 +01:00
Vincent Koc
e414e51761 perf(nextcloud-talk): split setup test hotspots 2026-04-03 19:09:49 +09:00
Peter Steinberger
80c5764482 refactor(telegram): streamline media runtime options 2026-04-03 19:09:13 +09:00
Peter Steinberger
122e6f0f79 fix(plugins): route runtime imports through sdk facades 2026-04-03 11:07:31 +01:00
Vincent Koc
ddd1c77b49 perf(feishu): narrow hotspot runtime seams 2026-04-03 19:06:49 +09:00
Ayaan Zaidi
1fabc96acc fix: keep device approval scopes aligned (#60208) 2026-04-03 15:34:05 +05:30
Ayaan Zaidi
403e0e6521 fix(pairing): mint tokens for merged device roles 2026-04-03 15:34:05 +05:30
Vincent Koc
61f13173c2 feat(providers): add model request transport overrides (#60200)
* feat(providers): add model request transport overrides

* chore(providers): finalize request override follow-ups

* fix(providers): narrow model request overrides
2026-04-03 19:00:06 +09:00
Peter Steinberger
55e43cbc7f test: isolate bundled plugin coverage from unit 2026-04-03 10:58:44 +01:00
Peter Steinberger
64755c52f2 test: move extension-owned coverage out of core 2026-04-03 10:58:44 +01:00
Vincent Koc
2bfbddb81f perf(browser): remove duplicate heavy test wrappers 2026-04-03 18:57:05 +09:00
Peter Steinberger
355dc7f3a8 fix(msteams): avoid discord approval auth import cycle 2026-04-03 10:55:47 +01:00
Vincent Koc
0657cfbb34 test(feishu): slim bot menu runtime fixtures 2026-04-03 18:54:42 +09:00
Peter Steinberger
6e2b46d666 docs: clarify DM pairing vs group auth 2026-04-03 18:51:51 +09:00
Peter Steinberger
0710cfaa71 chore: ignore local vitest timing artifact 2026-04-03 10:51:26 +01:00
Vincent Koc
294d425ae4 test(feishu): slim comment handler runtime fixtures 2026-04-03 18:49:43 +09:00
Peter Steinberger
86ff57518f fix: keep Discord proxy fallback local (#57465) (thanks @geekhuashan) 2026-04-03 18:49:14 +09:00
geekhuashan
3a4fd62135 test(discord): proxy fetch regression coverage for REST, webhook, and stagger 2026-04-03 18:49:14 +09:00
geekhuashan
c8223606ca fix(discord): proxy Carbon REST, webhook and monitor fetch paths; stagger multi-bot startup 2026-04-03 18:49:14 +09:00
Peter Steinberger
dfb423532b docs(telegram): clarify RFC2544 vs fake-IP SSRF guidance 2026-04-03 18:48:14 +09:00
Vincent Koc
726bfd3434 fix(plugin-sdk): export ssrf policy type 2026-04-03 18:47:31 +09:00
Vincent Koc
1bba19decb perf(msteams): narrow secret and ssrf runtime seams 2026-04-03 18:47:31 +09:00
Vincent Koc
8a58a18a0a test(feishu): slim broadcast runtime fixtures 2026-04-03 18:47:08 +09:00
Peter Steinberger
2ca97a7d48 docs(plugin-sdk): refresh seam cleanup docs 2026-04-03 10:45:11 +01:00
Peter Steinberger
f2d7a825b1 refactor(plugin-sdk): remove channel-specific sdk seams 2026-04-03 10:45:10 +01:00
Peter Steinberger
ad6fdf1e3c test: suppress expected nodes run stderr noise 2026-04-03 10:44:20 +01:00
Peter Steinberger
1a68e55f47 test: stabilize Windows startup fallback daemon tests 2026-04-03 10:43:42 +01:00
Vincent Koc
5e0decd9b5 test(msteams): slim messenger runtime fixtures 2026-04-03 18:42:59 +09:00
Vincent Koc
b55ac9e64d test(msteams): trim attachment test runtime footprint 2026-04-03 18:39:50 +09:00
Vincent Koc
e1093a3177 test(diffs): split render coverage from config tests 2026-04-03 18:39:50 +09:00
Peter Steinberger
4bfa9260ce fix(telegram): add dangerous private-network media opt-in 2026-04-03 18:39:17 +09:00
Peter Steinberger
f29c139a7a test: deduplicate provider discovery contract suite 2026-04-03 18:32:15 +09:00
Peter Steinberger
7bf0496dd8 feat: add qwen3.6-plus to modelstudio catalog 2026-04-03 18:32:14 +09:00
Vincent Koc
ddb7e4cc34 perf(test): add gh run ingestion for memory hotspots (#60187)
* perf(test): add gh run ingestion for memory hotspots

* perf(test): harden gh run hotspot ingestion
2026-04-03 18:30:51 +09:00
Peter Steinberger
87b7bb1d14 fix(agents): harden rate-limit fallback handoff
Co-authored-by: TechFath3r <thetechfath3r@gmail.com>
2026-04-03 18:28:56 +09:00
Vincent Koc
f5c3b409ea Config: separate core/plugin baseline entries (#60162)
* Config: separate core/plugin baseline entries

* Config: split config baseline by kind

* Config: split generated baselines by kind

* chore(build): skip generated baseline shards in local tooling

* chore(build): forbid generated docs in npm pack
2026-04-03 18:26:23 +09:00
Ayaan Zaidi
9e58a0892b fix: align mobile pairing secure endpoint UX (#60128) 2026-04-03 14:51:24 +05:30
Ayaan Zaidi
b9897eec7c fix: align mobile pairing secure endpoint UX (#60128) 2026-04-03 14:51:24 +05:30
Ayaan Zaidi
fadd627467 fix: align mobile pairing secure endpoint UX (#60128) 2026-04-03 14:51:24 +05:30
Ayaan Zaidi
a1d07796fc fix(pairing): honor operator scopes for mixed bootstrap approvals 2026-04-03 14:51:24 +05:30
Ayaan Zaidi
a2b53522eb fix(pairing): allow private lan mobile ws 2026-04-03 14:51:24 +05:30
Ayaan Zaidi
84add47525 fix(pairing): allow emulator ws setup urls 2026-04-03 14:51:24 +05:30
Ayaan Zaidi
acd5734aa9 fix(pairing): align mobile setup with secure endpoints 2026-04-03 14:51:24 +05:30
Vincent Koc
c6f95a0c37 test: summarize diagnostic report memory growth 2026-04-03 18:19:47 +09:00
@zimeg
f9785c63e7 docs(slack): add groups:history scope to app manifest 2026-04-03 02:15:53 -07:00
samzong
61fef8c689 [Fix] Isolate teardown steps so session lock release is unconditional (#59194)
Merged via squash.

Prepared head SHA: 52b3bb46bb
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-03 17:07:24 +08:00
Vincent Koc
44f1887f25 test(diffs): dispose highlighter after each render 2026-04-03 17:49:37 +09:00
Vincent Koc
9771d84c56 test: add report-on-signal diagnostics 2026-04-03 17:47:08 +09:00
Vincent Koc
97c542a67b test(memory-lancedb): avoid repeated dynamic imports 2026-04-03 17:47:08 +09:00
Vincent Koc
de2d4ecd9e test(nextcloud-talk): avoid per-test module resets 2026-04-03 17:47:08 +09:00
Vincent Koc
cb7f74b5eb perf(test): refresh extension memory hotspots from gh logs (#60159) 2026-04-03 17:43:44 +09:00
Vincent Koc
84970d325e docs(changelog): add missing media transport PR number 2026-04-03 17:41:48 +09:00
Vincent Koc
23719dd513 feat(media): add request transport overrides (#59848)
* style(providers): normalize request policy formatting

* style(providers): normalize request policy formatting

* feat(media): add request transport overrides

* fix(secrets): resolve media request secret refs

* fix(secrets): cover shared media request refs

* fix(secrets): scope media request ref activity

* fix(media): align request ref gating
2026-04-03 17:35:26 +09:00
Peter Steinberger
9e47c1a2c5 test: avoid windows task-owner tempdir hangs 2026-04-03 09:26:04 +01:00
Peter Steinberger
1849cf71c2 fix(image): skip inferred resolution for openai edits 2026-04-03 09:20:08 +01:00
@zimeg
dc45faaf4e docs(slack): order recommended scopes and events 2026-04-03 01:10:42 -07:00
@zimeg
6f23e513cc docs(slack): match recommended bot scopes in onboarding 2026-04-03 01:01:25 -07:00
Josh Lehman
2b28e75822 fix: enrich session_end lifecycle hooks (#59715)
Merged via squash.

Prepared head SHA: b3ef62b973
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-03 00:16:14 -07:00
hengm3467
52d8dc5b56 feat: add bundled StepFun provider plugin (#60032)
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
2026-04-02 23:53:50 -07:00
Peter Steinberger
a064da3bef docs(changelog): align 2026.4.x release notes 2026-04-03 15:23:22 +09:00
Peter Steinberger
051b5ddafe test: dedupe core test teardown paths 2026-04-03 07:14:58 +01:00
Peter Steinberger
b66197f932 test: reduce import wrapper reload churn 2026-04-03 07:14:58 +01:00
Peter Steinberger
9bba2ec0ad test: trim extension teardown churn 2026-04-03 07:14:58 +01:00
Peter Steinberger
376a042ba1 test: isolate runtime state in memory tests 2026-04-03 07:14:58 +01:00
Marcus Castro
d2d9a928b1 WhatsApp: honor block streaming config (#60069) 2026-04-03 03:12:37 -03:00
Brad Groux
dda53a2ff8 test: update gateway.mode test to match default-to-local behavior (#54801) (#60094)
The test previously asserted that a valid snapshot without gateway.mode
blocks startup. After defaulting gateway.mode to 'local' when unset,
the gateway should start successfully in this scenario — update the
test to verify the new expected behavior.

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-03 00:59:51 -05:00
Brad Groux
9978d2276b fix: default gateway.mode to 'local' when unset (#54801) (#60085)
After v2026.3.24 introduced a gateway.mode guard, startup fails on
Windows (and other platforms) when the config file exists but doesn't
contain an explicit gateway.mode value. This happens after 'openclaw
onboard' writes a minimal config without gateway settings.

Default to 'local' when the mode is unset, restoring pre-3.24 behavior
where the gateway started without requiring an explicit mode.

Fixes #54801

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-03 00:23:06 -05:00
Brad Groux
6e94b047e2 fix: improve WS handshake reliability on slow-startup environments (#60075)
* fix: import CHANNEL_IDS from leaf module to avoid TDZ on init (#48832)

schema.ts and validation.ts imported CHANNEL_IDS from channels/registry.js,
which re-exports from channels/ids.js but also imports plugins/runtime.js.
When the bundler resolves this dependency graph, the re-exported CHANNEL_IDS
can be undefined at the point config/validation.ts evaluates (temporal dead
zone), causing 'CHANNEL_IDS is not iterable' on startup.

Fix: import CHANNEL_IDS directly from channels/ids.js (the leaf module with
zero heavy dependencies) and normalizeChatChannelId from channels/chat-meta.js.

Fixes #48832

* fix: improve WS handshake reliability on slow-startup environments (#48736)

On Windows with large dist bundles (46MB/639 files), heavy synchronous
module loading blocks the event loop during CLI startup, preventing
timely processing of the connect.challenge frame and causing ~80%
handshake timeout failures.

Changes:
- Yield event loop (setImmediate) before starting WS connection in
  callGateway to let pending I/O drain after heavy module loading
- Add OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS env var override for
  client-side connect challenge timeout (server already has
  OPENCLAW_HANDSHAKE_TIMEOUT_MS)
- Include diagnostic timing in challenge timeout error messages
  (elapsed vs limit) for easier debugging
- Add tests for env var override and resolution logic

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-03 00:21:14 -05:00
Brad Groux
0aa98a8e3b fix: import CHANNEL_IDS from leaf module to avoid TDZ on init (#48832) (#60061)
schema.ts and validation.ts imported CHANNEL_IDS from channels/registry.js,
which re-exports from channels/ids.js but also imports plugins/runtime.js.
When the bundler resolves this dependency graph, the re-exported CHANNEL_IDS
can be undefined at the point config/validation.ts evaluates (temporal dead
zone), causing 'CHANNEL_IDS is not iterable' on startup.

Fix: import CHANNEL_IDS directly from channels/ids.js (the leaf module with
zero heavy dependencies) and normalizeChatChannelId from channels/chat-meta.js.

Fixes #48832

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
2026-04-03 00:20:17 -05:00
samzong
2b9981bd06 [Fix] Remove spurious plugin id mismatch warning (#59185)
Merged via squash.

Prepared head SHA: 82859a0f92
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
2026-04-03 12:58:56 +08:00
Ayaan Zaidi
251fa72798 fix: emit passive hooks for mention-skipped group messages (#60018)
* fix(channels): emit passive hooks for mention-skipped group messages

* fix(channels): honor signal ingest overrides

* fix(channels): honor telegram ingest fallback

* fix: emit passive hooks for mention-skipped group messages (#60018)
2026-04-03 10:14:48 +05:30
Vincent Koc
9b80344e58 docs: rewrite automation decision guide with current nomenclature 2026-04-03 13:26:32 +09:00
Vincent Koc
a2b6fdc3df docs: rebuild automation section for coherence and readability 2026-04-03 13:11:23 +09:00
Ayaan Zaidi
d5ea5f27ac fix: parse kimi tagged tool calls (#60051)
* fix: parse kimi tagged tool calls

* fix: parse kimi tagged tool calls (#60051)

* fix: parse kimi tagged tool calls (#60051)
2026-04-03 09:39:19 +05:30
Vincent Koc
98137f7f80 perf(test): isolate memory-heavy extension hotspots (#59879)
* perf(test): isolate memory-heavy extension hotspots

* fix(test): pin extension memory hotspot threshold
2026-04-03 13:01:09 +09:00
Peter Steinberger
e3674bcc04 test: streamline runtime wrapper test reloads 2026-04-03 04:41:38 +01:00
Peter Steinberger
ffd34f8896 test: reduce agent test import churn 2026-04-03 04:41:09 +01:00
Peter Steinberger
847faa3d04 test: trim extension test import churn 2026-04-03 04:41:08 +01:00
Vincent Koc
2f013b68f8 docs: add missing changelog entries and update context visibility security docs 2026-04-03 12:39:45 +09:00
v1p0r
3a7555a27b "fix(telegram): surface media placeholder and file_id when download f… (#59948)
* "fix(telegram): surface media placeholder and file_id when download fails"

* fix: unify telegram media placeholder selection

* fix: preserve telegram media context on captioned download failures (#59948) (thanks @v1p0r)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-03 09:08:05 +05:30
samzong
0e0ad692b5 fix: persist Telegram reaction ownership across restart (#59207) (thanks @samzong) 2026-04-03 08:57:06 +05:30
Ayaan Zaidi
a5d6e519eb fix: preserve explicit Telegram topic targets on replies (#59634) (thanks @dashhuang) 2026-04-03 08:50:02 +05:30
Cindy
c001c090b9 fix(telegram): keep topic thread when replying with message tool 2026-04-03 08:50:02 +05:30
Ted-developer
dd080b6fb0 fix(msteams): download DM inline images via Graph API (#52212)
Fix three bugs preventing inline image downloads in Teams 1:1 DM chats: wrong conversation ID format for Graph API, missing media URL extraction, and incorrect content type detection.

Fixes #24797

Thanks @Ted-developer
2026-04-02 22:14:02 -05:00
saram ali
985533efbc fix: cover buffered Telegram apiRoot downloads (#59544) (thanks @SARAMALI15792)
* test(telegram): add URL construction tests for custom apiRoot

Add comprehensive test cases to verify that file download URLs are correctly
constructed when using a custom apiRoot configuration for local Bot API servers.

Tests validate:
- Document downloads use the custom apiRoot in the constructed URL
- Sticker downloads use the custom apiRoot in the constructed URL
- SSRF policy correctly includes the custom hostname

This ensures issue #59512 (Telegram file downloads with local Bot API) is
properly covered by regression tests.

* refactor(telegram): improve media resolution code quality

Apply KISS and YAGNI principles to reduce code duplication and improve maintainability:

1. Extract media metadata resolution
   - Consolidate resolveMediaFileRef(), resolveTelegramFileName(), and
     resolveTelegramMimeType() into single resolveMediaMetadata() function
   - Returns typed MediaMetadata object with fileRef, fileName, mimeType
   - Reduces duplication and improves readability

2. Add logging for apiRoot parsing failures
   - Log when custom apiRoot URL parsing fails in buildTelegramMediaSsrfPolicy()
   - Helps debug configuration issues with local Bot API servers

3. Fix missing apiRoot in buffered messages
   - Add telegramCfg.apiRoot parameter to resolveMedia() calls in
     bot-handlers.buffers.ts (lines 150-159, 189)
   - Ensures reply media in buffered contexts respects custom apiRoot config
   - Fixes inconsistency where runtime handler passed apiRoot but buffers didn't

These changes improve code quality while maintaining backward compatibility and
ensuring issue #59512 (Telegram file downloads with local Bot API) works correctly
in all contexts.

* fix(telegram): resolve bot review issues

Address critical issues identified by Greptile code review:

1. Define telegramCfg in bot-handlers.buffers.ts
   - Extract telegramCfg from cfg.channels?.telegram
   - Fixes ReferenceError when accessing telegramCfg.apiRoot
   - Ensures buffered message handlers can access apiRoot configuration

2. Restore type safety for MediaMetadata.fileRef
   - Change from 'unknown' to proper union type
   - Preserves type information for downstream file_id access
   - Prevents TypeScript strict mode compilation errors

These fixes ensure the PR compiles correctly and handles buffered
media downloads with custom apiRoot configuration.

* fix(telegram): use optional chaining for telegramCfg.apiRoot

TypeScript strict mode requires optional chaining when accessing
properties on potentially undefined objects. Changed telegramCfg.apiRoot
to telegramCfg?.apiRoot to handle cases where telegramCfg is undefined.

Fixes TypeScript errors:
- TS18048: 'telegramCfg' is possibly 'undefined' (line 160)
- TS18048: 'telegramCfg' is possibly 'undefined' (line 191)

* fix(telegram): add missing optional chaining on line 191

Complete the fix for telegramCfg optional chaining.
Previous commit only fixed line 160, but line 191 also needs
the same fix to prevent TS18048 error.

* fix: cover buffered Telegram apiRoot downloads (#59544) (thanks @SARAMALI15792)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-03 08:41:41 +05:30
Derek YU
5f6e3499f3 fix: detect PID recycling in gateway lock on Windows/macOS + startup progress (#59843)
Fix stale lock files from crashed gateway processes blocking new invocations on Windows/macOS. Detect PID recycling to avoid false positive lock conflicts, and add startup progress indicator.

Thanks @TonyDerek-dot
2026-04-02 22:07:35 -05:00
Neerav Makwana
7c7098fd1d fix: keep inbound images readable on upgraded installs (#59971) (thanks @neeravmakwana)
* fix(telegram): allow inbound media from config dir

Made-with: Cursor

* test: simplify local roots regression

* fix: keep inbound images readable on upgraded installs (#59971) (thanks @neeravmakwana)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-03 08:24:29 +05:30
Gustavo Madeira Santana
0fdb55c4e4 Docs: refresh diffs plugin docs 2026-04-02 21:47:55 -04:00
Gustavo Madeira Santana
ebc9784f26 docs: fix Matrix plugin docs 2026-04-02 21:47:34 -04:00
Gustavo Madeira Santana
78a842d055 fix(matrix): align IDB snapshot lock timing 2026-04-02 21:44:08 -04:00
Gustavo Madeira Santana
1efa923ab8 Matrix: add native exec approvals (#58635)
Merged via squash.

Prepared head SHA: d9f048e827
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-02 21:08:54 -04:00
Gustavo Madeira Santana
9e2cbf9a30 chore: tighten pr push script 2026-04-02 20:56:51 -04:00
Alejandro Martinez
3a91a4f8d4 fix(matrix): add advisory file locking to IDB crypto persistence (#59851)
Merged via squash.

Prepared head SHA: 392e411ffd
Co-authored-by: al3mart <11448715+al3mart@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-02 20:19:40 -04:00
Alejandro Martinez
b894ca6702 fix(matrix): allow secret storage recreation during repair bootstrap (#59846)
Merged via squash.

Prepared head SHA: 06b10f61eb
Co-authored-by: al3mart <11448715+al3mart@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-02 20:11:58 -04:00
Hyojin Kwak
739ed1bf29 fix(msteams): preserve channel reply threading in proactive fallback (#55198)
When a thread reply's turn context is revoked and falls back to proactive messaging, the normalized conversation ID lost the thread suffix, causing replies to land in the channel root instead of the original thread.

Reconstructs the threaded conversation ID (`;messageid=<activityId>`) for channel conversations in the proactive fallback path, while correctly leaving group chat conversations flat.

Fixes #27189

Thanks @hyojin
2026-04-02 18:27:13 -05:00
Josh Lehman
ed8d5b3797 fix: add Telegram native progress placeholder opt-in for plugin commands (#59300)
Merged via squash.

Prepared head SHA: 4f5bc22a89
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-02 15:55:46 -07:00
Bruce MacDonald
5f4077cc7d fix(ollama): prefer real cloud auth over local marker 2026-04-02 15:51:57 -07:00
Peter Steinberger
64581c655d test: make plugin activation boundary env agnostic 2026-04-03 05:02:58 +09:00
Peter Steinberger
bff6025bde test: refresh generated baselines 2026-04-03 04:54:59 +09:00
Peter Steinberger
15b005489d chore: bump version to 2026.4.3 2026-04-03 04:54:59 +09:00
Peter Steinberger
49f67f54f4 docs: fix task flow release note command 2026-04-03 04:39:13 +09:00
Peter Steinberger
6f67347e00 ci: restore npm token auth for dist-tag promotion 2026-04-02 20:37:49 +01:00
Peter Steinberger
04cf29f613 refactor: speed up whatsapp inbound dispatch tests 2026-04-03 04:34:58 +09:00
Peter Steinberger
694d12a90b refactor: apply context visibility across channels 2026-04-03 04:34:57 +09:00
Peter Steinberger
35e1605147 feat: add configurable context visibility 2026-04-03 04:34:57 +09:00
Peter Steinberger
d4d2d9e479 ci: move npm promotion into trusted workflow 2026-04-02 20:29:57 +01:00
Peter Steinberger
658f0c5d2d ci: use oidc token for npm promotion 2026-04-02 20:23:56 +01:00
Peter Steinberger
dbfb13b93a build: update appcast for 2026.4.2 2026-04-02 20:08:40 +01:00
Vincent Koc
883df8c6a8 fix(plugins): reuse runtime registries for web provider snapshots (#59865)
* fix(plugins): reuse runtime registries for web providers

* test(plugins): clarify runtime reuse intent

* chore(changelog): note web provider runtime reuse
2026-04-03 04:07:43 +09:00
Agustin Rivera
193fdd6e3b fix(policy): preserve restrictive tool allowlists (#58476)
* fix(policy): preserve restrictive tool allowlists

Co-authored-by: David Silva <david.silva@gendigital.com>

* fix(policy): address review follow-ups

* fix(policy): restore additive alsoAllow semantics

* fix(policy): preserve optional tool opt-ins for allow-all configs

* fix(policy): narrow plugin-only allowlist warnings

* fix(policy): add changelog entry

* Revert "fix(policy): add changelog entry"

This reverts commit 4a996bf4ca.

* chore: add changelog for restrictive tool allowlists

---------

Co-authored-by: David Silva <david.silva@gendigital.com>
Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-02 12:55:36 -06:00
Peter Steinberger
9f85595d80 fix: pin anthropic sdk to patched version 2026-04-02 19:50:05 +01:00
Vincent Koc
d34bca3ce6 fix(plugins): reuse runtime registry for provider resolution (#59856)
* fix(plugins): reuse runtime registry for provider resolution

* test(plugins): align provider runtime helper names
2026-04-03 03:40:24 +09:00
Peter Steinberger
be4be5e783 fix: improve parallels smoke progress 2026-04-02 19:39:23 +01:00
Agustin Rivera
d631326c5e fix(tailscale): gate test binary override (#58468)
* fix(tailscale): gate test binary override

* fix(changelog): note tailscale override hardening

* fix(changelog): drop tailscale note from pr

* chore: add changelog for tailscale test binary gating

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-02 12:39:10 -06:00
Vincent Koc
f32a5b30db chore(skills): align taskflow skill with runtime 2026-04-03 03:38:02 +09:00
Peter Steinberger
d74a12264a fix: mirror bedrock runtime dep in root package 2026-04-02 19:26:56 +01:00
Vincent Koc
f911bbc353 refactor(plugins): separate activation from enablement (#59844)
* refactor(plugins): separate activation from enablement

* fix(cli): sanitize verbose plugin activation reasons
2026-04-03 03:22:37 +09:00
Vincent Koc
4aeb0255f3 docs: rename TaskFlow to Task Flow in prose 2026-04-03 03:22:01 +09:00
Vincent Koc
d9c662dc69 docs: restructure automation section as Automation & Tasks 2026-04-03 03:16:51 +09:00
Peter Steinberger
3bd2bbea34 docs: clarify npm release workflow inputs 2026-04-02 19:11:01 +01:00
Peter Steinberger
0ebb69b882 build: set release version to 2026.4.2 2026-04-02 19:09:58 +01:00
Peter Steinberger
38bd525888 test: align strict inline-eval awk denial expectation 2026-04-02 19:09:39 +01:00
Peter Steinberger
209535b7c7 build: make npm release tag configurable 2026-04-02 19:06:37 +01:00
Vincent Koc
bcd61e54e1 docs: fix TaskFlow CLI command path and CLI task notify policy 2026-04-03 03:03:00 +09:00
Peter Steinberger
9f3a26caa6 fix(discord): quiet Carbon reconcile log 2026-04-02 18:55:34 +01:00
Agustin Rivera
49d08382a9 iOS: restrict A2UI action dispatch to trusted canvas URLs (#58471)
* fix(ios): restrict a2ui bridge trust

* test(ios): cover fragment-strip trust and document raw-string equality

* fix(ios): normalize capability URL before trust comparison in canvas commands

* fix(ios): trim canvas.navigate url before trust comparison

* chore: add changelog for iOS A2UI trust boundary

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-02 11:51:09 -06:00
Peter Steinberger
00aa31a30c docs(changelog): remove duplicate entries 2026-04-02 18:48:27 +01:00
Vincent Koc
7aa22959e4 refactor(tasks): rename registry hooks to observers (#59829) 2026-04-03 02:42:59 +09:00
Agustin Rivera
676b748056 Limit connect snapshot metadata to admin-scoped clients (#58469)
* fix(gateway): gate connect snapshot metadata by scope

* fix(gateway): clarify connect snapshot trust boundary

* fix(gateway): note connect snapshot change in changelog

* fix(gateway): remove changelog changes from PR

* chore: add changelog for scoped gateway snapshot metadata

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-02 11:41:47 -06:00
Peter Steinberger
a4a372825e docs(changelog): reorder unreleased fixes 2026-04-02 18:39:31 +01:00
Peter Steinberger
45c8207ef2 fix(exec): clarify auto routing semantics (#58897) (thanks @vincentkoc) 2026-04-03 02:37:12 +09:00
Vincent Koc
938541999e Delete docs/internal/codex/2026-03-29-exec-target-override-fix.md 2026-04-03 02:37:12 +09:00
Vincent Koc
5dca81271c fix(exec): clarify and cover auto host override guard 2026-04-03 02:37:12 +09:00
Vincent Koc
dae6632da1 Security: block exec host overrides under auto target 2026-04-03 02:37:12 +09:00
Agustin Rivera
5874a387ae fix(windows): reject unresolved cmd wrappers (#58436)
* fix(windows): reject unresolved cmd wrappers

* fix(windows): add wrapper policy coverage

* fix(windows): document wrapper fallback migration

* fix(windows): drop changelog entry from pr

* chore: add changelog for Windows wrapper fail-closed behavior

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
2026-04-02 11:35:50 -06:00
Peter Steinberger
3e452f2671 fix: preserve strict inline-eval approval boundaries (#59780) (thanks @luoyanglang) 2026-04-02 18:30:29 +01:00
Peter Steinberger
f03d7c5a4c refactor: centralize Windows exec invocation 2026-04-02 18:27:53 +01:00
Peter Steinberger
d56415e353 fix(openai): support reference-image edits 2026-04-03 02:26:33 +09:00
luoyanglang
f0a4bbba33 test(tasks): close flow registry before temp-dir cleanup 2026-04-03 02:25:48 +09:00
luoyanglang
68d8e15a2e fix(exec): satisfy allowlist predicate type checks 2026-04-03 02:25:48 +09:00
luoyanglang
7c83cae425 fix(exec): keep strict inline-eval interpreter approvals reusable 2026-04-03 02:25:48 +09:00
Agustin Rivera
a941a4fef9 fix(android): require TLS for remote gateway endpoints (#58475)
* fix(android): require tls for remote gateway endpoints

* fix(android): expand loopback gateway coverage

* fix(android): validate scanned gateway endpoints

* fix(android): handle mapped loopback literals

* fix(android): allow emulator bridge host

* fix(changelog): note android gateway tls hardening

* fix(android): preserve first-time tls trust prompts

* fix(changelog): drop android gateway entry from pr

* fix(android): scope emulator bridge tls bypass

* fix(android): normalize ipv6 gateway hosts

* fix(android): preserve ipv6 gateway url brackets

* fix(android): preserve auth across tls trust prompt

* fix(android): normalize bracketed ipv6 gateway hosts

* chore: add changelog for Android remote gateway TLS

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
Co-authored-by: Devin Robison <drobison00@users.noreply.github.com>
2026-04-02 11:23:51 -06:00
Peter Steinberger
2ea0ca08f6 test: add cross-provider approval availability coverage (#59776) (thanks @joelnishanth) 2026-04-03 02:21:17 +09:00
joelnishanth
d5865bbcc2 fix: decouple approval availability from native delivery enablement (#59620)
getActionAvailabilityState in createApproverRestrictedNativeApprovalAdapter
was gating on both hasApprovers AND isNativeDeliveryEnabled, causing
Telegram exec approvals to report "not allowed" when
channels.telegram.execApprovals.target was configured but
execApprovals.enabled was not explicitly true. The availability check
should only depend on whether approvers exist; native delivery mode is
a routing concern handled downstream.
2026-04-03 02:21:17 +09:00
Peter Steinberger
9b48a4d90a docs: fix changelog conflict markers (#59466) 2026-04-03 02:19:32 +09:00
Peter Steinberger
bacc938c2a docs: note windows exec landing (#59466) (thanks @lawrence3699) 2026-04-03 02:19:32 +09:00
lawrence3699
2fd7f7ca52 fix(exec): hide windows console windows 2026-04-03 02:19:32 +09:00
pgondhi987
7eb094a00d fix(infra): align env key normalization in approval binding path (#59182)
* fix: address issue

* fix: address PR review feedback

* fix: address review feedback

* fix: address review feedback

* chore: add changelog for Windows env approval binding

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-02 11:14:33 -06:00
Vincent Koc
774beb8e5c refactor(plugin-sdk): add task domain runtime surfaces (#59805)
* refactor(plugin-sdk): add task domain runtime views

* chore(plugin-sdk): refresh api baseline

* fix(plugin-sdk): preserve task runtime owner isolation
2026-04-03 02:11:21 +09:00
Peter Steinberger
f30b4bc717 fix: remove leaked changelog conflict marker 2026-04-02 18:07:39 +01:00
Peter Steinberger
fc76f667c2 test: isolate task flow link validation stores 2026-04-03 02:04:26 +09:00
Peter Steinberger
a406045f2f test: accept Windows exec approval denial path 2026-04-03 02:04:26 +09:00
Peter Steinberger
247a06813e fix: avoid gateway cwd for node exec (#58977) (thanks @Starhappysh) 2026-04-03 02:04:26 +09:00
jianxing zhang
50b270a86b fix: widen HostExecApprovalParams.cwd to string | undefined
Remote node exec may have no explicit cwd when the gateway's own
process.cwd() is omitted. Allow undefined to flow through the
approval request type.
2026-04-03 02:04:26 +09:00
jianxing zhang
302c6e30bb fix: resolve type errors where workdir (string | undefined) flows to string-only params
After the node early-return, narrow workdir back to string via
resolvedWorkdir for gateway/sandbox paths. Update
buildExecApprovalPendingToolResult and buildApprovalPendingMessage
to accept string | undefined for cwd since node execution may omit it.
2026-04-03 02:04:26 +09:00
jianxing zhang
3b3191ab3a fix(exec): skip gateway cwd injection for remote node host
When exec runs with host=node and no explicit cwd is provided, the
gateway was injecting its own process.cwd() as the default working
directory. In cross-platform setups (e.g. Linux gateway + Windows node),
this gateway-local path does not exist on the node, causing
"SYSTEM_RUN_DENIED: approval requires an existing canonical cwd".

This change detects when no explicit workdir was provided (neither via
the tool call params.workdir nor via agent defaults.cwd) and passes
undefined instead of the gateway cwd. This lets the remote node use its
own default working directory.

Changes:
- bash-tools.exec.ts: Track whether workdir was explicitly provided;
  when host=node and no explicit workdir, pass undefined instead of
  gateway process.cwd()
- bash-tools.exec-host-node.ts: Accept workdir as string | undefined;
  only send cwd to system.run.prepare when defined
- bash-tools.exec-approval-request.ts: Accept workdir as
  string | undefined in HostExecApprovalParams

Fixes #58934

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 02:04:26 +09:00
pgondhi987
8aceaf5d0f fix(security): close fail-open bypass in exec script preflight [AI] (#59398)
* fix: address issue

* fix: finalize issue changes

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address review-pr skill feedback

* fix: address PR review feedback

* fix: address review-pr skill feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address review-pr skill feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address review-pr skill feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address review-pr skill feedback

* fix: address PR review feedback

* fix: address PR review feedback

* fix: address PR review feedback

* chore: add changelog for exec preflight fail-closed hardening

---------

Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-02 11:00:39 -06:00
Peter Steinberger
e36c563775 refactor(exec): dedupe executable candidate resolution 2026-04-03 01:58:37 +09:00
SudheerDev-AIML
48279dca84 UI: apply accent color to Settings page header and content headings
Fixes #52576 — the accent/theme color was not applied to the Settings
page title, breadcrumb, section headings, or theme card labels. Changed
four CSS rules from var(--text-strong) to var(--accent) so they reflect
the selected theme consistently.
2026-04-02 11:57:09 -05:00
Vincent Koc
990545181b fix(ci): preserve strict inline-eval denial after durable awk trust 2026-04-03 01:55:01 +09:00
Peter Steinberger
2170d36171 docs(changelog): add Windows drive-less exec fix note (#58040) (thanks @SnowSky1) 2026-04-03 01:53:25 +09:00
SnowSky1
e6ce31eb54 fix(exec): ignore malformed drive-less windows exec paths 2026-04-03 01:53:25 +09:00
Agustin Rivera
a26f4d0f3e Separate Gemini OAuth state from PKCE verifier (#59116)
* fix(google): separate oauth state from pkce verifier

* fix(google): drop unused oauth callback state arg

* docs(changelog): add #59116 google oauth state fix

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
2026-04-02 09:51:11 -07:00
Vincent Koc
367969759c perf(memory): trim matrix host validation imports 2026-04-03 01:48:09 +09:00
Vincent Koc
47f5d72931 chore(checks): serialize local heavy gates 2026-04-03 01:46:28 +09:00
Devin Robison
96b55821bc fix: share ACP owner-only approval classes (#201) (#59255)
Co-authored-by: OpenClaw Dummy Agent <octriage-dummy@example.invalid>
2026-04-02 10:45:41 -06:00
Jacob Tomlinson
176c059b05 node-host: bind pnpm dlx approval scripts (#58374)
* node-host: bind pnpm dlx approval scripts

* node-host: cover pnpm dlx package alias

* node-host: cover pnpm dlx flag forms

* node-host: fail closed on unsafe pnpm dlx flags

* node-host: narrow pnpm dlx fail-closed guard

* node-host: scan pnpm dlx past global --

* node-host: allow pnpm dlx file args

* node-host: allow pnpm dlx data args

* node-host: fail closed on unknown pnpm dlx flags

* node-host: support pnpm workspace-root flag

* node-host: restrict pnpm dlx tail scan

* node-host: support pnpm parallel flag

* changelog: node-host pnpm dlx approval binding (#58374)
2026-04-02 09:41:28 -07:00
pgondhi987
7cea7c2970 fix(zalo): scope replay dedupe cache key to path and account [AI] (#59387)
* fix: address issue #139

* changelog: add zalo replay dedupe fix entry

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
2026-04-02 09:36:35 -07:00
Peter Steinberger
d5b6bfc48c test(discord): align native approval fixture with auto mode 2026-04-02 17:33:35 +01:00
Vincent Koc
e4818a345e test(tasks): close flow registry before temp dir cleanup 2026-04-03 01:32:05 +09:00
Peter Steinberger
bf1fcf2e5f docs(approvals): clarify auto native approval routing 2026-04-02 17:31:02 +01:00
Peter Steinberger
17f6626ffe feat(approvals): auto-enable native chat approvals 2026-04-02 17:30:40 +01:00
Peter Steinberger
721cab2b8d refactor(exec): split allowlist segment evaluation helpers 2026-04-03 01:22:25 +09:00
Peter Steinberger
812a7636fb refactor: simplify exec approval followup delivery 2026-04-02 17:19:42 +01:00
Peter Steinberger
47dcfc49b8 fix: scope #57584 to shell allowlist changes 2026-04-03 01:11:20 +09:00
Ayaan Zaidi
34a5c47351 fix: preserve Android assistant auto-send queue 2026-04-02 21:39:24 +05:30
pgondhi987
462b4020bc fix(browser): block SSRF redirect bypass via real-time route interception (#58771)
Install a Playwright route handler before `page.goto()` so navigations
to private/internal IPs are intercepted and aborted mid-redirect instead
of being checked post-hoc after the request already reached the internal
host. Blocked targets are permanently marked and rejected for subsequent
tool calls.

Thanks @pgondhi987
2026-04-02 09:07:57 -07:00
biao
8d81e76f23 fix: evaluate shell wrapper inline commands against allowlist (#57377) (#57584)
When a skill constructs a compound command via a shell wrapper
(e.g. `sh -c "cat SKILL.md && gog-wrapper calendar events"`),
the allowlist check was comparing `/bin/sh` instead of the actual
target binaries, causing the entire command to be silently rejected.

This adds recursive inline command evaluation that:
- Detects chain operators (&&, ||, ;) in the -c payload
- Parses each sub-command independently via analyzeShellCommand
- Evaluates every sub-command against the allowlist
- Preserves per-sub-command segmentSatisfiedBy for accurate tracking
- Limits recursion depth to 3 to prevent abuse
- Skips recursion on Windows (no POSIX shell semantics)

Closes #57377

Co-authored-by: WZBbiao <wangzhenbiao326@gmail.com>
2026-04-03 01:06:40 +09:00
Peter Steinberger
578a0ed31a refactor(agent): dedupe tool error summary 2026-04-02 17:05:05 +01:00
Ayaan Zaidi
59bdf870b9 fix: add Android assistant auto-send changelog (#59721) 2026-04-02 21:27:14 +05:30
Ayaan Zaidi
5d524617e1 fix: clear stale Android assistant auto-send queue 2026-04-02 21:27:14 +05:30
Ayaan Zaidi
186647cb74 feat: auto-send Android assistant prompts 2026-04-02 21:27:14 +05:30
seonang
4207ca2eb8 Fix Telegram exec approval delivery and auto-resume fallback 2026-04-03 00:56:54 +09:00
Gustavo Madeira Santana
b5161042b7 Diffs: validate viewerBaseUrl in manifest schema
Reject invalid diffs viewerBaseUrl values during manifest config validation,
not later during plugin registration.

Keep runtime normalization intact and add manifest-level coverage so bad
protocols and query/hash values fail fast.
2026-04-02 11:55:05 -04:00
Priyansh Gupta
77e636cf78 fix(agents): include received keys in missing-param error for write tool (#55317)
Merged via squash.

Prepared head SHA: c1cf0691c9
Co-authored-by: priyansh19 <33621094+priyansh19@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-04-02 08:54:28 -07:00
Peter Steinberger
c0b6531ec7 docs: add changelog for cron exec timeout fix (#58247) (thanks @skainguyen1412) 2026-04-03 00:43:42 +09:00
spaceman1412
3b6825ab93 Cron: honor trigger for custom session timeouts 2026-04-03 00:43:42 +09:00
spaceman1412
102462b7a6 Cron: restrict exec visibility to timeouts 2026-04-03 00:43:42 +09:00
spaceman1412
d300a20440 Cron: surface exec timeouts in cron runs 2026-04-03 00:43:42 +09:00
Peter Steinberger
047b701859 refactor(telegram): unify callback-data byte limit checks 2026-04-03 00:38:44 +09:00
Peter Steinberger
7e2a450e31 docs: remove duplicated beta changelog fixes 2026-04-02 16:33:51 +01:00
Peter Steinberger
1f531d373b docs: dedupe changelog mirror fixes 2026-04-02 16:33:21 +01:00
Peter Steinberger
423f7c3487 build: prep 2026.4.2-beta.1 release 2026-04-02 16:33:21 +01:00
Vincent Koc
0ad2dbd307 fix(providers): route image generation through shared transport (#59729)
* fix(providers): route image generation through shared transport

* fix(providers): use normalized minimax image base url

* fix(providers): fail closed on image private routes

* fix(providers): bound shared HTTP fetches
2026-04-03 00:32:37 +09:00
Vincent Koc
d2ce3e9acc perf(plugins): keep gateway startup channel-only (#59754)
* perf(plugins): keep gateway startup channel-only

* fix(gateway): preserve startup sidecars in plugin scope
2026-04-03 00:28:15 +09:00
Peter Steinberger
988f7627de refactor(telegram): centralize approval callback shaping 2026-04-03 00:26:27 +09:00
Vincent Koc
efe9464f5f fix(tasks): tighten task-flow CLI surface (#59757)
* fix(tasks): tighten task-flow CLI surface

* fix(tasks): sanitize task-flow CLI text output
2026-04-03 00:25:10 +09:00
Peter Steinberger
0a76780f57 docs(changelog): mark 2026.4.1 as stable 2026-04-02 16:19:06 +01:00
Peter Steinberger
874a585d57 refactor(agent): share exec parser and runtime context codec 2026-04-03 00:15:43 +09:00
Vincent Koc
576337ef31 fix(tasks): use no-persist cleanup in executor tests 2026-04-03 00:15:02 +09:00
Peter Steinberger
8c3295038c test: harden task executor state-dir cleanup 2026-04-02 16:12:24 +01:00
Peter Steinberger
eb261fa690 fix: land Windows exec allowlist (#56285) (thanks @kpngr) 2026-04-03 00:09:28 +09:00
Peter Steinberger
36d953aab6 fix(exec): make Windows exec hints accurate and dynamic 2026-04-03 00:09:28 +09:00
Peter Steinberger
fff6333773 fix(exec): implement Windows argPattern allowlist flow 2026-04-03 00:09:28 +09:00
Vincent Koc
cc5146b9c6 fix(tasks): reset heartbeat and system event state in executor tests 2026-04-03 00:02:32 +09:00
Peter Steinberger
a5f99f4a30 test: stabilize docker test lanes 2026-04-02 15:59:23 +01:00
Vincent Koc
d46240090a test(tasks): add task-flow operator coverage (#59683) 2026-04-02 23:58:33 +09:00
Vincent Koc
3872a866a1 fix(xai): make x_search auth plugin-owned (#59691)
* fix(xai): make x_search auth plugin-owned

* fix(xai): restore x_search runtime migration fallback

* fix(xai): narrow legacy x_search auth migration

* fix(secrets): drop legacy x_search target registry entry

* fix(xai): no-op knob-only x_search migration fallback
2026-04-02 23:54:07 +09:00
Leo Zhang
b6debb4382 fix(agent): close remaining internal-context leak paths (#59649)
* fix(status): strip internal runtime context from task detail surfaces

* fix(agent): narrow legacy internal-context stripping

* fix(tasks): sanitize user-facing task status surfaces

* fix(agent): close remaining internal-context leak paths

* fix(agent): harden internal context delimiter sanitization

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-04-02 23:45:06 +09:00
Peter Steinberger
831729be4a docs(changelog): note telegram approval alias fix (#59217) (thanks @jameslcowan) 2026-04-02 23:41:12 +09:00
Peter Steinberger
52866656c3 fix(telegram): preserve allow-always callback alias 2026-04-02 23:41:12 +09:00
mappel-nv
53c29df2a9 Channel setup: ignore untrusted workspace shadows (#59158)
Keeps untrusted workspace channel metadata from overriding setup/login resolution for built-in channels. Workspace channel entries are only eligible during setup when the plugin is already explicitly trusted in config.

- Track discovered origin on channel catalog entries and add a setup-time catalog lookup that excludes workspace discoveries when needed
- Add resolver regression coverage for untrusted shadowing and trusted workspace overrides

Thanks @mappel-nv
2026-04-02 07:40:23 -07:00
Vincent Koc
4251ad6638 fix(telegram): allow trusted explicit proxy media fetches 2026-04-02 23:36:17 +09:00
James Cowan
7fea8250fb fix(approvals): use canonical decision values in interactive button payloads 2026-04-02 23:35:23 +09:00
4836 changed files with 228381 additions and 170474 deletions

View File

@@ -29,9 +29,11 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Preferred entrypoint: `pnpm test:parallels:npm-update`
- Flow: fresh snapshot -> install npm package baseline -> smoke -> install current main tgz on the same guest -> smoke again.
- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check.
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. On Peter's current host, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
- The aggregate npm-update wrapper must resolve the Linux VM with the same Ubuntu fallback policy as `parallels-linux-smoke.sh` before both fresh and update lanes. Treat any Ubuntu guest with major version `>= 24` as acceptable when the exact default VM is missing, preferring the closest version match. On Peter's current host today, missing `Ubuntu 24.04.3 ARM64` should fall back to `Ubuntu 25.10`.
- On macOS same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; launchd can otherwise report a loaded service while the old process has exited and the fresh process is not RPC-ready yet.
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
- For Windows same-guest update checks, prefer the done-file/log-drain PowerShell runner pattern over one long-lived `prlctl exec ... powershell -EncodedCommand ...` transport. The guest can finish successfully while the outer `prlctl exec` still hangs.
- The Windows same-guest update helper should write stage markers to its log before long steps like tgz download and `npm install -g` so the outer progress monitor does not sit on `waiting for first log line` during healthy but quiet installs.
- Linux same-guest update verification should also export `HOME=/root`, pass `OPENAI_API_KEY` via `prlctl exec ... /usr/bin/env`, and use `openclaw agent --local`; the fresh Linux baseline does not rely on persisted gateway credentials.
- The npm-update wrapper now prints per-lane progress from the nested log files. If a lane still looks stuck, inspect the nested logs in `runDir` first (`macos-fresh.log`, `windows-fresh.log`, `linux-fresh.log`, `macos-update.log`, `windows-update.log`, `linux-update.log`) instead of assuming the outer wrapper hung.
- If the wrapper fails a lane, read the auto-dumped tail first, then the full nested lane log under `/tmp/openclaw-parallels-npm-update.*`.
@@ -45,6 +47,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Preferred entrypoint: `pnpm test:parallels:macos`
- Default to the snapshot closest to `macOS 26.3.1 latest`.
- On Peter's Tahoe VM, `fresh-latest-march-2026` can hang in `prlctl snapshot-switch`; if restore times out there, rerun with `--snapshot-hint 'macOS 26.3.1 latest'` before blaming auth or the harness.
- `parallels-macos-smoke.sh` now retries `snapshot-switch` once after force-stopping a stuck running/suspended guest. If Tahoe still times out after that recovery path, then treat it as a real Parallels/host issue and rerun manually.
- The macOS smoke should include a dashboard load phase after gateway health: resolve the tokenized URL with `openclaw dashboard --no-open`, verify the served HTML contains the Control UI title/root shell, then open Safari and require an established localhost TCP connection from Safari to the gateway port.
- If a packaged install regresses with `500` on `/`, `/healthz`, or `__openclaw/control-ui-config.json` after `fresh.install-main` or `upgrade.install-main`, suspect bundled plugin runtime deps resolving from the package root `node_modules` rather than `dist/extensions/*/node_modules`. Repro quickly with a real `npm pack`/global install lane before blaming dashboard auth or Safari.
- `prlctl exec` is fine for deterministic repo commands, but use the guest Terminal or `prlctl enter` when installer parity or shell-sensitive behavior matters.
@@ -63,6 +66,7 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Use PowerShell only as the transport with `-ExecutionPolicy Bypass`, then call the `.cmd` shims from inside it.
- Multi-word `openclaw agent --message ...` checks should call `& $openclaw ...` inside PowerShell, not `Start-Process ... -ArgumentList` against `openclaw.cmd`, or Commander can see split argv and throw `too many arguments for 'agent'`.
- Windows installer/tgz phases now retry once after guest-ready recheck; keep new Windows smoke steps idempotent so a transport-flake retry is safe.
- If a Windows retry sees the VM become `suspended` or `stopped`, resume/start it before the next `prlctl exec`; otherwise the second attempt just repeats the same `rc=255`.
- Windows global `npm install -g` phases can stay quiet for a minute or more even when healthy; inspect the phase log before calling it hung, and only treat it as a regression once the retry wrapper or timeout trips.
- Fresh Windows ref-mode onboard should use the same background PowerShell runner plus done-file/log-drain pattern as the npm-update helper, including startup materialization checks, host-side timeouts on short poll `prlctl exec` calls, and retry-on-poll-failure behavior for transient transport flakes.
- Fresh Windows ref-mode agent verification should set `OPENAI_API_KEY` in the PowerShell environment before invoking `openclaw.cmd agent`, for the same pairing-required fallback reason as macOS.
@@ -75,12 +79,13 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Preferred entrypoint: `pnpm test:parallels:linux`
- Use the snapshot closest to fresh `Ubuntu 24.04.3 ARM64`.
- If that exact VM is missing on the host, fall back to the closest Ubuntu guest with a fresh poweroff snapshot. On Peter's host today, that is `Ubuntu 25.10`.
- If that exact VM is missing on the host, any Ubuntu guest with major version `>= 24` is acceptable; prefer the closest versioned Ubuntu guest with a fresh poweroff snapshot. On Peter's host today, that is `Ubuntu 25.10`.
- Use plain `prlctl exec`; `--current-user` is not the right transport on this snapshot.
- Fresh snapshots may be missing `curl`, and `apt-get update` can fail on clock skew. Bootstrap with `apt-get -o Acquire::Check-Date=false update` and install `curl ca-certificates`.
- Fresh `main` tgz smoke still needs the latest-release installer first because the snapshot has no Node or npm before bootstrap.
- This snapshot does not have a usable `systemd --user` session; managed daemon install is unsupported.
- The Linux smoke now falls back to a manual `setsid openclaw gateway run --bind loopback --port 18789 --force` launch with `HOME=/root` and the provider secret exported, then verifies `gateway status --deep --require-rpc` when available.
- The Linux manual gateway launch should wait for `gateway status --deep --require-rpc` inside the `gateway-start` phase; otherwise the first status probe can race the background bind and fail a healthy lane.
- If Linux gateway bring-up fails, inspect `/tmp/openclaw-parallels-linux-gateway.log` in the guest phase logs first; the common failure mode is a missing provider secret in the launched gateway environment.
## Discord roundtrip

View File

@@ -17,7 +17,7 @@ Use this skill for release and publish-time workflow. Keep ordinary development
## Keep release channel naming aligned
- `stable`: tagged releases only, published to npm `latest` and then mirrored onto npm `beta` unless `beta` already points at a newer prerelease
- `stable`: tagged releases only, published to npm `beta` by default; operators may target npm `latest` explicitly or promote later
- `beta`: prerelease tags like `vYYYY.M.D-beta.N`, with npm dist-tag `beta`
- Prefer `-beta.N`; do not mint new `-1` or `-2` beta suffixes
- `dev`: moving head on `main`
@@ -116,6 +116,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
## Use the right auth flow
- OpenClaw publish uses GitHub trusted publishing.
- Stable npm promotion from `beta` to `latest` is an explicit mode on
`.github/workflows/openclaw-npm-release.yml`, but it still needs a valid
`NPM_TOKEN` because `npm dist-tag` management is separate from trusted
publishing.
- The publish run must be started manually with `workflow_dispatch`.
- The npm workflow and the private mac publish workflow accept
`preflight_only=true` to run validation/build/package steps without uploading
@@ -218,8 +222,9 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
7. Create and push the git tag.
8. Create or refresh the matching GitHub release.
9. Start `.github/workflows/openclaw-npm-release.yml` with `preflight_only=true`
and wait for it to pass. Save that run id because the real publish requires
it to reuse the prepared npm tarball.
and choose the intended `npm_dist_tag` (`beta` default; `latest` only for
an intentional direct stable publish). Wait for it to pass. Save that run id
because the real publish requires it to reuse the prepared npm tarball.
10. Start `.github/workflows/macos-release.yml` in `openclaw/openclaw` and wait
for the public validation-only run to pass.
11. Start
@@ -234,21 +239,28 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
commit, and rerun all relevant preflights from scratch before continuing.
Never reuse old preflight results after the commit changes.
14. Start `.github/workflows/openclaw-npm-release.yml` with the same tag for
the real publish and pass the successful npm `preflight_run_id`.
the real publish, choose `npm_dist_tag` (`beta` default, `latest` only when
you intentionally want direct stable publish), keep it the same as the
preflight run, and pass the successful npm `preflight_run_id`.
15. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
16. Start
16. If the stable release was published to `beta`, start
`.github/workflows/openclaw-npm-release.yml` again after beta validation
passes with the same stable tag, `promote_beta_to_latest=true`,
`preflight_only=false`, empty `preflight_run_id`, and `npm_dist_tag=beta`,
then verify `latest` now points at that version.
17. Start
`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml`
for the real publish with the successful private mac `preflight_run_id` and
wait for success.
17. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
18. Verify the successful real private mac run uploaded the `.zip`, `.dmg`,
and `.dSYM.zip` artifacts to the existing GitHub release in
`openclaw/openclaw`.
18. For stable releases, download `macos-appcast-<tag>` from the successful
19. For stable releases, download `macos-appcast-<tag>` from the successful
private mac run, update `appcast.xml` on `main`, and verify the feed.
19. For beta releases, publish the mac assets but expect no shared production
20. For beta releases, publish the mac assets but expect no shared production
`appcast.xml` artifact and do not update the shared production feed unless a
separate beta feed exists.
20. After publish, verify npm and the attached release artifacts.
21. After publish, verify npm and the attached release artifacts.
## GHSA advisory work

View File

@@ -55,6 +55,8 @@ Check in this order:
- Was it fixed before release?
3. Exploit path
- Does the report show a real boundary bypass, not just prompt injection, local same-user control, or helper-level semantics?
- If data only moves between trusted workspace-memory files called out in `SECURITY.md`, do not treat "injection markers" alone as a security bug.
- In that case, frame sanitization as optional hardening only if it preserves expected memory workflows.
4. Functional tradeoff
- If a hardening change would reduce intended user functionality, call that out before proposing it.
- Prefer fixes that preserve user workflows over deny-by-default regressions unless the boundary demands it.
@@ -104,5 +106,6 @@ gh search prs --repo openclaw/openclaw --match title,body,comments -- "<terms>"
- “fixed on main, unreleased” is usually not a close.
- “needs attacker-controlled trusted local state first” is usually out of scope.
- “same-host same-user process can already read/write local state” is usually out of scope.
- “trusted workspace memory promotes/reindexes trusted workspace memory” is usually out of scope unless it crosses a documented boundary.
- “helper function behaves differently than documented config semantics” is usually invalid.
- If only the severity is wrong but the bug is real, keep it open and narrow the impact in the reply.

0
.codex Normal file
View File

View File

@@ -33,6 +33,8 @@ node_modules
**/.next
coverage
**/coverage
docs/.generated
**/.generated
*.log
tmp
**/tmp

View File

@@ -14,7 +14,7 @@ inputs:
pnpm-version:
description: pnpm version for corepack.
required: false
default: "10.23.0"
default: "10.32.1"
install-bun:
description: Whether to install Bun alongside Node.
required: false

View File

@@ -4,7 +4,7 @@ inputs:
pnpm-version:
description: pnpm version to activate via corepack.
required: false
default: "10.23.0"
default: "10.32.1"
cache-key-suffix:
description: Suffix appended to the cache key.
required: false

15
.github/labeler.yml vendored
View File

@@ -64,6 +64,17 @@
- any-glob-to-any-file:
- "extensions/qqbot/**"
- "docs/channels/qqbot.md"
"channel: qa-channel":
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-channel/**"
- "docs/channels/qa-channel.md"
"extensions: qa-lab":
- changed-files:
- any-glob-to-any-file:
- "extensions/qa-lab/**"
- "docs/concepts/qa-e2e-automation.md"
- "docs/channels/qa-channel.md"
"channel: signal":
- changed-files:
- any-glob-to-any-file:
@@ -246,6 +257,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/deepseek/**"
"extensions: stepfun":
- changed-files:
- any-glob-to-any-file:
- "extensions/stepfun/**"
"extensions: anthropic":
- changed-files:
- any-glob-to-any-file:

View File

@@ -35,19 +35,17 @@ If this PR fixes a plugin beta-release blocker, title it `fix(<plugin-id>): beta
- Related #
- [ ] This PR fixes a bug or regression
## Root Cause / Regression History (if applicable)
## Root Cause (if applicable)
For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write `N/A`. If the cause is unclear, write `Unknown`.
- Root cause:
- Missing detection / guardrail:
- Prior context (`git blame`, prior PR, issue, or refactor if known):
- Why this regressed now:
- If unknown, what was ruled out:
- Contributing context (if known):
## Regression Test Plan (if applicable)
For bug fixes or regressions, name the smallest reliable test coverage that should have caught this. Otherwise write `N/A`.
For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write `N/A`.
- Coverage level that should have caught this:
- [ ] Unit test

View File

@@ -407,8 +407,12 @@ jobs:
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
if (pullRequest) {
// `bad-barnacle` exempts PRs that Barnacle incorrectly marked dirty.
if (labelSet.has(dirtyLabel) && !labelSet.has(badBarnacleLabel)) {
if (labelSet.has(badBarnacleLabel)) {
core.info(`Skipping PR auto-response checks for #${pullRequest.number} because ${badBarnacleLabel} is present.`);
return;
}
if (labelSet.has(dirtyLabel)) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,

View File

@@ -17,8 +17,8 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
# Preflight: establish routing truth and planner-owned matrices once, then let
# real work fan out from a single source of truth.
# Preflight: establish routing truth and job matrices once, then let real
# work fan out from a single source of truth.
preflight:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
@@ -36,7 +36,8 @@ jobs:
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
checks_fast_matrix: ${{ steps.manifest.outputs.checks_fast_matrix }}
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
checks_fast_extensions_matrix: ${{ steps.manifest.outputs.checks_fast_extensions_matrix }}
run_checks: ${{ steps.manifest.outputs.run_checks }}
checks_matrix: ${{ steps.manifest.outputs.checks_matrix }}
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
@@ -103,7 +104,7 @@ jobs:
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import { listChangedExtensionIds } from "./scripts/test-extension.mjs";
import { listChangedExtensionIds } from "./scripts/lib/changed-extensions.mjs";
const extensionIds = listChangedExtensionIds({
base: process.env.BASE_SHA,
@@ -129,7 +130,150 @@ jobs:
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
run: node scripts/ci-write-manifest-outputs.mjs --workflow ci
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import {
createExtensionTestShards,
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
} from "./scripts/lib/extension-test-plan.mjs";
const parseBoolean = (value, fallback = false) => {
if (value === undefined) return fallback;
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1") return true;
if (normalized === "false" || normalized === "0" || normalized === "") return false;
return fallback;
};
const parseJson = (value, fallback) => {
try {
return value ? JSON.parse(value) : fallback;
} catch {
return fallback;
}
};
const createMatrix = (include) => ({ include });
const outputPath = process.env.GITHUB_OUTPUT;
const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request";
const isPush = eventName === "push";
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
const runMacos = parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly;
const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly;
const runWindows = parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly;
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
const hasChangedExtensions =
parseBoolean(process.env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS) && !docsOnly;
const changedExtensionsMatrix = hasChangedExtensions
? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] })
: { include: [] };
const extensionShardMatrix = createMatrix(
runNode
? createExtensionTestShards({
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
}).map((shard) => ({
check_name: shard.checkName,
extensions_csv: shard.extensionIds.join(","),
shard_index: shard.index + 1,
task: "extensions-batch",
}))
: [],
);
const manifest = {
docs_only: docsOnly,
docs_changed: docsChanged,
run_node: runNode,
run_macos: runMacos,
run_android: runAndroid,
run_skills_python: runSkillsPython,
run_windows: runWindows,
has_changed_extensions: hasChangedExtensions,
changed_extensions_matrix: changedExtensionsMatrix,
run_build_artifacts: runNode,
run_checks_fast: runNode,
checks_fast_core_matrix: createMatrix(
runNode
? [
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
{
check_name: "checks-fast-contracts-protocol",
runtime: "node",
task: "contracts-protocol",
},
]
: [],
),
checks_fast_extensions_matrix: extensionShardMatrix,
run_checks: runNode,
checks_matrix: createMatrix(
runNode
? [
{ check_name: "checks-node-test", runtime: "node", task: "test" },
{ check_name: "checks-node-channels", runtime: "node", task: "channels" },
...(isPush
? [
{
check_name: "checks-node-compat-node22",
runtime: "node",
task: "compat-node22",
node_version: "22.x",
cache_key_suffix: "node22",
},
]
: []),
]
: [],
),
run_extension_fast: hasChangedExtensions,
extension_fast_matrix: createMatrix(
hasChangedExtensions
? (changedExtensionsMatrix.include ?? []).map((entry) => ({
check_name: `extension-fast-${entry.extension}`,
extension: entry.extension,
}))
: [],
),
run_check: runNode,
run_check_additional: runNode,
run_build_smoke: runNode,
run_check_docs: docsChanged,
run_skills_python_job: runSkillsPython,
run_checks_windows: runWindows,
checks_windows_matrix: createMatrix(
runWindows
? [{ check_name: "checks-windows-node-test", runtime: "node", task: "test" }]
: [],
),
run_macos_node: runMacos,
macos_node_matrix: createMatrix(
runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [],
),
run_macos_swift: runMacos,
run_android_job: runAndroid,
android_matrix: createMatrix(
runAndroid
? [
{ check_name: "android-test-play", task: "test-play" },
{ check_name: "android-test-third-party", task: "test-third-party" },
{ check_name: "android-build-play", task: "build-play" },
{ check_name: "android-build-third-party", task: "build-third-party" },
]
: [],
),
};
for (const [key, value] of Object.entries(manifest)) {
appendFileSync(
outputPath,
`${key}=${typeof value === "string" ? value : JSON.stringify(value)}\n`,
"utf8",
);
}
EOF
# Run the fast security/SCM checks in parallel with scope detection so the
# main Node jobs do not have to wait for Python/pre-commit setup.
@@ -277,7 +421,7 @@ jobs:
include-hidden-files: true
retention-days: 1
checks-fast:
checks-fast-core:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
@@ -285,7 +429,7 @@ jobs:
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_matrix) }}
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_core_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -302,18 +446,12 @@ jobs:
- name: Run ${{ matrix.task }} (${{ matrix.runtime }})
env:
TASK: ${{ matrix.task }}
SHARD_COUNT: ${{ matrix.shard_count || '' }}
SHARD_INDEX: ${{ matrix.shard_index || '' }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
extensions)
if [ -n "$SHARD_COUNT" ] && [ -n "$SHARD_INDEX" ]; then
export OPENCLAW_TEST_SHARDS="$SHARD_COUNT"
export OPENCLAW_TEST_SHARD_INDEX="$SHARD_INDEX"
fi
pnpm test:extensions
bundled)
pnpm test:bundled
;;
contracts|contracts-protocol)
pnpm build
@@ -326,6 +464,49 @@ jobs:
;;
esac
checks-fast-extensions-shard:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_fast_extensions_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
submodules: false
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
use-sticky-disk: "false"
- name: Run extension shard
env:
OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }}
run: pnpm test:extensions:batch -- "$OPENCLAW_EXTENSION_BATCH"
checks-fast-extensions:
name: checks-fast-extensions
needs: [preflight, checks-fast-extensions-shard]
if: always() && needs.preflight.outputs.run_checks_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 5
steps:
- name: Verify extension shards
env:
SHARD_RESULT: ${{ needs.checks-fast-extensions-shard.result }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Extension shard checks failed: $SHARD_RESULT" >&2
exit 1
fi
checks:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
@@ -360,20 +541,10 @@ jobs:
if: (github.event_name != 'pull_request' || matrix.task != 'compat-node22') && matrix.runtime == 'node' && (matrix.task == 'test' || matrix.task == 'channels' || matrix.task == 'compat-node22')
env:
TASK: ${{ matrix.task }}
SHARD_COUNT: ${{ matrix.shard_count || '' }}
SHARD_INDEX: ${{ matrix.shard_index || '' }}
run: |
# `pnpm test` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes.
# Default heap limits have been too low on Linux CI (V8 OOM near 4GB).
echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV"
echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
if [ "$TASK" = "channels" ]; then
echo "OPENCLAW_TEST_WORKERS=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_ISOLATE=1" >> "$GITHUB_ENV"
fi
if [ -n "$SHARD_COUNT" ] && [ -n "$SHARD_INDEX" ]; then
echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV"
echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV"
fi
- name: Download dist artifact
@@ -394,6 +565,7 @@ jobs:
if: github.event_name != 'pull_request' || matrix.task != 'compat-node22'
env:
TASK: ${{ matrix.task }}
NODE_OPTIONS: --max-old-space-size=6144
shell: bash
run: |
set -euo pipefail
@@ -735,8 +907,7 @@ jobs:
env:
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
OPENCLAW_VITEST_MAX_WORKERS: 1
defaults:
run:
shell: bash
@@ -778,7 +949,7 @@ jobs:
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "10.23.0"
pnpm-version: "10.32.1"
cache-key-suffix: "node24"
# Sticky disk mount currently retries/fails on every shard and adds ~50s
# before install while still yielding zero pnpm store reuse.
@@ -809,15 +980,6 @@ jobs:
# caches can skip repeated rebuild/download work on later shards/runs.
pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true
- name: Configure test shard (Windows)
if: matrix.task == 'test'
env:
SHARD_COUNT: ${{ matrix.shard_count }}
SHARD_INDEX: ${{ matrix.shard_index }}
run: |
echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV"
- name: Download dist artifact
if: matrix.task == 'test'
uses: actions/download-artifact@v8
@@ -881,17 +1043,10 @@ jobs:
name: canvas-a2ui-bundle
path: src/canvas-host/a2ui/
- name: Configure test shard (macOS)
env:
SHARD_COUNT: ${{ matrix.shard_count }}
SHARD_INDEX: ${{ matrix.shard_index }}
run: |
echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV"
- name: TS tests (macOS)
env:
NODE_OPTIONS: --max-old-space-size=4096
OPENCLAW_VITEST_MAX_WORKERS: 2
TASK: ${{ matrix.task }}
shell: bash
run: |

View File

@@ -67,16 +67,18 @@ jobs:
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: "false"
OPENCLAW_CI_RUN_NODE: "false"
OPENCLAW_CI_RUN_MACOS: "false"
OPENCLAW_CI_RUN_ANDROID: "false"
OPENCLAW_CI_RUN_WINDOWS: "false"
OPENCLAW_CI_RUN_SKILLS_PYTHON: "false"
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: "false"
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: '{"include":[]}'
OPENCLAW_CI_RUN_CHANGED_SMOKE: ${{ steps.changed_scope.outputs.run_changed_smoke || 'false' }}
run: node scripts/ci-write-manifest-outputs.mjs --workflow install-smoke
run: |
docs_only="${OPENCLAW_CI_DOCS_ONLY:-false}"
run_changed_smoke="${OPENCLAW_CI_RUN_CHANGED_SMOKE:-false}"
run_install_smoke=false
if [ "$docs_only" != "true" ] && [ "$run_changed_smoke" = "true" ]; then
run_install_smoke=true
fi
{
echo "docs_only=$docs_only"
echo "run_install_smoke=$run_install_smoke"
} >> "$GITHUB_OUTPUT"
install-smoke:
needs: [preflight]

View File

@@ -20,7 +20,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
jobs:
validate_macos_release_request:

View File

@@ -16,19 +16,32 @@ on:
description: Existing successful preflight workflow run id to promote without rebuilding
required: false
type: string
npm_dist_tag:
description: npm dist-tag to publish to for stable releases
required: true
default: beta
type: choice
options:
- beta
- latest
promote_beta_to_latest:
description: Skip publish and promote the stable version already on npm beta to latest
required: true
default: false
type: boolean
concurrency:
group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
group: openclaw-npm-release-${{ github.event_name == 'workflow_dispatch' && format('{0}-{1}-{2}', inputs.tag, inputs.npm_dist_tag, inputs.promote_beta_to_latest) || github.ref }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
jobs:
preflight_openclaw_npm:
if: ${{ inputs.preflight_only }}
if: ${{ inputs.preflight_only && !inputs.promote_beta_to_latest }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -36,12 +49,17 @@ jobs:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish to npm dist-tag beta."
exit 1
fi
- name: Forbid preflight artifact promotion on validation-only runs
if: ${{ inputs.preflight_only && inputs.preflight_run_id != '' }}
@@ -98,6 +116,7 @@ jobs:
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_MAIN_REF: origin/main
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD)
@@ -110,11 +129,37 @@ jobs:
- name: Verify release contents
run: pnpm release:check
- name: Validate live cache credentials
if: ${{ github.ref == 'refs/heads/main' }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY}" ]]; then
echo "Missing OPENAI_API_KEY secret for release live cache validation." >&2
exit 1
fi
if [[ -z "${ANTHROPIC_API_KEY}" ]]; then
echo "Missing ANTHROPIC_API_KEY secret for release live cache validation." >&2
exit 1
fi
- name: Verify live prompt cache floors
if: ${{ github.ref == 'refs/heads/main' }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_LIVE_CACHE_TEST: "1"
OPENCLAW_LIVE_TEST: "1"
run: pnpm test:live:cache
- name: Pack prepared npm tarball
id: packed_tarball
env:
OPENCLAW_PREPACK_PREPARED: "1"
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
PACK_JSON="$(npm pack --json)"
@@ -131,6 +176,7 @@ jobs:
cp "$PACK_PATH" "$ARTIFACT_DIR/"
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
printf '%s\n' "$RELEASE_NPM_DIST_TAG" > "$ARTIFACT_DIR/release-npm-dist-tag.txt"
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
- name: Upload prepared npm publish bundle
@@ -141,7 +187,7 @@ jobs:
if-no-files-found: error
validate_publish_request:
if: ${{ !inputs.preflight_only }}
if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest }}
runs-on: ubuntu-latest
permissions:
contents: read
@@ -169,7 +215,7 @@ jobs:
publish_openclaw_npm:
# npm trusted publishing + provenance requires a GitHub-hosted runner.
needs: [validate_publish_request]
if: ${{ !inputs.preflight_only }}
if: ${{ !inputs.preflight_only && !inputs.promote_beta_to_latest }}
runs-on: ubuntu-latest
environment: npm-release
permissions:
@@ -180,12 +226,17 @@ jobs:
- name: Validate tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1
fi
if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish to npm dist-tag beta."
exit 1
fi
- name: Checkout
uses: actions/checkout@v6
@@ -249,18 +300,21 @@ jobs:
- name: Verify prepared tarball provenance
env:
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
EXPECTED_RELEASE_SHA="$(git rev-parse HEAD)"
TAG_FILE="preflight-tarball/release-tag.txt"
SHA_FILE="preflight-tarball/release-sha.txt"
if [[ ! -f "$TAG_FILE" || ! -f "$SHA_FILE" ]]; then
NPM_DIST_TAG_FILE="preflight-tarball/release-npm-dist-tag.txt"
if [[ ! -f "$TAG_FILE" || ! -f "$SHA_FILE" || ! -f "$NPM_DIST_TAG_FILE" ]]; then
echo "Prepared preflight metadata is missing." >&2
ls -la preflight-tarball >&2 || true
exit 1
fi
ARTIFACT_RELEASE_TAG="$(tr -d '\r\n' < "$TAG_FILE")"
ARTIFACT_RELEASE_SHA="$(tr -d '\r\n' < "$SHA_FILE")"
ARTIFACT_RELEASE_NPM_DIST_TAG="$(tr -d '\r\n' < "$NPM_DIST_TAG_FILE")"
if [[ "$ARTIFACT_RELEASE_TAG" != "$RELEASE_TAG" ]]; then
echo "Prepared preflight tag mismatch: expected $RELEASE_TAG, got $ARTIFACT_RELEASE_TAG" >&2
exit 1
@@ -269,6 +323,10 @@ jobs:
echo "Prepared preflight SHA mismatch: expected $EXPECTED_RELEASE_SHA, got $ARTIFACT_RELEASE_SHA" >&2
exit 1
fi
if [[ "$ARTIFACT_RELEASE_NPM_DIST_TAG" != "$RELEASE_NPM_DIST_TAG" ]]; then
echo "Prepared preflight npm dist-tag mismatch: expected $RELEASE_NPM_DIST_TAG, got $ARTIFACT_RELEASE_NPM_DIST_TAG" >&2
exit 1
fi
- name: Resolve publish tarball
id: publish_tarball
@@ -284,9 +342,8 @@ jobs:
- name: Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
OPENCLAW_PREPACK_PREPARED: "1"
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
publish_target="${{ steps.publish_tarball.outputs.path }}"
@@ -294,3 +351,99 @@ jobs:
publish_target="./${publish_target}"
fi
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
promote_beta_to_latest:
if: ${{ inputs.promote_beta_to_latest }}
runs-on: ubuntu-latest
environment: npm-release
permissions:
contents: read
steps:
- name: Require main workflow ref for promotion
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "Promotion runs must be dispatched from main."
exit 1
fi
- name: Validate promotion inputs
env:
PREFLIGHT_ONLY: ${{ inputs.preflight_only }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: |
set -euo pipefail
if [[ "${PREFLIGHT_ONLY}" == "true" ]]; then
echo "Promotion mode cannot run with preflight_only=true."
exit 1
fi
if [[ -n "${PREFLIGHT_RUN_ID}" ]]; then
echo "Promotion mode does not use preflight_run_id."
exit 1
fi
if [[ "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Promotion mode expects npm_dist_tag=beta because it moves beta to latest without publishing."
exit 1
fi
- name: Validate stable tag input format
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]; then
echo "Invalid stable release tag format: ${RELEASE_TAG}" >&2
exit 1
fi
echo "RELEASE_VERSION=${RELEASE_TAG#v}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
install-deps: "false"
- name: Validate npm dist-tags
env:
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
run: |
set -euo pipefail
beta_version="$(npm view openclaw dist-tags.beta)"
latest_version="$(npm view openclaw dist-tags.latest)"
echo "Current beta dist-tag: ${beta_version}"
echo "Current latest dist-tag: ${latest_version}"
if [[ "${beta_version}" != "${RELEASE_VERSION}" ]]; then
echo "npm beta points at ${beta_version}, expected ${RELEASE_VERSION}." >&2
exit 1
fi
if ! npm view "openclaw@${RELEASE_VERSION}" version >/dev/null 2>&1; then
echo "openclaw@${RELEASE_VERSION} is not published on npm." >&2
exit 1
fi
- name: Promote beta to latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
run: |
set -euo pipefail
npm whoami >/dev/null
npm dist-tag add "openclaw@${RELEASE_VERSION}" latest
promoted_latest="$(npm view openclaw dist-tags.latest)"
if [[ "${promoted_latest}" != "${RELEASE_VERSION}" ]]; then
echo "npm latest points at ${promoted_latest}, expected ${RELEASE_VERSION} after promotion." >&2
exit 1
fi
echo "Promoted openclaw@${RELEASE_VERSION} from beta to latest."

View File

@@ -0,0 +1,276 @@
name: Plugin ClawHub Release
on:
workflow_dispatch:
inputs:
publish_scope:
description: Publish the selected plugins or all ClawHub-publishable plugins from the workflow ref
required: true
default: selected
type: choice
options:
- selected
- all-publishable
plugins:
description: Comma-separated plugin package names to publish when publish_scope=selected
required: false
type: string
concurrency:
group: plugin-clawhub-release-${{ github.sha }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.32.1"
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "4af2bd50a71465683dbf8aa269af764b9d39bdf5"
jobs:
preview_plugins_clawhub:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
ref_sha: ${{ steps.ref.outputs.sha }}
has_candidates: ${{ steps.plan.outputs.has_candidates }}
candidate_count: ${{ steps.plan.outputs.candidate_count }}
skipped_published_count: ${{ steps.plan.outputs.skipped_published_count }}
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 0
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "false"
use-sticky-disk: "false"
- name: Resolve checked-out ref
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate ref is on main
run: |
set -euo pipefail
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor HEAD origin/main
- name: Validate publishable plugin metadata
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
HEAD_REF: ${{ steps.ref.outputs.sha }}
run: |
set -euo pipefail
if [[ -n "${PUBLISH_SCOPE}" ]]; then
release_args=(--selection-mode "${PUBLISH_SCOPE}")
if [[ -n "${RELEASE_PLUGINS}" ]]; then
release_args+=(--plugins "${RELEASE_PLUGINS}")
fi
pnpm release:plugins:clawhub:check -- "${release_args[@]}"
elif [[ -n "${BASE_REF}" ]]; then
pnpm release:plugins:clawhub:check -- --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}"
else
pnpm release:plugins:clawhub:check
fi
- name: Resolve plugin release plan
id: plan
env:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
BASE_REF: ${{ github.event_name != 'workflow_dispatch' && github.event.before || '' }}
HEAD_REF: ${{ steps.ref.outputs.sha }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
mkdir -p .local
if [[ -n "${PUBLISH_SCOPE}" ]]; then
plan_args=(--selection-mode "${PUBLISH_SCOPE}")
if [[ -n "${RELEASE_PLUGINS}" ]]; then
plan_args+=(--plugins "${RELEASE_PLUGINS}")
fi
node --import tsx scripts/plugin-clawhub-release-plan.ts "${plan_args[@]}" > .local/plugin-clawhub-release-plan.json
elif [[ -n "${BASE_REF}" ]]; then
node --import tsx scripts/plugin-clawhub-release-plan.ts --base-ref "${BASE_REF}" --head-ref "${HEAD_REF}" > .local/plugin-clawhub-release-plan.json
else
node --import tsx scripts/plugin-clawhub-release-plan.ts > .local/plugin-clawhub-release-plan.json
fi
cat .local/plugin-clawhub-release-plan.json
candidate_count="$(jq -r '.candidates | length' .local/plugin-clawhub-release-plan.json)"
skipped_published_count="$(jq -r '.skippedPublished | length' .local/plugin-clawhub-release-plan.json)"
has_candidates="false"
if [[ "${candidate_count}" != "0" ]]; then
has_candidates="true"
fi
matrix_json="$(jq -c '.candidates' .local/plugin-clawhub-release-plan.json)"
{
echo "candidate_count=${candidate_count}"
echo "skipped_published_count=${skipped_published_count}"
echo "has_candidates=${has_candidates}"
echo "matrix=${matrix_json}"
} >> "$GITHUB_OUTPUT"
echo "Plugin release candidates:"
jq -r '.candidates[]? | "- \(.packageName)@\(.version) [\(.publishTag)] from \(.packageDir)"' .local/plugin-clawhub-release-plan.json
echo "Already published / skipped:"
jq -r '.skippedPublished[]? | "- \(.packageName)@\(.version)"' .local/plugin-clawhub-release-plan.json
- name: Fail manual publish when target versions already exist
if: github.event_name == 'workflow_dispatch' && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'
run: |
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
exit 1
preview_plugin_pack:
needs: preview_plugins_clawhub
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
install-deps: "false"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
fetch-depth: 1
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: bun install --frozen-lockfile
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Preview publish command
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, preview_plugin_pack]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
use-sticky-disk: "false"
install-deps: "false"
- name: Checkout ClawHub CLI source
uses: actions/checkout@v6
with:
repository: ${{ env.CLAWHUB_REPOSITORY }}
ref: ${{ env.CLAWHUB_REF }}
path: clawhub-source
fetch-depth: 1
- name: Install ClawHub CLI dependencies
working-directory: clawhub-source
run: bun install --frozen-lockfile
- name: Bootstrap ClawHub CLI
run: |
cat > "$RUNNER_TEMP/clawhub" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
exec bun "$GITHUB_WORKSPACE/clawhub-source/packages/clawhub/src/cli.ts" "$@"
EOF
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Ensure version is not already published
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
encoded_name="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_NAME ?? ""))')"
encoded_version="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_VERSION ?? ""))')"
url="${CLAWHUB_REGISTRY%/}/api/v1/packages/${encoded_name}/versions/${encoded_version}"
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
if [[ "${status}" =~ ^2 ]]; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
exit 1
fi
if [[ "${status}" != "404" ]]; then
echo "Unexpected ClawHub response (${status}) for ${PACKAGE_NAME}@${PACKAGE_VERSION}."
exit 1
fi
- name: Publish
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
SOURCE_COMMIT: ${{ needs.preview_plugins_clawhub.outputs.ref_sha }}
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"

View File

@@ -38,7 +38,7 @@ concurrency:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.x"
PNPM_VERSION: "10.23.0"
PNPM_VERSION: "10.32.1"
jobs:
preview_plugins_npm:

10
.gitignore vendored
View File

@@ -4,7 +4,7 @@ node_modules
docker-compose.override.yml
docker-compose.extra.yml
dist
dist-runtime
dist-runtime/
pnpm-lock.yaml
bun.lock
bun.lockb
@@ -136,8 +136,16 @@ ui/src/ui/views/__screenshots__
ui/.vitest-attachments
docs/superpowers
# Generated docs baseline artifacts (locally generated, only hashes tracked)
docs/.generated/*.json
docs/.generated/*.jsonl
# Deprecated changelog fragment workflow
changelog/fragments/
# Local scratch workspace
.tmp/
test/fixtures/openclaw-vitest-unit-report.json
analysis/
.artifacts/qa-e2e/
extensions/qa-lab/web/dist/

View File

@@ -55,6 +55,11 @@
- Public docs: `docs/gateway/protocol.md`, `docs/gateway/bridge-protocol.md`, `docs/concepts/architecture.md`
- Definition files: `src/gateway/protocol/schema.ts`, `src/gateway/protocol/schema/*.ts`, `src/gateway/protocol/index.ts`
- Rule: protocol changes are contract changes. Prefer additive evolution; incompatible changes require explicit versioning, docs, and client/codegen follow-through.
- Config contract boundary:
- Canonical public config lives in exported config types, zod/schema surfaces, schema help/labels, generated config metadata, config baselines, and any user-facing gateway/config payloads. Keep those surfaces aligned.
- When a legacy config key is retired from the public contract, remove it from every public config surface above. Keep backward compatibility only through raw-config migration/doctor seams unless explicit product policy says otherwise.
- Do not reintroduce removed legacy aliases into public types/schema/help/baselines “for convenience”. If old configs still need to load, handle that in `legacy.migrations.*`, config ingest, or `openclaw doctor --fix`.
- `hooks.internal.entries` is the canonical public hook config model. `hooks.internal.handlers` is compatibility-only input and must not be re-exposed in public schema/help/baseline surfaces.
- Bundled plugin contract boundary:
- Public docs: `docs/plugins/architecture.md`, `docs/plugins/manifest.md`, `docs/plugins/sdk-overview.md`
- Definition files: `src/plugins/contracts/registry.ts`, `src/plugins/types.ts`, `src/plugins/public-artifacts.ts`
@@ -125,10 +130,10 @@
- Formatting gate: the pre-commit hook runs `pnpm format` before `pnpm check`. If you want a formatting-only preflight locally, run `pnpm format` explicitly.
- If you need a fast commit loop, `FAST_COMMIT=1 git commit ...` skips the hooks repo-wide `pnpm format` and `pnpm check`; use that only when you are deliberately covering the touched surface some other way.
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
- Generated baseline artifacts live together under `docs/.generated/`.
- Generated baseline drift detection uses SHA-256 hash files under `docs/.generated/` (`.sha256` files tracked in git; full JSON baselines are gitignored, generated locally for inspection).
- Config schema drift uses `pnpm config:docs:gen` / `pnpm config:docs:check`.
- Plugin SDK API drift uses `pnpm plugin-sdk:api:gen` / `pnpm plugin-sdk:api:check`.
- If you change config schema/help or the public Plugin SDK surface, update the matching baseline artifact and keep the two drift-check flows adjacent in scripts/workflows/docs guidance rather than inventing a third pattern.
- If you change config schema/help or the public Plugin SDK surface, run the matching gen command and commit the updated `.sha256` hash file. Keep the two drift-check flows adjacent in scripts/workflows/docs guidance rather than inventing a third pattern.
- For narrowly scoped changes, prefer narrowly scoped tests that directly validate the touched behavior. If no meaningful scoped test exists, say so explicitly and use the next most direct validation available.
- Verification modes for work on `main`:
- Default mode: `main` is relatively stable. Count pre-commit hook coverage when it already verified the current tree, avoid rerunning the exact same checks just for ceremony, and prefer keeping CI/main green before landing.
@@ -140,6 +145,14 @@
- For narrowly scoped changes, if unrelated failures already exist on latest `origin/main`, state that clearly, report the scoped tests you ran, and ask before broadening scope into unrelated fixes or landing despite those failures.
- Do not use scoped tests as permission to ignore plausibly related failures.
## Prompt Cache Stability
- Treat prompt-cache stability as correctness/perf-critical, not cosmetic.
- Any code that assembles model or tool payloads from maps, sets, registries, plugin lists, MCP catalogs, filesystem reads, or network results must make ordering deterministic before building the request.
- Do not rewrite older transcript/history bytes on every turn unless you intentionally want to invalidate the cached prefix. Legacy cleanup, pruning, normalization, and migration logic should preserve recent prompt bytes when possible.
- If truncation or compaction is required, prefer mutating newest or tail content first so the cached prefix stays byte-identical for as long as possible.
- For cache-sensitive changes, require a regression test that proves turn-to-turn prefix stability or deterministic request assembly; helper-local tests alone are not enough.
## Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
@@ -185,11 +198,16 @@
- Test performance guardrail: inside an extension package, prefer a thin local seam (`./api.ts`, `./runtime-api.ts`, or a narrower local `*.runtime-api.ts`) over direct `openclaw/plugin-sdk/*` imports for internal production code. Keep local seams curated and lightweight; only reach for direct `plugin-sdk/*` imports when you are crossing a real package boundary or when no suitable local seam exists yet.
- Test performance guardrail: keep expensive runtime fallback work such as snapshotting, migration, installs, or bootstrap behind dedicated `*.runtime.ts` boundaries so tests can mock the seam instead of accidentally invoking real work.
- Test performance guardrail: for import-only/runtime-wrapper tests, keep the wrapper lazy. Do not eagerly load heavy verification/bootstrap/runtime modules at module top level if the exported function can import them on demand.
- Test performance guardrail: prefer explicit mock factories over `importOriginal()` for broad modules. Reserve `importOriginal()` for narrow modules where partial-real behavior is genuinely needed.
- Test performance guardrail: do not partial-mock broad `openclaw/plugin-sdk/*` barrels in hot tests. Add a plugin-local `*.runtime.ts` seam and mock that seam instead.
- Test performance guardrail: when production code already accepts `deps`, callbacks, or runtime injection, use that seam in tests before adding module-level mocks.
- Test performance guardrail: prefer narrow public SDK subpaths such as `models-provider-runtime`, `skill-commands-runtime`, and `reply-dispatch-runtime` over older broad helper barrels when both expose the needed helper.
- Test performance guardrail: treat import-dominated test time as a boundary bug. Refactor the import surface before adding more cases to the slow file.
- Agents MUST NOT modify baseline, inventory, ignore, snapshot, or expected-failure files to silence failing checks without explicit approval in this chat.
- For targeted/local debugging, keep using the wrapper: `pnpm test -- <path-or-filter> [vitest args...]` (for example `pnpm test -- src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses wrapper config/profile/pool routing.
- For targeted/local debugging, use the native root-project entrypoint: `pnpm test <path-or-filter> [vitest args...]` (for example `pnpm test src/commands/onboard-search.test.ts -t "shows registered plugin providers"`); do not default to raw `pnpm vitest run ...` because it bypasses the repo's default config/profile/pool routing.
- Do not set test workers above 16; tried already.
- Keep Vitest on `forks` only. Do not introduce or reintroduce any non-`forks` Vitest pool or alternate execution mode in configs, wrapper scripts, or default test commands without explicit approval in this chat. This includes `threads`, `vmThreads`, `vmForks`, and any future/nonstandard pool variant.
- If local Vitest runs cause memory pressure, the wrapper now derives budgets from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_TEST_PROFILE=serial OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test`.
- Vitest now defaults to native root-project `threads`, with hard `forks` exceptions for `gateway`, `agents`, and `commands`. Keep new pool changes explicit and justified; use `OPENCLAW_VITEST_POOL=forks` for full local fork debugging.
- If local Vitest runs cause memory pressure, the default worker budget now derives from host capabilities (CPU, memory band, current load). For a conservative explicit override during land/gate runs, use `OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test`.
- Live tests (real keys): `OPENCLAW_LIVE_TEST=1 pnpm test:live` (OpenClaw-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- `pnpm test:live` defaults quiet now. Keep `[live]` progress; suppress profile/gateway chatter. Full logs: `OPENCLAW_LIVE_TEST_QUIET=0 pnpm test:live`.
- Full kit + whats covered: `docs/help/testing.md`.
@@ -247,6 +265,8 @@
- "Bump version everywhere" means all version locations above **except** `appcast.xml` (only touch appcast when cutting a new macOS Sparkle release).
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
- Mobile pairing: `ws://` (cleartext) is allowed for private LAN addresses (RFC 1918, link-local, mDNS `.local`) and loopback. Private LAN hosts typically lack PKI-backed identity, so requiring TLS there adds complexity without meaningful security gain. `wss://` is required for Tailscale and public endpoints.
- Security report scope: reports that treat cleartext `ws://` mobile pairing over private LAN as a vulnerability are out of scope unless they demonstrate a trust-boundary bypass beyond passive network observation on the same LAN.
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
- Release signing/notary credentials are managed outside the repo; maintainers keep that setup in the private [maintainer release docs](https://github.com/openclaw/maintainers/tree/main/release).

View File

@@ -6,106 +6,238 @@ Docs: https://docs.openclaw.ai
### Breaking
- Plugins/web fetch: move Firecrawl `web_fetch` config from the legacy core `tools.web.fetch.firecrawl.*` path to the plugin-owned `plugins.entries.firecrawl.config.webFetch.*` path, route `web_fetch` fallback through the new fetch-provider boundary instead of a Firecrawl-only core branch, and migrate legacy config with `openclaw doctor --fix`. (#59465) Thanks @vincentkoc.
- Plugins/xAI: move `x_search` settings from the legacy core `tools.web.x_search.*` path to the plugin-owned `plugins.entries.xai.config.xSearch.*` path, standardize `x_search` auth on `plugins.entries.xai.config.webSearch.apiKey` / `XAI_API_KEY`, and migrate legacy config with `openclaw doctor --fix`. (#59674) Thanks @vincentkoc.
- Config: remove legacy public config aliases such as `talk.voiceId` / `talk.apiKey`, `agents.*.sandbox.perSession`, `browser.ssrfPolicy.allowPrivateNetwork`, `hooks.internal.handlers`, and channel/group/room `allow` toggles in favor of the canonical public paths and `enabled`, while keeping load-time compatibility and `openclaw doctor --fix` migration support for existing configs. (#60726) Thanks @vincentkoc.
### Changes
- Exec defaults: make gateway/node host exec default to YOLO mode by requesting `security=full` with `ask=off`, and align host approval-file fallbacks plus docs/doctor reporting with that no-prompt default.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- Plugins/hooks: add `before_agent_reply` so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon
- Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.
- Diffs: add plugin-owned `viewerBaseUrl` so viewer links can use a stable proxy/public origin without passing `baseUrl` on every tool call. (#59341) Related #59227. Thanks @gumadeiras.
- Matrix/plugin: emit spec-compliant `m.mentions` metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.
- Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) thanks @wittam-01.
- WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr.
- Diffs: add plugin-owned `viewerBaseUrl` so viewer links can use a stable proxy/public origin without passing `baseUrl` on every tool call. (#59341) Related #59227. Thanks @gumadeiras.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg
- Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.
- Plugins/hooks: add `before_agent_reply` so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon
- Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.
- Tasks/TaskFlow: restore the core TaskFlow substrate with managed-vs-mirrored sync modes, durable flow state/revision tracking, and `openclaw flows` inspection/recovery primitives so background orchestration can persist and be operated separately from plugin authoring layers. (#58930) Thanks @mbelinky.
- Tasks/TaskFlow: add managed child task spawning plus sticky cancel intent, so external orchestrators can stop scheduling immediately and let parent TaskFlows settle to `cancelled` once active child tasks finish. (#59610) Thanks @mbelinky.
- Plugins/TaskFlow: add a bound `api.runtime.taskFlow` seam so plugins and trusted authoring layers can create and drive managed TaskFlows from host-resolved OpenClaw context without passing owner identifiers on each call. (#59622) Thanks @mbelinky.
- Android/assistant: add assistant-role entrypoints plus Google Assistant App Actions metadata so Android can launch OpenClaw from the assistant trigger and hand prompts into the chat composer. (#59596) Thanks @obviyus.
- Memory/dreaming (experimental): add weighted short-term recall promotion, managed dreaming modes (`off|core|rem|deep`), a `/dreaming` command, Dreams UI, multilingual conceptual tagging, and doctor/status repair support so durable memory promotion can run in the background with less manual setup. (#60569, #60697)
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
- Providers/StepFun: add the bundled StepFun provider plugin with standard and Step Plan endpoints, China/global onboarding choices, `step-3.5-flash` on both catalogs, and `step-3.5-flash-2603` currently exposed on Step Plan. (#60032) Thanks @hengm3467.
- Providers/Fireworks: add a bundled Fireworks AI provider plugin with `FIREWORKS_API_KEY` onboarding, Fire Pass Kimi defaults, and dynamic Fireworks model-id support.
- Providers/config: add `models.providers.*.request` overrides for headers and auth on model-provider paths, and full request transport overrides for media provider HTTP paths.
- MiniMax/TTS: add a bundled MiniMax speech provider backed by the T2A v2 API so speech synthesis can run through MiniMax-native voices and auth. (#55921) Thanks @duncanita.
- Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong.
- Providers/Ollama: add a bundled Ollama Web Search provider for key-free `web_search` via your configured Ollama host and `ollama signin`. (#59318) Thanks @BruceMacD.
- Plugins/onboarding: add plugin config TUI prompts to onboard and configure wizards so more plugin setup can stay in the guided flow. (#60590)
- Plugins/install: add `openclaw plugins install --force` to overwrite existing plugin and hook-pack install targets without using the dangerous-code override flag. (#60544) Thanks @gumadeiras.
- Providers/Anthropic: remove setup-token from new onboarding and auth-command setup paths, keep existing configured legacy token profiles runnable, and steer new Anthropic setup to Claude CLI or API keys.
- Providers/OpenAI Codex: add forward-compat `openai-codex/gpt-5.4-mini` synthesis across provider runtime, model catalog, and model listing so Codex mini works before bundled Pi catalog updates land.
- Tools/web_search: add a bundled MiniMax Search provider backed by the Coding Plan search API, with region reuse from `MINIMAX_API_HOST` and plugin-owned credential config. (#54648) Thanks @fengmk2.
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so quoted, threaded, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
- Providers/request overrides: add shared model and media request transport overrides across OpenAI-, Anthropic-, Google-, and compatible provider paths, including headers, auth, proxy, and TLS controls. (#60200)
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
- Agents/Claude CLI: expose OpenClaw tools to background Claude CLI runs through a loopback MCP bridge that reuses gateway tool policy, honors session/account/channel scoping, and only advertises the bridge when the local runtime is actually live. (#35676) Thanks @mylukin.
- Agents/Claude CLI: switch bundled Claude CLI runs to stdin + `stream-json` partial-message streaming so prompts stop riding argv, long replies show live progress, and final session/usage metadata still land cleanly.
- Prompt caching: keep prompt prefixes more reusable across transport fallback, deterministic MCP tool ordering, compaction, and embedded image history so follow-up turns hit cache more reliably. (#58036, #58037, #58038, #59054, #60603, #60691) Thanks @bcherny.
- Agents/cache: diagnostics: add prompt-cache break diagnostics, trace live cache scenarios through embedded runner paths, and show cache reuse explicitly in `openclaw status --verbose`. Thanks @vincentkoc.
- Agents/cache: stabilize cache-relevant system prompt fingerprints by normalizing equivalent structured prompt whitespace, line endings, hook-added system context, and runtime capability ordering so semantically unchanged prompts reuse KV/cache more reliably. Thanks @vincentkoc.
- Config/schema: enrich the exported `openclaw config schema` JSON Schema with field titles and descriptions so editors, agents, and other schema consumers receive the same config help metadata. (#60067) Thanks @solavrc.
- Providers/StepFun: add the bundled StepFun provider plugin with standard and Step Plan endpoints, China/global onboarding choices, `step-3.5-flash` on both catalogs, and `step-3.5-flash-2603` currently exposed on Step Plan. (#60032) Thanks @hengm3467.
### Fixes
- Gateway/macOS: let launchd `KeepAlive` own in-process gateway restarts again, adding a short supervised-exit delay so rapid restarts avoid launchd crash-loop unloads while `openclaw gateway restart` still reports real LaunchAgent errors synchronously.
- Synology Chat/security: route webhook token comparison through the shared constant-time secret helper for consistency with other bundled plugins.
- Models/MiniMax: honor `MINIMAX_API_HOST` for implicit bundled MiniMax provider catalogs so China-hosted API-key setups pick `api.minimaxi.com/anthropic` without manual provider config. (#34524) Thanks @caiqinghua.
- Usage/MiniMax: invert remaining-style `usage_percent` fields when MiniMax reports only remaining percentage data, so usage bars stop showing nearly-full remaining quota as nearly-exhausted usage. (#60254) Thanks @jwchmodx.
- MiniMax: advertise image input on bundled `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` model definitions so image-capable flows can route through the M2.7 family correctly. (#54843) Thanks @MerlinMiao88888888.
- Media understanding: auto-register image-capable config providers for vision routing, so custom GLM-style provider ids with image models stop failing with “no media-understanding provider registered”. (#51418) Thanks @xydt-610.
- Providers/OpenAI: preserve native `reasoning.effort: "none"` and strict tool schemas on direct OpenAI-family endpoints, keep compat routes on compat shaping, fix Responses WebSocket warm-up behavior, keep stable session and turn metadata, and fall back more gracefully after early WebSocket failures.
- Providers/OpenAI Codex: split native `contextWindow` from runtime `contextTokens`, keep the default effective cap at `272000`, and expose a per-model `contextTokens` override on `models.providers.*.models[]`.
- Providers/compat: stop forcing OpenAI-only defaults on proxy and custom OpenAI-compatible routes, preserve native vendor-specific reasoning/tool/streaming behavior across Anthropic-compatible, Moonshot, Mistral, ModelStudio, OpenRouter, xAI, and Z.ai endpoints, and route GitHub Copilot Claude models through Anthropic Messages instead of OpenAI Responses.
- Providers/Model Studio: preserve native streaming usage reporting for DashScope-compatible endpoints even when they are configured under a generic provider key, so streamed token totals stop sticking at zero. (#52395) Thanks @IVY-AI-gif.
- Providers/OpenAI-compatible WS: compute fallback token totals from normalized usage when providers omit or zero `total_tokens`, so DashScope-compatible sessions stop storing zero totals after alias normalization. (#54940) Thanks @lyfuci.
- Status/usage: let `/status` and `session_status` fall back to transcript token totals when the session meta store stayed at zero, so LM Studio, Ollama, DashScope, and similar OpenAI-compatible providers stop showing `Context: 0/...`. (#55041) Thanks @jjjojoj.
- Providers/Z.AI: preserve explicitly registered `glm-5-*` variants like `glm-5-turbo` instead of intercepting them with the generic GLM-5 forward-compat shim. (#48185) Thanks @haoyu-haoyu.
- Live model switching: only treat explicit user-driven model changes as pending live switches, so fallback rotation, heartbeat overrides, and compaction no longer trip `LiveSessionModelSwitchError` before making an API call. (#60266) Thanks @kiranvk-2011.
- Voice-call/OpenAI: pass full plugin config into realtime transcription provider resolution so streaming calls can discover the bundled OpenAI realtime transcription provider again. Fixes #60936. Thanks @sliekens and @vincentkoc.
- Plugins/OpenAI: enable `gpt-image-1` reference-image edits through `/images/edits` multipart uploads, and stop inferring unsupported resolution overrides when no explicit `size` or `resolution` is provided.
- Gateway/startup: default `gateway.mode` to `local` when unset, detect PID recycling in gateway lock files on Windows and macOS, and show startup progress so healthy restarts stop getting blocked by stale locks. (#54801, #60085, #59843)
- Mobile pairing/Android: tighten secure endpoint handling so Tailscale and public remote setup reject cleartext endpoints, private LAN pairing still works, merged-role approvals mint both node and operator device tokens, and bootstrap tokens survive node auto-pair until operator approval finishes. (#60128, #60208, #60221)
- Android/Talk Mode: restore spoken assistant replies on node-scoped sessions by keeping reply routing synced to the resolved node session key and pausing mic capture during reply playback. (#60306) Thanks @MKV21.
- Telegram: fix current-model checks in the model picker, HTML-format non-default `/model` confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and `file_id` preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971)
- Telegram/local Bot API: honor `channels.telegram.apiRoot` for buffered media downloads, add `channels.telegram.network.dangerouslyAllowPrivateNetwork` for trusted fake-IP setups, and require `channels.telegram.trustedLocalFileRoots` before reading absolute Bot API `file_path` values. (#59544, #60705)
- Matrix: recover more reliably when secret storage or recovery keys are missing by recreating secret storage during repair and backup reset, hold crypto snapshot locks during persistence, and surface explicit too-large attachment markers. (#59846, #59851, #60599, #60289)
- ACP/agents: inherit the target agent workspace for cross-agent ACP spawns and fall back safely when the inherited workspace no longer exists. (#58438) Thanks @zssggle-rgb.
- ACPX/Windows: preserve backslashes and absolute `.exe` paths in Claude CLI parsing, and fail fast on wrapper-script targets with guidance to use `cmd.exe /c`, `powershell.exe -File`, or `node <script>`. (#60689)
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loudly when `/Run` does not start, and report fast failed restarts accurately instead of pretending they timed out after 60 seconds. (#59335) Thanks @tmimmanuel.
- Discord: keep REST, webhook, and monitor traffic on the configured proxy, preserve component-only media sends, honor `@everyone` and `@here` mention gates, keep ACK reactions on the active account, and split voice connect/playback timeouts so auto-join is more reliable. (#57465, #60361, #60345)
- WhatsApp: restore `channels.whatsapp.blockStreaming` and reset watchdog timeouts after reconnect so quiet chats stop falling into reconnect loops. (#60007, #60069)
- Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267)
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198)
- Agents/Claude CLI: persist explicit `openclaw agent --session-id` runs under a stable session key so follow-ups can reuse the stored CLI binding and resume the same underlying Claude session.
- Agents/CLI backends: invalidate stored CLI session reuse when local CLI login state or the selected auth profile credential changes, so relogin and token rotation stop resuming stale sessions.
- Gateway/macOS: recover installed-but-unloaded LaunchAgents during `openclaw gateway start` and `restart`, while still preferring live unmanaged gateways during restart recovery. (#43766) Thanks @HenryC-3.
- Auth/failover: persist selected fallback overrides before retrying, shorten `auth_permanent` lockouts, and refresh websocket/shared-auth sessions only when real auth changes occur so retries and secret rotations behave predictably. (#60404, #60323, #60387)
- Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth.
- Plugins/media understanding: enable bundled Groq and Deepgram providers by default so configured transcription models work without extra plugin activation config. (#59982) Thanks @yxjsxy.
- Plugins/Kimi Coding: parse tagged tool calls and keep Anthropic-native tool payloads so Kimi coding endpoints execute tools instead of echoing raw markup. (#60051, #60391)
- Tools/web_search (Kimi): when `tools.web.search.kimi.baseUrl` is unset, inherit native Moonshot chat `baseUrl` (`.ai` / `.cn`) so China console keys authenticate on the same host as chat. Fixes #44851. (#56769) Thanks @tonga54.
- Plugins/marketplace: block remote marketplace symlink escapes without breaking ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit.
- Plugins/install: preserve unsafe override flags across linked plugin and hook-pack probes so local `--link` installs honor the documented override behavior. (#60624) Thanks @JerrettDavis.
- Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the rendered snapshot. (#28214) Thanks @solodmd.
- Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120)
- Exec approvals: reuse durable exact-command `allow-always` approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182)
- Agents/runtime: make default subagent allowlists, inherited skills/workspaces, and duplicate session-id resolution behave more predictably, and include value-shape hints in missing-parameter tool errors. (#59944, #59992, #59858, #55317)
- Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.
- Gateway/plugin routes: keep gateway-auth plugin runtime routes on write-only fallback scopes unless a trusted-proxy caller explicitly declares narrower `x-openclaw-scopes`, so plugin HTTP handlers no longer mint admin-level runtime scopes on missing or untrusted HTTP scope headers. (#59815) Thanks @pgondhi987.
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: "full"` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
- Tasks/maintenance: reconcile stale cron and chat-backed CLI task rows against live cron-job and agent-run ownership instead of treating any persisted session key as proof that the task is still running. (#60310) Thanks @lml2468.
- Providers/GitHub Copilot: send IDE identity headers on runtime model requests and GitHub token exchange so IDE-authenticated Copilot runs stop failing with missing `Editor-Version`. (#60641) Thanks @VACInc and @vincentkoc.
- Model picker/providers: treat bundled BytePlus and Volcengine plan aliases as their native providers during setup, and expose their bundled standard/coding catalogs before auth so setup can suggest the right models. (#58819) Thanks @Luckymingxuan.
- Prompt caching: route Codex Responses and Anthropic Vertex through boundary-aware cache shaping, and report the actual outbound system prompt in cache traces so cache reuse and misses line up with what providers really receive. Thanks @vincentkoc.
- Prompt caching: order stable workspace project-context files before `HEARTBEAT.md` and keep `HEARTBEAT.md` below the system-prompt cache boundary so heartbeat churn does not invalidate the stable project-context prefix. (#58979) Thanks @yozu and @vincentkoc.
- Plugins/cache: inherit the active gateway workspace for provider, web-search, and web-fetch snapshot loads when callers omit `workspaceDir`, so compatible plugin registries and snapshot caches stop missing on gateway-owned runtime paths. (#61138) Thanks @jzakirov.
- Agents/Kimi tool-call repair: preserve tool arguments that were already present on streamed tool calls when later malformed deltas fail reevaluation, while still dropping stale repair-only state before `toolcall_end`.
- MiniMax/pricing: keep bundled MiniMax highspeed pricing distinct in provider catalogs and preserve the lower M2.5 cache-read pricing when onboarding older MiniMax models. (#54214) Thanks @octo-patch.
- Agents/cache: preserve the full 3-turn prompt-cache image window across tool loops, keep colliding bundled MCP tool definitions deterministic, and reapply Anthropic Vertex cache shaping after payload hook replacements so KV/cache reuse stays stable. Thanks @vincentkoc.
- Device pairing: reject rotating device tokens into roles that were never approved during pairing, and keep reconnect role checks bounded to the paired device's approved role set. (#60462) Thanks @eleqtrizit.
- Mobile pairing/security: fail closed for internal `/pair` setup-code issuance, cleanup, and approval paths when gateway pairing scopes are missing, and keep approval-time requested-scope enforcement on the internal command path. (#55996) Thanks @coygeek.
- Status/cache: restore `cacheRead` and `cacheWrite` in transcript fallback so `/status` keeps showing cache hit percentages when session logs are the only complete usage source. (#59247) Thanks @stuartsy.
- Exec approvals/node host: forward prepared `system.run` approval plans on the async node invoke path so mutable script operands keep their approval-time binding and drift revalidation instead of dropping back to unbound execution.
- Synology Chat/security: default low-level HTTPS helper TLS verification to on so helper/API defaults match the shipped safe account default, and only explicit `allowInsecureSsl: true` opts out.
- Android/canvas security: require exact normalized A2UI URL matches before forwarding canvas bridge actions, rejecting query mismatches and descendant paths while still allowing fragment-only A2UI navigation.
- Cron: send failure notifications through the job's primary delivery channel using the same session context as successful delivery when no explicit `failureDestination` is configured. (#60622) Thanks @artwalker.
- Mobile pairing/bootstrap: keep QR bootstrap handoff tokens bounded to the mobile-safe contract so node handoff stays unscoped and operator handoff drops mixed `node.*`, `operator.admin`, and `operator.pairing` scopes.
- Gateway/auth: serialize async shared-secret auth attempts per client so concurrent Tailscale-capable failures cannot overrun the intended auth rate-limit budget. Thanks @Telecaster2147.
- Doctor/config: compare normalized `talk` configs by deep structural equality instead of key-order-sensitive serialization so `openclaw doctor --fix` stops repeatedly reporting/applying no-op `talk.provider/providers` normalization. (#59911) Thanks @ejames-dev.
- Providers/Anthropic Vertex: honor `cacheRetention: "long"` with the real 1-hour prompt-cache TTL on Vertex AI endpoints, and default `anthropic-vertex` cache retention like direct Anthropic. (#60888) Thanks @affsantos.
- Gateway/device auth: reuse cached device-token scopes only for cached-token reconnects, while keeping explicit `deviceToken` scope requests and empty-cache fallbacks intact so reconnects preserve `operator.read` without breaking explicit auth flows. (#46032) Thanks @caicongyang.
- Agents/scheduling: steer background-now work toward automatic completion wake and treat `process` polling as on-demand inspection or intervention instead of default completion handling. (#60877) Thanks @vincentkoc.
- Google Gemini CLI auth: improve OAuth credential discovery across Windows nvm and Homebrew libexec installs, and align Code Assist metadata so Gemini login stops failing on packaged CLI layouts. (#40729) Thanks @hughcube.
- Google Gemini CLI auth: detect bundled npm installs by scanning packaged bundle files for the Gemini OAuth client config, so `npm install -g @google/gemini-cli` layouts work again. (#60486) Thanks @wzfmini01.
- Mattermost/config schema: accept `groups.*.requireMention` again so existing Mattermost configs no longer fail strict validation after upgrade. (#58271) Thanks @MoerAI.
- Agents/failover: scope Anthropic `An unknown error occurred` failover matching by provider so generic internal unknown-error text no longer triggers retryable timeout fallback. (#59325) Thanks @aaron-he-zhu.
- Providers/OpenRouter failover: classify `403 "Key limit exceeded"` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
- Device pairing/security: keep non-operator device scope checks bound to the requested role prefix so bootstrap verification cannot redeem `operator.*` scopes through `node` auth. (#57258) Thanks @jlapenna.
- Gateway/device pairing: require non-admin paired-device sessions to manage only their own device for token rotate/revoke and paired-device removal, blocking cross-device token theft inside pairing-scoped sessions. (#50627) Thanks @coygeek.
- CLI/skills JSON: route `skills list --json`, `skills info --json`, and `skills check --json` output to stdout instead of stderr so machine-readable consumers receive JSON on the expected stream again. (#60914; fixes #57599; landed from contributor PR #57611 by @Aftabbs) Thanks @Aftabbs.
- Agents/subagents: honor allowlist validation, auth-profile handoff, and session override state when a subagent retries after `LiveSessionModelSwitchError`. (#58178) Thanks @openperf.
- Google image generation: disable pinned DNS for Gemini image requests and honor explicit `pinDns` overrides in shared provider HTTP helpers so proxy-backed image generation works again. (#59873) Thanks @luoyanglang.
- Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf.
- Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.
- Outbound/sanitizer: strip leaked `<tool_call>`, `<function_calls>`, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.
- Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw `<media:audio>` placeholders. (#61008) Thanks @manueltarouca.
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
- Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.
- Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
- CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.
- Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana.
- Agents/Claude CLI: keep non-interactive `--permission-mode bypassPermissions` when custom `cliBackends.claude-cli.args` override defaults, including fallback resolution before the runtime plugin registry is active, so cron and heartbeat Claude CLI runs do not regress to interactive approval mode. (#61114) Thanks @cathrynlavery and @thewilloftheshadow.
- Agents/skills: skip `.git` and `node_modules` when mirroring skills into sandbox workspaces so read-only sandboxes do not copy repo history or dependency trees. (#61090) Thanks @joelnishanth.
- Android/Talk Mode: cancel in-flight `talk.speak` playback when speech is explicitly stopped, so stale replies stop starting after barge-in or manual stop. (#61164) Thanks @obviyus.
- Plugins/onboarding: write dotted plugin uiHint paths like Brave `webSearch.mode` as nested plugin config so `llm-context` setup stops failing validation. (#61159) Thanks @obviyus.
- Android/Talk Mode: restore voice replies on gateway-backed talk mode sessions by updating embedded runner transport overrides to the current agent transport API. (#61214) Thanks @obviyus.
- Amazon Bedrock/aws-sdk auth: stop injecting the fake `AWS_PROFILE` apiKey marker when no AWS auth env vars exist, so instance-role and other default-chain setups keep working without poisoning provider config. (#61194) Thanks @wirjo.
## 2026.4.2
### Breaking
- Plugins/xAI: move `x_search` settings from the legacy core `tools.web.x_search.*` path to the plugin-owned `plugins.entries.xai.config.xSearch.*` path, standardize `x_search` auth on `plugins.entries.xai.config.webSearch.apiKey` / `XAI_API_KEY`, and migrate legacy config with `openclaw doctor --fix`. (#59674) Thanks @vincentkoc.
- Plugins/web fetch: move Firecrawl `web_fetch` config from the legacy core `tools.web.fetch.firecrawl.*` path to the plugin-owned `plugins.entries.firecrawl.config.webFetch.*` path, route `web_fetch` fallback through the new fetch-provider boundary instead of a Firecrawl-only core branch, and migrate legacy config with `openclaw doctor --fix`. (#59465) Thanks @vincentkoc.
### Changes
- Tasks/Task Flow: restore the core Task Flow substrate with managed-vs-mirrored sync modes, durable flow state/revision tracking, and `openclaw tasks flow` inspection/recovery primitives so background orchestration can persist and be operated separately from plugin authoring layers. (#58930) Thanks @mbelinky.
- Tasks/Task Flow: add managed child task spawning plus sticky cancel intent, so external orchestrators can stop scheduling immediately and let parent Task Flows settle to `cancelled` once active child tasks finish. (#59610) Thanks @mbelinky.
- Plugins/Task Flow: add a bound `api.runtime.taskFlow` seam so plugins and trusted authoring layers can create and drive managed Task Flows from host-resolved OpenClaw context without passing owner identifiers on each call. (#59622) Thanks @mbelinky.
- Android/assistant: add assistant-role entrypoints plus Google Assistant App Actions metadata so Android can launch OpenClaw from the assistant trigger and hand prompts into the chat composer. (#59596) Thanks @obviyus.
- Exec defaults: make gateway/node host exec default to YOLO mode by requesting `security=full` with `ask=off`, and align host approval-file fallbacks plus docs/doctor reporting with that no-prompt default.
- Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.
- Plugins/hooks: add `before_agent_reply` so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon.
- Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.
- Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.
- Matrix/plugin: emit spec-compliant `m.mentions` metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.
- Diffs: add plugin-owned `viewerBaseUrl` so viewer links can use a stable proxy/public origin without passing `baseUrl` on every tool call. (#59341) Related #59227. Thanks @gumadeiras.
- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg.
- Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.
- WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr.
- Exec approvals/channels: auto-enable DM-first native chat approvals when supported channels can infer approvers from existing owner config, while keeping channel fanout explicit and clarifying forwarding versus native approval client config.
- Android/assistant: auto-send Google Assistant App Actions prompts once chat is healthy and idle, while keeping bare assistant launches as open-only. (#59721) Thanks @obviyus.
### Fixes
- Sandbox/security: block credential-path binds even when sandbox home paths resolve through canonical aliases, so agent containers cannot mount user secret stores through alternate home-directory paths. (#59157) Thanks @eleqtrizit.
- Gateway/Windows scheduled tasks: preserve Task Scheduler settings on reinstall, fail loud when Scheduled Task `/Run` does not start, and report fast failed restarts with the actual elapsed time instead of a fake 60s timeout. (#59335) Thanks @tmimmanuel.
- Control UI/model picker: preserve already-qualified `provider/model` refs from the server so models whose ids already contain slashes stop being double-prefixed and remapped to the wrong provider. (#49874) Thanks @ShionEria.
- Models/selection: resolve bare model ids in session model switches against the configured allowlist before falling back to the current session provider, so Control UI model picks stop drifting into `google/k2p5` and similar wrong-provider refs. (#51580) Thanks @honwee.
## 2026.4.1-beta.1
- Providers/transport policy: centralize request auth, proxy, TLS, and header shaping across shared HTTP, stream, and websocket paths, block insecure TLS/runtime transport overrides, and keep proxy-hop TLS separate from target mTLS settings. (#59682) Thanks @vincentkoc.
- Providers/OpenRouter: gate documented OpenRouter attribution to native OpenRouter endpoints or the default route so custom proxy base URLs do not inherit OpenRouter request headers.
- Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. (#59644) Thanks @vincentkoc.
- Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.
- Providers/media HTTP: centralize base URL normalization, default auth/header injection, and explicit header override handling across shared OpenAI-compatible audio, Deepgram audio, Gemini media/image, and Moonshot video request paths. (#59469) Thanks @vincentkoc.
- Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.
- Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic `service_tier` handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.
- Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after `2026.3.31`. (#59092) Thanks @openperf.
- Agents/subagents: pin admin-only subagent gateway calls to `operator.admin` while keeping `agent` at least privilege, so `sessions_spawn` no longer dies on loopback scope-upgrade pairing with `close(1008) "pairing required"`. (#59555) Thanks @openperf.
- Exec approvals/config: strip invalid `security`, `ask`, and `askFallback` values from `~/.openclaw/exec-approvals.json` during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf.
- Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.
- Exec/runtime: treat `tools.exec.host=auto` as routing-only, keep implicit no-config exec on sandbox when available or gateway otherwise, and reject per-call host overrides that would bypass the configured sandbox or host target. (#58897) Thanks @vincentkoc.
- Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. (#59100) Thanks @jadewon.
- WhatsApp/presence: send `unavailable` presence on connect in self-chat mode so personal-phone users stop losing all push notifications while the gateway is running. (#59410) Thanks @mcaxtr.
- WhatsApp/media: add HTML, XML, and CSS to the MIME map and fall back gracefully for unknown media types instead of dropping the attachment. (#51562) Thanks @bobbyt74.
- Matrix/onboarding: restore guided setup in `openclaw channels add` and `openclaw configure --section channels`, while keeping custom plugin wizards on the shared `setupWizard` seam. (#59462) Thanks @gumadeiras.
- Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when `channels.matrix.blockStreaming` is enabled. (#59384) thanks @gumadeiras
- Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when `channels.matrix.blockStreaming` is enabled. (#59384) Thanks @gumadeiras.
- Feishu/comment threads: harden document comment-thread delivery so whole-document comments fall back to `add_comment`, delayed reply lookups retry more reliably, and user-visible replies avoid reasoning/planning spillover. (#59129) Thanks @wittam-01.
- MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux.
- Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380) Thanks @jacobtomlinson.
- Mattermost/probes: route status probes through the SSRF guard and honor `allowPrivateNetwork` so connectivity checks stay safe for self-hosted Mattermost deployments. (#58529) Thanks @mappel-nv.
- Zalo/webhook replay: scope replay dedupe key by chat and sender so reused message IDs across different chats or senders no longer collide, and harden metadata reads for partially missing payloads. (#58444)
- QQBot/structured payloads: restrict local file paths to QQ Bot-owned media storage, block traversal outside that root, reduce path leakage in logs, and keep inline image data URLs working. (#58453) Thanks @jacobtomlinson.
- Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.
- Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic `service_tier` handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.
- Providers/transport policy: centralize request auth, proxy, TLS, and header shaping across shared HTTP, stream, and websocket paths, block insecure TLS/runtime transport overrides, and keep proxy-hop TLS separate from target mTLS settings. (#59682) Thanks @vincentkoc.
- Image generation/providers: route OpenAI, MiniMax, and fal image requests through the shared provider HTTP transport path so custom base URLs, guarded private-network routing, and provider request defaults stay aligned with the rest of provider HTTP. Thanks @vincentkoc.
- Image generation/providers: stop inferring private-network access from configured OpenAI, MiniMax, and fal image base URLs, and cap shared HTTP error-body reads so hostile or misconfigured endpoints fail closed without relaxing SSRF policy or buffering unbounded error payloads. Thanks @vincentkoc.
- Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so `openclaw doctor browser` and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.
- Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after `2026.3.31`. (#59092) Thanks @openperf.
- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
- Browser/attach-only profiles: disconnect cached Playwright CDP sessions when stopping attach-only or remote CDP profiles, while still reporting never-started local managed profiles as not stopped. (#60097) Thanks @pedh.
- Agents/output sanitization: strip namespaced `antml:thinking` blocks from user-visible text so Anthropic-style internal monologue tags do not leak into replies. (#59550) Thanks @obviyus.
- Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus.
- Image tool/paths: resolve relative local media paths against the agent `workspaceDir` instead of `process.cwd()` so inputs like `inbox/receipt.png` pass the local-path allowlist reliably. (#57222) Thanks Priyansh Gupta.
- Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like `ws://localhost.:...` rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.
- Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so `openclaw doctor browser` and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.
- Podman/launch: remove noisy container output from `scripts/run-openclaw-podman.sh` and align the Podman install guidance with the quieter startup flow. (#59368) Thanks @sallyom.
- Plugins/runtime: keep LINE reply directives and browser-backed cleanup/reset flows working even when those plugins are disabled while tightening bundled plugin activation guards. (#59412) Thanks @vincentkoc.
- Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.
- Providers/media HTTP: centralize base URL normalization, default auth/header injection, and explicit header override handling across shared OpenAI-compatible audio, Deepgram audio, Gemini media/image, and Moonshot video request paths. (#59469) Thanks @vincentkoc.
- Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.
- Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic `service_tier` handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.
- Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. (#59644) Thanks @vincentkoc.
- ACP/gateway reconnects: keep ACP prompts alive across transient websocket drops while still failing boundedly when reconnect recovery does not complete. (#59473) Thanks @obviyus.
- ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.
- Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.
- Agents/subagents: pin admin-only subagent gateway calls to `operator.admin` while keeping `agent` at least privilege, so `sessions_spawn` no longer dies on loopback scope-upgrade pairing with `close(1008) "pairing required"`. (#59555) Thanks @openperf.
- Exec approvals/config: strip invalid `security`, `ask`, and `askFallback` values from `~/.openclaw/exec-approvals.json` during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf.
- Gateway/session kill: enforce HTTP operator scopes on session kill requests and gate authorization before session lookup so unauthenticated callers cannot probe session existence. (#59128) Thanks @jacobtomlinson.
- MS Teams/logging: format non-`Error` failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into `[object Object]`. (#59321) Thanks @bradgroux.
- Channels/setup: ignore untrusted workspace channel plugins during setup resolution so a shadowing workspace plugin cannot override built-in channel setup/login flows unless explicitly trusted in config. (#59158) Thanks @mappel-nv.
- Exec/Windows: restore allowlist enforcement with quote-aware `argPattern` matching across gateway and node exec, and surface accurate dynamic pre-approved executable hints in the exec tool description. (#56285) Thanks @kpngr.
- Gateway: prune empty `node-pending-work` state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.
- Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared `safeEqualSecret` helper and reject empty auth tokens in BlueBubbles. (#58432) Thanks @eleqtrizit.
- OpenShell/mirror: constrain `remoteWorkspaceDir` and `remoteAgentWorkspaceDir` to the managed `/sandbox` and `/agent` roots so mirror sync cannot escape the intended remote workspace paths. (#58515) Thanks @eleqtrizit.
- OpenShell/mirror: constrain `remoteWorkspaceDir` and `remoteAgentWorkspaceDir` to the managed `/sandbox` and `/agent` roots, and keep mirror sync from overwriting or removing user-added shell roots during config synchronization. (#58515) Thanks @eleqtrizit.
- Plugins/activation: preserve explicit, auto-enabled, and default activation provenance plus reason metadata across CLI, gateway bootstrap, and status surfaces so plugin enablement state stays accurate after auto-enable resolution. (#59641) Thanks @vincentkoc.
- Exec/env: block additional host environment override pivots for package roots, language runtimes, compiler include paths, and credential/config locations so request-scoped exec cannot redirect trusted toolchains or config lookups. (#59233) Thanks @drobison00.
- OpenShell/mirror sync: constrain mirror sync to managed roots only so user-added shell roots are no longer overwritten or removed during config synchronization. (#58515) Thanks @eleqtrizit.
- Dotenv/workspace overrides: block workspace `.env` files from overriding `OPENCLAW_PINNED_PYTHON` and `OPENCLAW_PINNED_WRITE_PYTHON` so trusted helper interpreters cannot be redirected by repo-local env injection. (#58473) Thanks @eleqtrizit.
- Plugins/install: accept JSON5 syntax in `openclaw.plugin.json` and bundle `plugin.json` manifests during install/validation, so third-party plugins with trailing commas, comments, or unquoted keys no longer fail to install. (#59084) Thanks @singleGanghood.
- Telegram/exec approvals: rewrite shared `/approve … allow-always` callback payloads to `/approve … always` before Telegram button rendering so plugin approval IDs still fit Telegram's `callback_data` limit and keep the Allow Always action visible. (#59217) Thanks @jameslcowan.
- Cron/exec timeouts: surface timed-out `exec` and `bash` failures in isolated cron runs even when `verbose: off`, including custom session-target cron jobs, so scheduled runs stop failing silently. (#58247) Thanks @skainguyen1412.
- Telegram/exec approvals: fall back to the origin session key for async approval followups and keep resume-failure status delivery sanitized so Telegram followups still land without leaking raw exec metadata. (#59351) Thanks @seonang.
- Node-host/exec approvals: bind `pnpm dlx` invocations through the approval planner's mutable-script path so the effective runtime command is resolved for approval instead of being left unbound. (#58374)
- Exec/node hosts: stop forwarding the gateway workspace cwd to remote node exec when no workdir was explicitly requested, so cross-platform node approvals fall back to the node default cwd instead of failing with `SYSTEM_RUN_DENIED`. (#58977) Thanks @Starhappysh.
- TUI/chat: keep pending local sends visible and reconciled across history reloads, make busy/error recovery clearer through fallback and terminal-error paths, and reclaim transcript width for long links and paths. (#59800) Thanks @vincentkoc.
- Exec approvals/channels: decouple initiating-surface approval availability from native delivery enablement so Telegram, Slack, and Discord still expose approvals when approvers exist and native target routing is configured separately. (#59776) Thanks @joelnishanth.
- Agents/logging: keep orphaned-user transcript repair warnings focused on interactive runs, and downgrade background-trigger repairs (`heartbeat`, `cron`, `memory`, `overflow`) to debug logs to reduce false-alarm gateway noise.
- Gateway/node pairing: require `operator.pairing` for node approvals end-to-end, while still requiring `operator.write` or `operator.admin` when the pending node commands need those higher scopes. (#60461) Thanks @eleqtrizit.
- Providers/OpenRouter: gate Anthropic prompt-cache `cache_control` markers to native/default OpenRouter routes and preserve them for native OpenRouter hosts behind custom provider ids. Thanks @vincentkoc.
- Browser/CDP: validate both initial and discovered CDP websocket endpoints before connect so strict SSRF policy blocks cross-host pivots and direct websocket targets. (#60469) Thanks @eleqtrizit.
- Browser/profiles: reject remote browser profile `cdpUrl` values that violate strict SSRF policy before saving config, with clearer validation errors for blocked endpoints. (#60477) Thanks @eleqtrizit.
- Browser/screenshots: stop sending `fromSurface: false` on CDP screenshots so managed Chrome 146+ browsers can capture images again. (#60682) Thanks @mvanhorn.
- Mattermost/slash commands: harden native slash-command callback token validation to use constant-time secret comparison, matching the existing interaction-token path.
- Control UI/mobile chat: reduce narrow-screen overflow by shrinking the chat pane minimum width, removing extra mobile padding, widening message groups, and hiding avatars on very small screens. (#60220) Thanks @macdao.
- Android/Talk Mode: route spoken replies through `talk.speak`, keep compressed playback cleanup deterministic, and fall back to local TTS for legacy gateways that omit Talk error reasons. (#60954) Thanks @obviyus.
- Android/Talk Mode: keep reply-speaker routing and teardown behavior aligned with the new remote playback path. (#60954) Thanks @MKV21.
## 2026.4.1-beta.1
- Plugins/runtime: stop ambient core helper and setup paths from loading non-selected bundled plugins, keep channel-setup snapshot scoping safe for custom channel plugins, and honor env-scoped plugin auth paths. (#59136) Thanks @vincentkoc.
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
- Config/Discord: coerce safe integer numeric Discord IDs to strings during config validation, keep unsafe or precision-losing numeric snowflakes rejected, and align `openclaw doctor` repair guidance with the same fail-closed behavior. (#45125) Thanks @moliendocode.
- Gateway/sessions: scope bare `sessions.create` aliases like `main` to the requested agent while preserving the canonical `global` and `unknown` sentinel keys. (#58207) thanks @jalehman.
- `/context detail` now compares the tracked prompt estimate with cached context usage and surfaces untracked provider/runtime overhead when present. (#28391) thanks @ImLukeF.
- Gateway/session reset: emit the typed `before_reset` hook for gateway `/new` and `/reset`, preserving reset-hook behavior even when the previous transcript has already been archived. (#53872) thanks @VACInc
- Plugins/commands: pass the active host `sessionKey` into plugin command contexts, and include `sessionId` when it is already available from the active session entry, so bundled and third-party commands can resolve the current conversation reliably. (#59044) Thanks @jalehman.
- Agents/auth: honor `models.providers.*.authHeader` for pi embedded runner model requests by injecting `Authorization: Bearer <apiKey>` when requested. (#54390) Thanks @lndyzwdxhs.
- UI/compaction: keep the compaction indicator in a retry-pending state until the run actually finishes, so the UI does not show `Context compacted` before compaction actually finishes. (#55132) Thanks @mpz4life.
- Cron/tool schemas: keep cron tool schemas strict-model-friendly while still preserving `failureAlert=false`, nullable `agentId`/`sessionKey`, and flattened add/update recovery for the newly exposed cron job fields. (#55043) Thanks @brunolorente.
- BlueBubbles/config: accept `enrichGroupParticipantsFromContacts` in the core strict config schema so gateways no longer fail validation or startup when the BlueBubbles plugin writes that field. (#56889) Thanks @zqchris.
- Agents/failover: classify AbortError and stream-abort messages as timeout so Ollama NDJSON stream aborts stop showing `reason=unknown` in model fallback logs. (#58324) Thanks @yelog
- Exec approvals: route Slack, Discord, and Telegram approvals through the shared channel approval-capability path so native approval auth, delivery, and `/approve` handling stay aligned across channels while preserving Telegram session-key agent filtering. (#58634) thanks @gumadeiras
- Matrix/runtime: resolve the verification/bootstrap runtime from a distinct packaged Matrix entry so global npm installs stop failing on crypto bootstrap with missing-module or recursive runtime alias errors. (#59249) Thanks @gumadeiras.
- Matrix/streaming: preserve ordered block flushes before tool, message, and agent boundaries, add explicit `channels.matrix.blockStreaming` opt-in so Matrix `streaming: "off"` stays final-only by default, and move MiniMax plain-text final handling into the MiniMax provider runtime instead of the shared core heuristic. (#59266) thanks @gumadeiras
- Agents/compaction: resolve compaction wait before final reply/channel flush completion so slow end-of-run delivery drains no longer delay compaction completion. (#59308) thanks @gumadeiras
- Exec approvals: align approval UX, effective-policy reporting, and `allow-always` availability with the host policy so CLI, doctor, and approval surfaces explain the real host-effective decision path. (#59283) Thanks @gumadeiras.
## 2026.4.2
## 2026.4.1
### Changes
- macOS/Voice Wake: add the Voice Wake option to trigger Talk Mode. (#58490) Thanks @SmoothExec.
- Tasks/chat: add `/tasks` as a chat-native background task board for the current session, with recent task details and agent-local fallback counts when no linked tasks are visible. Related #54226. Thanks @vincentkoc.
- Web search/SearXNG: add the bundled SearXNG provider plugin for `web_search` with configurable host support. (#57317) Thanks @cgdusek.
- Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.
- WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr.
- Telegram/errors: add configurable `errorPolicy` and `errorCooldownMs` controls so Telegram can suppress repeated delivery errors per account, chat, and topic without muting distinct failures. (#51914) Thanks @chinar-amrutkar
- Gateway/webchat: make `chat.history` text truncation configurable with `gateway.webchat.chatHistoryMaxChars` and per-request `maxChars`, while preserving silent-reply filtering and existing default payload limits. (#58900)
- Amazon Bedrock/Guardrails: add Bedrock Guardrails support to the bundled provider. (#58588) Thanks @MikeORed.
@@ -114,7 +246,6 @@ Docs: https://docs.openclaw.ai
- Agents/failover: cap prompt-side and assistant-side same-provider auth-profile retries for rate-limit failures before cross-provider model fallback, add the `auth.cooldowns.rateLimitedProfileRotations` knob, and document the new fallback behavior. (#58707) Thanks @Forgely3D
- Agents/compaction: resolve `agents.defaults.compaction.model` consistently for manual `/compact` and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg
- Cron/tools allowlist: add `openclaw cron --tools` for per-job tool allowlists. (#58504) Thanks @andyk-ms.
- Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.
### Fixes
@@ -127,41 +258,13 @@ Docs: https://docs.openclaw.ai
- Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov
- Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae
- QQBot/voice: lazy-load `silk-wasm` in `audio-convert.ts` so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.
- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.
- Ollama/model picker: show only Ollama models after provider selection in the CLI picker. (#55290) Thanks @Luckymingxuan.
- MiniMax/plugins: auto-enable the bundled MiniMax plugin for API-key auth/config so MiniMax image generation and other plugin-owned capabilities load without manual plugin allowlisting. (#57127) Thanks @tars90percent.
- Plugins/bundled runtimes: restore externalized bundled plugin runtime dependency staging across packed installs, Docker builds, and local runtime staging so bundled plugins keep their declared runtime deps after the 2026.3.31 externalization change. (#58782)
- LINE/runtime: resolve the packaged runtime contract from the built `dist/plugins/runtime` layout so LINE channels start correctly again after global npm installs on `2026.3.31`. (#58799) Thanks @vincentkoc.
- Tasks/status: hide stale completed background tasks from `/status` and `session_status`, prefer live task context, and show recent failures only when no active work remains. (#58661) Thanks @vincentkoc
- Tasks/gateway: keep the task registry maintenance sweep from stalling the gateway event loop under synchronous SQLite pressure, so upgraded gateways stop hanging about a minute after startup. (#58670) Thanks @openperf
- Tasks/gateway: re-check the current task record before maintenance marks runs lost or prunes them, so a task heartbeat or cleanup update that lands during a sweep no longer gets overwritten by stale snapshot state.
- Subagents/tasks: keep subagent completion and cleanup from crashing when task-registry writes fail, so a corrupt or missing task row no longer takes down the gateway during lifecycle finalization. Thanks @vincentkoc.
- Gateway/reload: ignore startup config writes by persisted hash in the config reloader so generated auth tokens and seeded Control UI origins do not trigger a restart loop, while real `gateway.auth.*` edits still require restart. (#58678) Thanks @yelog
- Exec/approvals: honor `exec-approvals.json` security defaults when inline or configured tool policy is unset, and keep Slack and Discord native approval handling aligned with inferred approvers and real channel enablement so remote exec stops falling into false approval timeouts and disabled states. Thanks @scoootscooob and @vincentkoc.
- Exec/approvals: make `allow-always` persist as durable user-approved trust instead of behaving like `allow-once`, reuse exact-command trust on shell-wrapper paths that cannot safely persist an executable allowlist entry, keep static allowlist entries from silently bypassing `ask:"always"`, and require explicit approval when Windows cannot build an allowlist execution plan instead of hard-dead-ending remote exec. Thanks @scoootscooob and @vincentkoc.
- Exec/cron: resolve isolated cron no-route approval dead-ends from the effective host fallback policy when trusted automation is allowed, and make `openclaw doctor` warn when `tools.exec` is broader than `~/.openclaw/exec-approvals.json` so stricter host-policy conflicts are explicit. Thanks @scoootscooob and @vincentkoc.
- Gateway/HTTP: skip failing HTTP request stages so one broken facade no longer forces every HTTP endpoint to return 500. (#58746) Thanks @yelog
- Gateway/nodes: stop pinning live node commands to the approved node-pair record. Node pairing remains a trust/token flow, while per-node `system.run` policy stays in that node's exec approvals config. Fixes #58824.
- WebChat/exec approvals: use native approval UI guidance in agent system prompts instead of telling agents to paste manual `/approve` commands in webchat sessions. Thanks @vincentkoc.
- Channels/QQ Bot: keep `/bot-logs` export gated behind a truly explicit QQBot allowlist, rejecting wildcard and mixed wildcard entries while preserving the real framework command path. Thanks @vincentkoc.
- Channels/plugins: keep bundled channel plugins loadable from legacy `channels.<id>` config even under restrictive plugin allowlists, and make `openclaw doctor` warn only on real plugin blockers instead of misleading setup guidance. (#58873) Thanks @obviyus
- CDP/profiles: prefer `cdpPort` over stale WebSocket URLs so browser automation reconnects cleanly. (#58499) Thanks @Mlightsnow.
- Media/paths: resolve relative `MEDIA` paths against the agent workspace so local attachment references keep working. (#58624) Thanks @aquaright1.
- Memory/session indexing: keep full reindexes from skipping session transcripts when sync is triggered by `session-start` or `watch`, so restart-driven reindexes preserve session memory. (#39732) Thanks @upupc
- Memory/QMD: prefer `--mask` over `--glob` when creating QMD collections so default memory collections keep their intended patterns and stop colliding on restart. (#58643) Thanks @GitZhangChi.
- Sandbox/browser: compare browser runtime inspection against `agents.defaults.sandbox.browser.image` so `openclaw sandbox list --browser` stops reporting healthy browser containers as image mismatches. (#58759) Thanks @sandpile.
- Plugins/install: forward `--dangerously-force-unsafe-install` through archive and npm-spec plugin installs so the documented override reaches the security scanner on those install paths. (#58879) Thanks @ryanlee-gemini.
- Auto-reply/commands: strip inbound metadata before slash command detection so wrapped `/model`, `/new`, and `/status` commands are recognized. (#58725) Thanks @Mlightsnow.
- Agents/Anthropic: preserve thinking blocks and signatures across replay, cache-control patching, and context pruning so compacted Anthropic sessions continue working instead of failing on later turns. (#58916) Thanks @obviyus
- Agents/Anthropic: recover cleanly after a crash leaves the latest assistant turn with incomplete thinking blocks, dropping or retrying the corrupted turn instead of getting stuck on later Anthropic requests. Thanks @explainanalyze. Maintainer refresh: vincentkoc.
- WhatsApp/groups: fix bot waking up on self-number quoted replies in groups with `selfChatMode` enabled. (#60148) Thanks @lurebat
- Device pairing: require `operator.pairing` or `operator.admin` for internal `/pair` setup-code, QR, and cleanup commands so lower-privilege gateway callers cannot mint or revoke pairing bootstrap material. (#60491) Thanks @eleqtrizit.
- Agents/failover: unify structured and raw provider error classification so provider-specific `400`/`422` payloads no longer get forced into generic format failures before retry, billing, or compaction logic can inspect them. (#58856) Thanks @aaron-he-zhu.
- Auth profiles/store: coerce misplaced SecretRef objects out of plaintext `key` and `token` fields during store load so agents without ACP runtime stop crashing on `.trim()` after upgrade. (#58923) Thanks @openperf.
- ACPX/runtime: repair `queue owner unavailable` session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana
- ACPX/runtime: retry dead-session queue-owner repair without `--resume-session` when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.
- Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to `auth-profiles.json` before returning them, so rotated Codex refresh tokens survive restart and stop falling into `refresh_token_reused` loops. (#53082)
- Agents/Anthropic: honor explicit `cacheRetention` for custom providers using `anthropic-messages`, so Anthropic-compatible proxy providers can reuse prompt caching when they opt in. (#59049) Thanks @wwerst and @vincentkoc.
- Discord/gateway: hand reconnect ownership back to Carbon, keep runtime status aligned with close/reconnect state, and force-stop sockets that open without reaching READY so Discord monitors recover promptly instead of waiting on stale health timeouts. (#59019) Thanks @obviyus
- Control UI/build: stop `pnpm ui:build` from reinstalling the UI with production-only dependencies, so fresh self-healing UI builds keep `vite` available instead of failing before asset generation. (#59267) Thanks @juliabush.
- Tools/web_search (Kimi): replay native Moonshot `$web_search` arguments verbatim, disable thinking for `kimi-k2.5`, and add Moonshot region/model setup prompts so bundled Kimi web search works again. (#59356) Thanks @Innocent-children.
## 2026.3.31
@@ -212,6 +315,7 @@ Docs: https://docs.openclaw.ai
- Slack: stop retry-driven duplicate replies when draft-finalization edits fail ambiguously, and log configured allowlisted users/channels by readable name instead of raw IDs.
- Agents/OpenAI Responses: normalize raw bundled MCP tool schemas on the WebSocket/Responses path so bare-object, object-ish, and top-level union MCP tools no longer get rejected by OpenAI during tool registration. (#58299) Thanks @yelog.
- ACP/security: replace ACP's dangerous-tool name override with semantic approval classes, so only narrow readonly reads/searches can auto-approve while indirect exec-capable and control-plane tools always require explicit prompt approval. Thanks @vincentkoc.
- ACP: derive owner-only approval classes from the shared tool-policy fallback map so `cron`, `nodes`, and `whatsapp_login` cannot drift out of prompt-required coverage.
- ACP/sessions_spawn: register ACP child runs for completion tracking and lifecycle cleanup, and make registration-failure cleanup explicitly best-effort so callers do not assume an already-started ACP turn was fully aborted. (#40885) Thanks @xaeon2026 and @vincentkoc.
- ACP/tasks: mark cleanly exited ACP runs as blocked when they end on deterministic write or authorization blockers, and wake the parent session with a follow-up instead of falsely reporting success.
- ACPX/runtime: derive the bundled ACPX expected version from the extension package metadata instead of hardcoding a separate literal, so plugin-local ACPX installs stop drifting out of health-check parity after version bumps. (#49089) Thanks @jiejiesks and @vincentkoc.
@@ -274,55 +378,8 @@ Docs: https://docs.openclaw.ai
## 2026.3.31-beta.1
### Breaking
- Nodes/exec: remove the duplicated `nodes.run` shell wrapper from the CLI and agent `nodes` tool so node shell execution always goes through `exec host=node`, keeping node-specific capabilities on `nodes invoke` and the dedicated media/location/notify actions.
- Plugin SDK: deprecate the legacy provider compat subpaths plus the older bundled provider setup and channel-runtime compatibility shims, emit migration warnings, and keep the current documented `openclaw/plugin-sdk/*` entrypoints plus local `api.ts` / `runtime-api.ts` barrels as the forward path ahead of a future major-release removal.
- Skills/install and Plugins/install: built-in dangerous-code `critical` findings and install-time scan failures now fail closed by default, so plugin installs and gateway-backed skill dependency installs that previously succeeded may now require an explicit dangerous override such as `--dangerously-force-unsafe-install` to proceed.
- Gateway/auth: `trusted-proxy` now rejects mixed shared-token configs, and local-direct fallback requires the configured token instead of implicitly authenticating same-host callers. Thanks @zhangning-agent, @jacobtomlinson, and @vincentkoc.
- Gateway/node commands: node commands now stay disabled until node pairing is approved, so device pairing alone is no longer enough to expose declared node commands. (#57777) Thanks @jacobtomlinson.
- Gateway/node events: node-originated runs now stay on a reduced trusted surface, so notification-driven or node-triggered flows that previously relied on broader host/session tool access may need adjustment. (#57691) Thanks @jacobtomlinson.
### Changes
- ACP/plugins: add an explicit default-off ACPX plugin-tools MCP bridge config, document the trust boundary, and harden the built-in bridge packaging/logging path so global installs and stdio MCP sessions work reliably. (#56867) Thanks @joe2643.
- Agents/LLM: add a configurable idle-stream timeout for embedded runner requests so stalled model streams abort cleanly instead of hanging until the broader run timeout fires. (#55072) Thanks @liuy.
- Agents/MCP: materialize bundle MCP tools with provider-safe names (`serverName__toolName`), support optional `streamable-http` transport selection plus per-server connection timeouts, and preserve real tool results from aborted/error turns unless truncation explicitly drops them. (#49505) Thanks @ziomancer.
- Android/notifications: add notification-forwarding controls with package filtering, quiet hours, rate limiting, and safer picker behavior for forwarded notification events. (#40175) Thanks @nimbleenigma.
- Background tasks: turn tasks into a real shared background-run control plane instead of ACP-only bookkeeping by unifying ACP, subagent, cron, and background CLI execution under one SQLite-backed ledger, routing detached lifecycle updates through the executor seam, adding audit/maintenance/status visibility, tightening auto-cleanup and lost-run recovery, improving task awareness in internal status/tool surfaces, and clarifying the split between heartbeat/main-session automation and detached scheduled runs. Thanks @mbelinky and @vincentkoc.
- Background tasks: add the first linear task flow control surface with `openclaw tasks list|show|cancel`, keep manual multi-task flows separate from one-task auto-sync flows, and surface doctor recovery hints for obviously orphaned or broken flow/task linkage. Thanks @mbelinky and @vincentkoc.
- Channels/QQ Bot: add QQ Bot as a bundled channel plugin with multi-account setup, SecretRef-aware credentials, slash commands, reminders, and media send/receive support. (#52986) Thanks @sliverp.
- Diffs: skip unused viewer-versus-file SSR preload work so `diffs` view-only and file-only runs do less render work while keeping mode outputs aligned. (#57909) thanks @gumadeiras.
- Tasks: add a minimal SQLite-backed task flow registry plus task-to-flow linkage scaffolding, so orchestrated work can start gaining a first-class parent record without changing current task delivery behavior. Thanks @mbelinky and @vincentkoc.
- Tasks: persist blocked state on one-task task flows and let the same flow reopen cleanly on retry, so blocked detached work can carry a parent-level reason and continue without fragmenting into a new job. Thanks @mbelinky and @vincentkoc.
- Tasks: route one-task ACP and subagent updates through a parent task-flow owner context, so detached work can emerge back through the intended parent thread/session instead of speaking only as a raw child task. Thanks @mbelinky and @vincentkoc.
- LINE/outbound media: add LINE image, video, and audio outbound sends on the LINE-specific delivery path, including explicit preview/tracking handling for videos while keeping generic media sends on the existing image-only route. (#45826) Thanks @masatohoshino.
- Matrix/history: add optional room history context for Matrix group triggers via `channels.matrix.historyLimit`, with per-agent watermarks and retry-safe snapshots so failed trigger retries do not drift into newer room messages. (#57022) thanks @chain710.
- Matrix/network: add explicit `channels.matrix.proxy` config for routing Matrix traffic through an HTTP(S) proxy, including account-level overrides and matching probe/runtime behavior. (#56931) thanks @patrick-yingxi-pan.
- Matrix/streaming: add draft streaming so partial Matrix replies update the same message in place instead of sending a new message for each chunk. (#56387) Thanks @jrusz.
- Matrix/threads: add per-DM `threadReplies` overrides and keep thread session isolation aligned with the effective room or DM thread policy from the triggering message onward. (#57995) thanks @teconomix.
- MCP: add remote HTTP/SSE server support for `mcp.servers` URL configs, including auth headers and safer config redaction for MCP credentials. (#50396) Thanks @dhananjai1729.
- Memory/QMD: add per-agent `memorySearch.qmd.extraCollections` so agents can opt into cross-agent session search without flattening every transcript collection into one shared QMD namespace. Thanks @vincentkoc.
- Microsoft Teams/member info: add a Graph-backed member info action so Teams automations and tools can resolve channel member details directly from Microsoft Graph. (#57528) Thanks @sudie-codes.
- Nostr/inbound DMs: verify inbound event signatures before pairing or sender-authorization side effects, so forged DM events no longer create pairing requests or trigger reply attempts. Thanks @smaeljaish771 and @vincentkoc.
- OpenAI/Responses: forward configured `text.verbosity` across Responses HTTP and WebSocket transports, surface it in `/status`, and keep per-agent verbosity precedence aligned with runtime behavior. (#47106) Thanks @merc1305 and @vincentkoc.
- Pi/Codex: add native Codex web search support for embedded Pi runs, including config/docs/wizard coverage and managed-tool suppression when native Codex search is active. (#46579) Thanks @Evizero.
- Slack/exec approvals: add native Slack approval routing and approver authorization so exec approval prompts can stay in Slack instead of falling back to the Web UI or terminal. Thanks @vincentkoc.
- TTS: Add structured provider diagnostics and fallback attempt analytics. (#57954) Thanks @joshavant.
- WhatsApp/reactions: agents can now react with emoji on incoming WhatsApp messages, enabling more natural conversational interactions like acknowledging a photo with ❤️ instead of typing a reply. Thanks @mcaxtr.
- Agents/BTW: force `/btw` side questions to disable provider reasoning so Anthropic adaptive-thinking sessions stop failing with `No BTW response generated`. Fixes #55376. Thanks @Catteres and @vincentkoc.
- CLI/onboarding: reset the remote gateway URL prompt to the safe loopback default after declining a discovered endpoint, so onboarding does not keep a previously rejected remote URL. (#57828)
- Agents/exec defaults: honor per-agent `tools.exec` defaults when no inline directive or session override is present, so configured exec host, security, ask, and node settings actually apply. (#57689)
- Sandbox/networking: sanitize SSH subprocess env vars through the shared sandbox policy and route marketplace archive downloads plus Ollama discovery, auth, and pull requests through the guarded fetch path so sandboxed execution and remote fetches follow the repo's trust boundaries. (#57848, #57850)
### Fixes
- Agents/OpenAI Responses: normalize raw bundled MCP tool schemas on the WebSocket/Responses path so bare-object, object-ish, and top-level union MCP tools no longer get rejected by OpenAI during tool registration. (#58299) Thanks @yelog.
- ACP/security: replace ACP's dangerous-tool name override with semantic approval classes, so only narrow readonly reads/searches can auto-approve while indirect exec-capable and control-plane tools always require explicit prompt approval. Thanks @vincentkoc.
- ACP/sessions_spawn: register ACP child runs for completion tracking and lifecycle cleanup, and make registration-failure cleanup explicitly best-effort so callers do not assume an already-started ACP turn was fully aborted. (#40885) Thanks @xaeon2026 and @vincentkoc.
- ACP/tasks: mark cleanly exited ACP runs as blocked when they end on deterministic write or authorization blockers, and wake the parent session with a follow-up instead of falsely reporting success.
- ACPX/runtime: derive the bundled ACPX expected version from the extension package metadata instead of hardcoding a separate literal, so plugin-local ACPX installs stop drifting out of health-check parity after version bumps. (#49089) Thanks @jiejiesks and @vincentkoc.
- Agents/Anthropic failover: treat Anthropic `api_error` payloads with `An unexpected error occurred while processing the response` as transient so retry/fallback can engage instead of surfacing a terminal failure. (#57441) Thanks @zijiess and @vincentkoc.
- Agents/compaction: keep late compaction-retry rejections handled after the aggregate timeout path wins without swallowing real pre-timeout wait failures, so timed-out retries no longer surface an unhandled rejection on later unsubscribe. (#57451) Thanks @mpz4life and @vincentkoc.
- Agents/context pruning: count supplementary-plane CJK characters with the shared code-point-aware estimator so context pruning stops underestimating Japanese and Chinese text that uses Extension B ideographs. (#39985) Thanks @Edward-Qiang-2024.
- Agents/Kimi: preserve already-valid Anthropic-compatible tool call argument objects while still clearing cached repairs when later trailing junk exceeds the repair allowance. (#54491) Thanks @yuanaichi.
@@ -343,49 +400,6 @@ Docs: https://docs.openclaw.ai
- Tasks: add a small task-flow runtime substrate for authoring layers with persisted wait targets and output bags, plus bundled skills/Lobster examples and richer `flows show` / `doctor` recovery hints for multi-task flow state. (#58336) Thanks @mbelinky and @vincentkoc.
- Config/legacy cleanup: stop probing obsolete alternate legacy config names and service labels during local config/service detection, while keeping the active `~/.openclaw/openclaw.json` path canonical.
- Config/runtime: pin the first successful config load in memory for the running process and refresh that snapshot on successful writes/reloads, so hot paths stop reparsing `openclaw.json` between watcher-driven swaps.
- Config/SecretRef + Control UI: harden SecretRef redaction round-trip restore, block unsafe raw fallback (force Form mode when raw is unavailable), and preflight submitted-config SecretRefs before config write RPC persistence. (#58044) Thanks @joshavant.
- Config/Telegram: migrate removed `channels.telegram.groupMentionsOnly` into `channels.telegram.groups["*"].requireMention` on load so legacy configs no longer crash at startup. (#55336) thanks @jameslcowan.
- Config/update: stop `openclaw doctor` write-backs from persisting plugin-injected channel defaults, so `openclaw update` no longer seeds config keys that later break service refresh validation. (#56834) Thanks @openperf.
- Control UI/agents: auto-load agent workspace files on initial Files panel open, and populate overview model/workspace/fallbacks from effective runtime agent metadata so defaulted models no longer show as `Not set`. (#56637) Thanks @dxsx84.
- Control UI/slash commands: make `/steer` and `/redirect` work from the chat command palette with visible pending state for active-run `/steer`, correct redirected-run tracking, and a single canonical `/steer` entry in the command menu. (#54625) Thanks @fuller-stack-dev.
- Cron/announce: preserve all deliverable text payloads for announce mode instead of collapsing to the last chunk, so multi-line cron reports deliver in full to Telegram forum topics.
- Cron/isolated sessions: carry the full live-session provider, model, and auth-profile selection across retry restarts so cron jobs with model overrides no longer fail or loop on mid-run model-switch requests. (#57972) Thanks @issaba1.
- Diffs/config: preserve schema-shaped plugin config parsing from `diffsPluginConfigSchema.safeParse()`, so direct callers keep `defaults` and `security` sections instead of receiving flattened tool defaults. (#57904) Thanks @gumadeiras.
- Diffs: fall back to plain text when `lang` hints are invalid during diff render and viewer hydration, so bad or stale language values no longer break the diff viewer. (#57902) Thanks @gumadeiras.
- Discord/voice: enforce the same guild channel and member allowlist checks on spoken voice ingress before transcription, so joined voice channels no longer accept speech from users outside the configured Discord access policy. Thanks @cyjhhh and @vincentkoc.
- Docker/setup: force BuildKit for local image builds (including sandbox image builds) so `./docker-setup.sh` no longer fails on `RUN --mount=...` when hosts default to Docker's legacy builder. (#56681) Thanks @zhanghui-china.
- Docs/anchors: fix broken English docs links and make Mint anchor audits run against the English-source docs tree. (#57039) thanks @velvet-shark.
- Doctor/plugins: skip false Matrix legacy-helper warnings when no migration plans exist, and keep bundled `enabledByDefault` plugins in the gateway startup set. (#57931) Thanks @dinakars777.
- Exec approvals/macOS: unwrap `arch` and `xcrun` before deriving shell payloads and allow-always patterns, so wrapper approvals stay bound to the carried command instead of the outer carrier. Thanks @tdjackey and @vincentkoc.
- Exec approvals: unwrap `caffeinate` and `sandbox-exec` before persisting allow-always trust so later shell payload changes still require a fresh approval. Thanks @tdjackey and @vincentkoc.
- Exec/approvals: infer Discord and Telegram exec approvers from existing owner config when `execApprovals.approvers` is unset, extend the default approval window to 30 minutes, and clarify approval-unavailable guidance so approvals do not appear to silently disappear.
- Exec/approvals: keep `awk` and `sed` family binaries out of the low-risk `safeBins` fast path, and stop doctor profile scaffolding from treating them like ordinary custom filters. Thanks @vincentkoc.
- Exec/env: block proxy, TLS, and Docker endpoint env overrides in host execution so request-scoped commands cannot silently reroute outbound traffic or trust attacker-supplied certificate settings. Thanks @AntAISecurityLab.
- Exec/env: block Python package index override variables from request-scoped host exec environment sanitization so package fetches cannot be redirected through a caller-supplied index. Thanks @nexrin and @vincentkoc.
- Exec/node: stop gateway-side workdir fallback from rewriting explicit `host=node` cwd values to the gateway filesystem, so remote node exec approval and runs keep using the intended node-local directory. (#50961) Thanks @openperf.
- Exec/runtime: default implicit exec to `host=auto`, resolve that target to sandbox only when a sandbox runtime exists, keep explicit `host=sandbox` fail-closed without sandbox, and show `/exec` effective host state in runtime status/docs.
- Exec: fail closed when the implicit sandbox host has no sandbox runtime, and stop denied async approval followups from reusing prior command output from the same session. (#56800) Thanks @scoootscooob.
- Feishu/groups: keep quoted replies and topic bootstrap context aligned with group sender allowlists so only allowlisted thread messages seed agent context. Thanks @AntAISecurityLab and @vincentkoc.
- Gateway/attachments: offload large inbound images without leaking `media://` markers into text-only runs, preserve mixed attachment order for model input/transcripts, and fail closed when model image capability cannot be resolved. (#55513) Thanks @Syysean.
- Gateway/auth: keep shared-auth rate limiting active during WebSocket handshake attempts even when callers also send device-token candidates, so bogus device-token fields no longer suppress shared-secret brute-force tracking. Thanks @kexinoh and @vincentkoc.
- Gateway/auth: reject mismatched browser `Origin` headers on trusted-proxy HTTP operator requests while keeping origin-less headless proxy clients working. Thanks @AntAISecurityLab and @vincentkoc.
- Gateway/device tokens: disconnect active device sessions after token rotation so newly rotated credentials revoke existing live connections immediately instead of waiting for those sockets to close naturally. Thanks @zsxsoft and @vincentkoc.
- Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u.
- Gateway/pairing: restore QR bootstrap onboarding handoff so fresh `/pair qr` iPhone setup can auto-approve the initial node pairing, receive a reusable node device token, and stop retrying with spent bootstrap auth. (#58382) Thanks @ngutman.
- Gateway/OpenAI compatibility: accept flat Responses API function tool definitions on `/v1/responses` and preserve `strict` when normalizing hosted tools into the embedded runner, so spec-compliant clients like Codex no longer fail validation or silently lose strict tool enforcement. Thanks @malaiwah and @vincentkoc.
- Gateway/OpenAI HTTP: restore default operator scopes for bearer-authenticated requests that omit `x-openclaw-scopes`, so headless `/v1/chat/completions` and session-history callers work again after the recent method-scope hardening. (#57596) Thanks @openperf.
- Gateway/plugins: scope plugin-auth HTTP route runtime clients to read-only access and keep gateway-authenticated plugin routes on write scope, so plugin-owned webhook handlers do not inherit write-capable runtime access by default. Thanks @davidluzsilva and @vincentkoc.
- Gateway/SecretRef: resolve restart token drift checks with merged service/runtime env sources and hard-fail unsupported mutable SecretRef plus OAuth-profile combinations so restart warnings and policy enforcement match runtime behavior. (#58141) Thanks @joshavant.
- Gateway/tools HTTP: tighten HTTP tool-invoke authorization so owner-only tools stay off HTTP invoke paths. (#57773) Thanks @jacobtomlinson.
- Harden async approval followup delivery in webchat-only sessions (#57359) Thanks @joshavant.
- Heartbeat/auth: prevent exec-event heartbeat runs from inheriting owner-only tool access from the session delivery target, so node exec output stays on the non-owner tool surface even when the target session belongs to the owner. Thanks @AntAISecurityLab and @vincentkoc.
- Hooks/config: accept runtime channel plugin ids in `hooks.mappings[].channel` (for example `feishu`) instead of rejecting non-core channels during config validation. (#56226) Thanks @AiKrai001.
- Hooks/session routing: rebind hook-triggered `agent:` session keys to the actual target agent before isolated dispatch so dedicated hook agents keep their own session-scoped tool and plugin identity. Thanks @kexinoh and @vincentkoc.
- Host exec/env: block additional request-scoped env overrides that can redirect Docker endpoints, trust roots, compiler include paths, package resolution, or Python environment roots during approved host runs. Thanks @tdjackey and @vincentkoc.
- Image generation/build: write stable runtime alias files into `dist/` and route provider-auth runtime lookups through those aliases so image-generation providers keep resolving auth/runtime modules after rebuilds instead of crashing on missing hashed chunk files.
- iOS/Live Activities: mark the `ActivityKit` import in `LiveActivityManager.swift` as `@preconcurrency` so Xcode 26.4 / Swift 6 builds stop failing on strict concurrency checks. (#57180) Thanks @ngutman.
- LINE/ACP: add current-conversation binding and inbound binding-routing parity so `/acp spawn ... --thread here`, configured ACP bindings, and active conversation-bound ACP sessions work on LINE like the other conversation channels.
- LINE/markdown: preserve underscores inside Latin, Cyrillic, and CJK words when stripping markdown, while still removing standalone `_italic_` markers on the shared text-runtime path used by LINE and TTS. (#47465) Thanks @jackjin1997.
- LINE/status: stop `openclaw status` from warning about missing credentials when sanitized LINE snapshots are already configured, while still surfacing whether the missing field is the token or secret. (#45701) Thanks @tamaosamu.
- macOS/local gateway: stop OpenClaw.app from killing healthy local gateway listeners after startup by recognizing the current `openclaw-gateway` process title and using the current `openclaw gateway` launch shape.
- macOS/wide-area discovery: switch gateway discovery to Tailscale MagicDNS names so Mac clients recover more reliably across changing tailnet IPs. (#57833) Thanks @jacobtomlinson.
@@ -535,8 +549,6 @@ Docs: https://docs.openclaw.ai
- Telegram/delivery: skip whitespace-only and hook-blanked text replies in bot delivery to prevent GrammyError 400 empty-text crashes. (#56620)
- Telegram/send: validate `replyToMessageId` at all four API sinks with a shared normalizer that rejects non-numeric, NaN, and mixed-content strings. (#56587)
- Telegram/cron topics: route announce target parsing through the Telegram extension seam and carry explicit `delivery.threadId` through cron delivery resolution, so legacy `group:` routes and topic-targeted cron sends keep their forum topic destination. (#58489) Thanks @cwmine.
- Approvals/UI: keep the newest pending approval at the front of the Control UI queue so approving one request does not accidentally target an older expired id. Thanks @vincentkoc.
- Plugin approvals: accept unique short approval-id prefixes on `plugin.approval.resolve`, matching exec approvals and restoring `/approve` fallback flows on chat approval surfaces. Thanks @vincentkoc.
- Mistral: normalize OpenAI-compatible request flags so official Mistral API runs no longer fail with remaining `422 status code (no body)` chat errors.
- Control UI/config: keep sensitive raw config hidden by default, replace the blank blocked editor with an explicit reveal-to-edit state, and restore raw JSON editing without auto-exposing secrets. Fixes #55322.
- CLI/zsh: defer `compdef` registration until `compinit` is available so zsh completion loads cleanly with plugin managers and manual setups. (#56555)
@@ -581,7 +593,6 @@ Docs: https://docs.openclaw.ai
- Control UI/Skills: open skill detail dialogs with the browser modal lifecycle so clicking a skill row keeps the panel centered instead of rendering it off-screen at the bottom of the page.
- Matrix/replies: include quoted poll question/options in inbound reply context so the agent sees the original poll content when users reply to Matrix poll messages. (#55056) Thanks @alberthild.
- Matrix/plugins: keep plugin bootstrap from crashing when built runtime mixes bare and deep `matrix-js-sdk` entrypoints, so unrelated channels do not get taken down during plugin load. (#56273) Thanks @aquaright1.
- Agents/sandbox: honor `tools.sandbox.tools.alsoAllow`, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.
- Agents/sandbox: make blocked-tool guidance glob-aware again, redact/sanitize session-specific explain hints for safer copy-paste, and avoid leaking control-character session keys in those hints. (#54684) Thanks @ngutman.
- Agents/compaction: trigger timeout recovery compaction before retrying high-context LLM timeouts so embedded runs stop repeating oversized requests. (#46417) thanks @joeykrug.
- Agents/compaction: reconcile `sessions.json.compactionCount` after a late embedded auto-compaction success so persisted session counts catch up once the handler reports completion. (#45493) Thanks @jackal092927.
@@ -627,6 +638,9 @@ Docs: https://docs.openclaw.ai
- Plugins/Matrix: encrypt E2EE image thumbnails with `thumbnail_file` while keeping unencrypted-room previews on `thumbnail_url`, so encrypted Matrix image events keep thumbnail metadata without leaking plaintext previews. (#54711) thanks @frischeDaten.
- Telegram/forum topics: keep native `/new` and `/reset` routed to the active topic by preserving the topic target on forum-thread command context. (#35963)
- Status/port diagnostics: treat single-process dual-stack loopback gateway listeners as healthy in `openclaw status --all`, suppressing false "port already in use" conflict warnings. (#53398) Thanks @DanWebb1949.
- CLI/Docker: treat loopback private-host CLI gateway connects as local for silent pairing auto-approval, while keeping remote backend and public-host CLI connects behind pairing. (#55113) Thanks @sar618.
## 2026.3.24
### Breaking
@@ -635,8 +649,6 @@ Docs: https://docs.openclaw.ai
## 2026.3.24
### Breaking
### Changes
- Gateway/OpenAI compatibility: add `/v1/models` and `/v1/embeddings`, and forward explicit model overrides through `/v1/chat/completions` and `/v1/responses` for broader client and RAG compatibility. Thanks @vincentkoc.
@@ -684,60 +696,17 @@ Docs: https://docs.openclaw.ai
- Security/path resolution: prefer non-user-writable absolute helper binaries for OpenClaw CLI, ffmpeg, and OpenSSL resolution so PATH hijacks cannot replace trusted helpers with attacker-controlled executables.
- Security/gateway command scopes: require `operator.admin` before Telegram target writeback and Talk Voice `/voice set` config writes persist through gateway message flows.
- Security/OpenShell mirror: exclude workspace `hooks/` from mirror sync so untrusted sandbox files cannot become trusted host hooks on gateway startup.
- Exec approvals/channels: unify Discord and Telegram exec approval runtime handling, move approval buttons onto the shared interactive reply model, and fix Telegram approval buttons and typed `/approve` commands so configured approvers can resolve requests reliably again. (#57516) Thanks @scoootscooob.
## 2026.3.24-beta.2
### Breaking
### Changes
### Fixes
- Outbound media/local files: align outbound media access with the configured fs policy so host-local files and inbound-media paths keep sending when `workspaceOnly` is off, while strict workspace-only agents remain sandboxed.
- Runtime/install: lower the supported Node 22 floor to `22.14+` while continuing to recommend Node 24, so npm installs and self-updates do not strand Node 22.14 users on older releases.
- CLI/update: preflight the target npm package `engines.node` before `openclaw update` runs a global package install, so outdated Node runtimes fail with a clear upgrade message instead of attempting an unsupported latest release.
- Tests/security audit: isolate audit-test home and personal skill resolution so local `~/.agents/skills` installs no longer make maintainer prep runs fail nondeterministically. (#54473) thanks @huntharo
## 2026.3.24-beta.1
### Breaking
### Changes
- Gateway/OpenAI compatibility: add `/v1/models` and `/v1/embeddings`, and forward explicit model overrides through `/v1/chat/completions` and `/v1/responses` for broader client and RAG compatibility. Thanks @vincentkoc.
- Agents/tools: make `/tools` show the tools the current agent can actually use right now, add a compact default view with an optional detailed mode, and add a live "Available Right Now" section in the Control UI so it is easier to see what will work before you ask.
- Microsoft Teams: migrate to the official Teams SDK and add AI-agent UX best practices including streaming 1:1 replies, welcome cards with prompt starters, feedback/reflection, informative status updates, typing indicators, and native AI labeling. (#51808)
- Microsoft Teams: add message edit and delete support for sent messages, including in-thread fallbacks when no explicit target is provided. (#49925)
- Skills/install metadata: add one-click install recipes to bundled skills (coding-agent, gh-issues, openai-whisper-api, session-logs, tmux, trello, weather) so the CLI and Control UI can offer dependency installation when requirements are missing. (#53411) Thanks @BunsDev.
- Control UI/skills: add status-filter tabs (All / Ready / Needs Setup / Disabled) with counts, replace inline skill cards with a click-to-detail dialog showing requirements, toggle switch, install action, API key entry, source metadata, and homepage link. (#53411) Thanks @BunsDev.
- Slack/interactive replies: restore rich reply parity for direct deliveries, auto-render simple trailing `Options:` lines as buttons/selects, improve Slack interactive setup defaults, and isolate reply controls from plugin interactive handlers. (#53389) Thanks @vincentkoc.
- CLI/containers: add `--container` and `OPENCLAW_CONTAINER` to run `openclaw` commands inside a running Docker or Podman OpenClaw container. (#52651) Thanks @sallyom.
- Discord/auto threads: add optional `autoThreadName: "generated"` naming so new auto-created threads can be renamed asynchronously with concise LLM-generated titles while keeping the existing message-based naming as the default. (#43366) Thanks @davidguttman.
- Plugins/hooks: add `before_dispatch` with canonical inbound metadata and route handled replies through the normal final-delivery path, preserving TTS and routed delivery semantics. (#50444) Thanks @gfzhx.
- Control UI/agents: convert agent workspace file rows to expandable `<details>` with lazy-loaded inline markdown preview, and add comprehensive `.sidebar-markdown` styles for headings, lists, code blocks, tables, blockquotes, and details/summary elements. (#53411) Thanks @BunsDev.
- Control UI/markdown preview: restyle the agent workspace file preview dialog with a frosted backdrop, sized panel, and styled header, and integrate `@create-markdown/preview` v2 system theme for rich markdown rendering (headings, tables, code blocks, callouts, blockquotes) that auto-adapts to the app's light/dark design tokens. (#53411) Thanks @BunsDev.
- macOS app/config: replace horizontal pill-based subsection navigation with a collapsible tree sidebar using disclosure chevrons and indented subsection rows. (#53411) Thanks @BunsDev.
- CLI/skills: soften missing-requirements label from "missing" to "needs setup" and surface API key setup guidance (where to get a key, CLI save command, storage path) in `openclaw skills info` output. (#53411) Thanks @BunsDev.
- macOS app/skills: add "Get your key" homepage link and storage-path hint to the API key editor dialog, and show the config path in save confirmation messages. (#53411) Thanks @BunsDev.
- Control UI/agents: add a "Not set" placeholder to the default agent model selector dropdown. (#53411) Thanks @BunsDev.
### Fixes
- Security/sandbox media dispatch: close the `mediaUrl`/`fileUrl` alias bypass so outbound tool and message actions cannot escape media-root restrictions. (#54034)
- Gateway/restart sentinel: wake the interrupted agent session via heartbeat after restart instead of only sending a best-effort restart note, retry outbound delivery once on transient failure, and preserve explicit thread/topic routing through the wake path so replies land in the correct Telegram topic or Slack thread. (#53940) Thanks @VACInc.
- Docker/setup: avoid the pre-start `openclaw-cli` shared-network namespace loop by routing setup-time onboard/config writes through `openclaw-gateway`, so fresh Docker installs stop failing before the gateway comes up. (#53385) Thanks @amsminn.
- Gateway/channels: keep channel startup sequential while isolating per-channel boot failures, so one broken channel no longer blocks later channels from starting. (#54215) Thanks @JonathanJing.
- Embedded runs/secrets: stop unresolved `SecretRef` config from crashing embedded agent runs by falling back to the resolved runtime snapshot when needed. Fixes #45838.
- WhatsApp/groups: track recent gateway-sent message IDs and suppress only matching group echoes, preserving owner `/status`, `/new`, and `/activation` commands from linked-account `fromMe` traffic. (#53624) Thanks @w-sss.
- WhatsApp/reply-to-bot detection: restore implicit group reply detection by unwrapping `botInvokeMessage` payloads and reading `selfLid` from `creds.json`, so reply-based mentions reach the bot again in linked-account group chats.
- Telegram/forum topics: recover `#General` topic `1` routing when Telegram omits forum metadata, including native commands, interactive callbacks, inbound message context, and fallback error replies. (#53699) thanks @huntharo
- Discord/gateway supervision: centralize gateway error handling behind a lifetime-owned supervisor so early, active, and late-teardown Carbon gateway errors stay classified consistently and stop surfacing as process-killing teardown crashes.
- Discord/timeouts: send a visible timeout reply when the inbound Discord worker times out before a final reply starts, including created auto-thread targets and queued-run ordering. (#53823) Thanks @Kimbo7870.
- ACP/direct chats: always deliver a terminal ACP result when final TTS does not yield audio, even if block text already streamed earlier, and skip redundant empty-text final synthesis. (#53692) Thanks @w-sss.
- Telegram/outbound errors: preserve actionable 403 membership/block/kick details and treat `bot not a member` as a permanent delivery failure so Telegram sends stop retrying doomed chats. (#53635) Thanks @w-sss.
- Telegram/photos: preflight Telegram photo dimension and aspect-ratio rules, and fall back to document sends when image metadata is invalid or unavailable so photo uploads stop failing with `PHOTO_INVALID_DIMENSIONS`. (#52545) Thanks @hnshah.
- Slack/runtime defaults: trim Slack DM reply overhead, restore Codex auto transport, and tighten Slack/web-search runtime defaults around DM preview threading, cache scoping, warning dedupe, and explicit web-search opt-in. (#53957) Thanks @vincentkoc.
- Doctor/image generation: seed migrated legacy Nano Banana Google provider config with the `/v1beta` API root and an empty model list so `openclaw doctor --fix` completes and the migrated native Google image path keeps hitting the correct endpoint. (#53757) Thanks @mahopan.
- Models/google: normalize bare Google Generative AI API roots for custom provider names, and keep built-in Google model-id rewrites working when `api` is declared only on individual models, so custom Google lanes and older configs stop missing `/v1beta` or preview-id normalization. (#44969) Thanks @Kathie-yu.
- Feishu/startup: treat unresolved `SecretRef` app credentials as not configured during account resolution so CLI startup and read-only Feishu config surfaces stop crashing before runtime-backed secret resolution is available. (#53675) Thanks @hpt.
@@ -774,8 +743,6 @@ Docs: https://docs.openclaw.ai
## 2026.3.23
### Breaking
### Changes
- ModelStudio/Qwen: add standard (pay-as-you-go) DashScope endpoints for China and global Qwen API keys alongside the existing Coding Plan endpoints, and relabel the provider group to `Qwen (Alibaba Cloud Model Studio)`. (#43878)
@@ -953,9 +920,7 @@ Docs: https://docs.openclaw.ai
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
- Gateway/Discord startup: load only configured channel plugins during gateway boot, and lazy-load Discord provider/session runtime setup so startup stops importing unrelated providers and trims cold-start delay. Thanks @vincentkoc.
- Agents/inbound: lazy-load media and link understanding for plain-text turns and cache synced auth stores by auth-file state so ordinary inbound replies avoid unnecessary startup churn. Thanks @vincentkoc.
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
- Agents/openai-responses: strip `prompt_cache_key` and `prompt_cache_retention` for non-OpenAI-compatible Responses endpoints while keeping them on direct OpenAI and Azure OpenAI paths, so third-party OpenAI-compatible providers no longer reject those requests with HTTP 400. (#49877) Thanks @ShaunTsai.
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
- Telegram/replies: set `allow_sending_without_reply` on reply-targeted sends and media-error notices so deleted parent messages no longer drop otherwise valid replies. (#52524) Thanks @moltbot886.
@@ -1007,7 +972,6 @@ Docs: https://docs.openclaw.ai
- Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob.
- Onboarding/custom providers: keep Azure AI Foundry `*.services.ai.azure.com` custom endpoints on the selected compatibility path instead of forcing Responses, so chat-completions Foundry models still work after setup. Fixes #50528. (#50535) Thanks @obviyus.
- make `openclaw update status` explicitly say `up to date` when the local version already matches npm latest, while keeping the availability logic unchanged. (#51409) Thanks @dongzhenye.
- Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.
- Browser/node proxy: enforce `nodeHost.browserProxy.allowProfiles` across `query.profile` and `body.profile`, block proxy-side profile create/delete when the allowlist is set, and keep the default full proxy surface when the allowlist is empty.
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
- Security/exec safe bins: remove `jq` from the default safe-bin allowlist and fail closed on the `jq` `env` builtin when operators explicitly opt `jq` back in, so `jq -n env` cannot dump host secrets without an explicit trust path. Thanks @gladiator9797 for reporting.
@@ -1052,6 +1016,9 @@ Docs: https://docs.openclaw.ai
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied. (#47968) Thanks @Takhoffman.
- Feishu/webhooks: harden signed webhook verification to use constant-time signature comparison and keep malformed short signatures fail-closed in webhook E2E coverage.
- Ollama/tool replay: deserialize stringified tool-call arguments on native and OpenAI-compatible Ollama paths while preserving unsafe integer ids across round-trips. (#52253) Thanks @Adam-Researchh.
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) Thanks @MonkeyLeeT.
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274) Thanks @obviyus.
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969) Thanks @obviyus.
@@ -1131,7 +1098,6 @@ Docs: https://docs.openclaw.ai
- Plugins/subagents: preserve gateway-owned plugin subagent access across runtime, tool, and embedded-runner load paths so gateway plugin tools and context engines can still spawn and manage subagents after the loader cache split. (#46648) Thanks @jalehman.
- Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman.
- Tests/OpenAI Codex auth: align login expectations with the default `gpt-5.4` model so CI coverage stays consistent with the current OpenAI Codex default. (#44367) Thanks @jrrcdev.
- Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.
- Plugins/discovery: distinguish missing package entry files from package-path escape violations so startup skips absent plugin entry paths without raising false security diagnostics. (#52491) Thanks @hclsys.
- Plugins/Matrix: accept shared send-tool media aliases (`mediaUrl`, `filePath`, `path`) and preserve `asVoice` / `audioAsVoice` through Matrix action dispatch so media-only sends and voice-message intents reach the plugin send layer correctly. Thanks @psacc and @vincentkoc.
- Plugins/runtime-api: pin extension runtime-api export surfaces with explicit guardrail coverage so future surface creep becomes a deliberate diff. Thanks @vincentkoc.
@@ -1139,7 +1105,6 @@ Docs: https://docs.openclaw.ai
- Plugins/update: let `openclaw plugins update <npm-spec>` target tracked npm installs by dist-tag or exact version, and preserve the recorded npm spec for later id-based updates. (#49998) Thanks @huntharo.
- Tests/CLI: reduce command-secret gateway test import pressure while keeping the real protocol payload validator in place, so the isolated lane no longer carries the heavier runtime-web and message-channel graphs. (#50663) Thanks @huntharo.
- Gateway/plugins: share plugin interactive callback routing and plugin bind approval state across duplicate module graphs so Telegram Codex picker buttons and plugin bind approvals no longer fall through to normal inbound message routing. (#50722) Thanks @huntharo.
- Plugins/context engines: retry strict legacy `assemble()` calls without the new `prompt` field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.
- Plugins/runtime state: share plugin-facing infra singleton state across duplicate module graphs and keep session-binding adapter ownership stable until the active owner unregisters. (#50725) thanks @huntharo.
- Discord/pickers: keep `/codex_resume --browse-projects` picker callbacks alive in Discord by sharing component callback state across duplicate module graphs, preserving callback fallbacks, and acknowledging matched plugin interactions before dispatch. (#51260) Thanks @huntharo.
- Telegram/Mattermost message tool: keep plugin button schemas optional in isolated and cron sessions so plain sends do not fail validation when no current channel is active. (#52589) Thanks @tylerliu612.
@@ -1317,8 +1282,6 @@ Docs: https://docs.openclaw.ai
- Docs/Brave pricing: escape literal dollar signs in Brave Search cost text so the docs render the free credit and per-request pricing correctly. (#44989) Thanks @keelanfh.
- Feishu/file uploads: preserve literal UTF-8 filenames in `im.file.create` so Chinese and other non-ASCII filenames no longer appear percent-encoded in chat. (#34262) Thanks @fabiaodemianyang and @KangShuaiFu.
- Agents/compaction safeguard: trim large kept `toolResult` payloads consistently for budgeting, pruning, and identifier seeding, then restore preserved payloads after prune so oversized safeguard summaries stay stable. (#44133) thanks @SayrWolfridge.
- Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv.
- Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman.
- Agents/Ollama overflow: rewrite Ollama `prompt too long` API payloads through the normal context-overflow sanitizer so embedded sessions keep the friendly overflow copy and auto-compaction trigger. (#34019) thanks @lishuaigit.
- Control UI/auth: restore one-time legacy `?token=` imports for shared Control UI links while keeping `#token=` preferred, and carry pending query tokens through gateway URL confirmation so compatibility links still authenticate after confirmation. (#43979) Thanks @stim64045-spec.
@@ -1362,7 +1325,6 @@ Docs: https://docs.openclaw.ai
- macOS/LaunchAgent install: tighten LaunchAgent directory and plist permissions during install so launchd bootstrap does not fail when the target home path or generated plist inherited group/world-writable modes.
- Discord/reply chunking: resolve the effective `maxLinesPerMessage` config across live reply paths and preserve `chunkMode` in the fast send path so long Discord replies no longer split unexpectedly at the default 17-line limit. (#40133) thanks @rbutera.
- Feishu/local image auto-convert: pass `mediaLocalRoots` through the `sendText` local-image shim so allowed local image paths upload as Feishu images again instead of falling back to raw path text. (#40623) Thanks @ayanesakura.
- Models/Kimi Coding: send `anthropic-messages` tools in native Anthropic format again so `kimi-coding` stops degrading tool calls into XML/plain-text pseudo invocations instead of real `tool_use` blocks. (#38669, #39907, #40552) Thanks @opriz.
- Telegram/outbound HTML sends: chunk long HTML-mode messages, preserve plain-text fallback and silent-delivery params across retries, and cut over to plain text when HTML chunk planning cannot safely preserve the full message. (#42240) thanks @obviyus.
- Telegram/final preview delivery: split active preview lifecycle from cleanup retention so missing archived preview edits avoid duplicate fallback sends without clearing the live preview or blocking later in-place finalization. (#41662) thanks @hougangdev.
- Telegram/final preview delivery followup: keep ambiguous missing-`message_id` finals only when a preview was already visible, while first-preview/no-id cases still fall back so Telegram users do not lose the final reply. (#41932) thanks @hougangdev.
@@ -1542,7 +1504,6 @@ Docs: https://docs.openclaw.ai
- CLI/skills tables: keep terminal table borders aligned for wide graphemes, use full reported terminal width, and switch a few ambiguous skill icons to Terminal-safe emoji so `openclaw skills` renders more consistently in Terminal.app and iTerm. Thanks @vincentkoc.
- Memory/Gemini: normalize returned Gemini embeddings across direct query, direct batch, and async batch paths so memory search uses consistent vector handling for Gemini too. (#43409) Thanks @gumadeiras.
- Agents/failover: recognize additional serialized network errno strings plus `EHOSTDOWN` and `EPIPE` structured codes so transient transport failures trigger timeout failover more reliably. (#42830) Thanks @jnMetaCode.
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
- Agents/embedded runner: carry provider-observed overflow token counts into compaction so overflow retries and diagnostics use the rejected live prompt size instead of only transcript estimates. (#40357) thanks @rabsef-bicrym.
- Agents/compaction transcript updates: emit a transcript-update event immediately after successful embedded compaction so downstream listeners observe the post-compact transcript without waiting for a later write. (#25558) thanks @rodrigouroz.
- Agents/sessions_spawn: use the target agent workspace for cross-agent spawned runs instead of inheriting the caller workspace, so child sessions load the correct workspace-scoped instructions and persona files. (#40176) Thanks @moshehbenavraham.

View File

@@ -85,6 +85,12 @@ Welcome to the lobster tank! 🦞
4. **Test/CI-only PRs for known `main` failures** → Don't open a PR. The Maintainer team is already tracking those failures, and PRs that only tweak tests or CI to chase them will be closed unless they are required to validate a new fix.
5. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
## PR Limits
We cap at **10 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
For coordinated change sets that genuinely need more than 10 PRs, join the **#clawtributors** channel in Discord and talk to maintainers first.
## Before You PR
- Test locally with your OpenClaw instance

View File

@@ -34,7 +34,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
<table>
<tr>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://openai.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/openai-light.svg">
@@ -42,7 +42,15 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://github.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/github-light.svg">
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/github.svg" alt="GitHub" height="28">
</picture>
</a>
</td>
<td align="center" width="16.66%">
<a href="https://www.nvidia.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/nvidia.svg">
@@ -50,7 +58,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://vercel.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/vercel-light.svg">
@@ -58,7 +66,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://blacksmith.sh/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/blacksmith-light.svg">
@@ -66,7 +74,7 @@ New install? Start here: [Getting started](https://docs.openclaw.ai/start/gettin
</picture>
</a>
</td>
<td align="center" width="20%">
<td align="center" width="16.66%">
<a href="https://www.convex.dev/">
<picture>
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/sponsors/convex-light.svg">

View File

@@ -56,6 +56,7 @@ These are frequently reported but are typically closed with no code change:
- Authorized user-triggered local actions presented as privilege escalation. Example: an allowlisted/owner sender running `/export-session /absolute/path.html` to write on the host. In this trust model, authorized user actions are trusted host actions unless you demonstrate an auth/sandbox/boundary bypass.
- Reports that only show a malicious plugin executing privileged actions after a trusted operator installs/enables it.
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
- Reports that only show quoted/replied/thread/forwarded supplemental context from non-allowlisted senders being visible to the model, without demonstrating an auth, policy, approval, or sandbox boundary bypass.
- Reports that treat the Gateway HTTP compatibility endpoints (`POST /v1/chat/completions`, `POST /v1/responses`) as if they implemented scoped operator auth (`operator.write` vs `operator.admin`). These endpoints authenticate the shared Gateway bearer secret/password and are documented full operator-access surfaces, not per-user/per-scope boundaries.
- Reports that assume `x-openclaw-scopes` can reduce or redefine shared-secret bearer auth on the OpenAI-compatible HTTP endpoints. For shared-secret auth (`gateway.auth.mode="token"` or `"password"`), those endpoints ignore narrower bearer-declared scopes and restore the full default operator scope set plus owner semantics.
- Reports that treat `POST /tools/invoke` under shared-secret bearer auth (`gateway.auth.mode="token"` or `"password"`) as a narrower per-request/per-scope authorization surface. That endpoint is designed as the same trusted-operator HTTP boundary: shared-secret bearer auth is full operator access there, narrower `x-openclaw-scopes` values do not reduce that path, and owner-only tool policy follows the shared-secret operator contract.
@@ -167,6 +168,24 @@ OpenClaw's security model is "personal assistant" (one trusted operator, potenti
- For company-shared setups, use a dedicated machine/VM/container and dedicated accounts; avoid mixing personal data on that runtime.
- If that host/browser profile is logged into personal accounts (for example Apple/Google/personal password manager), you have collapsed the boundary and increased personal-data exposure risk.
## Context Visibility and Allowlists
OpenClaw distinguishes:
- **Trigger authorization**: who can trigger the agent (`dmPolicy`, `groupPolicy`, allowlists, mention gates)
- **Context visibility**: what supplemental context is provided to the model (reply body, quoted text, thread history, forwarded metadata)
In current releases, allowlists primarily gate triggering and owner-style command access. They do not guarantee universal supplemental-context redaction across every channel/surface.
Current channel behavior is not fully uniform:
- some channels already filter parts of supplemental context by sender allowlist
- other channels still pass supplemental context as received
Reports that only show supplemental-context visibility differences are typically hardening/consistency findings unless they also demonstrate a documented boundary bypass (auth, policy, approvals, sandbox, or equivalent).
Hardening roadmap may add explicit visibility modes (for example `all`, `allowlist`, `allowlist_quote`) so operators can opt into stricter context filtering with predictable tradeoffs.
## Agent and Model Assumptions
- The model/agent is **not** a trusted principal. Assume prompt/content injection can manipulate behavior.

View File

@@ -2,6 +2,120 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.4.2</title>
<pubDate>Thu, 02 Apr 2026 18:57:54 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026040290</sparkle:version>
<sparkle:shortVersionString>2026.4.2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.4.2</h2>
<h3>Breaking</h3>
<ul>
<li>Plugins/xAI: move <code>x_search</code> settings from the legacy core <code>tools.web.x_search.*</code> path to the plugin-owned <code>plugins.entries.xai.config.xSearch.*</code> path, standardize <code>x_search</code> auth on <code>plugins.entries.xai.config.webSearch.apiKey</code> / <code>XAI_API_KEY</code>, and migrate legacy config with <code>openclaw doctor --fix</code>. (#59674) Thanks @vincentkoc.</li>
<li>Plugins/web fetch: move Firecrawl <code>web_fetch</code> config from the legacy core <code>tools.web.fetch.firecrawl.*</code> path to the plugin-owned <code>plugins.entries.firecrawl.config.webFetch.*</code> path, route <code>web_fetch</code> fallback through the new fetch-provider boundary instead of a Firecrawl-only core branch, and migrate legacy config with <code>openclaw doctor --fix</code>. (#59465) Thanks @vincentkoc.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Tasks/Task Flow: restore the core Task Flow substrate with managed-vs-mirrored sync modes, durable flow state/revision tracking, and <code>openclaw flows</code> inspection/recovery primitives so background orchestration can persist and be operated separately from plugin authoring layers. (#58930) Thanks @mbelinky.</li>
<li>Tasks/Task Flow: add managed child task spawning plus sticky cancel intent, so external orchestrators can stop scheduling immediately and let parent Task Flows settle to <code>cancelled</code> once active child tasks finish. (#59610) Thanks @mbelinky.</li>
<li>Plugins/Task Flow: add a bound <code>api.runtime.taskFlow</code> seam so plugins and trusted authoring layers can create and drive managed Task Flows from host-resolved OpenClaw context without passing owner identifiers on each call. (#59622) Thanks @mbelinky.</li>
<li>Android/assistant: add assistant-role entrypoints plus Google Assistant App Actions metadata so Android can launch OpenClaw from the assistant trigger and hand prompts into the chat composer. (#59596) Thanks @obviyus.</li>
<li>Exec defaults: make gateway/node host exec default to YOLO mode by requesting <code>security=full</code> with <code>ask=off</code>, and align host approval-file fallbacks plus docs/doctor reporting with that no-prompt default.</li>
<li>Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman.</li>
<li>Plugins/hooks: add <code>before_agent_reply</code> so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) Thanks @JoshuaLelon.</li>
<li>Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths.</li>
<li>Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and <code>feishu_drive</code> comment actions for document collaboration workflows. (#58497) Thanks @wittam-01.</li>
<li>Matrix/plugin: emit spec-compliant <code>m.mentions</code> metadata across text sends, media captions, edits, poll fallback text, and action-driven edits so Matrix mentions notify reliably in clients like Element. (#59323) Thanks @gumadeiras.</li>
<li>Diffs: add plugin-owned <code>viewerBaseUrl</code> so viewer links can use a stable proxy/public origin without passing <code>baseUrl</code> on every tool call. (#59341) Related #59227. Thanks @gumadeiras.</li>
<li>Agents/compaction: resolve <code>agents.defaults.compaction.model</code> consistently for manual <code>/compact</code> and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg.</li>
<li>Agents/compaction: add <code>agents.defaults.compaction.notifyUser</code> so the <code>🧹 Compacting context...</code> start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327.</li>
<li>WhatsApp/reactions: add <code>reactionLevel</code> guidance for agent reactions. Thanks @mcaxtr.</li>
<li>Exec approvals/channels: auto-enable DM-first native chat approvals when supported channels can infer approvers from existing owner config, while keeping channel fanout explicit and clarifying forwarding versus native approval client config.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Providers/transport policy: centralize request auth, proxy, TLS, and header shaping across shared HTTP, stream, and websocket paths, block insecure TLS/runtime transport overrides, and keep proxy-hop TLS separate from target mTLS settings. (#59682) Thanks @vincentkoc.</li>
<li>Providers/Copilot: classify native GitHub Copilot API hosts in the shared provider endpoint resolver and harden token-derived proxy endpoint parsing so Copilot base URL routing stays centralized and fails closed on malformed hints. (#59644) Thanks @vincentkoc.</li>
<li>Providers/streaming headers: centralize default and attribution header merging across OpenAI websocket, embedded-runner, and proxy stream paths so provider-specific headers stay consistent and caller overrides only win where intended. (#59542) Thanks @vincentkoc.</li>
<li>Providers/media HTTP: centralize base URL normalization, default auth/header injection, and explicit header override handling across shared OpenAI-compatible audio, Deepgram audio, Gemini media/image, and Moonshot video request paths. (#59469) Thanks @vincentkoc.</li>
<li>Providers/OpenAI-compatible routing: centralize native-vs-proxy request policy so hidden attribution and related OpenAI-family defaults only apply on verified native endpoints across stream, websocket, and shared audio HTTP paths. (#59433) Thanks @vincentkoc.</li>
<li>Providers/Anthropic routing: centralize native-vs-proxy endpoint classification for direct Anthropic <code>service_tier</code> handling so spoofed or proxied hosts do not inherit native Anthropic defaults. (#59608) Thanks @vincentkoc.</li>
<li>Gateway/exec loopback: restore legacy-role fallback for empty paired-device token maps and allow silent local role upgrades so local exec and node clients stop failing with pairing-required errors after <code>2026.3.31</code>. (#59092) Thanks @openperf.</li>
<li>Agents/subagents: pin admin-only subagent gateway calls to <code>operator.admin</code> while keeping <code>agent</code> at least privilege, so <code>sessions_spawn</code> no longer dies on loopback scope-upgrade pairing with <code>close(1008) "pairing required"</code>. (#59555) Thanks @openperf.</li>
<li>Exec approvals/config: strip invalid <code>security</code>, <code>ask</code>, and <code>askFallback</code> values from <code>~/.openclaw/exec-approvals.json</code> during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf.</li>
<li>Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras.</li>
<li>Exec/runtime: treat <code>tools.exec.host=auto</code> as routing-only, keep implicit no-config exec on sandbox when available or gateway otherwise, and reject per-call host overrides that would bypass the configured sandbox or host target. (#58897) Thanks @vincentkoc.</li>
<li>Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. (#59100) Thanks @jadewon.</li>
<li>WhatsApp/presence: send <code>unavailable</code> presence on connect in self-chat mode so personal-phone users stop losing all push notifications while the gateway is running. (#59410) Thanks @mcaxtr.</li>
<li>WhatsApp/media: add HTML, XML, and CSS to the MIME map and fall back gracefully for unknown media types instead of dropping the attachment. (#51562) Thanks @bobbyt74.</li>
<li>Matrix/onboarding: restore guided setup in <code>openclaw channels add</code> and <code>openclaw configure --section channels</code>, while keeping custom plugin wizards on the shared <code>setupWizard</code> seam. (#59462) Thanks @gumadeiras.</li>
<li>Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when <code>channels.matrix.blockStreaming</code> is enabled. (#59384) Thanks @gumadeiras.</li>
<li>Feishu/comment threads: harden document comment-thread delivery so whole-document comments fall back to <code>add_comment</code>, delayed reply lookups retry more reliably, and user-visible replies avoid reasoning/planning spillover. (#59129) Thanks @wittam-01.</li>
<li>MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux.</li>
<li>Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380) Thanks @jacobtomlinson.</li>
<li>Mattermost/probes: route status probes through the SSRF guard and honor <code>allowPrivateNetwork</code> so connectivity checks stay safe for self-hosted Mattermost deployments. (#58529) Thanks @mappel-nv.</li>
<li>Zalo/webhook replay: scope replay dedupe key by chat and sender so reused message IDs across different chats or senders no longer collide, and harden metadata reads for partially missing payloads. (#58444)</li>
<li>QQBot/structured payloads: restrict local file paths to QQ Bot-owned media storage, block traversal outside that root, reduce path leakage in logs, and keep inline image data URLs working. (#58453) Thanks @jacobtomlinson.</li>
<li>Image generation/providers: route OpenAI, MiniMax, and fal image requests through the shared provider HTTP transport path so custom base URLs, guarded private-network routing, and provider request defaults stay aligned with the rest of provider HTTP. Thanks @vincentkoc.</li>
<li>Image generation/providers: stop inferring private-network access from configured OpenAI, MiniMax, and fal image base URLs, and cap shared HTTP error-body reads so hostile or misconfigured endpoints fail closed without relaxing SSRF policy or buffering unbounded error payloads. Thanks @vincentkoc.</li>
<li>Browser/host inspection: keep static Chrome inspection helpers out of the activated browser runtime so <code>openclaw doctor browser</code> and related checks do not eagerly load the bundled browser plugin. (#59471) Thanks @vincentkoc.</li>
<li>Browser/CDP: normalize trailing-dot localhost absolute-form hosts before loopback checks so remote CDP websocket URLs like <code>ws://localhost.:...</code> rewrite back to the configured remote host. (#59236) Thanks @mappel-nv.</li>
<li>Agents/output sanitization: strip namespaced <code>antml:thinking</code> blocks from user-visible text so Anthropic-style internal monologue tags do not leak into replies. (#59550) Thanks @obviyus.</li>
<li>Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus.</li>
<li>Image tool/paths: resolve relative local media paths against the agent <code>workspaceDir</code> instead of <code>process.cwd()</code> so inputs like <code>inbox/receipt.png</code> pass the local-path allowlist reliably. (#57222) Thanks Priyansh Gupta.</li>
<li>Podman/launch: remove noisy container output from <code>scripts/run-openclaw-podman.sh</code> and align the Podman install guidance with the quieter startup flow. (#59368) Thanks @sallyom.</li>
<li>Plugins/runtime: keep LINE reply directives and browser-backed cleanup/reset flows working even when those plugins are disabled while tightening bundled plugin activation guards. (#59412) Thanks @vincentkoc.</li>
<li>ACP/gateway reconnects: keep ACP prompts alive across transient websocket drops while still failing boundedly when reconnect recovery does not complete. (#59473) Thanks @obviyus.</li>
<li>ACP/gateway reconnects: reject stale pre-ack ACP prompts after reconnect grace expiry so callers fail cleanly instead of hanging indefinitely when the gateway never confirms the run.</li>
<li>Gateway/session kill: enforce HTTP operator scopes on session kill requests and gate authorization before session lookup so unauthenticated callers cannot probe session existence. (#59128) Thanks @jacobtomlinson.</li>
<li>MS Teams/logging: format non-<code>Error</code> failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into <code>[object Object]</code>. (#59321) Thanks @bradgroux.</li>
<li>Channels/setup: ignore untrusted workspace channel plugins during setup resolution so a shadowing workspace plugin cannot override built-in channel setup/login flows unless explicitly trusted in config. (#59158) Thanks @mappel-nv.</li>
<li>Exec/Windows: restore allowlist enforcement with quote-aware <code>argPattern</code> matching across gateway and node exec, and surface accurate dynamic pre-approved executable hints in the exec tool description. (#56285) Thanks @kpngr.</li>
<li>Gateway: prune empty <code>node-pending-work</code> state entries after explicit acknowledgments and natural expiry so the per-node state map no longer grows indefinitely. (#58179) Thanks @gavyngong.</li>
<li>Webhooks/secret comparison: replace ad-hoc timing-safe secret comparisons across BlueBubbles, Feishu, Mattermost, Telegram, Twilio, and Zalo webhook handlers with the shared <code>safeEqualSecret</code> helper and reject empty auth tokens in BlueBubbles. (#58432) Thanks @eleqtrizit.</li>
<li>OpenShell/mirror: constrain <code>remoteWorkspaceDir</code> and <code>remoteAgentWorkspaceDir</code> to the managed <code>/sandbox</code> and <code>/agent</code> roots, and keep mirror sync from overwriting or removing user-added shell roots during config synchronization. (#58515) Thanks @eleqtrizit.</li>
<li>Plugins/activation: preserve explicit, auto-enabled, and default activation provenance plus reason metadata across CLI, gateway bootstrap, and status surfaces so plugin enablement state stays accurate after auto-enable resolution. (#59641) Thanks @vincentkoc.</li>
<li>Exec/env: block additional host environment override pivots for package roots, language runtimes, compiler include paths, and credential/config locations so request-scoped exec cannot redirect trusted toolchains or config lookups. (#59233) Thanks @drobison00.</li>
<li>Dotenv/workspace overrides: block workspace <code>.env</code> files from overriding <code>OPENCLAW_PINNED_PYTHON</code> and <code>OPENCLAW_PINNED_WRITE_PYTHON</code> so trusted helper interpreters cannot be redirected by repo-local env injection. (#58473) Thanks @eleqtrizit.</li>
<li>Plugins/install: accept JSON5 syntax in <code>openclaw.plugin.json</code> and bundle <code>plugin.json</code> manifests during install/validation, so third-party plugins with trailing commas, comments, or unquoted keys no longer fail to install. (#59084) Thanks @singleGanghood.</li>
<li>Telegram/exec approvals: rewrite shared <code>/approve … allow-always</code> callback payloads to <code>/approve … always</code> before Telegram button rendering so plugin approval IDs still fit Telegram's <code>callback_data</code> limit and keep the Allow Always action visible. (#59217) Thanks @jameslcowan.</li>
<li>Cron/exec timeouts: surface timed-out <code>exec</code> and <code>bash</code> failures in isolated cron runs even when <code>verbose: off</code>, including custom session-target cron jobs, so scheduled runs stop failing silently. (#58247) Thanks @skainguyen1412.</li>
<li>Telegram/exec approvals: fall back to the origin session key for async approval followups and keep resume-failure status delivery sanitized so Telegram followups still land without leaking raw exec metadata. (#59351) Thanks @seonang.</li>
<li>Node-host/exec approvals: bind <code>pnpm dlx</code> invocations through the approval planner's mutable-script path so the effective runtime command is resolved for approval instead of being left unbound. (#58374)</li>
<li>Exec/node hosts: stop forwarding the gateway workspace cwd to remote node exec when no workdir was explicitly requested, so cross-platform node approvals fall back to the node default cwd instead of failing with <code>SYSTEM_RUN_DENIED</code>. (#58977) Thanks @Starhappysh.</li>
<li>Exec approvals/channels: decouple initiating-surface approval availability from native delivery enablement so Telegram, Slack, and Discord still expose approvals when approvers exist and native target routing is configured separately. (#59776) Thanks @joelnishanth.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>macOS/Voice Wake: add the Voice Wake option to trigger Talk Mode. (#58490) Thanks @SmoothExec.</li>
<li>Tasks/chat: add <code>/tasks</code> as a chat-native background task board for the current session, with recent task details and agent-local fallback counts when no linked tasks are visible. Related #54226. Thanks @vincentkoc.</li>
<li>Web search/SearXNG: add the bundled SearXNG provider plugin for <code>web_search</code> with configurable host support. (#57317) Thanks @cgdusek.</li>
<li>Telegram/errors: add configurable <code>errorPolicy</code> and <code>errorCooldownMs</code> controls so Telegram can suppress repeated delivery errors per account, chat, and topic without muting distinct failures. (#51914) Thanks @chinar-amrutkar</li>
<li>Gateway/webchat: make <code>chat.history</code> text truncation configurable with <code>gateway.webchat.chatHistoryMaxChars</code> and per-request <code>maxChars</code>, while preserving silent-reply filtering and existing default payload limits. (#58900)</li>
<li>Amazon Bedrock/Guardrails: add Bedrock Guardrails support to the bundled provider. (#58588) Thanks @MikeORed.</li>
<li>ZAI/models: add <code>glm-5.1</code> and <code>glm-5v-turbo</code> to the bundled Z.AI provider catalog. (#58793) Thanks @tomsun28</li>
<li>Agents/default params: add <code>agents.defaults.params</code> for global default provider parameters. (#58548) Thanks @lpender.</li>
<li>Agents/failover: cap prompt-side and assistant-side same-provider auth-profile retries for rate-limit failures before cross-provider model fallback, add the <code>auth.cooldowns.rateLimitedProfileRotations</code> knob, and document the new fallback behavior. (#58707) Thanks @Forgely3D</li>
<li>Agents/compaction: resolve <code>agents.defaults.compaction.model</code> consistently for manual <code>/compact</code> and other context-engine compaction paths, so engine-owned compaction uses the configured override model across runtime entrypoints. (#56710) Thanks @oliviareid-svg</li>
<li>Cron/tools allowlist: add <code>openclaw cron --tools</code> for per-job tool allowlists. (#58504) Thanks @andyk-ms.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Chat/error replies: stop leaking raw provider/runtime failures into external chat channels, return a friendly retry message instead, and add a specific <code>/new</code> hint for Bedrock toolResult/toolUse session mismatches. (#58831) Thanks @ImLukeF.</li>
<li>Sessions/model switching: keep <code>/model</code> changes queued behind busy runs instead of interrupting the active turn, and retarget queued followups so later work picks up the new model as soon as the current turn finishes.</li>
<li>Web UI/OpenResponses: preserve rewritten stream snapshots in webchat and keep OpenResponses final streamed text aligned when models rewind earlier output. (#58641) Thanks @neeravmakwana</li>
<li>Discord/inbound media: pass Discord attachment and sticker downloads through the shared idle-timeout and worker-abort path so slow or stuck inbound media fetches stop hanging message processing. (#58593) Thanks @aquaright1</li>
<li>Telegram/retries: keep non-idempotent sends on the strict safe-send path, retry wrapped pre-connect failures, and preserve <code>429</code> / <code>retry_after</code> backoff for safe delivery retries. (#51895) Thanks @chinar-amrutkar</li>
<li>Telegram/exec approvals: route topic-aware exec approval followups through Telegram-owned threading and approval-target parsing, so forum-topic approvals stay in the originating topic instead of falling back to the root chat. (#58783)</li>
<li>Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov</li>
<li>Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae</li>
<li>QQBot/voice: lazy-load <code>silk-wasm</code> in <code>audio-convert.ts</code> so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.</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.4.2/OpenClaw-2026.4.2.zip" length="25843797" type="application/octet-stream" sparkle:edSignature="bNNXr4BJEU8W7ghXOujLJTYHZL2PL/r/p4llGBw0BFL+46mJ2Bir+IK8XQaCj5zp+O5JSuh5mY+Y/Nrq6TR7Cg=="/>
</item>
<item>
<title>2026.4.1</title>
<pubDate>Wed, 01 Apr 2026 17:14:12 +0000</pubDate>
@@ -189,146 +303,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.3.31/OpenClaw-2026.3.31.zip" length="25820093" type="application/octet-stream" sparkle:edSignature="NjpuH/j7OaNASEatBTpQ4uQy6+oUNq/lIwjrY69rJfkgGSk3/kU8vgxo9osjSgx034m7TpuZvWyulu57OBsQCg=="/>
</item>
<item>
<title>2026.3.28</title>
<pubDate>Sun, 29 Mar 2026 02:10:40 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026032890</sparkle:version>
<sparkle:shortVersionString>2026.3.28</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.3.28</h2>
<h3>Breaking</h3>
<ul>
<li>Providers/Qwen: remove the deprecated <code>qwen-portal-auth</code> OAuth integration for <code>portal.qwen.ai</code>; migrate to Model Studio with <code>openclaw onboard --auth-choice modelstudio-api-key</code>. (#52709) Thanks @pomelo-nwu.</li>
<li>Config/Doctor: drop automatic config migrations older than two months; very old legacy keys now fail validation instead of being rewritten on load or by <code>openclaw doctor</code>.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>xAI/tools: move the bundled xAI provider to the Responses API, add first-class <code>x_search</code>, and auto-enable the xAI plugin from owned web-search and tool config so bundled Grok auth/configured search flows work without manual plugin toggles. (#56048) Thanks @huntharo.</li>
<li>xAI/onboarding: let the bundled Grok web-search plugin offer optional <code>x_search</code> setup during <code>openclaw onboard</code> and <code>openclaw configure --section web</code>, including an x_search model picker with the shared xAI key.</li>
<li>MiniMax: add image generation provider for <code>image-01</code> model, supporting generate and image-to-image editing with aspect ratio control. (#54487) Thanks @liyuan97.</li>
<li>Plugins/hooks: add async <code>requireApproval</code> to <code>before_tool_call</code> hooks, letting plugins pause tool execution and prompt the user for approval via the exec approval overlay, Telegram buttons, Discord interactions, or the <code>/approve</code> command on any channel. The <code>/approve</code> command now handles both exec and plugin approvals with automatic fallback. (#55339) Thanks @vaclavbelak and @joshavant.</li>
<li>ACP/channels: add current-conversation ACP binds for Discord, BlueBubbles, and iMessage so <code>/acp spawn codex --bind here</code> can turn the current chat into a Codex-backed workspace without creating a child thread, and document the distinction between chat surface, ACP session, and runtime workspace.</li>
<li>OpenAI/apply_patch: enable <code>apply_patch</code> by default for OpenAI and OpenAI Codex models, and align its sandbox policy access with <code>write</code> permissions.</li>
<li>Plugins/CLI backends: move bundled Claude CLI, Codex CLI, and Gemini CLI inference defaults onto the plugin surface, add bundled Gemini CLI backend support, and replace <code>gateway run --claude-cli-logs</code> with generic <code>--cli-backend-logs</code> while keeping the old flag as a compatibility alias.</li>
<li>Plugins/startup: auto-load bundled provider and CLI-backend plugins from explicit config refs, so bundled Claude CLI, Codex CLI, and Gemini CLI message-provider setups no longer need manual <code>plugins.allow</code> entries.</li>
<li>Podman: simplify the container setup around the current rootless user, install the launch helper under <code>~/.local/bin</code>, and document the host-CLI <code>openclaw --container <name> ...</code> workflow instead of a dedicated <code>openclaw</code> service user.</li>
<li>Slack/tool actions: add an explicit <code>upload-file</code> Slack action that routes file uploads through the existing Slack upload transport, with optional filename/title/comment overrides for channels and DMs.</li>
<li>Message actions/files: start unifying file-first sends on the canonical <code>upload-file</code> action by adding explicit support for Microsoft Teams and Google Chat, and by exposing BlueBubbles file sends through <code>upload-file</code> while keeping the legacy <code>sendAttachment</code> alias.</li>
<li>Plugins/Matrix TTS: send auto-TTS replies as native Matrix voice bubbles instead of generic audio attachments. (#37080) thanks @Matthew19990919.</li>
<li>CLI: add <code>openclaw config schema</code> to print the generated JSON schema for <code>openclaw.json</code>. (#54523) Thanks @kvokka.</li>
<li>Config/TTS: auto-migrate legacy speech config on normal reads and secret resolution, keep legacy diagnostics for Doctor, and remove regular-mode runtime fallback for old bundled <code>tts.<provider></code> API-key shapes.</li>
<li>Memory/plugins: move the pre-compaction memory flush plan behind the active memory plugin contract so <code>memory-core</code> owns flush prompts and target-path policy instead of hardcoded core logic.</li>
<li>MiniMax: trim model catalog to M2.7 only, removing legacy M2, M2.1, M2.5, and VL-01 models. (#54487) Thanks @liyuan97.</li>
<li>Plugins/runtime: expose <code>runHeartbeatOnce</code> in the plugin runtime <code>system</code> namespace so plugins can trigger a single heartbeat cycle with an explicit delivery target override (e.g. <code>heartbeat: { target: "last" }</code>). (#40299) Thanks @loveyana.</li>
<li>Agents/compaction: preserve the post-compaction AGENTS refresh on stale-usage preflight compaction for both immediate replies and queued followups. (#49479) Thanks @jared596.</li>
<li>Agents/compaction: surface safeguard-specific cancel reasons and relabel benign manual <code>/compact</code> no-op cases as skipped instead of failed. (#51072) Thanks @afurm.</li>
<li>Docs: add <code>pnpm docs:check-links:anchors</code> for Mintlify anchor validation while keeping <code>scripts/docs-link-audit.mjs</code> as the stable link-audit entrypoint. (#55912) Thanks @velvet-shark.</li>
<li>Tavily: mark outbound API requests with <code>X-Client-Source: openclaw</code> so Tavily can attribute OpenClaw-originated traffic. (#55335) Thanks @lakshyaag-tavily.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Agents/Anthropic: recover unhandled provider stop reasons (e.g. <code>sensitive</code>) as structured assistant errors instead of crashing the agent run. (#56639)</li>
<li>Google/models: resolve Gemini 3.1 pro, flash, and flash-lite for all Google provider aliases by passing the actual runtime provider ID and adding a template-provider fallback; fix flash-lite prefix ordering. (#56567)</li>
<li>OpenAI Codex/image tools: register Codex for media understanding and route image prompts through Codex instructions so image analysis no longer fails on missing provider registration or missing <code>instructions</code>. (#54829) Thanks @neeravmakwana.</li>
<li>Agents/image tool: restore the generic image-runtime fallback when no provider-specific media-understanding provider is registered, so image analysis works again for providers like <code>openrouter</code> and <code>minimax-portal</code>. (#54858) Thanks @MonkeyLeeT.</li>
<li>WhatsApp: fix infinite echo loop in self-chat DM mode where the bot's own outbound replies were re-processed as new inbound user messages. (#54570) Thanks @joelnishanth</li>
<li>Telegram/splitting: replace proportional text estimate with verified HTML-length search so long messages split at word boundaries instead of mid-word; gracefully degrade when tag overhead exceeds the limit. (#56595)</li>
<li>Telegram/delivery: skip whitespace-only and hook-blanked text replies in bot delivery to prevent GrammyError 400 empty-text crashes. (#56620)</li>
<li>Telegram/send: validate <code>replyToMessageId</code> at all four API sinks with a shared normalizer that rejects non-numeric, NaN, and mixed-content strings. (#56587)</li>
<li>Mistral: normalize OpenAI-compatible request flags so official Mistral API runs no longer fail with remaining <code>422 status code (no body)</code> chat errors.</li>
<li>Control UI/config: keep sensitive raw config hidden by default, replace the blank blocked editor with an explicit reveal-to-edit state, and restore raw JSON editing without auto-exposing secrets. Fixes #55322.</li>
<li>CLI/zsh: defer <code>compdef</code> registration until <code>compinit</code> is available so zsh completion loads cleanly with plugin managers and manual setups. (#56555)</li>
<li>BlueBubbles/debounce: guard debounce flush against null message text by sanitizing at the enqueue boundary and adding an independent combiner guard. (#56573)</li>
<li>Auto-reply: suppress JSON-wrapped <code>{"action":"NO_REPLY"}</code> control envelopes before channel delivery with a strict single-key detector; preserves media when text is only a silent envelope. (#56612)</li>
<li>ACP/ACPX agent registry: align OpenClaw's ACPX built-in agent mirror with the latest <code>openclaw/acpx</code> command defaults and built-in aliases, pin versioned <code>npx</code> built-ins to exact versions, and stop unknown ACP agent ids from falling through to raw <code>--agent</code> command execution on the MCP-proxy path. (#28321) Thanks @m0nkmaster and @vincentkoc.</li>
<li>Security/audit: extend web search key audit to recognize Gemini, Grok/xAI, Kimi, Moonshot, and OpenRouter credentials via a boundary-safe bundled-web-search registry shim. (#56540)</li>
<li>Docs/FAQ: remove broken Xfinity SSL troubleshooting cross-links from English and zh-CN FAQ entries — both sections already contain the full workaround inline. (#56500)</li>
<li>Telegram: deliver verbose tool summaries inside forum topic sessions again, so threaded topic chats now match DM verbose behavior. (#43236) Thanks @frankbuild.</li>
<li>BlueBubbles/CLI agents: restore inbound prompt image refs for CLI routed turns, reapply embedded runner image size guardrails, and cover both CLI image transport paths with regression tests. (#51373)</li>
<li>BlueBubbles/groups: optionally enrich unnamed participant lists with local macOS Contacts names after group gating passes, so group member context can show names instead of only raw phone numbers.</li>
<li>Discord/reconnect: drain stale gateway sockets, clear cached resume state before forced fresh reconnects, and fail closed when old sockets refuse to die so Discord recovery stops looping on poisoned resume state. (#54697) Thanks @ngutman.</li>
<li>iMessage: stop leaking inline <code>[[reply_to:...]]</code> tags into delivered text by sending <code>reply_to</code> as RPC metadata and stripping stray directive tags from outbound messages. (#39512) Thanks @mvanhorn.</li>
<li>CLI/plugins: make routed commands use the same auto-enabled bundled-channel snapshot as gateway startup, so configured bundled channels like Slack load without requiring a prior config rewrite. (#54809) Thanks @neeravmakwana.</li>
<li>CLI/message send: write manual <code>openclaw message send</code> deliveries into the resolved agent session transcript again by always threading the default CLI agent through outbound mirroring. (#54187) Thanks @KevInTheCloud5617.</li>
<li>CLI/onboarding: show the Kimi Code API key option again in the Moonshot setup menu so the interactive picker includes all Kimi setup paths together. Fixes #54412 Thanks @sparkyrider</li>
<li>Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so <code>/status</code> shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana.</li>
<li>OpenAI/WebSocket: preserve reasoning replay metadata and tool-call item ids on WebSocket tool turns, and start a fresh response chain when full-context resend is required. (#53856) Thanks @xujingchen1996.</li>
<li>OpenAI/WS: restore reasoning blocks for Responses WebSocket runs and keep reasoning/tool-call replay metadata intact so resumed sessions do not lose or break follow-up reasoning-capable turns. (#53856) Thanks @xujingchen1996.</li>
<li>Agents/errors: surface provider quota/reset details when available, but keep HTML/Cloudflare rate-limit pages on the generic fallback so raw error pages are not shown to users. (#54512) Thanks @bugkill3r.</li>
<li>Claude CLI: switch the bundled Claude CLI backend to <code>stream-json</code> output so watchdogs see progress on long runs, and keep session/usage metadata even when Claude finishes with an empty result line. (#49698) Thanks @felear2022.</li>
<li>Claude CLI/MCP: always pass a strict generated <code>--mcp-config</code> overlay for background Claude CLI runs, including the empty-server case, so Claude does not inherit ambient user/global MCP servers. (#54961) Thanks @markojak.</li>
<li>Agents/embedded replies: surface mid-turn 429 and overload failures when embedded runs end without a user-visible reply, while preserving successful media-only replies that still use legacy <code>mediaUrl</code>. (#50930) Thanks @infichen.</li>
<li>Chat/UI: move the chat send button onto the shared ghost-button theme styling, while keeping the stop button icon readable on the danger state. (#55075) Thanks @bottenbenny.</li>
<li>WhatsApp/allowFrom: show a specific allowFrom policy error for valid blocked targets instead of the misleading <code><E.164|group JID></code> format hint. Thanks @mcaxtr.</li>
<li>Agents/cooldowns: scope rate-limit cooldowns per model so one 429 no longer blocks every model on the same auth profile, replace the exponential 1 min -> 1 h escalation with a stepped 30 s / 1 min / 5 min ladder, and surface a user-facing countdown message when all models are rate-limited. (#49834) Thanks @kiranvk-2011.</li>
<li>Agents/embedded transport errors: distinguish common network failures like connection refused, DNS lookup failure, and interrupted sockets from true timeouts in embedded-run user messaging and lifecycle diagnostics. (#51419) Thanks @scoootscooob.</li>
<li>Telegram/pairing: ignore self-authored DM <code>message</code> updates so bot-pinned status cards and similar service updates do not trigger bogus pairing requests or re-enter inbound dispatch. (#54530) thanks @huntharo</li>
<li>Mattermost/replies: keep pairing replies, slash-command fallback replies, and model-picker messages on the resolved config path so <code>exec:</code> SecretRef bot tokens work across all outbound reply branches. (#48347) thanks @mathiasnagler.</li>
<li>Microsoft Teams/config: accept the existing <code>welcomeCard</code>, <code>groupWelcomeCard</code>, <code>promptStarters</code>, and feedback/reflection keys in strict config validation so already-supported Teams runtime settings stop failing schema checks. (#54679) Thanks @gumclaw.</li>
<li>MCP/channels: add a Gateway-backed channel MCP bridge with Codex/Claude-facing conversation tools, Claude channel notifications, and safer stdio bridge lifecycle handling for reconnects and routed session discovery.</li>
<li>Plugins/SDK: thread <code>moduleUrl</code> through plugin-sdk alias resolution so user-installed plugins outside the openclaw directory correctly resolve <code>openclaw/plugin-sdk/*</code> subpath imports, and gate <code>plugin-sdk:check-exports</code> in <code>release:check</code>. (#54283) Thanks @xieyongliang.</li>
<li>Config/web fetch: allow the documented <code>tools.web.fetch.maxResponseBytes</code> setting in runtime schema validation so valid configs no longer fail with unrecognized-key errors. (#53401) Thanks @erhhung.</li>
<li>Message tool/buttons: keep the shared <code>buttons</code> schema optional in merged tool definitions so plain <code>action=send</code> calls stop failing validation when no buttons are provided. (#54418) Thanks @adzendo.</li>
<li>Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate <code>tool_call_id</code> values with HTTP 400. (#40996) Thanks @xaeon2026.</li>
<li>Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition <code>strict</code> fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.</li>
<li>Plugins/context engines: retry strict legacy <code>assemble()</code> calls without the new <code>prompt</code> field when older engines reject it, preserving prompt-aware retrieval compatibility for pre-prompt plugins. (#50848) thanks @danhdoan.</li>
<li>CLI/update status: explicitly say <code>up to date</code> when the local version already matches npm latest, while keeping the availability logic unchanged. (#51409) Thanks @dongzhenye.</li>
<li>Daemon/Linux: stop flagging non-gateway systemd services as duplicate gateways just because their unit files mention OpenClaw, reducing false-positive doctor/log noise. (#45328) Thanks @gregretkowski.</li>
<li>Feishu: close WebSocket connections on monitor stop/abort so ghost connections no longer persist, preventing duplicate event processing and resource leaks across restart cycles. (#52844) Thanks @schumilin.</li>
<li>Feishu: use the original message <code>create_time</code> instead of <code>Date.now()</code> for inbound timestamps so offline-retried messages carry the correct authoring time, preventing mis-targeted agent actions on stale instructions. (#52809) Thanks @schumilin.</li>
<li>Control UI/Skills: open skill detail dialogs with the browser modal lifecycle so clicking a skill row keeps the panel centered instead of rendering it off-screen at the bottom of the page.</li>
<li>Matrix/replies: include quoted poll question/options in inbound reply context so the agent sees the original poll content when users reply to Matrix poll messages. (#55056) Thanks @alberthild.</li>
<li>Matrix/plugins: keep plugin bootstrap from crashing when built runtime mixes bare and deep <code>matrix-js-sdk</code> entrypoints, so unrelated channels do not get taken down during plugin load. (#56273) Thanks @aquaright1.</li>
<li>Agents/sandbox: honor <code>tools.sandbox.tools.alsoAllow</code>, let explicit sandbox re-allows remove matching built-in default-deny tools, and keep sandbox explain/error guidance aligned with the effective sandbox tool policy. (#54492) Thanks @ngutman.</li>
<li>Agents/sandbox: make blocked-tool guidance glob-aware again, redact/sanitize session-specific explain hints for safer copy-paste, and avoid leaking control-character session keys in those hints. (#54684) Thanks @ngutman.</li>
<li>Agents/compaction: trigger timeout recovery compaction before retrying high-context LLM timeouts so embedded runs stop repeating oversized requests. (#46417) thanks @joeykrug.</li>
<li>Agents/compaction: reconcile <code>sessions.json.compactionCount</code> after a late embedded auto-compaction success so persisted session counts catch up once the handler reports completion. (#45493) Thanks @jackal092927.</li>
<li>Agents/failover: classify Codex accountId token extraction failures as auth errors so model fallback continues to the next configured candidate. (#55206) Thanks @cosmicnet.</li>
<li>Plugins/runtime: reuse only compatible active plugin registries across tools, providers, web search, and channel bootstrap, align <code>/tools/invoke</code> plugin loading with the session workspace, and retry outbound channel recovery when the pinned channel surface changes so plugin tools and channels stop disappearing or re-registering from mismatched runtime loads. Thanks @gumadeiras.</li>
<li>Talk/macOS: stop direct system-voice failures from replaying system speech, use app-locale fallback for shared watchdog timing, and add regression coverage for the macOS fallback route and language-aware timeout policy. (#53511) thanks @hongsw.</li>
<li>Discord/gateway cleanup: keep late Carbon reconnect-exhausted errors suppressed through startup/dispose cleanup so Discord monitor shutdown no longer crashes on late gateway close events. (#55373) Thanks @Takhoffman.</li>
<li>Discord/gateway shutdown: treat expected reconnect-exhausted events during intentional lifecycle stop as clean shutdowns so startup-abort cleanup no longer surfaces false gateway failures. (#55324) Thanks @joelnishanth.</li>
<li>Discord/gateway shutdown: suppress reconnect-exhausted events that were already buffered before teardown flips <code>lifecycleStopping</code>, so stale-socket Discord restarts no longer crash the whole gateway. Fixes #55403 and #55421. Thanks @lml2468 and @vincentkoc.</li>
<li>GitHub Copilot/auth refresh: treat large <code>expires_at</code> values as seconds epochs and clamp far-future runtime auth refresh timers so Copilot token refresh cannot fall into a <code>setTimeout</code> overflow hot loop. (#55360) Thanks @michael-abdo.</li>
<li>Agents/status: use the persisted runtime session model in <code>session_status</code> when no explicit override exists, and honor per-agent <code>thinkingDefault</code> in both <code>session_status</code> and <code>/status</code>. (#55425) Thanks @scoootscooob, @xaeon2026, and @ysfbsf.</li>
<li>Heartbeat/runner: guarantee the interval timer is re-armed after heartbeat runs and unexpected runner errors so scheduled heartbeats do not silently stop after an interrupted cycle. (#52270) Thanks @MiloStack.</li>
<li>Config/Doctor: rewrite stale bundled plugin load paths from legacy bundled-plugin locations to the packaged bundled path, including directory-name mismatches and slash-suffixed config entries. (#55054) Thanks @SnowSky1.</li>
<li>WhatsApp/mentions: stop treating mentions embedded in quoted messages as direct mentions so replying to a message that @mentioned the bot no longer falsely triggers mention gating. (#52711) Thanks @lurebat.</li>
<li>Matrix: keep separate 2-person rooms out of DM routing after <code>m.direct</code> seeds successfully, while still honoring explicit <code>is_direct</code> state and startup fallback recovery. (#54890) thanks @private-peter</li>
<li>Agents/ollama fallback: surface non-2xx Ollama HTTP errors with a leading status code so HTTP 503 responses trigger model fallback again. (#55214) Thanks @bugkill3r.</li>
<li>Feishu/tools: stop synthetic agent ids like <code>agent-spawner</code> from being treated as Feishu account ids during tool execution, so tools fall back to the configured/default Feishu account unless the contextual id is a real enabled Feishu account. (#55627) Thanks @MonkeyLeeT.</li>
<li>Google/tools: strip empty <code>required: []</code> arrays from Gemini tool schemas so optional-only tool parameters no longer trigger Google validator 400s. (#52106) Thanks @oliviareid-svg.</li>
<li>Onboarding/TUI/local gateways: show the resolved gateway port in setup output, clarify no-daemon local health/dashboard messaging, and preserve loopback Control UI auth on reruns and explicit local gateway URLs so local quickstart flows recover cleanly. (#55730) Thanks @shakkernerd.</li>
<li>TUI/chat log: keep system messages as single logical entries and prune overflow at whole-message boundaries so wrapped system spacing stays intact. (#55732) Thanks @shakkernerd.</li>
<li>TUI/activation: validate <code>/activation</code> arguments in the TUI and reject invalid values instead of silently coercing them to <code>mention</code>. (#55733) Thanks @shakkernerd.</li>
<li>Agents/model switching: apply <code>/model</code> changes to active embedded runs at the next safe retry boundary, so overloaded or retrying turns switch to the newly selected model instead of staying pinned to the old provider.</li>
<li>Agents/Codex fallback: classify Codex <code>server_error</code> payloads as failoverable, sanitize <code>Codex error:</code> payloads before they reach chat, preserve context-overflow guidance for prefixed <code>invalid_request_error</code> payloads, and omit provider <code>request_id</code> values from user-facing UI copy. (#42892) Thanks @xaeon2026.</li>
<li>Memory/search: share memory embedding provider registrations across split plugin runtimes so memory search no longer fails with unknown provider errors after memory-core registers built-in adapters. (#55945) Thanks @glitch418x.</li>
<li>Discord/Carbon beta: update <code>@buape/carbon</code> to the latest beta and pass the new <code>RateLimitError</code> request argument so Discord stays compatible with the upstream beta constructor change. (#55980) Thanks @ngutman.</li>
<li>Plugins/inbound claims: pass full inbound attachment arrays through <code>inbound_claim</code> hook metadata while keeping the legacy singular media attachment fields for compatibility. (#55452) Thanks @huntharo.</li>
<li>Plugins/Matrix: preserve sender filenames for inbound media by forwarding <code>originalFilename</code> to <code>saveMediaBuffer</code>. (#55692) thanks @esrehmki.</li>
<li>Matrix/mentions: recognize <code>matrix.to</code> mentions whose visible label uses the bot's room display name, so <code>requireMention: true</code> rooms respond correctly in modern Matrix clients. (#55393) thanks @nickludlam.</li>
<li>Ollama/thinking off: route <code>thinkingLevel=off</code> through the live Ollama extension request path so thinking-capable Ollama models now receive top-level <code>think: false</code> instead of silently generating hidden reasoning tokens. (#53200) Thanks @BruceMacD.</li>
<li>Plugins/diffs: stage bundled <code>@pierre/diffs</code> runtime dependencies during packaged updates so the bundled diff viewer keeps loading after global installs and updates. (#56077) Thanks @gumadeiras.</li>
<li>Plugins/diffs: load bundled Pierre themes without JSON module imports so diff rendering keeps working on newer Node builds. (#45869) thanks @NickHood1984.</li>
<li>Plugins/uninstall: remove owned <code>channels.<id></code> config when uninstalling channel plugins, and keep the uninstall preview aligned with explicit channel ownership so built-in channels and shared keys stay intact. (#35915) Thanks @wbxl2000.</li>
<li>Plugins/Matrix: prefer explicit DM signals when choosing outbound direct rooms and routing unmapped verification summaries, so strict 2-person fallback rooms do not outrank the real DM. (#56076) thanks @gumadeiras</li>
<li>Plugins/Matrix: resolve env-backed <code>accessToken</code> and <code>password</code> SecretRefs against the active Matrix config env path during startup, and officially accept SecretRef <code>accessToken</code> config values. (#54980) thanks @kakahu2015.</li>
<li>Microsoft Teams/proactive DMs: prefer the freshest personal conversation reference for <code>user:<aadObjectId></code> sends when multiple stored references exist, so replies stop targeting stale DM threads. (#54702) Thanks @gumclaw.</li>
<li>Gateway/plugins: reuse the session workspace when building HTTP <code>/tools/invoke</code> tool lists and harden tool construction to infer the session agent workspace by default, so workspace plugins do not re-register on repeated HTTP tool calls. (#56101) thanks @neeravmakwana</li>
<li>Brave/web search: normalize unsupported Brave <code>country</code> filters to <code>ALL</code> before request and cache-key generation so locale-derived values like <code>VN</code> stop failing with upstream 422 validation errors. (#55695) Thanks @chen-zhang-cs-code.</li>
<li>Discord/replies: preserve leading indentation when stripping inline reply tags so reply-tagged plain text and fenced code blocks keep their formatting. (#55960) Thanks @Nanako0129.</li>
<li>Daemon/status: surface immediate gateway close reasons from lightweight probes and prefer those concrete auth or pairing failures over generic timeouts in <code>openclaw daemon status</code>. (#56282) Thanks @mbelinky.</li>
<li>Agents/failover: classify HTTP 410 errors as retryable timeouts by default while still preserving explicit session-expired, billing, and auth signals from the payload. (#55201) thanks @nikus-pan.</li>
<li>Agents/subagents: restore completion announce delivery for extension channels like BlueBubbles. (#56348)</li>
<li>Plugins/Matrix: load bundled <code>@matrix-org/matrix-sdk-crypto-nodejs</code> through <code>createRequire(...)</code> so E2EE media send and receive keep the package-local native binding lookup working in packaged ESM builds. (#54566) thanks @joelnishanth.</li>
<li>Plugins/Matrix: encrypt E2EE image thumbnails with <code>thumbnail_file</code> while keeping unencrypted-room previews on <code>thumbnail_url</code>, so encrypted Matrix image events keep thumbnail metadata without leaking plaintext previews. (#54711) thanks @frischeDaten.</li>
<li>Telegram/forum topics: keep native <code>/new</code> and <code>/reset</code> routed to the active topic by preserving the topic target on forum-thread command context. (#35963)</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.28/OpenClaw-2026.3.28.zip" length="25811288" type="application/octet-stream" sparkle:edSignature="SJp4ptVaGlOIXRPevS89DbfN2WKP0bKMXQoaT0fmLhy7pataDfHN0kxC3zu6P0Q/HtsxaESEhJUw48SCUNNKDA=="/>
</item>
</channel>
</rss>

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026040101
versionName = "2026.4.2"
versionCode = 2026040401
versionName = "2026.4.4"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -16,6 +16,7 @@ enum class HomeDestination {
data class AssistantLaunchRequest(
val source: String,
val prompt: String?,
val autoSend: Boolean,
)
fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
@@ -25,6 +26,7 @@ fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
AssistantLaunchRequest(
source = "assist",
prompt = null,
autoSend = false,
)
actionAskOpenClaw -> {
@@ -32,6 +34,7 @@ fun parseAssistantLaunchIntent(intent: Intent?): AssistantLaunchRequest? {
AssistantLaunchRequest(
source = "app_action",
prompt = prompt,
autoSend = prompt != null,
)
}

View File

@@ -31,6 +31,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
val requestedHomeDestination: StateFlow<HomeDestination?> = _requestedHomeDestination
private val _chatDraft = MutableStateFlow<String?>(null)
val chatDraft: StateFlow<String?> = _chatDraft
private val _pendingAssistantAutoSend = MutableStateFlow<String?>(null)
val pendingAssistantAutoSend: StateFlow<String?> = _pendingAssistantAutoSend
private fun ensureRuntime(): NodeRuntime {
runtimeRef.value?.let { return it }
@@ -252,6 +254,12 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
fun handleAssistantLaunch(request: AssistantLaunchRequest) {
_requestedHomeDestination.value = HomeDestination.Chat
if (request.autoSend) {
_pendingAssistantAutoSend.value = request.prompt
_chatDraft.value = null
return
}
_pendingAssistantAutoSend.value = null
_chatDraft.value = request.prompt
}
@@ -263,6 +271,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
_chatDraft.value = null
}
fun clearPendingAssistantAutoSend() {
_pendingAssistantAutoSend.value = null
}
fun setMicEnabled(enabled: Boolean) {
ensureRuntime().setMicEnabled(enabled)
}
@@ -354,4 +366,16 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
}
suspend fun sendChatAwaitAcceptance(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
): Boolean {
return ensureRuntime().sendChatAwaitAcceptance(
message = message,
thinking = thinking,
attachments = attachments,
)
}
}

View File

@@ -16,6 +16,8 @@ import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewayDiscovery
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
import ai.openclaw.app.gateway.GatewayTlsProbeResult
import ai.openclaw.app.gateway.probeGatewayTlsFingerprint
import ai.openclaw.app.node.*
import ai.openclaw.app.protocol.OpenClawCanvasA2UIAction
@@ -44,6 +46,7 @@ import java.util.concurrent.atomic.AtomicLong
class NodeRuntime(
context: Context,
val prefs: SecurePrefs = SecurePrefs(context.applicationContext),
private val tlsFingerprintProbe: suspend (String, Int) -> GatewayTlsProbeResult = ::probeGatewayTlsFingerprint,
) {
data class GatewayConnectAuth(
val token: String?,
@@ -68,6 +71,7 @@ class NodeRuntime(
private val identityStore = DeviceIdentityStore(appContext)
private var connectedEndpoint: GatewayEndpoint? = null
private var activeGatewayAuth: GatewayConnectAuth? = null
private val cameraHandler: CameraHandler = CameraHandler(
appContext = appContext,
@@ -189,6 +193,7 @@ class NodeRuntime(
data class GatewayTrustPrompt(
val endpoint: GatewayEndpoint,
val fingerprintSha256: String,
val auth: GatewayConnectAuth,
)
private val _isConnected = MutableStateFlow(false)
@@ -295,6 +300,11 @@ class NodeRuntime(
_canvasRehydrateErrorText.value = null
updateStatus()
showLocalCanvasOnConnect()
val endpoint = connectedEndpoint
val auth = activeGatewayAuth
if (endpoint != null && auth != null) {
maybeStartOperatorSessionAfterNodeConnect(endpoint, auth)
}
},
onDisconnected = { message ->
_nodeConnected.value = false
@@ -341,6 +351,8 @@ class NodeRuntime(
session = operatorSession,
supportsChatSubscribe = false,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
).also { speaker ->
speaker.setPlaybackEnabled(prefs.speakerEnabled.value)
}
@@ -369,11 +381,10 @@ class NodeRuntime(
parseChatSendRunId(response) ?: idempotencyKey
},
speakAssistantReply = { text ->
// Skip if TalkModeManager is handling TTS (ttsOnAllResponses) to avoid
// double-speaking the same assistant reply from both pipelines.
if (!talkMode.ttsOnAllResponses) {
voiceReplySpeaker.speakAssistantReply(text)
}
// Voice-tab replies should speak through the dedicated reply speaker.
// Relying on talkMode.ttsOnAllResponses here can drop playback if the
// chat-event path misses the terminal event for this turn.
voiceReplySpeaker.speakAssistantReply(text)
},
)
}
@@ -412,14 +423,19 @@ class NodeRuntime(
session = operatorSession,
supportsChatSubscribe = true,
isConnected = { operatorConnected },
onBeforeSpeak = { micCapture.pauseForTts() },
onAfterSpeak = { micCapture.resumeAfterTts() },
)
}
private fun syncMainSessionKey(agentId: String?) {
val resolvedKey = resolveNodeMainSessionKey(agentId)
// Always push the resolved session key into TalkMode, even when the
// state flow value is unchanged, so lazy TalkMode instances do not
// stay on the default "main" session key.
talkMode.setMainSessionKey(resolvedKey)
if (_mainSessionKey.value == resolvedKey) return
_mainSessionKey.value = resolvedKey
talkMode.setMainSessionKey(resolvedKey)
chat.applyMainSessionKey(resolvedKey)
updateHomeCanvasState()
}
@@ -579,12 +595,11 @@ class NodeRuntime(
scope.launch {
prefs.talkEnabled.collect { enabled ->
// MicCaptureManager handles STT + send to gateway.
// TalkModeManager plays TTS on assistant responses.
// MicCaptureManager handles STT + send to gateway, while the dedicated
// reply speaker handles TTS for assistant replies in the voice tab.
micCapture.setMicEnabled(enabled)
if (enabled) {
// Mic on = user is on voice screen and wants TTS responses.
talkMode.ttsOnAllResponses = true
talkMode.ttsOnAllResponses = false
scope.launch { talkMode.ensureChatSubscribed() }
}
externalAudioCaptureActive.value = enabled
@@ -745,8 +760,8 @@ class NodeRuntime(
prefs.setTalkEnabled(value)
if (value) {
// Tapping mic on interrupts any active TTS (barge-in)
talkMode.stopTts()
talkMode.ttsOnAllResponses = true
stopVoicePlayback()
talkMode.ttsOnAllResponses = false
scope.launch { talkMode.ensureChatSubscribed() }
}
micCapture.setMicEnabled(value)
@@ -761,18 +776,25 @@ class NodeRuntime(
if (voiceReplySpeakerLazy.isInitialized()) {
voiceReplySpeaker.setPlaybackEnabled(value)
}
// Keep TalkMode in sync so speaker mute works when ttsOnAllResponses is active.
// Keep TalkMode in sync so any active Talk playback also respects speaker mute.
talkMode.setPlaybackEnabled(value)
}
private fun stopActiveVoiceSession() {
talkMode.ttsOnAllResponses = false
talkMode.stopTts()
stopVoicePlayback()
micCapture.setMicEnabled(false)
prefs.setTalkEnabled(false)
externalAudioCaptureActive.value = false
}
private fun stopVoicePlayback() {
talkMode.stopTts()
if (voiceReplySpeakerLazy.isInitialized()) {
voiceReplySpeaker.stopTts()
}
}
fun refreshGatewayConnection() {
val endpoint =
connectedEndpoint ?: run {
@@ -789,15 +811,14 @@ class NodeRuntime(
auth: GatewayConnectAuth,
reconnect: Boolean = false,
) {
activeGatewayAuth = auth
val tls = connectionManager.resolveTlsParams(endpoint)
val connectOperator =
shouldConnectOperatorSession(
auth.token,
auth.bootstrapToken,
auth.password,
loadStoredRoleDeviceToken("operator"),
val operatorAuth =
resolveOperatorSessionConnectAuth(
auth = auth,
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
)
if (!connectOperator) {
if (operatorAuth == null) {
operatorConnected = false
operatorStatusText = "Offline"
operatorSession.disconnect()
@@ -805,9 +826,9 @@ class NodeRuntime(
} else {
operatorSession.connect(
endpoint,
auth.token,
auth.bootstrapToken,
auth.password,
operatorAuth.token,
operatorAuth.bootstrapToken,
operatorAuth.password,
connectionManager.buildOperatorConnectOptions(),
tls,
)
@@ -820,7 +841,7 @@ class NodeRuntime(
connectionManager.buildNodeConnectOptions(),
tls,
)
if (reconnect && connectOperator) {
if (reconnect && operatorAuth != null) {
operatorSession.reconnect()
}
if (reconnect) {
@@ -828,17 +849,22 @@ class NodeRuntime(
}
}
fun connect(endpoint: GatewayEndpoint) {
private fun beginConnect(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
val tls = connectionManager.resolveTlsParams(endpoint)
if (tls?.required == true && tls.expectedFingerprint.isNullOrBlank()) {
// First-time TLS: capture fingerprint, ask user to verify out-of-band, then store and connect.
_statusText.value = "Verify gateway TLS fingerprint…"
scope.launch {
val fp = probeGatewayTlsFingerprint(endpoint.host, endpoint.port) ?: run {
_statusText.value = "Failed: can't read TLS fingerprint"
val tlsProbe = tlsFingerprintProbe(endpoint.host, endpoint.port)
val fp = tlsProbe.fingerprintSha256 ?: run {
_statusText.value = gatewayTlsProbeFailureMessage(tlsProbe.failure)
return@launch
}
_pendingGatewayTrust.value = GatewayTrustPrompt(endpoint = endpoint, fingerprintSha256 = fp)
_pendingGatewayTrust.value =
GatewayTrustPrompt(endpoint = endpoint, fingerprintSha256 = fp, auth = auth)
}
return
}
@@ -847,18 +873,18 @@ class NodeRuntime(
operatorStatusText = "Connecting…"
nodeStatusText = "Connecting…"
updateStatus()
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth())
connectWithAuth(endpoint = endpoint, auth = auth)
}
fun connect(endpoint: GatewayEndpoint) {
beginConnect(endpoint = endpoint, auth = resolveGatewayConnectAuth())
}
fun connect(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
connectedEndpoint = endpoint
operatorStatusText = "Connecting…"
nodeStatusText = "Connecting…"
updateStatus()
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(auth))
beginConnect(endpoint = endpoint, auth = resolveGatewayConnectAuth(auth))
}
internal fun resolveGatewayConnectAuth(explicitAuth: GatewayConnectAuth? = null): GatewayConnectAuth {
@@ -874,7 +900,7 @@ class NodeRuntime(
val prompt = _pendingGatewayTrust.value ?: return
_pendingGatewayTrust.value = null
prefs.saveGatewayTlsFingerprint(prompt.endpoint.stableId, prompt.fingerprintSha256)
connect(prompt.endpoint)
beginConnect(endpoint = prompt.endpoint, auth = prompt.auth)
}
fun declineGatewayTrustPrompt() {
@@ -882,6 +908,15 @@ class NodeRuntime(
_statusText.value = "Offline"
}
private fun gatewayTlsProbeFailureMessage(failure: GatewayTlsProbeFailure?): String {
return when (failure) {
GatewayTlsProbeFailure.TLS_UNAVAILABLE ->
"Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected."
GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE, null ->
"Failed: couldn't reach the secure gateway endpoint for this host."
}
}
private fun hasRecordAudioPermission(): Boolean {
return (
ContextCompat.checkSelfPermission(appContext, Manifest.permission.RECORD_AUDIO) ==
@@ -904,8 +939,33 @@ class NodeRuntime(
return deviceAuthStore.loadToken(deviceId, role)
}
private fun maybeStartOperatorSessionAfterNodeConnect(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
) {
if (operatorConnected || operatorStatusText == "Connecting…") {
return
}
val operatorAuth =
resolveOperatorSessionConnectAuth(
auth = auth,
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
) ?: return
operatorStatusText = "Connecting…"
updateStatus()
operatorSession.connect(
endpoint,
operatorAuth.token,
operatorAuth.bootstrapToken,
operatorAuth.password,
connectionManager.buildOperatorConnectOptions(),
connectionManager.resolveTlsParams(endpoint),
)
}
fun disconnect() {
connectedEndpoint = null
activeGatewayAuth = null
_pendingGatewayTrust.value = null
operatorSession.disconnect()
nodeSession.disconnect()
@@ -1016,6 +1076,14 @@ class NodeRuntime(
chat.sendMessage(message = message, thinkingLevel = thinking, attachments = attachments)
}
suspend fun sendChatAwaitAcceptance(
message: String,
thinking: String,
attachments: List<OutgoingAttachment>,
): Boolean {
return chat.sendMessageAwaitAcceptance(message = message, thinkingLevel = thinking, attachments = attachments)
}
private fun handleGatewayEvent(event: String, payloadJson: String?) {
micCapture.handleGatewayEvent(event, payloadJson)
talkMode.handleGatewayEvent(event, payloadJson)
@@ -1233,18 +1301,47 @@ class NodeRuntime(
}
internal fun resolveOperatorSessionConnectAuth(
auth: NodeRuntime.GatewayConnectAuth,
storedOperatorToken: String?,
): NodeRuntime.GatewayConnectAuth? {
val explicitToken = auth.token?.trim()?.takeIf { it.isNotEmpty() }
if (explicitToken != null) {
return NodeRuntime.GatewayConnectAuth(
token = explicitToken,
bootstrapToken = null,
password = null,
)
}
val explicitPassword = auth.password?.trim()?.takeIf { it.isNotEmpty() }
if (explicitPassword != null) {
return NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = null,
password = explicitPassword,
)
}
val storedToken = storedOperatorToken?.trim()?.takeIf { it.isNotEmpty() }
if (storedToken != null) {
// Bootstrap can seed the operator token, but operator should reconnect
// through the stored device-token path rather than bootstrap auth itself.
return NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = null,
password = null,
)
}
return null
}
internal fun shouldConnectOperatorSession(
token: String?,
bootstrapToken: String?,
password: String?,
auth: NodeRuntime.GatewayConnectAuth,
storedOperatorToken: String?,
): Boolean {
return (
!token.isNullOrBlank() ||
!bootstrapToken.isNullOrBlank() ||
!password.isNullOrBlank() ||
!storedOperatorToken.isNullOrBlank()
)
return resolveOperatorSessionConnectAuth(auth, storedOperatorToken) != null
}
private enum class HomeCanvasGatewayState {

View File

@@ -130,11 +130,25 @@ class ChatController(
thinkingLevel: String,
attachments: List<OutgoingAttachment>,
) {
scope.launch {
sendMessageAwaitAcceptance(
message = message,
thinkingLevel = thinkingLevel,
attachments = attachments,
)
}
}
suspend fun sendMessageAwaitAcceptance(
message: String,
thinkingLevel: String,
attachments: List<OutgoingAttachment>,
): Boolean {
val trimmed = message.trim()
if (trimmed.isEmpty() && attachments.isEmpty()) return
if (trimmed.isEmpty() && attachments.isEmpty()) return false
if (!_healthOk.value) {
_errorText.value = "Gateway health not OK; cannot send"
return
return false
}
val runId = UUID.randomUUID().toString()
@@ -177,45 +191,45 @@ class ChatController(
pendingToolCallsById.clear()
publishPendingToolCalls()
scope.launch {
try {
val params =
buildJsonObject {
put("sessionKey", JsonPrimitive(sessionKey))
put("message", JsonPrimitive(text))
put("thinking", JsonPrimitive(thinking))
put("timeoutMs", JsonPrimitive(30_000))
put("idempotencyKey", JsonPrimitive(runId))
if (attachments.isNotEmpty()) {
put(
"attachments",
JsonArray(
attachments.map { att ->
buildJsonObject {
put("type", JsonPrimitive(att.type))
put("mimeType", JsonPrimitive(att.mimeType))
put("fileName", JsonPrimitive(att.fileName))
put("content", JsonPrimitive(att.base64))
}
},
),
)
}
}
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
clearPendingRun(runId)
armPendingRunTimeout(actualRunId)
synchronized(pendingRuns) {
pendingRuns.add(actualRunId)
_pendingRunCount.value = pendingRuns.size
return try {
val params =
buildJsonObject {
put("sessionKey", JsonPrimitive(sessionKey))
put("message", JsonPrimitive(text))
put("thinking", JsonPrimitive(thinking))
put("timeoutMs", JsonPrimitive(30_000))
put("idempotencyKey", JsonPrimitive(runId))
if (attachments.isNotEmpty()) {
put(
"attachments",
JsonArray(
attachments.map { att ->
buildJsonObject {
put("type", JsonPrimitive(att.type))
put("mimeType", JsonPrimitive(att.mimeType))
put("fileName", JsonPrimitive(att.fileName))
put("content", JsonPrimitive(att.base64))
}
},
),
)
}
}
} catch (err: Throwable) {
val res = session.request("chat.send", params.toString())
val actualRunId = parseRunId(res) ?: runId
if (actualRunId != runId) {
clearPendingRun(runId)
_errorText.value = err.message
armPendingRunTimeout(actualRunId)
synchronized(pendingRuns) {
pendingRuns.add(actualRunId)
_pendingRunCount.value = pendingRuns.size
}
}
true
} catch (err: Throwable) {
clearPendingRun(runId)
_errorText.value = err.message
false
}
}

View File

@@ -1,32 +1,92 @@
package ai.openclaw.app.gateway
import ai.openclaw.app.SecurePrefs
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
data class DeviceAuthEntry(
val token: String,
val role: String,
val scopes: List<String>,
val updatedAtMs: Long,
)
@Serializable
private data class PersistedDeviceAuthMetadata(
val scopes: List<String> = emptyList(),
val updatedAtMs: Long = 0L,
)
interface DeviceAuthTokenStore {
fun loadToken(deviceId: String, role: String): String?
fun saveToken(deviceId: String, role: String, token: String)
fun loadEntry(deviceId: String, role: String): DeviceAuthEntry?
fun loadToken(deviceId: String, role: String): String? = loadEntry(deviceId, role)?.token
fun saveToken(deviceId: String, role: String, token: String, scopes: List<String> = emptyList())
fun clearToken(deviceId: String, role: String)
}
class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
override fun loadToken(deviceId: String, role: String): String? {
private val json = Json { ignoreUnknownKeys = true }
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? {
val key = tokenKey(deviceId, role)
return prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() }
val token = prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() } ?: return null
val normalizedRole = normalizeRole(role)
val metadata =
prefs.getString(metadataKey(deviceId, role))
?.let { raw ->
runCatching { json.decodeFromString<PersistedDeviceAuthMetadata>(raw) }.getOrNull()
}
return DeviceAuthEntry(
token = token,
role = normalizedRole,
scopes = metadata?.scopes ?: emptyList(),
updatedAtMs = metadata?.updatedAtMs ?: 0L,
)
}
override fun saveToken(deviceId: String, role: String, token: String) {
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) {
val normalizedScopes = normalizeScopes(scopes)
val key = tokenKey(deviceId, role)
prefs.putString(key, token.trim())
prefs.putString(
metadataKey(deviceId, role),
json.encodeToString(
PersistedDeviceAuthMetadata(
scopes = normalizedScopes,
updatedAtMs = System.currentTimeMillis(),
),
),
)
}
override fun clearToken(deviceId: String, role: String) {
val key = tokenKey(deviceId, role)
prefs.remove(key)
prefs.remove(metadataKey(deviceId, role))
}
private fun tokenKey(deviceId: String, role: String): String {
val normalizedDevice = deviceId.trim().lowercase()
val normalizedRole = role.trim().lowercase()
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceToken.$normalizedDevice.$normalizedRole"
}
private fun metadataKey(deviceId: String, role: String): String {
val normalizedDevice = normalizeDeviceId(deviceId)
val normalizedRole = normalizeRole(role)
return "gateway.deviceTokenMeta.$normalizedDevice.$normalizedRole"
}
private fun normalizeDeviceId(deviceId: String): String = deviceId.trim().lowercase()
private fun normalizeRole(role: String): String = role.trim().lowercase()
private fun normalizeScopes(scopes: List<String>): List<String> {
return scopes
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
.sorted()
}
}

View File

@@ -0,0 +1,124 @@
package ai.openclaw.app.gateway
import android.os.Build
import java.net.InetAddress
import java.util.Locale
internal fun isLoopbackGatewayHost(
rawHost: String?,
allowEmulatorBridgeAlias: Boolean = isAndroidEmulatorRuntime(),
): Boolean {
var host =
rawHost
?.trim()
?.lowercase(Locale.US)
?.trim('[', ']')
.orEmpty()
if (host.endsWith(".")) {
host = host.dropLast(1)
}
val zoneIndex = host.indexOf('%')
if (zoneIndex >= 0) return false
if (host.isEmpty()) return false
if (host == "localhost") return true
if (allowEmulatorBridgeAlias && host == "10.0.2.2") return true
parseIpv4Address(host)?.let { ipv4 ->
return ipv4.first() == 127.toByte()
}
if (!host.contains(':') || !host.all(::isIpv6LiteralChar)) return false
val address = runCatching { InetAddress.getByName(host) }.getOrNull()?.address ?: return false
if (address.size == 4) {
return address[0] == 127.toByte()
}
if (address.size != 16) return false
// `::1` is 15 zero bytes followed by `0x01`.
val isIpv6Loopback = address.copyOfRange(0, 15).all { it == 0.toByte() } && address[15] == 1.toByte()
if (isIpv6Loopback) return true
val isMappedIpv4 =
address.copyOfRange(0, 10).all { it == 0.toByte() } &&
address[10] == 0xFF.toByte() &&
address[11] == 0xFF.toByte()
return isMappedIpv4 && address[12] == 127.toByte()
}
internal fun isPrivateLanGatewayHost(
rawHost: String?,
allowEmulatorBridgeAlias: Boolean = isAndroidEmulatorRuntime(),
): Boolean {
var host =
rawHost
?.trim()
?.lowercase(Locale.US)
?.trim('[', ']')
.orEmpty()
if (host.endsWith(".")) {
host = host.dropLast(1)
}
val zoneIndex = host.indexOf('%')
if (zoneIndex >= 0) {
host = host.substring(0, zoneIndex)
}
if (host.isEmpty()) return false
if (isLoopbackGatewayHost(host, allowEmulatorBridgeAlias = allowEmulatorBridgeAlias)) return true
if (host.endsWith(".local")) return true
if (!host.contains('.') && !host.contains(':')) return true
parseIpv4Address(host)?.let { ipv4 ->
val first = ipv4[0].toInt() and 0xff
val second = ipv4[1].toInt() and 0xff
return when {
first == 10 -> true
first == 172 && second in 16..31 -> true
first == 192 && second == 168 -> true
first == 169 && second == 254 -> true
else -> false
}
}
if (!host.contains(':') || !host.all(::isIpv6LiteralChar)) return false
val address = runCatching { InetAddress.getByName(host) }.getOrNull() ?: return false
return when {
address.isLinkLocalAddress -> true
address.isSiteLocalAddress -> true
else -> {
val bytes = address.address
bytes.size == 16 && (bytes[0].toInt() and 0xfe) == 0xfc
}
}
}
private fun isAndroidEmulatorRuntime(): Boolean {
val fingerprint = Build.FINGERPRINT?.lowercase(Locale.US).orEmpty()
val model = Build.MODEL?.lowercase(Locale.US).orEmpty()
val manufacturer = Build.MANUFACTURER?.lowercase(Locale.US).orEmpty()
val brand = Build.BRAND?.lowercase(Locale.US).orEmpty()
val device = Build.DEVICE?.lowercase(Locale.US).orEmpty()
val product = Build.PRODUCT?.lowercase(Locale.US).orEmpty()
return fingerprint.contains("generic") ||
fingerprint.contains("robolectric") ||
model.contains("emulator") ||
model.contains("sdk_gphone") ||
manufacturer.contains("genymotion") ||
(brand.contains("generic") && device.contains("generic")) ||
product.contains("sdk_gphone") ||
product.contains("emulator") ||
product.contains("simulator")
}
private fun parseIpv4Address(host: String): ByteArray? {
val parts = host.split('.')
if (parts.size != 4) return null
val bytes = ByteArray(4)
for ((index, part) in parts.withIndex()) {
val value = part.toIntOrNull() ?: return null
if (value !in 0..255) return null
bytes[index] = value.toByte()
}
return bytes
}
private fun isIpv6LiteralChar(char: Char): Boolean = char in '0'..'9' || char in 'a'..'f' || char == ':' || char == '.'

View File

@@ -64,6 +64,7 @@ data class GatewayConnectErrorDetails(
val code: String?,
val canRetryWithDeviceToken: Boolean,
val recommendedNextStep: String?,
val reason: String? = null,
)
private data class SelectedConnectAuth(
@@ -116,6 +117,8 @@ class GatewaySession(
val details: GatewayConnectErrorDetails? = null,
)
data class RpcResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?)
private val json = Json { ignoreUnknownKeys = true }
private val writeLock = Mutex()
private val pending = ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>()
@@ -196,6 +199,13 @@ class GatewaySession(
}
suspend fun request(method: String, paramsJson: String?, timeoutMs: Long = 15_000): String {
val res = requestDetailed(method = method, paramsJson = paramsJson, timeoutMs = timeoutMs)
if (res.ok) return res.payloadJson ?: ""
val err = res.error
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
}
suspend fun requestDetailed(method: String, paramsJson: String?, timeoutMs: Long = 15_000): RpcResult {
val conn = currentConnection ?: throw IllegalStateException("not connected")
val params =
if (paramsJson.isNullOrBlank()) {
@@ -204,9 +214,7 @@ class GatewaySession(
json.parseToJsonElement(paramsJson)
}
val res = conn.request(method, params, timeoutMs)
if (res.ok) return res.payloadJson ?: ""
val err = res.error
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
}
suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean {
@@ -268,16 +276,10 @@ class GatewaySession(
private var socket: WebSocket? = null
private val loggerTag = "OpenClawGateway"
val remoteAddress: String =
if (endpoint.host.contains(":")) {
"[${endpoint.host}]:${endpoint.port}"
} else {
"${endpoint.host}:${endpoint.port}"
}
val remoteAddress: String = formatGatewayAuthority(endpoint.host, endpoint.port)
suspend fun connect() {
val scheme = if (tls != null) "wss" else "ws"
val url = "$scheme://${endpoint.host}:${endpoint.port}"
val url = buildGatewayWebSocketUrl(endpoint.host, endpoint.port, tls != null)
val request = Request.Builder().url(url).build()
socket = client.newWebSocket(request, Listener())
try {
@@ -424,11 +426,63 @@ class GatewaySession(
}
throw GatewayConnectFailure(error)
}
handleConnectSuccess(res, identity.deviceId)
handleConnectSuccess(res, identity.deviceId, selectedAuth.authSource)
connectDeferred.complete(Unit)
}
private fun handleConnectSuccess(res: RpcResponse, deviceId: String) {
private fun shouldPersistBootstrapHandoffTokens(authSource: GatewayConnectAuthSource): Boolean {
if (authSource != GatewayConnectAuthSource.BOOTSTRAP_TOKEN) return false
if (isLoopbackGatewayHost(endpoint.host)) return true
return tls != null
}
private fun filteredBootstrapHandoffScopes(role: String, scopes: List<String>): List<String>? {
return when (role.trim()) {
"node" -> emptyList()
"operator" -> {
val allowedOperatorScopes =
setOf(
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
)
scopes.filter { allowedOperatorScopes.contains(it) }.distinct().sorted()
}
else -> null
}
}
private fun persistBootstrapHandoffToken(
deviceId: String,
role: String,
token: String,
scopes: List<String>,
) {
val filteredScopes = filteredBootstrapHandoffScopes(role, scopes) ?: return
deviceAuthStore.saveToken(deviceId, role, token, filteredScopes)
}
private fun persistIssuedDeviceToken(
authSource: GatewayConnectAuthSource,
deviceId: String,
role: String,
token: String,
scopes: List<String>,
) {
if (authSource == GatewayConnectAuthSource.BOOTSTRAP_TOKEN) {
if (!shouldPersistBootstrapHandoffTokens(authSource)) return
persistBootstrapHandoffToken(deviceId, role, token, scopes)
return
}
deviceAuthStore.saveToken(deviceId, role, token, scopes)
}
private fun handleConnectSuccess(
res: RpcResponse,
deviceId: String,
authSource: GatewayConnectAuthSource,
) {
val payloadJson = res.payloadJson ?: throw IllegalStateException("connect failed: missing payload")
val obj = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: throw IllegalStateException("connect failed")
pendingDeviceTokenRetry = false
@@ -438,8 +492,27 @@ class GatewaySession(
val authObj = obj["auth"].asObjectOrNull()
val deviceToken = authObj?.get("deviceToken").asStringOrNull()
val authRole = authObj?.get("role").asStringOrNull() ?: options.role
val authScopes =
authObj?.get("scopes").asArrayOrNull()
?.mapNotNull { it.asStringOrNull() }
?: emptyList()
if (!deviceToken.isNullOrBlank()) {
deviceAuthStore.saveToken(deviceId, authRole, deviceToken)
persistIssuedDeviceToken(authSource, deviceId, authRole, deviceToken, authScopes)
}
if (shouldPersistBootstrapHandoffTokens(authSource)) {
authObj?.get("deviceTokens").asArrayOrNull()
?.mapNotNull { it.asObjectOrNull() }
?.forEach { tokenEntry ->
val handoffToken = tokenEntry["deviceToken"].asStringOrNull()
val handoffRole = tokenEntry["role"].asStringOrNull()
val handoffScopes =
tokenEntry["scopes"].asArrayOrNull()
?.mapNotNull { it.asStringOrNull() }
?: emptyList()
if (!handoffToken.isNullOrBlank() && !handoffRole.isNullOrBlank()) {
persistBootstrapHandoffToken(deviceId, handoffRole, handoffToken, handoffScopes)
}
}
}
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint, isTlsConnection = tls != null)
@@ -566,6 +639,7 @@ class GatewaySession(
code = it["code"].asStringOrNull(),
canRetryWithDeviceToken = it["canRetryWithDeviceToken"].asBooleanOrNull() == true,
recommendedNextStep = it["recommendedNextStep"].asStringOrNull(),
reason = it["reason"].asStringOrNull(),
)
}
ErrorShape(code, msg, details)
@@ -752,7 +826,7 @@ class GatewaySession(
// If raw URL is a non-loopback address and this connection uses TLS,
// normalize scheme/port to the endpoint we actually connected to.
if (trimmed.isNotBlank() && host.isNotBlank() && !isLoopbackHost(host)) {
if (trimmed.isNotBlank() && host.isNotBlank() && !isLoopbackGatewayHost(host)) {
val needsTlsRewrite =
isTlsConnection &&
(
@@ -781,7 +855,7 @@ class GatewaySession(
private fun buildCanvasUrl(host: String, scheme: String, port: Int, suffix: String): String {
val loweredScheme = scheme.lowercase()
val formattedHost = if (host.contains(":")) "[${host}]" else host
val formattedHost = formatGatewayAuthorityHost(host)
val portSuffix = if ((loweredScheme == "https" && port == 443) || (loweredScheme == "http" && port == 80)) "" else ":$port"
return "$loweredScheme://$formattedHost$portSuffix$suffix"
}
@@ -794,15 +868,6 @@ class GatewaySession(
return "$path$query$fragment"
}
private fun isLoopbackHost(raw: String?): Boolean {
val host = raw?.trim()?.lowercase().orEmpty()
if (host.isEmpty()) return false
if (host == "localhost") return true
if (host == "::1") return true
if (host == "0.0.0.0" || host == "::") return true
return host.startsWith("127.")
}
private fun selectConnectAuth(
endpoint: GatewayEndpoint,
tls: GatewayTlsParams?,
@@ -891,15 +956,31 @@ class GatewaySession(
endpoint: GatewayEndpoint,
tls: GatewayTlsParams?,
): Boolean {
if (isLoopbackHost(endpoint.host)) {
if (isLoopbackGatewayHost(endpoint.host)) {
return true
}
return tls?.expectedFingerprint?.trim()?.isNotEmpty() == true
}
}
internal fun buildGatewayWebSocketUrl(host: String, port: Int, useTls: Boolean): String {
val scheme = if (useTls) "wss" else "ws"
return "$scheme://${formatGatewayAuthority(host, port)}"
}
internal fun formatGatewayAuthority(host: String, port: Int): String {
return "${formatGatewayAuthorityHost(host)}:$port"
}
private fun formatGatewayAuthorityHost(host: String): String {
val normalizedHost = host.trim().trim('[', ']')
return if (normalizedHost.contains(":")) "[${normalizedHost}]" else normalizedHost
}
private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject
private fun JsonElement?.asArrayOrNull(): JsonArray? = this as? JsonArray
private fun JsonElement?.asStringOrNull(): String? =
when (this) {
is JsonNull -> null

View File

@@ -3,7 +3,11 @@ package ai.openclaw.app.gateway
import android.annotation.SuppressLint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.EOFException
import java.net.ConnectException
import java.net.InetSocketAddress
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.cert.CertificateException
@@ -12,6 +16,7 @@ import java.util.Locale
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLException
import javax.net.ssl.SSLParameters
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.SNIHostName
@@ -32,6 +37,16 @@ data class GatewayTlsConfig(
val hostnameVerifier: HostnameVerifier,
)
enum class GatewayTlsProbeFailure {
TLS_UNAVAILABLE,
ENDPOINT_UNREACHABLE,
}
data class GatewayTlsProbeResult(
val fingerprintSha256: String? = null,
val failure: GatewayTlsProbeFailure? = null,
)
fun buildGatewayTlsConfig(
params: GatewayTlsParams?,
onStore: ((String) -> Unit)? = null,
@@ -85,10 +100,10 @@ suspend fun probeGatewayTlsFingerprint(
host: String,
port: Int,
timeoutMs: Int = 3_000,
): String? {
): GatewayTlsProbeResult {
val trimmedHost = host.trim()
if (trimmedHost.isEmpty()) return null
if (port !in 1..65535) return null
if (trimmedHost.isEmpty()) return GatewayTlsProbeResult(failure = GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE)
if (port !in 1..65535) return GatewayTlsProbeResult(failure = GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE)
return withContext(Dispatchers.IO) {
val trustAll =
@@ -121,10 +136,21 @@ suspend fun probeGatewayTlsFingerprint(
}
socket.startHandshake()
val cert = socket.session.peerCertificates.firstOrNull() as? X509Certificate ?: return@withContext null
sha256Hex(cert.encoded)
} catch (_: Throwable) {
null
val cert =
socket.session.peerCertificates.firstOrNull() as? X509Certificate
?: return@withContext GatewayTlsProbeResult(failure = GatewayTlsProbeFailure.TLS_UNAVAILABLE)
GatewayTlsProbeResult(fingerprintSha256 = sha256Hex(cert.encoded))
} catch (err: Throwable) {
val failure =
when (err) {
is SSLException,
is EOFException -> GatewayTlsProbeFailure.TLS_UNAVAILABLE
is ConnectException,
is SocketTimeoutException,
is UnknownHostException -> GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE
else -> GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE
}
GatewayTlsProbeResult(failure = failure)
} finally {
try {
socket.close()

View File

@@ -14,30 +14,31 @@ object CanvasActionTrust {
if (candidateUri.scheme.equals("file", ignoreCase = true)) {
return false
}
val normalizedCandidate = normalizeTrustedRemoteA2uiUri(candidateUri) ?: return false
return trustedA2uiUrls.any { trusted ->
isTrustedA2uiPage(candidateUri, trusted)
matchesTrustedRemoteA2uiUrlExact(normalizedCandidate, trusted)
}
}
private fun isTrustedA2uiPage(candidateUri: URI, trustedUrl: String): Boolean {
private fun matchesTrustedRemoteA2uiUrlExact(candidateUri: URI, trustedUrl: String): Boolean {
val trustedUri = parseUri(trustedUrl) ?: return false
if (!candidateUri.scheme.equals(trustedUri.scheme, ignoreCase = true)) return false
if (candidateUri.host?.equals(trustedUri.host, ignoreCase = true) != true) return false
if (effectivePort(candidateUri) != effectivePort(trustedUri)) return false
val trustedPath = trustedUri.rawPath?.takeIf { it.isNotBlank() } ?: return false
val candidatePath = candidateUri.rawPath?.takeIf { it.isNotBlank() } ?: return false
val trustedPrefix = if (trustedPath.endsWith("/")) trustedPath else "$trustedPath/"
return candidatePath == trustedPath || candidatePath.startsWith(trustedPrefix)
val normalizedTrusted = normalizeTrustedRemoteA2uiUri(trustedUri) ?: return false
return candidateUri == normalizedTrusted
}
private fun effectivePort(uri: URI): Int {
if (uri.port >= 0) return uri.port
return when (uri.scheme?.lowercase()) {
"https" -> 443
"http" -> 80
else -> -1
private fun normalizeTrustedRemoteA2uiUri(uri: URI): URI? {
// Keep Android trust normalization aligned with iOS ScreenController:
// exact remote URL match, scheme/host normalized, fragment ignored.
val scheme = uri.scheme?.lowercase() ?: return null
if (scheme != "http" && scheme != "https") return null
val host = uri.host?.trim()?.takeIf { it.isNotEmpty() }?.lowercase() ?: return null
return try {
URI(scheme, uri.userInfo, host, uri.port, uri.rawPath, uri.rawQuery, null)
} catch (_: Throwable) {
null
}
}

View File

@@ -7,6 +7,7 @@ import ai.openclaw.app.gateway.GatewayClientInfo
import ai.openclaw.app.gateway.GatewayConnectOptions
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewayTlsParams
import ai.openclaw.app.gateway.isPrivateLanGatewayHost
import ai.openclaw.app.LocationMode
import ai.openclaw.app.VoiceWakeMode
@@ -33,9 +34,10 @@ class ConnectionManager(
val stableId = endpoint.stableId
val stored = storedFingerprint?.trim().takeIf { !it.isNullOrEmpty() }
val isManual = stableId.startsWith("manual|")
val cleartextAllowedHost = isPrivateLanGatewayHost(endpoint.host)
if (isManual) {
if (!manualTlsEnabled) return null
if (!manualTlsEnabled && cleartextAllowedHost) return null
if (!stored.isNullOrBlank()) {
return GatewayTlsParams(
required = true,
@@ -73,6 +75,15 @@ class ConnectionManager(
)
}
if (!cleartextAllowedHost) {
return GatewayTlsParams(
required = true,
expectedFingerprint = null,
allowTOFU = false,
stableId = stableId,
)
}
return null
}
}

View File

@@ -163,7 +163,7 @@ private fun disableForceDarkIfSupported(settings: WebSettings) {
WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF)
}
private class CanvasA2UIActionBridge(
internal class CanvasA2UIActionBridge(
private val isTrustedPage: () -> Boolean,
private val onMessage: (String) -> Unit,
) {

View File

@@ -256,9 +256,23 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
if (config == null) {
validationText =
if (inputMode == ConnectInputMode.SetupCode) {
"Paste a valid setup code to connect."
val parsedSetup = decodeGatewaySetupCode(setupCode)
if (parsedSetup == null) {
"Paste a valid setup code to connect."
} else {
val parsedGateway = parseGatewayEndpointResult(parsedSetup.url)
gatewayEndpointValidationMessage(
parsedGateway.error ?: GatewayEndpointValidationError.INVALID_URL,
GatewayEndpointInputSource.SETUP_CODE,
)
}
} else {
"Enter a valid manual host and port to connect."
val manualUrl = composeGatewayManualUrl(manualHostInput, manualPortInput, manualTlsInput)
val parsedGateway = manualUrl?.let(::parseGatewayEndpointResult)
gatewayEndpointValidationMessage(
parsedGateway?.error ?: GatewayEndpointValidationError.INVALID_URL,
GatewayEndpointInputSource.MANUAL,
)
}
return@Button
}
@@ -386,6 +400,11 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
Text("Run these on the gateway host:", style = mobileCallout, color = mobileTextSecondary)
CommandBlock("openclaw qr --setup-code-only")
CommandBlock("openclaw qr --json")
Text(
"For Tailscale or public hosts, use wss:// or Tailscale Serve. Private LAN ws:// remains supported.",
style = mobileCaption1,
color = mobileTextSecondary,
)
if (inputMode == ConnectInputMode.SetupCode) {
Text("Setup Code", style = mobileCaption1.copy(fontWeight = FontWeight.SemiBold), color = mobileTextSecondary)
@@ -468,7 +487,11 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("Use TLS", style = mobileHeadline, color = mobileText)
Text("Switch to secure websocket (`wss`).", style = mobileCallout, color = mobileTextSecondary)
Text(
"Turn this on for Tailscale or public hosts. Private LAN ws:// remains supported.",
style = mobileCallout,
color = mobileTextSecondary,
)
}
Switch(
checked = manualTlsInput,

View File

@@ -1,5 +1,6 @@
package ai.openclaw.app.ui
import ai.openclaw.app.gateway.isPrivateLanGatewayHost
import java.util.Base64
import java.util.Locale
import java.net.URI
@@ -32,7 +33,32 @@ internal data class GatewayConnectConfig(
val password: String,
)
internal enum class GatewayEndpointValidationError {
INVALID_URL,
INSECURE_REMOTE_URL,
}
internal enum class GatewayEndpointInputSource {
SETUP_CODE,
MANUAL,
QR_SCAN,
}
internal data class GatewayEndpointParseResult(
val config: GatewayEndpointConfig? = null,
val error: GatewayEndpointValidationError? = null,
)
internal data class GatewayScannedSetupCodeResult(
val setupCode: String? = null,
val error: GatewayEndpointValidationError? = null,
)
private val gatewaySetupJson = Json { ignoreUnknownKeys = true }
private const val remoteGatewaySecurityRule =
"Tailscale and public mobile nodes require wss:// or Tailscale Serve. ws:// is allowed for private LAN, localhost, and the Android emulator."
private const val remoteGatewaySecurityFix =
"Use a private LAN host/address, or enable Tailscale Serve / expose a wss:// gateway URL."
internal fun resolveGatewayConnectConfig(
useSetupCode: Boolean,
@@ -49,7 +75,7 @@ internal fun resolveGatewayConnectConfig(
): GatewayConnectConfig? {
if (useSetupCode) {
val setup = decodeGatewaySetupCode(setupCode) ?: return null
val parsed = parseGatewayEndpoint(setup.url) ?: return null
val parsed = parseGatewayEndpointResult(setup.url).config ?: return null
val setupBootstrapToken = setup.bootstrapToken?.trim().orEmpty()
val sharedToken =
when {
@@ -74,10 +100,10 @@ internal fun resolveGatewayConnectConfig(
}
val manualUrl = composeGatewayManualUrl(manualHostInput, manualPortInput, manualTlsInput) ?: return null
val parsed = parseGatewayEndpoint(manualUrl) ?: return null
val parsed = parseGatewayEndpointResult(manualUrl).config ?: return null
val savedManualEndpoint =
composeGatewayManualUrl(savedManualHost, savedManualPort, savedManualTls)
?.let(::parseGatewayEndpoint)
?.let { parseGatewayEndpointResult(it).config }
val preserveBootstrapToken =
savedManualEndpoint != null &&
savedManualEndpoint.host == parsed.host &&
@@ -96,13 +122,19 @@ internal fun resolveGatewayConnectConfig(
}
internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
return parseGatewayEndpointResult(rawInput).config
}
internal fun parseGatewayEndpointResult(rawInput: String): GatewayEndpointParseResult {
val raw = rawInput.trim()
if (raw.isEmpty()) return null
if (raw.isEmpty()) return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INVALID_URL)
val normalized = if (raw.contains("://")) raw else "https://$raw"
val uri = runCatching { URI(normalized) }.getOrNull() ?: return null
val host = uri.host?.trim().orEmpty()
if (host.isEmpty()) return null
val uri =
runCatching { URI(normalized) }.getOrNull()
?: return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INVALID_URL)
val host = uri.host?.trim()?.trim('[', ']').orEmpty()
if (host.isEmpty()) return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INVALID_URL)
val scheme = uri.scheme?.trim()?.lowercase(Locale.US).orEmpty()
val tls =
@@ -111,6 +143,9 @@ internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
"wss", "https" -> true
else -> true
}
if (!tls && !isPrivateLanGatewayHost(host)) {
return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INSECURE_REMOTE_URL)
}
val defaultPort =
when (scheme) {
"wss", "https" -> 443
@@ -124,14 +159,17 @@ internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
else -> 443
}
val port = uri.port.takeIf { it in 1..65535 } ?: defaultPort
val displayHost = if (host.contains(":")) "[$host]" else host
val displayUrl =
if (port == displayPort && defaultPort == displayPort) {
"${if (tls) "https" else "http"}://$host"
"${if (tls) "https" else "http"}://$displayHost"
} else {
"${if (tls) "https" else "http"}://$host:$port"
"${if (tls) "https" else "http"}://$displayHost:$port"
}
return GatewayEndpointConfig(host = host, port = port, tls = tls, displayUrl = displayUrl)
return GatewayEndpointParseResult(
config = GatewayEndpointConfig(host = host, port = port, tls = tls, displayUrl = displayUrl),
)
}
internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
@@ -162,8 +200,44 @@ internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
}
internal fun resolveScannedSetupCode(rawInput: String): String? {
val setupCode = resolveSetupCodeCandidate(rawInput) ?: return null
return setupCode.takeIf { decodeGatewaySetupCode(it) != null }
return resolveScannedSetupCodeResult(rawInput).setupCode
}
internal fun resolveScannedSetupCodeResult(rawInput: String): GatewayScannedSetupCodeResult {
val setupCode =
resolveSetupCodeCandidate(rawInput)
?: return GatewayScannedSetupCodeResult(error = GatewayEndpointValidationError.INVALID_URL)
val decoded =
decodeGatewaySetupCode(setupCode)
?: return GatewayScannedSetupCodeResult(error = GatewayEndpointValidationError.INVALID_URL)
val parsed = parseGatewayEndpointResult(decoded.url)
if (parsed.config == null) {
return GatewayScannedSetupCodeResult(error = parsed.error)
}
return GatewayScannedSetupCodeResult(setupCode = setupCode)
}
internal fun gatewayEndpointValidationMessage(
error: GatewayEndpointValidationError,
source: GatewayEndpointInputSource,
): String {
return when (error) {
GatewayEndpointValidationError.INSECURE_REMOTE_URL ->
when (source) {
GatewayEndpointInputSource.SETUP_CODE ->
"Setup code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix"
GatewayEndpointInputSource.QR_SCAN ->
"QR code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix"
GatewayEndpointInputSource.MANUAL ->
"$remoteGatewaySecurityRule $remoteGatewaySecurityFix"
}
GatewayEndpointValidationError.INVALID_URL ->
when (source) {
GatewayEndpointInputSource.SETUP_CODE -> "Setup code has invalid gateway URL."
GatewayEndpointInputSource.QR_SCAN -> "QR code did not contain a valid setup code."
GatewayEndpointInputSource.MANUAL -> "Enter a valid manual host and port to connect."
}
}
}
internal fun composeGatewayManualUrl(hostInput: String, portInput: String, tls: Boolean): String? {

View File

@@ -49,6 +49,7 @@ internal fun buildGatewayDiagnosticsReport(
Please:
- pick one route only: same machine, same LAN, Tailscale, or public URL
- classify this as pairing/auth, TLS trust, wrong advertised route, wrong address/port, or gateway down
- remember: Tailscale/public mobile routes require wss:// or Tailscale Serve; private LAN ws:// is still allowed
- quote the exact app status/error below
- tell me whether `openclaw devices list` should show a pending pairing request
- if more signal is needed, ask for `openclaw qr --json`, `openclaw devices list`, and `openclaw nodes status`

View File

@@ -566,12 +566,16 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
if (contents.isEmpty()) {
return@addOnSuccessListener
}
val scannedSetupCode = resolveScannedSetupCode(contents)
if (scannedSetupCode == null) {
gatewayError = "QR code did not contain a valid setup code."
val scannedSetupCode = resolveScannedSetupCodeResult(contents)
if (scannedSetupCode.setupCode == null) {
gatewayError =
gatewayEndpointValidationMessage(
scannedSetupCode.error ?: GatewayEndpointValidationError.INVALID_URL,
GatewayEndpointInputSource.QR_SCAN,
)
return@addOnSuccessListener
}
setupCode = scannedSetupCode
setupCode = scannedSetupCode.setupCode
gatewayInputMode = GatewayInputMode.SetupCode
gatewayError = null
attemptedConnect = false
@@ -799,9 +803,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
gatewayError = "Scan QR code first, or use Advanced setup."
return@Button
}
val parsedGateway = parseGatewayEndpoint(parsedSetup.url)
if (parsedGateway == null) {
gatewayError = "Setup code has invalid gateway URL."
val parsedGateway = parseGatewayEndpointResult(parsedSetup.url)
if (parsedGateway.config == null) {
gatewayError =
gatewayEndpointValidationMessage(
parsedGateway.error ?: GatewayEndpointValidationError.INVALID_URL,
GatewayEndpointInputSource.SETUP_CODE,
)
return@Button
}
gatewayUrl = parsedSetup.url
@@ -819,12 +827,16 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
}
} else {
val manualUrl = composeGatewayManualUrl(manualHost, manualPort, manualTls)
val parsedGateway = manualUrl?.let(::parseGatewayEndpoint)
if (parsedGateway == null) {
gatewayError = "Manual endpoint is invalid."
val parsedGateway = manualUrl?.let(::parseGatewayEndpointResult)
if (parsedGateway?.config == null) {
gatewayError =
gatewayEndpointValidationMessage(
parsedGateway?.error ?: GatewayEndpointValidationError.INVALID_URL,
GatewayEndpointInputSource.MANUAL,
)
return@Button
}
gatewayUrl = parsedGateway.displayUrl
gatewayUrl = parsedGateway.config.displayUrl
viewModel.setGatewayBootstrapToken("")
}
step = OnboardingStep.Permissions
@@ -863,19 +875,23 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
} else {
Button(
onClick = {
val parsed = parseGatewayEndpoint(gatewayUrl)
if (parsed == null) {
val parsed = parseGatewayEndpointResult(gatewayUrl)
if (parsed.config == null) {
step = OnboardingStep.Gateway
gatewayError = "Invalid gateway URL."
gatewayError =
gatewayEndpointValidationMessage(
parsed.error ?: GatewayEndpointValidationError.INVALID_URL,
GatewayEndpointInputSource.MANUAL,
)
return@Button
}
val token = persistedGatewayToken.trim()
val password = gatewayPassword.trim()
attemptedConnect = true
viewModel.setManualEnabled(true)
viewModel.setManualHost(parsed.host)
viewModel.setManualPort(parsed.port)
viewModel.setManualTls(parsed.tls)
viewModel.setManualHost(parsed.config.host)
viewModel.setManualPort(parsed.config.port)
viewModel.setManualTls(parsed.config.tls)
if (gatewayInputMode == GatewayInputMode.Manual) {
viewModel.setGatewayBootstrapToken("")
}
@@ -886,7 +902,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
}
viewModel.setGatewayPassword(password)
viewModel.connect(
GatewayEndpoint.manual(host = parsed.host, port = parsed.port),
GatewayEndpoint.manual(host = parsed.config.host, port = parsed.config.port),
token = token.ifEmpty { null },
bootstrapToken =
if (gatewayInputMode == GatewayInputMode.SetupCode) {
@@ -1040,7 +1056,7 @@ private fun GatewayStep(
StepShell(title = "Gateway Connection") {
Text(
"Run `openclaw qr` on your gateway host, then scan the code with this device.",
"Run `openclaw qr` on your gateway host, then scan the code with this device. For Tailscale or public hosts, use wss:// or Tailscale Serve.",
style = onboardingCalloutStyle,
color = onboardingTextSecondary,
)
@@ -1072,7 +1088,7 @@ private fun GatewayStep(
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("Advanced setup", style = onboardingHeadlineStyle, color = onboardingText)
Text("Paste setup code or enter host/port manually.", style = onboardingCaption1Style, color = onboardingTextSecondary)
Text("Paste setup code or enter host/port manually. Private LAN ws:// is supported; Tailscale/public hosts need wss://.", style = onboardingCaption1Style, color = onboardingTextSecondary)
}
Icon(
imageVector = if (advancedOpen) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
@@ -1153,7 +1169,11 @@ private fun GatewayStep(
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text("Use TLS", style = onboardingHeadlineStyle, color = onboardingText)
Text("Switch to secure websocket (`wss`).", style = onboardingCalloutStyle.copy(lineHeight = 18.sp), color = onboardingTextSecondary)
Text(
"Turn this on for Tailscale or public hosts. Private LAN ws:// remains supported.",
style = onboardingCalloutStyle.copy(lineHeight = 18.sp),
color = onboardingTextSecondary,
)
}
Switch(
checked = manualTls,

View File

@@ -48,6 +48,31 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal fun resolvePendingAssistantAutoSend(
pendingPrompt: String?,
healthOk: Boolean,
pendingRunCount: Int,
): String? {
val prompt = pendingPrompt?.trim()?.ifEmpty { null } ?: return null
if (!healthOk || pendingRunCount > 0) return null
return prompt
}
internal suspend fun dispatchPendingAssistantAutoSend(
pendingPrompt: String?,
healthOk: Boolean,
pendingRunCount: Int,
dispatch: suspend (String) -> Boolean,
): Boolean {
val prompt =
resolvePendingAssistantAutoSend(
pendingPrompt = pendingPrompt,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
) ?: return false
return dispatch(prompt)
}
@Composable
fun ChatSheetContent(viewModel: MainViewModel) {
val messages by viewModel.chatMessages.collectAsState()
@@ -61,11 +86,29 @@ fun ChatSheetContent(viewModel: MainViewModel) {
val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState()
val sessions by viewModel.chatSessions.collectAsState()
val chatDraft by viewModel.chatDraft.collectAsState()
val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState()
LaunchedEffect(Unit) {
viewModel.loadChat(mainSessionKey)
}
LaunchedEffect(pendingAssistantAutoSend, healthOk, pendingRunCount, thinkingLevel) {
val accepted =
dispatchPendingAssistantAutoSend(
pendingPrompt = pendingAssistantAutoSend,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
) { prompt ->
viewModel.sendChatAwaitAcceptance(
message = prompt,
thinking = thinkingLevel,
attachments = emptyList(),
)
}
if (!accepted) return@LaunchedEffect
viewModel.clearPendingAssistantAutoSend()
}
val context = LocalContext.current
val resolver = context.contentResolver
val scope = rememberCoroutineScope()

View File

@@ -14,11 +14,13 @@ import android.speech.SpeechRecognizer
import androidx.core.content.ContextCompat
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@@ -88,6 +90,7 @@ class MicCaptureManager(
val isSending: StateFlow<Boolean> = _isSending
private val messageQueue = ArrayDeque<String>()
private val messageQueueLock = Any()
private var flushedPartialTranscript: String? = null
private var pendingRunId: String? = null
private var pendingAssistantEntryId: String? = null
@@ -99,11 +102,63 @@ class MicCaptureManager(
private var transcriptFlushJob: Job? = null
private var pendingRunTimeoutJob: Job? = null
private var stopRequested = false
private val ttsPauseLock = Any()
private var ttsPauseDepth = 0
private var resumeMicAfterTts = false
private fun enqueueMessage(message: String) {
synchronized(messageQueueLock) {
messageQueue.addLast(message)
}
}
private fun snapshotMessageQueue(): List<String> {
return synchronized(messageQueueLock) {
messageQueue.toList()
}
}
private fun hasQueuedMessages(): Boolean {
return synchronized(messageQueueLock) {
messageQueue.isNotEmpty()
}
}
private fun firstQueuedMessage(): String? {
return synchronized(messageQueueLock) {
messageQueue.firstOrNull()
}
}
private fun removeFirstQueuedMessage(): String? {
return synchronized(messageQueueLock) {
if (messageQueue.isEmpty()) null else messageQueue.removeFirst()
}
}
private fun queuedMessageCount(): Int {
return synchronized(messageQueueLock) {
messageQueue.size
}
}
fun setMicEnabled(enabled: Boolean) {
if (_micEnabled.value == enabled) return
_micEnabled.value = enabled
if (enabled) {
val pausedForTts =
synchronized(ttsPauseLock) {
if (ttsPauseDepth > 0) {
resumeMicAfterTts = true
true
} else {
false
}
}
if (pausedForTts) {
_statusText.value = if (_isSending.value) "Speaking · waiting for reply" else "Speaking…"
return
}
start()
sendQueuedIfIdle()
} else {
@@ -126,6 +181,58 @@ class MicCaptureManager(
}
}
suspend fun pauseForTts() {
val shouldPause =
synchronized(ttsPauseLock) {
ttsPauseDepth += 1
if (ttsPauseDepth > 1) return@synchronized false
resumeMicAfterTts = _micEnabled.value
val active = resumeMicAfterTts || recognizer != null || _isListening.value
if (!active) return@synchronized false
stopRequested = true
restartJob?.cancel()
restartJob = null
transcriptFlushJob?.cancel()
transcriptFlushJob = null
_isListening.value = false
_inputLevel.value = 0f
_liveTranscript.value = null
_statusText.value = if (_isSending.value) "Speaking · waiting for reply" else "Speaking…"
true
}
if (!shouldPause) return
withContext(Dispatchers.Main) {
recognizer?.cancel()
recognizer?.destroy()
recognizer = null
}
}
suspend fun resumeAfterTts() {
val shouldResume =
synchronized(ttsPauseLock) {
if (ttsPauseDepth == 0) return@synchronized false
ttsPauseDepth -= 1
if (ttsPauseDepth > 0) return@synchronized false
val resume = resumeMicAfterTts && _micEnabled.value
resumeMicAfterTts = false
if (!resume) {
_statusText.value =
when {
_micEnabled.value && _isSending.value -> "Listening · sending queued voice"
_micEnabled.value -> "Listening"
_isSending.value -> "Mic off · sending…"
else -> "Mic off"
}
}
resume
}
if (!shouldResume) return
stopRequested = false
start()
sendQueuedIfIdle()
}
fun onGatewayConnectionChanged(connected: Boolean) {
gatewayConnected = connected
if (connected) {
@@ -137,7 +244,7 @@ class MicCaptureManager(
pendingRunId = null
pendingAssistantEntryId = null
_isSending.value = false
if (messageQueue.isNotEmpty()) {
if (hasQueuedMessages()) {
_statusText.value = queuedWaitingStatus()
}
}
@@ -245,7 +352,7 @@ class MicCaptureManager(
_statusText.value =
when {
_isSending.value -> "Listening · sending queued voice"
messageQueue.isNotEmpty() -> "Listening · ${messageQueue.size} queued"
hasQueuedMessages() -> "Listening · ${queuedMessageCount()} queued"
else -> "Listening"
}
_isListening.value = true
@@ -278,7 +385,7 @@ class MicCaptureManager(
role = VoiceConversationRole.User,
text = message,
)
messageQueue.addLast(message)
enqueueMessage(message)
publishQueue()
}
@@ -297,12 +404,12 @@ class MicCaptureManager(
}
private fun publishQueue() {
_queuedMessages.value = messageQueue.toList()
_queuedMessages.value = snapshotMessageQueue()
}
private fun sendQueuedIfIdle() {
if (_isSending.value) return
if (messageQueue.isEmpty()) {
if (!hasQueuedMessages()) {
if (_micEnabled.value) {
_statusText.value = "Listening"
} else {
@@ -315,7 +422,7 @@ class MicCaptureManager(
return
}
val next = messageQueue.first()
val next = firstQueuedMessage() ?: return
_isSending.value = true
pendingRunTimeoutJob?.cancel()
pendingRunTimeoutJob = null
@@ -333,7 +440,7 @@ class MicCaptureManager(
if (runId == null) {
pendingRunTimeoutJob?.cancel()
pendingRunTimeoutJob = null
messageQueue.removeFirst()
removeFirstQueuedMessage()
publishQueue()
_isSending.value = false
pendingAssistantEntryId = null
@@ -379,8 +486,7 @@ class MicCaptureManager(
private fun completePendingTurn() {
pendingRunTimeoutJob?.cancel()
pendingRunTimeoutJob = null
if (messageQueue.isNotEmpty()) {
messageQueue.removeFirst()
if (removeFirstQueuedMessage() != null) {
publishQueue()
}
pendingRunId = null
@@ -390,7 +496,7 @@ class MicCaptureManager(
}
private fun queuedWaitingStatus(): String {
return "${messageQueue.size} queued · waiting for gateway"
return "${queuedMessageCount()} queued · waiting for gateway"
}
private fun appendConversation(

View File

@@ -0,0 +1,242 @@
package ai.openclaw.app.voice
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
import android.media.MediaPlayer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File
internal class TalkAudioPlayer(
private val context: Context,
) {
private val lock = Any()
private var active: ActivePlayback? = null
suspend fun play(audio: TalkSpeakAudio) {
when (val mode = resolvePlaybackMode(audio)) {
is TalkPlaybackMode.Pcm -> playPcm(audio.bytes, mode.sampleRate)
is TalkPlaybackMode.Compressed -> playCompressed(audio.bytes, mode.fileExtension)
}
}
fun stop() {
synchronized(lock) {
active?.cancel()
active = null
}
}
internal fun resolvePlaybackMode(audio: TalkSpeakAudio): TalkPlaybackMode {
return resolvePlaybackMode(
outputFormat = audio.outputFormat,
mimeType = audio.mimeType,
fileExtension = audio.fileExtension,
)
}
companion object {
internal fun resolvePlaybackMode(
outputFormat: String?,
mimeType: String?,
fileExtension: String?,
): TalkPlaybackMode {
val normalizedOutputFormat = outputFormat?.trim()?.lowercase()
if (normalizedOutputFormat != null) {
val pcmSampleRate = parsePcmSampleRate(normalizedOutputFormat)
if (pcmSampleRate != null) {
return TalkPlaybackMode.Pcm(sampleRate = pcmSampleRate)
}
}
val normalizedMimeType = mimeType?.trim()?.lowercase()
val extension =
normalizeExtension(
fileExtension ?: inferExtension(outputFormat = normalizedOutputFormat, mimeType = normalizedMimeType),
)
if (extension != null) {
return TalkPlaybackMode.Compressed(fileExtension = extension)
}
throw IllegalStateException("Unsupported talk audio format")
}
private fun parsePcmSampleRate(outputFormat: String): Int? {
return when (outputFormat) {
"pcm_16000" -> 16_000
"pcm_22050" -> 22_050
"pcm_24000" -> 24_000
"pcm_44100" -> 44_100
else -> null
}
}
private fun inferExtension(outputFormat: String?, mimeType: String?): String? {
return when {
outputFormat == "mp3" || outputFormat?.startsWith("mp3_") == true || mimeType == "audio/mpeg" -> ".mp3"
outputFormat == "opus" || outputFormat?.startsWith("opus_") == true || mimeType == "audio/ogg" -> ".ogg"
outputFormat?.endsWith("-wav") == true || mimeType == "audio/wav" -> ".wav"
outputFormat?.endsWith("-webm") == true || mimeType == "audio/webm" -> ".webm"
else -> null
}
}
private fun normalizeExtension(value: String?): String? {
val trimmed = value?.trim()?.lowercase().orEmpty()
if (trimmed.isEmpty()) return null
return if (trimmed.startsWith(".")) trimmed else ".$trimmed"
}
}
private suspend fun playPcm(bytes: ByteArray, sampleRate: Int) {
withContext(Dispatchers.IO) {
val minBufferSize =
AudioTrack.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
)
if (minBufferSize <= 0) {
throw IllegalStateException("AudioTrack buffer unavailable")
}
val track =
AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build(),
)
.setTransferMode(AudioTrack.MODE_STATIC)
.setBufferSizeInBytes(maxOf(minBufferSize, bytes.size))
.build()
val finished = CompletableDeferred<Unit>()
val playback =
ActivePlayback(
cancel = {
finished.completeExceptionally(CancellationException("assistant speech cancelled"))
runCatching { track.pause() }
runCatching { track.flush() }
runCatching { track.stop() }
},
)
register(playback)
try {
val written = track.write(bytes, 0, bytes.size)
if (written != bytes.size) {
throw IllegalStateException("AudioTrack write failed")
}
val totalFrames = bytes.size / 2
track.play()
while (track.playState == AudioTrack.PLAYSTATE_PLAYING) {
if (track.playbackHeadPosition >= totalFrames) {
finished.complete(Unit)
break
}
delay(20)
}
if (!finished.isCompleted) {
finished.complete(Unit)
}
finished.await()
} finally {
clear(playback)
runCatching { track.pause() }
runCatching { track.flush() }
runCatching { track.stop() }
track.release()
}
}
}
private suspend fun playCompressed(bytes: ByteArray, fileExtension: String) {
val tempFile = withContext(Dispatchers.IO) {
File.createTempFile("talk-audio-", fileExtension, context.cacheDir).apply {
writeBytes(bytes)
}
}
try {
val finished = CompletableDeferred<Unit>()
val player =
withContext(Dispatchers.Main) {
MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
)
setDataSource(tempFile.absolutePath)
setOnCompletionListener {
finished.complete(Unit)
}
setOnErrorListener { _, what, extra ->
finished.completeExceptionally(IllegalStateException("MediaPlayer error ($what/$extra)"))
true
}
prepare()
}
}
val playback =
ActivePlayback(
cancel = {
finished.completeExceptionally(CancellationException("assistant speech cancelled"))
runCatching { player.stop() }
},
)
register(playback)
try {
withContext(Dispatchers.Main) {
player.start()
}
finished.await()
} finally {
clear(playback)
withContext(Dispatchers.Main) {
runCatching { player.stop() }
player.release()
}
}
} finally {
withContext(Dispatchers.IO) {
tempFile.delete()
}
}
}
private fun register(playback: ActivePlayback) {
synchronized(lock) {
active?.cancel()
active = playback
}
}
private fun clear(playback: ActivePlayback) {
synchronized(lock) {
if (active === playback) {
active = null
}
}
}
}
internal sealed interface TalkPlaybackMode {
data class Pcm(val sampleRate: Int) : TalkPlaybackMode
data class Compressed(val fileExtension: String) : TalkPlaybackMode
}
private class ActivePlayback(
val cancel: () -> Unit,
)

View File

@@ -14,19 +14,21 @@ import android.os.SystemClock
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.util.Log
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Log
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import java.util.Locale
import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,6 +48,8 @@ class TalkModeManager(
private val session: GatewaySession,
private val supportsChatSubscribe: Boolean,
private val isConnected: () -> Boolean,
private val onBeforeSpeak: suspend () -> Unit = {},
private val onAfterSpeak: suspend () -> Unit = {},
) {
companion object {
private const val tag = "TalkMode"
@@ -57,6 +61,8 @@ class TalkModeManager(
private val mainHandler = Handler(Looper.getMainLooper())
private val json = Json { ignoreUnknownKeys = true }
private val talkSpeakClient = TalkSpeakClient(session = session, json = json)
private val talkAudioPlayer = TalkAudioPlayer(context)
private val _isEnabled = MutableStateFlow(false)
val isEnabled: StateFlow<Boolean> = _isEnabled
@@ -101,6 +107,7 @@ class TalkModeManager(
private val playbackGeneration = AtomicLong(0L)
private var ttsJob: Job? = null
private val ttsJobLock = Any()
private val ttsLock = Any()
private var textToSpeech: TextToSpeech? = null
private var textToSpeechInit: CompletableDeferred<TextToSpeech>? = null
@@ -163,8 +170,11 @@ class TalkModeManager(
?: waitForAssistantText(session, startedAt, if (ok) 12_000 else 25_000)
if (!assistant.isNullOrBlank()) {
val playbackToken = playbackGeneration.incrementAndGet()
cancelActivePlayback()
_statusText.value = "Speaking…"
playAssistant(assistant, playbackToken)
runPlaybackSession(playbackToken) {
playAssistant(assistant, playbackToken)
}
} else {
_statusText.value = "No reply"
}
@@ -180,14 +190,12 @@ class TalkModeManager(
fun playTtsForText(text: String) {
val playbackToken = playbackGeneration.incrementAndGet()
ttsJob?.cancel()
ttsJob = scope.launch {
cancelActivePlayback()
scope.launch {
reloadConfig()
ensurePlaybackActive(playbackToken)
_isSpeaking.value = true
_statusText.value = "Speaking…"
playAssistant(text, playbackToken)
ttsJob = null
runPlaybackSession(playbackToken) {
playAssistant(text, playbackToken)
}
}
}
@@ -258,7 +266,6 @@ class TalkModeManager(
if (playbackEnabled == enabled) return
playbackEnabled = enabled
if (!enabled) {
playbackGeneration.incrementAndGet()
stopSpeaking()
}
}
@@ -270,10 +277,11 @@ class TalkModeManager(
suspend fun speakAssistantReply(text: String) {
if (!playbackEnabled) return
val playbackToken = playbackGeneration.incrementAndGet()
stopSpeaking(resetInterrupt = false)
cancelActivePlayback()
ensureConfigLoaded()
ensurePlaybackActive(playbackToken)
playAssistant(text, playbackToken)
runPlaybackSession(playbackToken) {
playAssistant(text, playbackToken)
}
}
private fun start() {
@@ -483,9 +491,10 @@ class TalkModeManager(
}
Log.d(tag, "assistant text ok chars=${assistant.length}")
val playbackToken = playbackGeneration.incrementAndGet()
stopSpeaking(resetInterrupt = false)
ensurePlaybackActive(playbackToken)
playAssistant(assistant, playbackToken)
cancelActivePlayback()
runPlaybackSession(playbackToken) {
playAssistant(assistant, playbackToken)
}
} catch (err: Throwable) {
if (err is CancellationException) {
Log.d(tag, "finalize speech cancelled")
@@ -655,22 +664,87 @@ class TalkModeManager(
requestAudioFocusForTts()
try {
val ttsStarted = SystemClock.elapsedRealtime()
speakWithSystemTts(cleaned, directive, playbackToken)
Log.d(tag, "system tts ok durMs=${SystemClock.elapsedRealtime() - ttsStarted}")
val started = SystemClock.elapsedRealtime()
when (val result = talkSpeakClient.synthesize(text = cleaned, directive = directive)) {
is TalkSpeakResult.Success -> {
ensurePlaybackActive(playbackToken)
talkAudioPlayer.play(result.audio)
ensurePlaybackActive(playbackToken)
Log.d(tag, "talk.speak ok durMs=${SystemClock.elapsedRealtime() - started}")
}
is TalkSpeakResult.FallbackToLocal -> {
Log.d(tag, "talk.speak unavailable; using local TTS: ${result.message}")
speakWithSystemTts(cleaned, directive, playbackToken)
Log.d(tag, "system tts ok durMs=${SystemClock.elapsedRealtime() - started}")
}
is TalkSpeakResult.Failure -> {
throw IllegalStateException(result.message)
}
}
} catch (err: Throwable) {
if (isPlaybackCancelled(err, playbackToken)) {
Log.d(tag, "assistant speech cancelled")
return
}
_statusText.value = "Speak failed: ${err.message ?: err::class.simpleName}"
Log.w(tag, "system tts failed: ${err.message ?: err::class.simpleName}")
Log.w(tag, "talk playback failed: ${err.message ?: err::class.simpleName}")
} finally {
_isSpeaking.value = false
}
}
private suspend fun runPlaybackSession(
playbackToken: Long,
block: suspend () -> Unit,
) {
val currentJob = coroutineContext[Job]
var shouldResumeAfterSpeak = false
try {
val claimedPlayback =
synchronized(ttsJobLock) {
if (!playbackEnabled || playbackToken != playbackGeneration.get()) {
false
} else {
ttsJob = currentJob
true
}
}
if (!claimedPlayback) {
ensurePlaybackActive(playbackToken)
return
}
ensurePlaybackActive(playbackToken)
shouldResumeAfterSpeak = true
onBeforeSpeak()
ensurePlaybackActive(playbackToken)
_isSpeaking.value = true
_statusText.value = "Speaking…"
block()
} finally {
synchronized(ttsJobLock) {
if (ttsJob === currentJob) {
ttsJob = null
}
}
_isSpeaking.value = false
if (shouldResumeAfterSpeak) {
withContext(NonCancellable) {
onAfterSpeak()
}
}
}
}
private fun cancelActivePlayback() {
val activeJob =
synchronized(ttsJobLock) {
ttsJob
}
activeJob?.cancel()
talkAudioPlayer.stop()
stopTextToSpeechPlayback()
}
private suspend fun speakWithSystemTts(text: String, directive: TalkDirective?, playbackToken: Long) {
ensurePlaybackActive(playbackToken)
val engine = ensureTextToSpeech()
@@ -755,15 +829,16 @@ class TalkModeManager(
}
private fun stopSpeaking(resetInterrupt: Boolean = true) {
playbackGeneration.incrementAndGet()
if (!_isSpeaking.value) {
stopTextToSpeechPlayback()
cancelActivePlayback()
abandonAudioFocus()
return
}
if (resetInterrupt) {
lastInterruptedAtSeconds = null
}
stopTextToSpeechPlayback()
cancelActivePlayback()
_isSpeaking.value = false
abandonAudioFocus()
}

View File

@@ -0,0 +1,143 @@
package ai.openclaw.app.voice
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
internal data class TalkSpeakAudio(
val bytes: ByteArray,
val provider: String,
val outputFormat: String?,
val voiceCompatible: Boolean?,
val mimeType: String?,
val fileExtension: String?,
)
internal sealed interface TalkSpeakResult {
data class Success(val audio: TalkSpeakAudio) : TalkSpeakResult
data class FallbackToLocal(val message: String) : TalkSpeakResult
data class Failure(val message: String) : TalkSpeakResult
}
internal class TalkSpeakClient(
private val session: GatewaySession? = null,
private val json: Json = Json { ignoreUnknownKeys = true },
private val requestDetailed: (suspend (String, String, Long) -> GatewaySession.RpcResult)? = null,
) {
suspend fun synthesize(text: String, directive: TalkDirective?): TalkSpeakResult {
val response =
try {
performRequest(
method = "talk.speak",
paramsJson = json.encodeToString(TalkSpeakRequest.from(text = text, directive = directive)),
timeoutMs = 45_000,
)
} catch (err: Throwable) {
return TalkSpeakResult.Failure(err.message ?: "talk.speak request failed")
}
if (!response.ok) {
val error = response.error
val message = error?.message ?: "talk.speak request failed"
return if (isFallbackEligible(error)) {
TalkSpeakResult.FallbackToLocal(message)
} else {
TalkSpeakResult.Failure(message)
}
}
val payload =
try {
json.decodeFromString<TalkSpeakResponse>(response.payloadJson ?: "")
} catch (err: Throwable) {
return TalkSpeakResult.Failure(err.message ?: "talk.speak payload invalid")
}
val bytes =
try {
android.util.Base64.decode(payload.audioBase64, android.util.Base64.DEFAULT)
} catch (err: Throwable) {
return TalkSpeakResult.Failure(err.message ?: "talk.speak audio decode failed")
}
if (bytes.isEmpty()) {
return TalkSpeakResult.Failure("talk.speak returned empty audio")
}
return TalkSpeakResult.Success(
TalkSpeakAudio(
bytes = bytes,
provider = payload.provider,
outputFormat = payload.outputFormat,
voiceCompatible = payload.voiceCompatible,
mimeType = payload.mimeType,
fileExtension = payload.fileExtension,
),
)
}
private fun isFallbackEligible(error: GatewaySession.ErrorShape?): Boolean {
val reason = error?.details?.reason
if (reason == null) return true
return reason == "talk_unconfigured" ||
reason == "talk_provider_unsupported" ||
reason == "method_unavailable"
}
private suspend fun performRequest(
method: String,
paramsJson: String,
timeoutMs: Long,
): GatewaySession.RpcResult {
requestDetailed?.let { return it(method, paramsJson, timeoutMs) }
val activeSession = session ?: throw IllegalStateException("session missing")
return activeSession.requestDetailed(method = method, paramsJson = paramsJson, timeoutMs = timeoutMs)
}
}
@Serializable
internal data class TalkSpeakRequest(
val text: String,
val voiceId: String? = null,
val modelId: String? = null,
val outputFormat: String? = null,
val speed: Double? = null,
val rateWpm: Int? = null,
val stability: Double? = null,
val similarity: Double? = null,
val style: Double? = null,
val speakerBoost: Boolean? = null,
val seed: Long? = null,
val normalize: String? = null,
val language: String? = null,
val latencyTier: Int? = null,
) {
companion object {
fun from(text: String, directive: TalkDirective?): TalkSpeakRequest {
return TalkSpeakRequest(
text = text,
voiceId = directive?.voiceId,
modelId = directive?.modelId,
outputFormat = directive?.outputFormat,
speed = directive?.speed,
rateWpm = directive?.rateWpm,
stability = directive?.stability,
similarity = directive?.similarity,
style = directive?.style,
speakerBoost = directive?.speakerBoost,
seed = directive?.seed,
normalize = directive?.normalize,
language = directive?.language,
latencyTier = directive?.latencyTier,
)
}
}
}
@Serializable
private data class TalkSpeakResponse(
val audioBase64: String,
val provider: String,
val outputFormat: String? = null,
val voiceCompatible: Boolean? = null,
val mimeType: String? = null,
val fileExtension: String? = null,
)

View File

@@ -2,7 +2,9 @@ package ai.openclaw.app
import android.content.Intent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -18,6 +20,7 @@ class AssistantLaunchTest {
requireNotNull(parsed)
assertEquals("assist", parsed.source)
assertNull(parsed.prompt)
assertFalse(parsed.autoSend)
}
@Test
@@ -30,6 +33,7 @@ class AssistantLaunchTest {
requireNotNull(parsed)
assertEquals("app_action", parsed.source)
assertEquals("summarize my unread texts", parsed.prompt)
assertTrue(parsed.autoSend)
}
@Test

View File

@@ -1,5 +1,10 @@
package ai.openclaw.app
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
import ai.openclaw.app.gateway.GatewayTlsProbeResult
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -9,23 +14,79 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.lang.reflect.Field
import java.util.UUID
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class GatewayBootstrapAuthTest {
@Test
fun connectsOperatorSessionWhenBootstrapAuthExists() {
assertTrue(shouldConnectOperatorSession(token = "", bootstrapToken = "bootstrap-1", password = "", storedOperatorToken = ""))
assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = "bootstrap-1", password = null, storedOperatorToken = null))
fun skipsOperatorSessionWhenOnlyBootstrapAuthExists() {
assertFalse(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = "", bootstrapToken = "bootstrap-1", password = ""),
storedOperatorToken = "",
),
)
assertFalse(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = null,
),
)
}
@Test
fun skipsOperatorSessionOnlyWhenNoSharedBootstrapOrStoredAuthExists() {
assertTrue(shouldConnectOperatorSession(token = "shared-token", bootstrapToken = "bootstrap-1", password = null, storedOperatorToken = null))
assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = "bootstrap-1", password = "shared-password", storedOperatorToken = null))
assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = null, password = null, storedOperatorToken = "stored-token"))
assertFalse(shouldConnectOperatorSession(token = null, bootstrapToken = "", password = null, storedOperatorToken = null))
fun connectsOperatorSessionWhenSharedPasswordOrStoredAuthExists() {
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = null,
),
)
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = "shared-password"),
storedOperatorToken = null,
),
)
assertTrue(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = "stored-token",
),
)
assertFalse(
shouldConnectOperatorSession(
NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "", password = null),
storedOperatorToken = null,
),
)
}
@Test
fun resolveOperatorSessionConnectAuthUsesStoredTokenPathAfterBootstrapHandoff() {
val resolved =
resolveOperatorSessionConnectAuth(
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = "stored-token",
)
assertEquals(NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = null, password = null), resolved)
}
@Test
fun resolveOperatorSessionConnectAuthPrefersExplicitSharedAuth() {
val resolved =
resolveOperatorSessionConnectAuth(
auth = NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = "bootstrap-1", password = "shared-password"),
storedOperatorToken = "stored-token",
)
assertEquals(
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
resolved,
)
}
@Test
@@ -55,4 +116,105 @@ class GatewayBootstrapAuthTest {
assertEquals("setup-bootstrap-token", auth.bootstrapToken)
assertNull(auth.password)
}
@Test
fun acceptGatewayTrustPrompt_preservesExplicitSetupAuth() =
runBlocking {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
android.content.Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
prefs.setGatewayToken("stale-shared-token")
prefs.setGatewayBootstrapToken("")
prefs.setGatewayPassword("stale-password")
val runtime =
NodeRuntime(
app,
prefs,
tlsFingerprintProbe = { _, _ -> GatewayTlsProbeResult(fingerprintSha256 = "fp-1") },
)
val endpoint = GatewayEndpoint.manual(host = "gateway.example", port = 18789)
val explicitAuth =
NodeRuntime.GatewayConnectAuth(
token = null,
bootstrapToken = "setup-bootstrap-token",
password = null,
)
runtime.connect(endpoint, explicitAuth)
val prompt = waitForGatewayTrustPrompt(runtime)
assertEquals("setup-bootstrap-token", prompt.auth.bootstrapToken)
runtime.acceptGatewayTrustPrompt()
assertEquals("fp-1", prefs.loadGatewayTlsFingerprint(endpoint.stableId))
assertEquals("setup-bootstrap-token", desiredBootstrapToken(runtime, "nodeSession"))
assertNull(desiredBootstrapToken(runtime, "operatorSession"))
}
@Test
fun connect_showsSecureEndpointGuidanceWhenTlsProbeFails() {
val app = RuntimeEnvironment.getApplication()
val runtime =
NodeRuntime(
app,
tlsFingerprintProbe = { _, _ ->
GatewayTlsProbeResult(failure = GatewayTlsProbeFailure.TLS_UNAVAILABLE)
},
)
runtime.connect(
GatewayEndpoint.manual(host = "gateway.example", port = 18789),
NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
)
assertEquals(
"Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected.",
waitForStatusText(runtime),
)
assertNull(runtime.pendingGatewayTrust.value)
}
private fun waitForGatewayTrustPrompt(runtime: NodeRuntime): NodeRuntime.GatewayTrustPrompt {
repeat(50) {
runtime.pendingGatewayTrust.value?.let { return it }
Thread.sleep(10)
}
error("Expected pending gateway trust prompt")
}
private fun waitForStatusText(runtime: NodeRuntime): String {
repeat(50) {
val status = runtime.statusText.value
if (status != "Verify gateway TLS fingerprint…") {
return status
}
Thread.sleep(10)
}
error("Expected status text update")
}
private fun desiredBootstrapToken(runtime: NodeRuntime, sessionFieldName: String): String? {
val session = readField<GatewaySession>(runtime, sessionFieldName)
val desired = readField<Any?>(session, "desired") ?: return null
return readField(desired, "bootstrapToken")
}
private fun <T> readField(target: Any, name: String): T {
var type: Class<*>? = target.javaClass
while (type != null) {
try {
val field: Field = type.getDeclaredField(name)
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
return field.get(target) as T
} catch (_: NoSuchFieldException) {
type = type.superclass
}
}
error("Field $name not found on ${target.javaClass.name}")
}
}

View File

@@ -0,0 +1,63 @@
package ai.openclaw.app.gateway
import ai.openclaw.app.SecurePrefs
import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.util.UUID
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class DeviceAuthStoreTest {
@Test
fun saveTokenPersistsNormalizedScopesMetadata() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
val store = DeviceAuthStore(prefs)
store.saveToken(
deviceId = " Device-1 ",
role = " Operator ",
token = " operator-token ",
scopes = listOf("operator.write", "operator.read", "operator.write", " "),
)
val entry = store.loadEntry("device-1", "operator")
assertNotNull(entry)
assertEquals("operator-token", entry?.token)
assertEquals("operator", entry?.role)
assertEquals(listOf("operator.read", "operator.write"), entry?.scopes)
assertTrue((entry?.updatedAtMs ?: 0L) > 0L)
}
@Test
fun loadEntryReadsLegacyTokenWithoutMetadata() {
val app = RuntimeEnvironment.getApplication()
val securePrefs =
app.getSharedPreferences(
"openclaw.node.secure.test.${UUID.randomUUID()}",
Context.MODE_PRIVATE,
)
val prefs = SecurePrefs(app, securePrefsOverride = securePrefs)
prefs.putString("gateway.deviceToken.device-1.operator", "legacy-token")
val store = DeviceAuthStore(prefs)
val entry = store.loadEntry("device-1", "operator")
assertNotNull(entry)
assertEquals("legacy-token", entry?.token)
assertEquals("operator", entry?.role)
assertEquals(emptyList<String>(), entry?.scopes)
assertEquals(0L, entry?.updatedAtMs)
}
}

View File

@@ -35,12 +35,18 @@ private const val CONNECT_CHALLENGE_FRAME =
"""{"type":"event","event":"connect.challenge","payload":{"nonce":"android-test-nonce"}}"""
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
private val tokens = mutableMapOf<String, String>()
private val tokens = mutableMapOf<String, DeviceAuthEntry>()
override fun loadToken(deviceId: String, role: String): String? = tokens["${deviceId.trim()}|${role.trim()}"]?.trim()?.takeIf { it.isNotEmpty() }
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? = tokens["${deviceId.trim()}|${role.trim()}"]
override fun saveToken(deviceId: String, role: String, token: String) {
tokens["${deviceId.trim()}|${role.trim()}"] = token.trim()
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) {
tokens["${deviceId.trim()}|${role.trim()}"] =
DeviceAuthEntry(
token = token.trim(),
role = role.trim(),
scopes = scopes,
updatedAtMs = System.currentTimeMillis(),
)
}
override fun clearToken(deviceId: String, role: String) {
@@ -213,6 +219,144 @@ class GatewaySessionInvokeTest {
}
}
@Test
fun connect_storesPrimaryDeviceTokenFromSuccessfulSharedTokenConnect() = runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
when (method) {
"connect" -> {
webSocket.send(
connectResponseFrame(
id,
authJson = """{"deviceToken":"shared-node-token","role":"node","scopes":[]}""",
),
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = "shared-auth-token",
bootstrapToken = null,
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
assertEquals("shared-node-token", harness.deviceAuthStore.loadToken(deviceId, "node"))
assertNull(harness.deviceAuthStore.loadToken(deviceId, "operator"))
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun bootstrapConnect_storesAdditionalBoundedDeviceTokensOnTrustedTransport() = runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
when (method) {
"connect" -> {
webSocket.send(
connectResponseFrame(
id,
authJson =
"""{"deviceToken":"bootstrap-node-token","role":"node","scopes":[],"deviceTokens":[{"deviceToken":"bootstrap-operator-token","role":"operator","scopes":["operator.admin","operator.approvals","operator.read","operator.talk.secrets","operator.write"]}]}""",
),
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = null,
bootstrapToken = "bootstrap-token",
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
val nodeEntry = harness.deviceAuthStore.loadEntry(deviceId, "node")
val operatorEntry = harness.deviceAuthStore.loadEntry(deviceId, "operator")
assertEquals("bootstrap-node-token", nodeEntry?.token)
assertEquals(emptyList<String>(), nodeEntry?.scopes)
assertEquals("bootstrap-operator-token", operatorEntry?.token)
assertEquals(
listOf("operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"),
operatorEntry?.scopes,
)
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun nonBootstrapConnect_ignoresAdditionalBootstrapDeviceTokens() = runBlocking {
val json = testJson()
val connected = CompletableDeferred<Unit>()
val lastDisconnect = AtomicReference("")
val server =
startGatewayServer(json) { webSocket, id, method, _ ->
when (method) {
"connect" -> {
webSocket.send(
connectResponseFrame(
id,
authJson =
"""{"deviceToken":"shared-node-token","role":"node","scopes":[],"deviceTokens":[{"deviceToken":"shared-operator-token","role":"operator","scopes":["operator.approvals","operator.read"]}]}""",
),
)
webSocket.close(1000, "done")
}
}
}
val harness =
createNodeHarness(
connected = connected,
lastDisconnect = lastDisconnect,
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
try {
connectNodeSession(
session = harness.session,
port = server.port,
token = "shared-auth-token",
bootstrapToken = null,
)
awaitConnectedOrThrow(connected, lastDisconnect, server)
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
assertEquals("shared-node-token", harness.deviceAuthStore.loadToken(deviceId, "node"))
assertNull(harness.deviceAuthStore.loadToken(deviceId, "operator"))
} finally {
shutdownHarness(harness, server)
}
}
@Test
fun nodeInvokeRequest_roundTripsInvokeResult() = runBlocking {
val handshakeOrigin = AtomicReference<String?>(null)
@@ -470,9 +614,14 @@ class GatewaySessionInvokeTest {
}
}
private fun connectResponseFrame(id: String, canvasHostUrl: String? = null): String {
private fun connectResponseFrame(
id: String,
canvasHostUrl: String? = null,
authJson: String? = null,
): String {
val canvas = canvasHostUrl?.let { "\"canvasHostUrl\":\"$it\"," } ?: ""
return """{"type":"res","id":"$id","ok":true,"payload":{$canvas"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
val auth = authJson?.let { "\"auth\":$it," } ?: ""
return """{"type":"res","id":"$id","ok":true,"payload":{$canvas$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
}
private fun startGatewayServer(

View File

@@ -4,6 +4,23 @@ import org.junit.Assert.assertEquals
import org.junit.Test
class GatewaySessionInvokeTimeoutTest {
@Test
fun formatGatewayAuthority_bracketsIpv6Hosts() {
assertEquals("[::1]:18789", formatGatewayAuthority("::1", 18_789))
}
@Test
fun buildGatewayWebSocketUrl_bracketsIpv6Hosts() {
assertEquals("ws://[::1]:18789", buildGatewayWebSocketUrl("::1", 18_789, useTls = false))
assertEquals("wss://[::1]:443", buildGatewayWebSocketUrl("::1", 443, useTls = true))
}
@Test
fun buildGatewayWebSocketUrl_normalizesPersistedBracketedIpv6Hosts() {
assertEquals("ws://[::1]:18789", buildGatewayWebSocketUrl("[::1]", 18_789, useTls = false))
assertEquals("wss://[::1]:443", buildGatewayWebSocketUrl("[::1]", 443, useTls = true))
}
@Test
fun resolveInvokeResultAckTimeoutMs_usesFloorWhenMissingOrTooSmall() {
assertEquals(15_000L, resolveInvokeResultAckTimeoutMs(null))

View File

@@ -39,4 +39,34 @@ class CanvasActionTrustTest {
),
)
}
@Test
fun acceptsFragmentOnlyDifferenceForTrustedA2uiPage() {
assertTrue(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android#step2",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsQueryMismatchOnTrustedOriginAndPath() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=ios",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsDescendantPathUnderTrustedA2uiRoot() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/child/index.html?platform=android",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
}

View File

@@ -10,6 +10,8 @@ import ai.openclaw.app.protocol.OpenClawLocationCommand
import ai.openclaw.app.protocol.OpenClawMotionCommand
import ai.openclaw.app.protocol.OpenClawSmsCommand
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.isLoopbackGatewayHost
import ai.openclaw.app.gateway.isPrivateLanGatewayHost
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -69,7 +71,7 @@ class ConnectionManagerTest {
@Test
fun resolveTlsParamsForEndpoint_manualRespectsManualTlsToggle() {
val endpoint = GatewayEndpoint.manual(host = "example.com", port = 443)
val endpoint = GatewayEndpoint.manual(host = "127.0.0.1", port = 443)
val off =
ConnectionManager.resolveTlsParamsForEndpoint(
@@ -89,6 +91,278 @@ class ConnectionManagerTest {
assertEquals(false, on?.allowTOFU)
}
@Test
fun resolveTlsParamsForEndpoint_manualNonLoopbackForcesTlsWhenToggleIsOff() {
val endpoint = GatewayEndpoint.manual(host = "example.com", port = 443)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
}
@Test
fun resolveTlsParamsForEndpoint_manualPrivateLanCanStayCleartextWhenToggleIsOff() {
val endpoint = GatewayEndpoint.manual(host = "192.168.1.20", port = 18789)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryTailnetWithoutHintsStillRequiresTls() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "100.64.0.9",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryPrivateLanWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "192.168.1.20",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryLoopbackWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "127.0.0.1",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryLocalhostWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "localhost",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryAndroidEmulatorWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "10.0.2.2",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun isLoopbackGatewayHost_onlyTreatsEmulatorBridgeAsLocalWhenAllowed() {
assertTrue(isLoopbackGatewayHost("10.0.2.2", allowEmulatorBridgeAlias = true))
assertFalse(isLoopbackGatewayHost("10.0.2.2", allowEmulatorBridgeAlias = false))
}
@Test
fun isPrivateLanGatewayHost_acceptsLanHostsButRejectsTailnetHosts() {
assertTrue(isPrivateLanGatewayHost("192.168.1.20"))
assertTrue(isPrivateLanGatewayHost("gateway.local"))
assertFalse(isPrivateLanGatewayHost("100.64.0.9"))
assertFalse(isPrivateLanGatewayHost("gateway.tailnet.ts.net"))
}
@Test
fun resolveTlsParamsForEndpoint_discoveryIpv6LoopbackWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "::1",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryMappedIpv4LoopbackWithoutHintsCanStayCleartext() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "::ffff:127.0.0.1",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertNull(params)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryNonLoopbackIpv6WithoutHintsRequiresTls() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "2001:db8::1",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryUnspecifiedIpv4WithoutHintsRequiresTls() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "0.0.0.0",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
}
@Test
fun resolveTlsParamsForEndpoint_discoveryUnspecifiedIpv6WithoutHintsRequiresTls() {
val endpoint =
GatewayEndpoint(
stableId = "_openclaw-gw._tcp.|local.|Test",
name = "Test",
host = "::",
port = 18789,
tlsEnabled = false,
tlsFingerprintSha256 = null,
)
val params =
ConnectionManager.resolveTlsParamsForEndpoint(
endpoint,
storedFingerprint = null,
manualTlsEnabled = false,
)
assertEquals(true, params?.required)
assertNull(params?.expectedFingerprint)
assertEquals(false, params?.allowTOFU)
}
@Test
fun buildNodeConnectOptions_advertisesRequestableSmsSearchWithoutSmsCapability() {
val options =

View File

@@ -0,0 +1,50 @@
package ai.openclaw.app.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class CanvasA2UIActionBridgeTest {
@Test
fun forwardsTrimmedPayloadFromTrustedPage() {
val forwarded = mutableListOf<String>()
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { true },
onMessage = { forwarded += it },
)
bridge.postMessage(" {\"ok\":true} ")
assertEquals(listOf("{\"ok\":true}"), forwarded)
}
@Test
fun rejectsPayloadFromUntrustedPage() {
val forwarded = mutableListOf<String>()
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { false },
onMessage = { forwarded += it },
)
bridge.postMessage("{\"ok\":true}")
assertTrue(forwarded.isEmpty())
}
@Test
fun rejectsBlankPayloadBeforeForwarding() {
val forwarded = mutableListOf<String>()
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { true },
onMessage = { forwarded += it },
)
bridge.postMessage(" ")
bridge.postMessage(null)
assertTrue(forwarded.isEmpty())
}
}

View File

@@ -25,18 +25,17 @@ class GatewayConfigResolverTest {
}
@Test
fun parseGatewayEndpointUsesDefaultCleartextPortForBareWsUrls() {
fun parseGatewayEndpointRejectsNonLoopbackCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://gateway.example")
assertEquals(
GatewayEndpointConfig(
host = "gateway.example",
port = 18789,
tls = false,
displayUrl = "http://gateway.example:18789",
),
parsed,
)
assertNull(parsed)
}
@Test
fun parseGatewayEndpointRejectsTailnetCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://100.64.0.9:18789")
assertNull(parsed)
}
@Test
@@ -55,30 +54,158 @@ class GatewayConfigResolverTest {
}
@Test
fun parseGatewayEndpointKeepsExplicitNonDefaultPortInDisplayUrl() {
val parsed = parseGatewayEndpoint("http://gateway.example:8080")
fun parseGatewayEndpointAllowsLoopbackCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://127.0.0.1")
assertEquals(
GatewayEndpointConfig(
host = "gateway.example",
port = 8080,
host = "127.0.0.1",
port = 18789,
tls = false,
displayUrl = "http://gateway.example:8080",
displayUrl = "http://127.0.0.1:18789",
),
parsed,
)
}
@Test
fun parseGatewayEndpointKeepsExplicitCleartextPort80InDisplayUrl() {
val parsed = parseGatewayEndpoint("http://gateway.example:80")
fun parseGatewayEndpointAllowsLocalhostCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://localhost:18789")
assertEquals(
GatewayEndpointConfig(
host = "gateway.example",
host = "localhost",
port = 18789,
tls = false,
displayUrl = "http://localhost:18789",
),
parsed,
)
}
@Test
fun parseGatewayEndpointAllowsAndroidEmulatorCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://10.0.2.2:18789")
assertEquals(
GatewayEndpointConfig(
host = "10.0.2.2",
port = 18789,
tls = false,
displayUrl = "http://10.0.2.2:18789",
),
parsed,
)
}
@Test
fun parseGatewayEndpointAllowsPrivateLanCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://192.168.1.20:18789")
assertEquals(
GatewayEndpointConfig(
host = "192.168.1.20",
port = 18789,
tls = false,
displayUrl = "http://192.168.1.20:18789",
),
parsed,
)
}
@Test
fun parseGatewayEndpointAllowsMdnsCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://gateway.local:18789")
assertEquals(
GatewayEndpointConfig(
host = "gateway.local",
port = 18789,
tls = false,
displayUrl = "http://gateway.local:18789",
),
parsed,
)
}
@Test
fun parseGatewayEndpointAllowsIpv6LoopbackCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://[::1]")
assertEquals("::1", parsed?.host)
assertEquals(18789, parsed?.port)
assertEquals(false, parsed?.tls)
assertEquals("http://[::1]:18789", parsed?.displayUrl)
}
@Test
fun parseGatewayEndpointAllowsIpv4MappedIpv6LoopbackCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://[::ffff:127.0.0.1]")
assertEquals("::ffff:127.0.0.1", parsed?.host)
assertEquals(18789, parsed?.port)
assertEquals(false, parsed?.tls)
assertEquals("http://[::ffff:127.0.0.1]:18789", parsed?.displayUrl)
}
@Test
fun parseGatewayEndpointRejectsCleartextLoopbackPrefixBypassHost() {
val parsed = parseGatewayEndpoint("http://127.attacker.example:80")
assertNull(parsed)
}
@Test
fun parseGatewayEndpointRejectsNonLoopbackIpv6CleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://[2001:db8::1]")
assertNull(parsed)
}
@Test
fun parseGatewayEndpointAllowsLinkLocalIpv6ZoneCleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://[fe80::1%25eth0]")
assertEquals("fe80::1%25eth0", parsed?.host)
assertEquals(18789, parsed?.port)
assertEquals(false, parsed?.tls)
assertEquals("http://[fe80::1%25eth0]:18789", parsed?.displayUrl)
}
@Test
fun parseGatewayEndpointAllowsSecureIpv6ZoneUrls() {
val parsed = parseGatewayEndpoint("wss://[fe80::1%25wlan0]:443")
assertEquals("fe80::1%25wlan0", parsed?.host)
assertEquals(443, parsed?.port)
assertEquals(true, parsed?.tls)
assertEquals("https://[fe80::1%25wlan0]", parsed?.displayUrl)
}
@Test
fun parseGatewayEndpointRejectsUnspecifiedIpv4CleartextHttpUrls() {
val parsed = parseGatewayEndpoint("http://0.0.0.0:80")
assertNull(parsed)
}
@Test
fun parseGatewayEndpointRejectsUnspecifiedIpv6CleartextWsUrls() {
val parsed = parseGatewayEndpoint("ws://[::]")
assertNull(parsed)
}
@Test
fun parseGatewayEndpointAllowsLoopbackCleartextHttpUrls() {
val parsed = parseGatewayEndpoint("http://localhost:80")
assertEquals(
GatewayEndpointConfig(
host = "localhost",
port = 80,
tls = false,
displayUrl = "http://gateway.example:80",
displayUrl = "http://localhost:80",
),
parsed,
)
@@ -133,6 +260,51 @@ class GatewayConfigResolverTest {
assertNull(resolved)
}
@Test
fun resolveScannedSetupCodeRejectsNonLoopbackCleartextGateway() {
val setupCode =
encodeSetupCode("""{"url":"ws://attacker.example:18789","bootstrapToken":"bootstrap-1"}""")
val resolved = resolveScannedSetupCode(setupCode)
assertNull(resolved)
}
@Test
fun resolveScannedSetupCodeResultFlagsInsecureRemoteGateway() {
val setupCode =
encodeSetupCode("""{"url":"ws://attacker.example:18789","bootstrapToken":"bootstrap-1"}""")
val resolved = resolveScannedSetupCodeResult(setupCode)
assertNull(resolved.setupCode)
assertEquals(GatewayEndpointValidationError.INSECURE_REMOTE_URL, resolved.error)
}
@Test
fun parseGatewayEndpointResultFlagsInsecureRemoteGateway() {
val parsed = parseGatewayEndpointResult("ws://gateway.example:18789")
assertNull(parsed.config)
assertEquals(GatewayEndpointValidationError.INSECURE_REMOTE_URL, parsed.error)
}
@Test
fun parseGatewayEndpointResultAcceptsLanCleartextGateway() {
val parsed = parseGatewayEndpointResult("ws://192.168.1.20:18789")
assertEquals(
GatewayEndpointConfig(
host = "192.168.1.20",
port = 18789,
tls = false,
displayUrl = "http://192.168.1.20:18789",
),
parsed.config,
)
assertNull(parsed.error)
}
@Test
fun decodeGatewaySetupCodeParsesBootstrapToken() {
val setupCode =
@@ -208,10 +380,10 @@ class GatewayConfigResolverTest {
resolveGatewayConnectConfig(
useSetupCode = false,
setupCode = "",
savedManualHost = "192.168.31.100",
savedManualHost = "127.0.0.1",
savedManualPort = "18789",
savedManualTls = false,
manualHostInput = "192.168.31.100",
manualHostInput = "127.0.0.1",
manualPortInput = "18789",
manualTlsInput = false,
fallbackBootstrapToken = "bootstrap-1",
@@ -219,7 +391,7 @@ class GatewayConfigResolverTest {
fallbackPassword = "",
)
assertEquals("192.168.31.100", resolved?.host)
assertEquals("127.0.0.1", resolved?.host)
assertEquals(18789, resolved?.port)
assertEquals(false, resolved?.tls)
assertEquals("bootstrap-1", resolved?.bootstrapToken)
@@ -233,10 +405,10 @@ class GatewayConfigResolverTest {
resolveGatewayConnectConfig(
useSetupCode = false,
setupCode = "",
savedManualHost = "192.168.31.100",
savedManualHost = "127.0.0.1",
savedManualPort = "18789",
savedManualTls = false,
manualHostInput = "192.168.31.100",
manualHostInput = "127.0.0.1",
manualPortInput = "18789",
manualTlsInput = false,
fallbackBootstrapToken = "bootstrap-1",
@@ -255,10 +427,10 @@ class GatewayConfigResolverTest {
resolveGatewayConnectConfig(
useSetupCode = false,
setupCode = "",
savedManualHost = "192.168.31.100",
savedManualHost = "127.0.0.1",
savedManualPort = "18789",
savedManualTls = false,
manualHostInput = "192.168.31.101",
manualHostInput = "127.0.0.2",
manualPortInput = "18789",
manualTlsInput = false,
fallbackBootstrapToken = "bootstrap-1",
@@ -267,7 +439,29 @@ class GatewayConfigResolverTest {
)
assertEquals("", resolved?.bootstrapToken)
assertEquals("192.168.31.101", resolved?.host)
assertEquals("127.0.0.2", resolved?.host)
}
@Test
fun resolveGatewayConnectConfigAllowsPrivateLanManualCleartextEndpoint() {
val resolved =
resolveGatewayConnectConfig(
useSetupCode = false,
setupCode = "",
savedManualHost = "",
savedManualPort = "",
savedManualTls = false,
manualHostInput = "192.168.31.100",
manualPortInput = "18789",
manualTlsInput = false,
fallbackBootstrapToken = "bootstrap-1",
fallbackToken = "",
fallbackPassword = "",
)
assertEquals("192.168.31.100", resolved?.host)
assertEquals(18789, resolved?.port)
assertEquals(false, resolved?.tls)
}
private fun encodeSetupCode(payloadJson: String): String {

View File

@@ -0,0 +1,72 @@
package ai.openclaw.app.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlinx.coroutines.runBlocking
class ChatSheetContentTest {
@Test
fun resolvesPendingAssistantAutoSendOnlyWhenChatIsReady() {
assertNull(
resolvePendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = false,
pendingRunCount = 0,
),
)
assertNull(
resolvePendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = true,
pendingRunCount = 1,
),
)
assertEquals(
"summarize mail",
resolvePendingAssistantAutoSend(
pendingPrompt = " summarize mail ",
healthOk = true,
pendingRunCount = 0,
),
)
}
@Test
fun keepsPendingAssistantAutoSendWhenDispatchRejected() = runBlocking {
var dispatchedPrompt: String? = null
val consumed =
dispatchPendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = true,
pendingRunCount = 0,
) { prompt ->
dispatchedPrompt = prompt
false
}
assertFalse(consumed)
assertEquals("summarize mail", dispatchedPrompt)
}
@Test
fun clearsPendingAssistantAutoSendOnlyAfterAcceptedDispatch() = runBlocking {
var dispatchedPrompt: String? = null
val consumed =
dispatchPendingAssistantAutoSend(
pendingPrompt = "summarize mail",
healthOk = true,
pendingRunCount = 0,
) { prompt ->
dispatchedPrompt = prompt
true
}
assertTrue(consumed)
assertEquals("summarize mail", dispatchedPrompt)
}
}

View File

@@ -0,0 +1,44 @@
package ai.openclaw.app.voice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class TalkAudioPlayerTest {
@Test
fun resolvesPcmPlaybackFromOutputFormat() {
val mode =
TalkAudioPlayer.resolvePlaybackMode(
outputFormat = "pcm_24000",
mimeType = null,
fileExtension = null,
)
assertEquals(TalkPlaybackMode.Pcm(sampleRate = 24_000), mode)
}
@Test
fun resolvesCompressedPlaybackFromMimeType() {
val mode =
TalkAudioPlayer.resolvePlaybackMode(
outputFormat = null,
mimeType = "audio/mpeg",
fileExtension = null,
)
assertEquals(TalkPlaybackMode.Compressed(fileExtension = ".mp3"), mode)
}
@Test
fun preservesProvidedExtensionForCompressedPlayback() {
val mode =
TalkAudioPlayer.resolvePlaybackMode(
outputFormat = null,
mimeType = "audio/webm",
fileExtension = "webm",
)
assertTrue(mode is TalkPlaybackMode.Compressed)
assertEquals(".webm", (mode as TalkPlaybackMode.Compressed).fileExtension)
}
}

View File

@@ -0,0 +1,97 @@
package ai.openclaw.app.voice
import ai.openclaw.app.gateway.DeviceAuthEntry
import ai.openclaw.app.gateway.DeviceAuthTokenStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class TalkModeManagerTest {
@Test
fun stopTtsCancelsTrackedPlaybackJob() {
val manager = createManager()
val playbackJob = Job()
setPrivateField(manager, "ttsJob", playbackJob)
playbackGeneration(manager).set(7L)
manager.stopTts()
assertTrue(playbackJob.isCancelled)
assertEquals(8L, playbackGeneration(manager).get())
}
@Test
fun disablingPlaybackCancelsTrackedJobOnce() {
val manager = createManager()
val playbackJob = Job()
setPrivateField(manager, "ttsJob", playbackJob)
playbackGeneration(manager).set(11L)
manager.setPlaybackEnabled(false)
manager.setPlaybackEnabled(false)
assertTrue(playbackJob.isCancelled)
assertEquals(12L, playbackGeneration(manager).get())
}
private fun createManager(): TalkModeManager {
val app = RuntimeEnvironment.getApplication()
val sessionJob = SupervisorJob()
val session =
GatewaySession(
scope = CoroutineScope(sessionJob + Dispatchers.Default),
identityStore = DeviceIdentityStore(app),
deviceAuthStore = InMemoryDeviceAuthStore(),
onConnected = { _, _, _ -> },
onDisconnected = {},
onEvent = { _, _ -> },
)
return TalkModeManager(
context = app,
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
session = session,
supportsChatSubscribe = false,
isConnected = { true },
)
}
@Suppress("UNCHECKED_CAST")
private fun playbackGeneration(manager: TalkModeManager): AtomicLong {
return readPrivateField(manager, "playbackGeneration") as AtomicLong
}
private fun setPrivateField(target: Any, name: String, value: Any?) {
val field = target.javaClass.getDeclaredField(name)
field.isAccessible = true
field.set(target, value)
}
private fun readPrivateField(target: Any, name: String): Any? {
val field = target.javaClass.getDeclaredField(name)
field.isAccessible = true
return field.get(target)
}
}
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? = null
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) = Unit
override fun clearToken(deviceId: String, role: String) = Unit
}

View File

@@ -0,0 +1,128 @@
package ai.openclaw.app.voice
import ai.openclaw.app.gateway.GatewayConnectErrorDetails
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class TalkSpeakClientTest {
@Test
fun buildsRequestFromDirective() {
val request =
TalkSpeakRequest.from(
text = "Hello from talk mode.",
directive =
TalkDirective(
voiceId = "voice-123",
modelId = "model-abc",
speed = 1.1,
rateWpm = 190,
stability = 0.5,
similarity = 0.7,
style = 0.2,
speakerBoost = true,
seed = 42,
normalize = "auto",
language = "en",
outputFormat = "pcm_24000",
latencyTier = 3,
once = true,
),
)
assertEquals("Hello from talk mode.", request.text)
assertEquals("voice-123", request.voiceId)
assertEquals("model-abc", request.modelId)
assertEquals(1.1, request.speed)
assertEquals(190, request.rateWpm)
assertEquals(0.5, request.stability)
assertEquals(0.7, request.similarity)
assertEquals(0.2, request.style)
assertEquals(true, request.speakerBoost)
assertEquals(42L, request.seed)
assertEquals("auto", request.normalize)
assertEquals("en", request.language)
assertEquals("pcm_24000", request.outputFormat)
assertEquals(3, request.latencyTier)
}
@Test
fun fallsBackOnlyForUnavailableReasons() = runTest {
val client =
TalkSpeakClient(
requestDetailed = { _, _, _ ->
GatewaySession.RpcResult(
ok = false,
payloadJson = null,
error =
GatewaySession.ErrorShape(
code = "UNAVAILABLE",
message = "talk unavailable",
details =
GatewayConnectErrorDetails(
code = null,
canRetryWithDeviceToken = false,
recommendedNextStep = null,
reason = "talk_unconfigured",
),
),
)
},
)
val result = client.synthesize(text = "Hello", directive = null)
assertTrue(result is TalkSpeakResult.FallbackToLocal)
}
@Test
fun doesNotFallBackForSynthesisFailure() = runTest {
val client =
TalkSpeakClient(
requestDetailed = { _, _, _ ->
GatewaySession.RpcResult(
ok = false,
payloadJson = null,
error =
GatewaySession.ErrorShape(
code = "UNAVAILABLE",
message = "provider failed",
details =
GatewayConnectErrorDetails(
code = null,
canRetryWithDeviceToken = false,
recommendedNextStep = null,
reason = "synthesis_failed",
),
),
)
},
)
val result = client.synthesize(text = "Hello", directive = null)
assertTrue(result is TalkSpeakResult.Failure)
}
@Test
fun fallsBackWhenGatewayOmitsReason() = runTest {
val client =
TalkSpeakClient(
requestDetailed = { _, _, _ ->
GatewaySession.RpcResult(
ok = false,
payloadJson = null,
error =
GatewaySession.ErrorShape(
code = "INVALID_REQUEST",
message = "unknown method: talk.speak",
details = null,
),
)
},
)
val result = client.synthesize(text = "Hello", directive = null)
assertTrue(result is TalkSpeakResult.FallbackToLocal)
}
}

View File

@@ -1,8 +1,8 @@
// Shared iOS version defaults.
// Generated overrides live in build/Version.xcconfig (git-ignored).
OPENCLAW_GATEWAY_VERSION = 2026.4.2
OPENCLAW_MARKETING_VERSION = 2026.4.2
OPENCLAW_BUILD_VERSION = 2026040101
OPENCLAW_GATEWAY_VERSION = 2026.4.4
OPENCLAW_MARKETING_VERSION = 2026.4.4
OPENCLAW_BUILD_VERSION = 2026040401
#include? "../build/Version.xcconfig"

View File

@@ -33,6 +33,19 @@ extension NodeAppModel {
return base.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=ios"
}
/// Normalize a URL string for trust comparison: lowercase scheme/host and strip fragment.
/// This matches the normalization applied by ScreenController.isTrustedCanvasUIURL so that
/// SPA hash-routing fragments and scheme/host casing do not silently prevent trust being set.
static func normalizeURLForTrustComparison(_ raw: String) -> String {
guard let url = URL(string: raw),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
else { return raw }
components.fragment = nil
components.scheme = components.scheme?.lowercased()
components.host = components.host?.lowercased()
return components.url?.absoluteString ?? raw
}
func showA2UIOnConnectIfNeeded() async {
await MainActor.run {
// Keep the bundled home canvas as the default connected view.
@@ -46,7 +59,7 @@ extension NodeAppModel {
guard let initialUrl = await self.resolveA2UIHostURLWithCapabilityRefresh() else {
return .hostNotConfigured
}
self.screen.navigate(to: initialUrl)
self.screen.navigate(to: initialUrl, trustA2UIActions: true)
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
return .ready(initialUrl)
}
@@ -54,7 +67,7 @@ extension NodeAppModel {
// First render can fail when scoped capability rotates between reconnects.
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return .hostUnavailable }
guard let refreshedUrl = await self.resolveA2UIHostURL() else { return .hostUnavailable }
self.screen.navigate(to: refreshedUrl)
self.screen.navigate(to: refreshedUrl, trustA2UIActions: true)
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
return .ready(refreshedUrl)
}

View File

@@ -851,7 +851,8 @@ final class NodeAppModel {
if url.isEmpty {
self.screen.showDefaultCanvas()
} else {
self.screen.navigate(to: url)
let trustedA2UIURL = await self.resolveA2UIHostURL()
self.screen.navigate(to: url, trustA2UIActions: trustedA2UIURL == Self.normalizeURLForTrustComparison(url))
}
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.hide.rawValue:
@@ -859,7 +860,9 @@ final class NodeAppModel {
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(OpenClawCanvasNavigateParams.self, from: req.paramsJSON)
self.screen.navigate(to: params.url)
let trimmedURL = params.url.trimmingCharacters(in: .whitespacesAndNewlines)
let trustedA2UIURL = await self.resolveA2UIHostURL()
self.screen.navigate(to: trimmedURL, trustA2UIActions: trustedA2UIURL == Self.normalizeURLForTrustComparison(trimmedURL))
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(OpenClawCanvasEvalParams.self, from: req.paramsJSON)
@@ -1813,7 +1816,7 @@ private extension NodeAppModel {
return DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role) != nil
}
static func shouldStartOperatorGatewayLoop(
nonisolated static func shouldStartOperatorGatewayLoop(
token: String?,
bootstrapToken: String?,
password: String?,
@@ -1834,7 +1837,7 @@ private extension NodeAppModel {
return hasStoredOperatorToken
}
static func clearingBootstrapToken(in config: GatewayConnectConfig?) -> GatewayConnectConfig? {
nonisolated static func clearingBootstrapToken(in config: GatewayConnectConfig?) -> GatewayConnectConfig? {
guard let config else { return nil }
let trimmedBootstrapToken = config.bootstrapToken?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
@@ -1875,6 +1878,36 @@ private extension NodeAppModel {
GatewaySettingsStore.clearGatewayBootstrapToken(instanceId: trimmedInstanceId)
}
private func handleSuccessfulBootstrapGatewayOnboarding(
url: URL,
stableID: String,
token: String?,
password: String?,
nodeOptions: GatewayConnectOptions,
sessionBox: WebSocketSessionBox?) async
{
self.clearPersistedGatewayBootstrapTokenIfNeeded()
if self.operatorGatewayTask == nil && self.shouldStartOperatorGatewayLoop(
token: token,
bootstrapToken: nil,
password: password,
stableID: stableID)
{
self.startOperatorGatewayLoop(
url: url,
stableID: stableID,
token: token,
bootstrapToken: nil,
password: password,
nodeOptions: nodeOptions,
sessionBox: sessionBox)
}
// QR bootstrap onboarding should surface the system notification permission
// prompt immediately so visible APNs alerts work without a second manual step.
_ = await self.requestNotificationAuthorizationIfNeeded()
}
func refreshBackgroundReconnectSuppressionIfNeeded(source: String) {
guard self.isBackgrounded else { return }
guard !self.backgroundReconnectSuppressed else { return }
@@ -1924,17 +1957,20 @@ private extension NodeAppModel {
continue
}
let reconnectAuth = self.currentGatewayReconnectAuth(
fallbackToken: token,
fallbackBootstrapToken: bootstrapToken,
fallbackPassword: password)
let effectiveClientId =
GatewaySettingsStore.loadGatewayClientIdOverride(stableID: stableID) ?? nodeOptions.clientId
let operatorOptions = self.makeOperatorConnectOptions(
clientId: effectiveClientId,
displayName: nodeOptions.clientDisplayName)
displayName: nodeOptions.clientDisplayName,
includeApprovalScope: self.shouldRequestOperatorApprovalScope(
token: reconnectAuth.token,
password: reconnectAuth.password))
do {
let reconnectAuth = self.currentGatewayReconnectAuth(
fallbackToken: token,
fallbackBootstrapToken: bootstrapToken,
fallbackPassword: password)
try await self.operatorGateway.connect(
url: url,
token: reconnectAuth.token,
@@ -2046,13 +2082,14 @@ private extension NodeAppModel {
fallbackToken: token,
fallbackBootstrapToken: bootstrapToken,
fallbackPassword: password)
let connectedOptions = currentOptions
GatewayDiagnostics.log("connect attempt epochMs=\(epochMs) url=\(url.absoluteString)")
try await self.nodeGateway.connect(
url: url,
token: reconnectAuth.token,
bootstrapToken: reconnectAuth.bootstrapToken,
password: reconnectAuth.password,
connectOptions: currentOptions,
connectOptions: connectedOptions,
sessionBox: sessionBox,
onConnected: { [weak self] in
guard let self else { return }
@@ -2068,24 +2105,13 @@ private extension NodeAppModel {
reconnectAuth.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty == false
if usedBootstrapToken {
await MainActor.run {
self.clearPersistedGatewayBootstrapTokenIfNeeded()
if self.operatorGatewayTask == nil && self.shouldStartOperatorGatewayLoop(
token: reconnectAuth.token,
bootstrapToken: nil,
password: reconnectAuth.password,
stableID: stableID)
{
self.startOperatorGatewayLoop(
url: url,
stableID: stableID,
token: reconnectAuth.token,
bootstrapToken: nil,
password: reconnectAuth.password,
nodeOptions: currentOptions,
sessionBox: sessionBox)
}
}
await self.handleSuccessfulBootstrapGatewayOnboarding(
url: url,
stableID: stableID,
token: reconnectAuth.token,
password: reconnectAuth.password,
nodeOptions: connectedOptions,
sessionBox: sessionBox)
}
let relayData = await MainActor.run {
(
@@ -2243,10 +2269,47 @@ private extension NodeAppModel {
}
}
func makeOperatorConnectOptions(clientId: String, displayName: String?) -> GatewayConnectOptions {
GatewayConnectOptions(
func shouldRequestOperatorApprovalScope(token: String?, password: String?) -> Bool {
let identity = DeviceIdentityStore.loadOrCreate()
let storedOperatorScopes = DeviceAuthStore
.loadToken(deviceId: identity.deviceId, role: "operator")?
.scopes ?? []
return Self.shouldRequestOperatorApprovalScope(
token: token,
password: password,
storedOperatorScopes: storedOperatorScopes)
}
nonisolated static func shouldRequestOperatorApprovalScope(
token: String?,
password: String?,
storedOperatorScopes: [String]
) -> Bool {
let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedToken.isEmpty {
return true
}
let trimmedPassword = password?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedPassword.isEmpty {
return true
}
return storedOperatorScopes.contains("operator.approvals")
}
func makeOperatorConnectOptions(
clientId: String,
displayName: String?,
includeApprovalScope: Bool
) -> GatewayConnectOptions {
var scopes = ["operator.read", "operator.write", "operator.talk.secrets"]
// Preserve reconnect compatibility for older paired operator tokens that were
// approved before iOS requested operator.approvals by default.
if includeApprovalScope {
scopes.append("operator.approvals")
}
return GatewayConnectOptions(
role: "operator",
scopes: ["operator.read", "operator.write", "operator.talk.secrets"],
scopes: scopes,
caps: [],
commands: [],
permissions: [:],
@@ -3134,11 +3197,22 @@ extension NodeAppModel {
await self.applyPendingForegroundNodeActions(mapped, trigger: "test")
}
func _test_makeOperatorConnectOptions(
clientId: String,
displayName: String?,
includeApprovalScope: Bool
) -> GatewayConnectOptions {
self.makeOperatorConnectOptions(
clientId: clientId,
displayName: displayName,
includeApprovalScope: includeApprovalScope)
}
static func _test_currentDeepLinkKey() -> String {
self.expectedDeepLinkKey()
}
static func _test_shouldStartOperatorGatewayLoop(
nonisolated static func _test_shouldStartOperatorGatewayLoop(
token: String?,
bootstrapToken: String?,
password: String?,
@@ -3151,6 +3225,41 @@ extension NodeAppModel {
hasStoredOperatorToken: hasStoredOperatorToken)
}
nonisolated static func _test_shouldRequestOperatorApprovalScope(
token: String?,
password: String?,
storedOperatorScopes: [String]
) -> Bool {
self.shouldRequestOperatorApprovalScope(
token: token,
password: password,
storedOperatorScopes: storedOperatorScopes)
}
nonisolated static func _test_clearingBootstrapToken(
in config: GatewayConnectConfig?
) -> GatewayConnectConfig? {
self.clearingBootstrapToken(in: config)
}
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
await self.handleSuccessfulBootstrapGatewayOnboarding(
url: URL(string: "wss://gateway.example")!,
stableID: "test-gateway",
token: nil,
password: nil,
nodeOptions: GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios",
clientMode: "node",
clientDisplayName: nil),
sessionBox: nil)
}
}
#endif
// swiftlint:enable type_body_length file_length

View File

@@ -7,6 +7,7 @@ import WebKit
@Observable
final class ScreenController {
private weak var activeWebView: WKWebView?
private var trustedRemoteA2UIURL: URL?
var urlString: String = ""
var errorText: String?
@@ -26,10 +27,11 @@ final class ScreenController {
self.reload()
}
func navigate(to urlString: String) {
func navigate(to urlString: String, trustA2UIActions: Bool = false) {
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
self.urlString = ""
self.trustedRemoteA2UIURL = nil
self.reload()
return
}
@@ -43,6 +45,7 @@ final class ScreenController {
return
}
self.urlString = (trimmed == "/" ? "" : trimmed)
self.trustedRemoteA2UIURL = trustA2UIActions ? Self.normalizeTrustedRemoteA2UIURL(from: trimmed) : nil
self.reload()
}
@@ -72,6 +75,7 @@ final class ScreenController {
func showDefaultCanvas() {
self.urlString = ""
self.trustedRemoteA2UIURL = nil
self.reload()
}
@@ -237,28 +241,17 @@ final class ScreenController {
subdirectory: "CanvasScaffold")
func isTrustedCanvasUIURL(_ url: URL) -> Bool {
guard url.isFileURL else { return false }
let std = url.standardizedFileURL
if let expected = Self.canvasScaffoldURL,
std == expected.standardizedFileURL
{
return true
if url.isFileURL {
let std = url.standardizedFileURL
if let expected = Self.canvasScaffoldURL,
std == expected.standardizedFileURL
{
return true
}
return false
}
return false
}
private func applyScrollBehavior() {
guard let webView = self.activeWebView else { return }
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
let allowScroll = !trimmed.isEmpty
let scrollView = webView.scrollView
// Default canvas needs raw touch events; external pages should scroll.
scrollView.isScrollEnabled = allowScroll
scrollView.bounces = allowScroll
}
func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
LocalNetworkURLSupport.isLocalNetworkHTTPURL(url)
guard let trusted = self.trustedRemoteA2UIURL else { return false }
return Self.normalizeTrustedRemoteA2UIURL(from: url) == trusted
}
nonisolated static func parseA2UIActionBody(_ body: Any) -> [String: Any]? {
@@ -278,6 +271,36 @@ final class ScreenController {
}
return nil
}
private func applyScrollBehavior() {
guard let webView = self.activeWebView else { return }
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
let allowScroll = !trimmed.isEmpty
let scrollView = webView.scrollView
// Default canvas needs raw touch events; external pages should scroll.
scrollView.isScrollEnabled = allowScroll
scrollView.bounces = allowScroll
}
private static func normalizeTrustedRemoteA2UIURL(from raw: String) -> URL? {
guard let url = URL(string: raw) else { return nil }
return self.normalizeTrustedRemoteA2UIURL(from: url)
}
private static func normalizeTrustedRemoteA2UIURL(from url: URL) -> URL? {
guard !url.isFileURL else { return nil }
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
return nil
}
guard let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty else {
return nil
}
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.scheme = scheme
components?.host = host.lowercased()
components?.fragment = nil
return components?.url
}
}
extension Double {

View File

@@ -180,12 +180,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
guard let controller else { return }
guard let url = message.webView?.url else { return }
if url.isFileURL {
guard controller.isTrustedCanvasUIURL(url) else { return }
} else {
// For security, only accept actions from local-network pages (e.g. the canvas host).
guard controller.isLocalNetworkCanvasURL(url) else { return }
}
guard controller.isTrustedCanvasUIURL(url) else { return }
guard let body = ScreenController.parseA2UIActionBody(message.body) else { return }

View File

@@ -70,6 +70,52 @@ import UIKit
}
}
@Test @MainActor func operatorConnectOptionsOnlyRequestApprovalScopeWhenEnabled() {
let appModel = NodeAppModel()
let withoutApprovalScope = appModel._test_makeOperatorConnectOptions(
clientId: "openclaw-ios",
displayName: "OpenClaw iOS",
includeApprovalScope: false)
let withApprovalScope = appModel._test_makeOperatorConnectOptions(
clientId: "openclaw-ios",
displayName: "OpenClaw iOS",
includeApprovalScope: true)
#expect(withoutApprovalScope.role == "operator")
#expect(withoutApprovalScope.scopes.contains("operator.read"))
#expect(withoutApprovalScope.scopes.contains("operator.write"))
#expect(!withoutApprovalScope.scopes.contains("operator.approvals"))
#expect(withoutApprovalScope.scopes.contains("operator.talk.secrets"))
#expect(withApprovalScope.scopes.contains("operator.approvals"))
}
@Test func operatorApprovalScopeRequestsStayBackwardCompatible() {
#expect(
!NodeAppModel._test_shouldRequestOperatorApprovalScope(
token: nil,
password: nil,
storedOperatorScopes: ["operator.read", "operator.write", "operator.talk.secrets"])
)
#expect(
NodeAppModel._test_shouldRequestOperatorApprovalScope(
token: nil,
password: nil,
storedOperatorScopes: [
"operator.approvals",
"operator.read",
"operator.write",
"operator.talk.secrets",
])
)
#expect(
NodeAppModel._test_shouldRequestOperatorApprovalScope(
token: "shared-token",
password: nil,
storedOperatorScopes: [])
)
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {

View File

@@ -2,6 +2,7 @@ import OpenClawKit
import Foundation
import Testing
import UIKit
import UserNotifications
@testable import OpenClaw
private func makeAgentDeepLinkURL(
@@ -68,6 +69,28 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
}
}
private final class MockBootstrapNotificationCenter: NotificationCentering, @unchecked Sendable {
var status: NotificationAuthorizationStatus = .notDetermined
var requestAuthorizationResult = false
var requestAuthorizationCalls = 0
func authorizationStatus() async -> NotificationAuthorizationStatus {
self.status
}
func requestAuthorization(options _: UNAuthorizationOptions) async throws -> Bool {
self.requestAuthorizationCalls += 1
if self.requestAuthorizationResult {
self.status = .authorized
} else {
self.status = .denied
}
return self.requestAuthorizationResult
}
func add(_: UNNotificationRequest) async throws {}
}
@Suite(.serialized) struct NodeAppModelInvokeTests {
@Test @MainActor func decodeParamsFailsWithoutJSON() {
#expect(throws: Error.self) {
@@ -127,6 +150,15 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
)
}
@Test @MainActor func successfulBootstrapOnboardingRequestsNotificationAuthorization() async {
let center = MockBootstrapNotificationCenter()
let appModel = NodeAppModel(notificationCenter: center)
await appModel._test_handleSuccessfulBootstrapGatewayOnboarding()
#expect(center.requestAuthorizationCalls == 1)
}
@Test func clearingBootstrapTokenStripsReconnectConfigEvenWithoutPersistence() {
let config = GatewayConnectConfig(
url: URL(string: "wss://gateway.example")!,
@@ -145,7 +177,7 @@ private final class MockWatchMessagingService: @preconcurrency WatchMessagingSer
clientMode: "node",
clientDisplayName: nil))
let cleared = NodeAppModel.clearingBootstrapToken(in: config)
let cleared = NodeAppModel._test_clearingBootstrapToken(in: config)
#expect(cleared?.bootstrapToken == nil)
#expect(cleared?.url == config.url)
#expect(cleared?.stableID == config.stableID)

View File

@@ -66,17 +66,26 @@ private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoo
}
}
@Test @MainActor func localNetworkCanvasURLsAreAllowed() {
@Test @MainActor func trustedRemoteA2UIURLMustMatchExactly() {
let screen = ScreenController()
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://localhost:18789/")!) == true)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://openclaw.local:18789/")!) == true)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://peters-mac-studio-1:18789/")!) == true)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "https://peters-mac-studio-1.ts.net:18789/")!) == true)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://192.168.0.10:18789/")!) == true)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://10.0.0.10:18789/")!) == true)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://100.123.224.76:18789/")!) == true) // Tailscale CGNAT
#expect(screen.isLocalNetworkCanvasURL(URL(string: "https://example.com/")!) == false)
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://8.8.8.8/")!) == false)
let trusted = "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios"
screen.navigate(to: trusted, trustA2UIActions: true)
#expect(screen.isTrustedCanvasUIURL(URL(string: trusted)!) == true)
// Fragment differences must not affect trust (SPA hash routing).
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios#step2")!) == true)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=android")!) == false)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/canvas/")!) == false)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://evil.ts.net:18789/__openclaw__/a2ui/?platform=ios")!) == false)
#expect(screen.isTrustedCanvasUIURL(URL(string: "http://192.168.0.10:18789/")!) == false)
}
@Test @MainActor func genericNavigationClearsTrustedRemoteA2UIURL() {
let screen = ScreenController()
screen.navigate(to: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios", trustA2UIActions: true)
screen.navigate(to: "https://evil.ts.net:18789/")
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios")!) == false)
}
@Test func parseA2UIActionBodyAcceptsJSONString() throws {

View File

@@ -1,5 +1,5 @@
{
"originHash" : "1c9c9d251b760ed3234ecff741a88eb4bf42315ad6f50ac7392b187cf226c16c",
"originHash" : "fb90e7b1977f43661ac91681d16da11f9ddd85630407ef170eaada0a6ee39972",
"pins" : [
{
"identity" : "axorcist",
@@ -24,7 +24,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
"revision" : "7e3c948d8340abe3977014f3de020edf221e9269",
"version" : "0.1.0"
}
},

View File

@@ -110,6 +110,7 @@ enum HostEnvSecurityPolicy {
"PIP_TRUSTED_HOST",
"UV_INDEX",
"UV_INDEX_URL",
"UV_PYTHON",
"UV_EXTRA_INDEX_URL",
"UV_DEFAULT_INDEX",
"DOCKER_HOST",

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.2</string>
<string>2026.4.4</string>
<key>CFBundleVersion</key>
<string>2026040101</string>
<string>2026040401</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -43,8 +43,8 @@ enum WideAreaGatewayDiscovery {
guard let statusJson = context.tailscaleStatus(),
!collectTailnetIPv4s(statusJson: statusJson).isEmpty,
let discovery = loadWideAreaPtrRecords(
remaining: remaining,
dig: context.dig)
remaining: remaining,
dig: context.dig)
else { return [] }
let domainTrimmed = discovery.domainTrimmed

View File

@@ -2019,6 +2019,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let modelid: String?
public let outputformat: String?
public let speed: Double?
public let ratewpm: Int?
public let stability: Double?
public let similarity: Double?
public let style: Double?
@@ -2026,6 +2027,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let seed: Int?
public let normalize: String?
public let language: String?
public let latencytier: Int?
public init(
text: String,
@@ -2033,19 +2035,22 @@ public struct TalkSpeakParams: Codable, Sendable {
modelid: String?,
outputformat: String?,
speed: Double?,
ratewpm: Int?,
stability: Double?,
similarity: Double?,
style: Double?,
speakerboost: Bool?,
seed: Int?,
normalize: String?,
language: String?)
language: String?,
latencytier: Int?)
{
self.text = text
self.voiceid = voiceid
self.modelid = modelid
self.outputformat = outputformat
self.speed = speed
self.ratewpm = ratewpm
self.stability = stability
self.similarity = similarity
self.style = style
@@ -2053,6 +2058,7 @@ public struct TalkSpeakParams: Codable, Sendable {
self.seed = seed
self.normalize = normalize
self.language = language
self.latencytier = latencytier
}
private enum CodingKeys: String, CodingKey {
@@ -2061,6 +2067,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case modelid = "modelId"
case outputformat = "outputFormat"
case speed
case ratewpm = "rateWpm"
case stability
case similarity
case style
@@ -2068,6 +2075,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case seed
case normalize
case language
case latencytier = "latencyTier"
}
}
@@ -2893,6 +2901,78 @@ public struct SkillsBinsResult: Codable, Sendable {
}
}
public struct SkillsSearchParams: Codable, Sendable {
public let query: String?
public let limit: Int?
public init(
query: String?,
limit: Int?)
{
self.query = query
self.limit = limit
}
private enum CodingKeys: String, CodingKey {
case query
case limit
}
}
public struct SkillsSearchResult: Codable, Sendable {
public let results: [[String: AnyCodable]]
public init(
results: [[String: AnyCodable]])
{
self.results = results
}
private enum CodingKeys: String, CodingKey {
case results
}
}
public struct SkillsDetailParams: Codable, Sendable {
public let slug: String
public init(
slug: String)
{
self.slug = slug
}
private enum CodingKeys: String, CodingKey {
case slug
}
}
public struct SkillsDetailResult: Codable, Sendable {
public let skill: AnyCodable
public let latestversion: AnyCodable?
public let metadata: AnyCodable?
public let owner: AnyCodable?
public init(
skill: AnyCodable,
latestversion: AnyCodable?,
metadata: AnyCodable?,
owner: AnyCodable?)
{
self.skill = skill
self.latestversion = latestversion
self.metadata = metadata
self.owner = owner
}
private enum CodingKeys: String, CodingKey {
case skill
case latestversion = "latestVersion"
case metadata
case owner
}
}
public struct CronJob: Codable, Sendable {
public let id: String
public let agentid: String?

View File

@@ -542,6 +542,77 @@ public actor GatewayChannelActor {
authSource: authSource)
}
private func shouldPersistBootstrapHandoffTokens() -> Bool {
guard self.lastAuthSource == .bootstrapToken else { return false }
let scheme = self.url.scheme?.lowercased()
if scheme == "wss" {
return true
}
if let host = self.url.host, LoopbackHost.isLoopback(host) {
return true
}
return false
}
private func filteredBootstrapHandoffScopes(role: String, scopes: [String]) -> [String]? {
let normalizedRole = role.trimmingCharacters(in: .whitespacesAndNewlines)
switch normalizedRole {
case "node":
return []
case "operator":
let allowedOperatorScopes: Set<String> = [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
]
return Array(Set(scopes.filter { allowedOperatorScopes.contains($0) })).sorted()
default:
return nil
}
}
private func persistBootstrapHandoffToken(
deviceId: String,
role: String,
token: String,
scopes: [String]
) {
guard let filteredScopes = self.filteredBootstrapHandoffScopes(role: role, scopes: scopes) else {
return
}
_ = DeviceAuthStore.storeToken(
deviceId: deviceId,
role: role,
token: token,
scopes: filteredScopes)
}
private func persistIssuedDeviceToken(
authSource: GatewayAuthSource,
deviceId: String,
role: String,
token: String,
scopes: [String]
) {
if authSource == .bootstrapToken {
guard self.shouldPersistBootstrapHandoffTokens() else {
return
}
self.persistBootstrapHandoffToken(
deviceId: deviceId,
role: role,
token: token,
scopes: scopes)
return
}
_ = DeviceAuthStore.storeToken(
deviceId: deviceId,
role: role,
token: token,
scopes: scopes)
}
private func handleConnectResponse(
_ res: ResponseFrame,
identity: DeviceIdentity?,
@@ -572,18 +643,37 @@ public actor GatewayChannelActor {
} else if let tick = ok.policy["tickIntervalMs"]?.value as? Int {
self.tickIntervalMs = Double(tick)
}
if let auth = ok.auth,
let deviceToken = auth["deviceToken"]?.value as? String {
let authRole = auth["role"]?.value as? String ?? role
let scopes = (auth["scopes"]?.value as? [ProtoAnyCodable])?
.compactMap { $0.value as? String } ?? []
if let identity {
_ = DeviceAuthStore.storeToken(
if let auth = ok.auth, let identity {
if let deviceToken = auth["deviceToken"]?.value as? String {
let authRole = auth["role"]?.value as? String ?? role
let scopes = (auth["scopes"]?.value as? [ProtoAnyCodable])?
.compactMap { $0.value as? String } ?? []
self.persistIssuedDeviceToken(
authSource: self.lastAuthSource,
deviceId: identity.deviceId,
role: authRole,
token: deviceToken,
scopes: scopes)
}
if self.shouldPersistBootstrapHandoffTokens(),
let tokenEntries = auth["deviceTokens"]?.value as? [ProtoAnyCodable]
{
for entry in tokenEntries {
guard let rawEntry = entry.value as? [String: ProtoAnyCodable],
let deviceToken = rawEntry["deviceToken"]?.value as? String,
let authRole = rawEntry["role"]?.value as? String
else {
continue
}
let scopes = (rawEntry["scopes"]?.value as? [ProtoAnyCodable])?
.compactMap { $0.value as? String } ?? []
self.persistBootstrapHandoffToken(
deviceId: identity.deviceId,
role: authRole,
token: deviceToken,
scopes: scopes)
}
}
}
self.lastTick = Date()
self.tickTask?.cancel()

View File

@@ -2019,6 +2019,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let modelid: String?
public let outputformat: String?
public let speed: Double?
public let ratewpm: Int?
public let stability: Double?
public let similarity: Double?
public let style: Double?
@@ -2026,6 +2027,7 @@ public struct TalkSpeakParams: Codable, Sendable {
public let seed: Int?
public let normalize: String?
public let language: String?
public let latencytier: Int?
public init(
text: String,
@@ -2033,19 +2035,22 @@ public struct TalkSpeakParams: Codable, Sendable {
modelid: String?,
outputformat: String?,
speed: Double?,
ratewpm: Int?,
stability: Double?,
similarity: Double?,
style: Double?,
speakerboost: Bool?,
seed: Int?,
normalize: String?,
language: String?)
language: String?,
latencytier: Int?)
{
self.text = text
self.voiceid = voiceid
self.modelid = modelid
self.outputformat = outputformat
self.speed = speed
self.ratewpm = ratewpm
self.stability = stability
self.similarity = similarity
self.style = style
@@ -2053,6 +2058,7 @@ public struct TalkSpeakParams: Codable, Sendable {
self.seed = seed
self.normalize = normalize
self.language = language
self.latencytier = latencytier
}
private enum CodingKeys: String, CodingKey {
@@ -2061,6 +2067,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case modelid = "modelId"
case outputformat = "outputFormat"
case speed
case ratewpm = "rateWpm"
case stability
case similarity
case style
@@ -2068,6 +2075,7 @@ public struct TalkSpeakParams: Codable, Sendable {
case seed
case normalize
case language
case latencytier = "latencyTier"
}
}
@@ -2893,6 +2901,78 @@ public struct SkillsBinsResult: Codable, Sendable {
}
}
public struct SkillsSearchParams: Codable, Sendable {
public let query: String?
public let limit: Int?
public init(
query: String?,
limit: Int?)
{
self.query = query
self.limit = limit
}
private enum CodingKeys: String, CodingKey {
case query
case limit
}
}
public struct SkillsSearchResult: Codable, Sendable {
public let results: [[String: AnyCodable]]
public init(
results: [[String: AnyCodable]])
{
self.results = results
}
private enum CodingKeys: String, CodingKey {
case results
}
}
public struct SkillsDetailParams: Codable, Sendable {
public let slug: String
public init(
slug: String)
{
self.slug = slug
}
private enum CodingKeys: String, CodingKey {
case slug
}
}
public struct SkillsDetailResult: Codable, Sendable {
public let skill: AnyCodable
public let latestversion: AnyCodable?
public let metadata: AnyCodable?
public let owner: AnyCodable?
public init(
skill: AnyCodable,
latestversion: AnyCodable?,
metadata: AnyCodable?,
owner: AnyCodable?)
{
self.skill = skill
self.latestversion = latestversion
self.metadata = metadata
self.owner = owner
}
private enum CodingKeys: String, CodingKey {
case skill
case latestversion = "latestVersion"
case metadata
case owner
}
}
public struct CronJob: Codable, Sendable {
public let id: String
public let agentid: String?

View File

@@ -13,6 +13,7 @@ private extension NSLock {
private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Sendable {
private let lock = NSLock()
private let helloAuth: [String: Any]?
private var _state: URLSessionTask.State = .suspended
private var connectRequestId: String?
private var connectAuth: [String: Any]?
@@ -20,6 +21,10 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
private var pendingReceiveHandler:
(@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
init(helloAuth: [String: Any]? = nil) {
self.helloAuth = helloAuth
}
var state: URLSessionTask.State {
get { self.lock.withLock { self._state } }
set { self.lock.withLock { self._state = newValue } }
@@ -79,11 +84,11 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
for _ in 0..<50 {
let id = self.lock.withLock { self.connectRequestId }
if let id {
return .data(Self.connectOkData(id: id))
return .data(Self.connectOkData(id: id, auth: self.helloAuth))
}
try await Task.sleep(nanoseconds: 1_000_000)
}
return .data(Self.connectOkData(id: "connect"))
return .data(Self.connectOkData(id: "connect", auth: self.helloAuth))
}
func receive(
@@ -110,8 +115,8 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
return (try? JSONSerialization.data(withJSONObject: frame)) ?? Data()
}
private static func connectOkData(id: String) -> Data {
let payload: [String: Any] = [
private static func connectOkData(id: String, auth: [String: Any]? = nil) -> Data {
var payload: [String: Any] = [
"type": "hello-ok",
"protocol": 2,
"server": [
@@ -137,6 +142,9 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
"tickIntervalMs": 30_000,
],
]
if let auth {
payload["auth"] = auth
}
let frame: [String: Any] = [
"type": "res",
"id": id,
@@ -149,9 +157,14 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
private final class FakeGatewayWebSocketSession: WebSocketSessioning, @unchecked Sendable {
private let lock = NSLock()
private let helloAuth: [String: Any]?
private var tasks: [FakeGatewayWebSocketTask] = []
private var makeCount = 0
init(helloAuth: [String: Any]? = nil) {
self.helloAuth = helloAuth
}
func snapshotMakeCount() -> Int {
self.lock.withLock { self.makeCount }
}
@@ -164,7 +177,7 @@ private final class FakeGatewayWebSocketSession: WebSocketSessioning, @unchecked
_ = url
return self.lock.withLock {
self.makeCount += 1
let task = FakeGatewayWebSocketTask()
let task = FakeGatewayWebSocketTask(helloAuth: self.helloAuth)
self.tasks.append(task)
return WebSocketTaskBox(task: task)
}
@@ -177,6 +190,7 @@ private actor SeqGapProbe {
func value() -> Bool { self.saw }
}
@Suite(.serialized)
struct GatewayNodeSessionTests {
@Test
func scannedSetupCodePrefersBootstrapAuthOverStoredDeviceToken() async throws {
@@ -234,6 +248,210 @@ struct GatewayNodeSessionTests {
await gateway.disconnect()
}
@Test
func bootstrapHelloStoresAdditionalDeviceTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
defer {
if let previousStateDir {
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
} else {
unsetenv("OPENCLAW_STATE_DIR")
}
try? FileManager.default.removeItem(at: tempDir)
}
let identity = DeviceIdentityStore.loadOrCreate()
let session = FakeGatewayWebSocketSession(helloAuth: [
"deviceToken": "node-device-token",
"role": "node",
"scopes": [],
"issuedAtMs": 1000,
"deviceTokens": [
[
"deviceToken": "operator-device-token",
"role": "operator",
"scopes": [
"node.exec",
"operator.admin",
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.talk.secrets",
"operator.write",
],
"issuedAtMs": 1001,
],
],
])
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "node",
clientDisplayName: "iOS Test",
includeDeviceIdentity: true)
try await gateway.connect(
url: URL(string: "wss://example.invalid")!,
token: nil,
bootstrapToken: "fresh-bootstrap-token",
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
let nodeEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node"))
let operatorEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator"))
#expect(nodeEntry.token == "node-device-token")
#expect(nodeEntry.scopes == [])
#expect(operatorEntry.token == "operator-device-token")
#expect(operatorEntry.scopes == [
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
])
await gateway.disconnect()
}
@Test
func nonBootstrapHelloStoresPrimaryDeviceTokenButNotAdditionalBootstrapTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
defer {
if let previousStateDir {
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
} else {
unsetenv("OPENCLAW_STATE_DIR")
}
try? FileManager.default.removeItem(at: tempDir)
}
let identity = DeviceIdentityStore.loadOrCreate()
let session = FakeGatewayWebSocketSession(helloAuth: [
"deviceToken": "server-node-token",
"role": "node",
"scopes": [],
"deviceTokens": [
[
"deviceToken": "server-operator-token",
"role": "operator",
"scopes": ["operator.admin"],
],
],
])
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "node",
clientDisplayName: "iOS Test",
includeDeviceIdentity: true)
try await gateway.connect(
url: URL(string: "wss://example.invalid")!,
token: "shared-token",
bootstrapToken: nil,
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
let nodeEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node"))
#expect(nodeEntry.token == "server-node-token")
#expect(nodeEntry.scopes == [])
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") == nil)
await gateway.disconnect()
}
@Test
func untrustedBootstrapHelloDoesNotPersistBootstrapHandoffTokens() async throws {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
defer {
if let previousStateDir {
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
} else {
unsetenv("OPENCLAW_STATE_DIR")
}
try? FileManager.default.removeItem(at: tempDir)
}
let identity = DeviceIdentityStore.loadOrCreate()
let session = FakeGatewayWebSocketSession(helloAuth: [
"deviceToken": "untrusted-node-token",
"role": "node",
"scopes": [],
"deviceTokens": [
[
"deviceToken": "untrusted-operator-token",
"role": "operator",
"scopes": [
"operator.approvals",
"operator.read",
],
],
],
])
let gateway = GatewayNodeSession()
let options = GatewayConnectOptions(
role: "node",
scopes: [],
caps: [],
commands: [],
permissions: [:],
clientId: "openclaw-ios-test",
clientMode: "node",
clientDisplayName: "iOS Test",
includeDeviceIdentity: true)
try await gateway.connect(
url: URL(string: "ws://example.invalid")!,
token: nil,
bootstrapToken: "fresh-bootstrap-token",
password: nil,
connectOptions: options,
sessionBox: WebSocketSessionBox(session: session),
onConnected: {},
onDisconnected: { _ in },
onInvoke: { req in
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
})
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node") == nil)
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") == nil)
await gateway.disconnect()
}
@Test
func normalizeCanvasHostUrlPreservesExplicitSecureCanvasPort() {
let normalized = canonicalizeCanvasHostUrl(

View File

@@ -1,6 +1,7 @@
import XCTest
@testable import OpenClawKit
@MainActor
final class TalkSystemSpeechSynthesizerTests: XCTestCase {
func testWatchdogTimeoutDefaultsToLatinProfile() {
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(

View File

@@ -1,12 +1,21 @@
# Generated Docs Artifacts
These baseline artifacts are generated from the repo-owned OpenClaw config schema and bundled channel/plugin metadata.
SHA-256 hash files are the tracked drift-detection artifacts. The full JSON
baselines are generated locally (gitignored) for inspection only.
- Do not edit `config-baseline.json` by hand.
- Do not edit `config-baseline.jsonl` by hand.
- Do not edit `plugin-sdk-api-baseline.json` by hand.
- Do not edit `plugin-sdk-api-baseline.jsonl` by hand.
- Regenerate config baseline artifacts with `pnpm config:docs:gen`.
- Validate config baseline artifacts in CI or locally with `pnpm config:docs:check`.
- Regenerate Plugin SDK API baseline artifacts with `pnpm plugin-sdk:api:gen`.
- Validate Plugin SDK API baseline artifacts in CI or locally with `pnpm plugin-sdk:api:check`.
**Tracked (committed to git):**
- `config-baseline.sha256` hashes of config baseline JSON artifacts.
- `plugin-sdk-api-baseline.sha256` hashes of Plugin SDK API baseline artifacts.
**Local only (gitignored):**
- `config-baseline.json`, `config-baseline.core.json`, `config-baseline.channel.json`, `config-baseline.plugin.json`
- `plugin-sdk-api-baseline.json`, `plugin-sdk-api-baseline.jsonl`
Do not edit any of these files by hand.
- Regenerate config baseline: `pnpm config:docs:gen`
- Validate config baseline: `pnpm config:docs:check`
- Regenerate Plugin SDK API baseline: `pnpm plugin-sdk:api:gen`
- Validate Plugin SDK API baseline: `pnpm plugin-sdk:api:check`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
20a882f9991e17310013471756ac7ec62c272e29490daeede9c0901bd51c0e69 config-baseline.json
8ba6e5c959d5fc3eee9e6c5d1d8f764f164052f4207c0352bb39e2a7dbad64a8 config-baseline.core.json
ca6d1fa8a3507566979ea2da2b88a6a7ae49d650f3ebd3eee14a22ed18e5be89 config-baseline.channel.json
17fd37605bf6cb087932ec2ebcfa9dd22e669fa6b8b93081ab2deac9d24821c5 config-baseline.plugin.json

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
cbffdf76d6a7254d8b2d3a601e1206d7b6c835bc44f170d4038bc711a35ef756 plugin-sdk-api-baseline.json
fe026bf3ba1e3b55f6c0b560d76940f3c301d8f593d6f0f6dcc4625745c76d31 plugin-sdk-api-baseline.jsonl

View File

@@ -47,6 +47,38 @@
"source": "Quick Start",
"target": "快速开始"
},
{
"source": "Chutes",
"target": "Chutes"
},
{
"source": "Qwen",
"target": "Qwen"
},
{
"source": "Feishu",
"target": "Feishu"
},
{
"source": "Mattermost",
"target": "Mattermost"
},
{
"source": "BytePlus (International)",
"target": "BytePlus国际版"
},
{
"source": "Anthropic (API + Claude CLI)",
"target": "AnthropicAPI + Claude CLI"
},
{
"source": "Moonshot AI",
"target": "Moonshot AI"
},
{
"source": "Additional bundled variants",
"target": "其他内置变体"
},
{
"source": "Diffs",
"target": "Diffs"
@@ -143,6 +175,10 @@
"source": "Network model",
"target": "网络模型"
},
{
"source": "Bridge protocol (legacy nodes, historical)",
"target": "Bridge protocol旧版节点历史参考"
},
{
"source": "Doctor",
"target": "Doctor"
@@ -151,6 +187,10 @@
"source": "Polls",
"target": "投票"
},
{
"source": "QQ Bot",
"target": "QQ Bot"
},
{
"source": "Release Policy",
"target": "发布策略"
@@ -199,6 +239,14 @@
"source": "CLI",
"target": "CLI"
},
{
"source": "/cli/gateway",
"target": "/cli/gateway"
},
{
"source": "/gateway#multiple-gateways-same-host",
"target": "/gateway#multiple-gateways-same-host"
},
{
"source": "install sanity",
"target": "安装完整性检查"
@@ -215,6 +263,10 @@
"source": "FAQ",
"target": "常见问题"
},
{
"source": "Ollama Web Search",
"target": "Ollama Web 搜索"
},
{
"source": "onboarding",
"target": "新手引导"

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#111827" aria-hidden="true">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>

After

Width:  |  Height:  |  Size: 829 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#fff" aria-hidden="true">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@@ -17,13 +17,15 @@ This document defines the canonical credential eligibility and resolution semant
The goal is to keep selection-time and runtime behavior aligned.
## Stable Reason Codes
## Stable Probe Reason Codes
- `ok`
- `excluded_by_auth_order`
- `missing_credential`
- `invalid_expires`
- `expired`
- `unresolved_ref`
- `no_model`
## Token Credentials
@@ -44,6 +46,24 @@ Token credentials (`type: "token"`) support inline `token` and/or `tokenRef`.
2. For eligible profiles, token material may be resolved from inline value or `tokenRef`.
3. Unresolvable refs produce `unresolved_ref` in `models status --probe` output.
## Explicit Auth Order Filtering
- When `auth.order.<provider>` or the auth-store order override is set for a
provider, `models status --probe` only probes profile ids that remain in the
resolved auth order for that provider.
- A stored profile for that provider that is omitted from the explicit order is
not silently tried later. Probe output reports it with
`reasonCode: excluded_by_auth_order` and the detail
`Excluded by auth.order for this provider.`
## Probe Target Resolution
- Probe targets can come from auth profiles, environment credentials, or
`models.json`.
- If a provider has credentials but OpenClaw cannot resolve a probeable model
candidate for it, `models status --probe` reports `status: no_model` with
`reasonCode: no_model`.
## OAuth SecretRef Policy Guard
- SecretRef input is for static credentials only.

View File

@@ -1,44 +1,8 @@
---
summary: "Monitor OAuth expiry for model providers"
read_when:
- Setting up auth expiry monitoring or alerts
- Automating Claude Code / Codex OAuth refresh checks
summary: "Redirect to /gateway/authentication"
title: "Auth Monitoring"
---
# Auth monitoring
# Auth Monitoring
OpenClaw exposes OAuth expiry health via `openclaw models status`. Use that for
automation and alerting; scripts are optional extras for phone workflows.
## Preferred: CLI check (portable)
```bash
openclaw models status --check
```
Exit codes:
- `0`: OK
- `1`: expired or missing credentials
- `2`: expiring soon (within 24h)
This works in cron/systemd and requires no extra scripts.
## Optional scripts (ops / phone workflows)
These live under `scripts/` and are **optional**. They assume SSH access to the
gateway host and are tuned for systemd + Termux.
- `scripts/claude-auth-status.sh` now uses `openclaw models status --json` as the
source of truth (falling back to direct file reads if the CLI is unavailable),
so keep `openclaw` on `PATH` for timers.
- `scripts/auth-monitor.sh`: cron/systemd timer target; sends alerts (ntfy or phone).
- `scripts/systemd/openclaw-auth-monitor.{service,timer}`: systemd user timer.
- `scripts/claude-auth-status.sh`: Claude Code + OpenClaw auth checker (full/json/simple).
- `scripts/mobile-reauth.sh`: guided reauth flow over SSH.
- `scripts/termux-quick-auth.sh`: onetap widget status + open auth URL.
- `scripts/termux-auth-widget.sh`: full guided widget flow.
- `scripts/termux-sync-widget.sh`: sync Claude Code creds → OpenClaw.
If you dont need phone automation or systemd timers, skip these scripts.
This page moved to [Authentication](/gateway/authentication). See [Authentication](/gateway/authentication) for auth monitoring documentation.

View File

@@ -1,8 +1,8 @@
---
summary: "Redirect to TaskFlow"
summary: "Redirect to Task Flow"
title: "ClawFlow"
---
# ClawFlow
ClawFlow was renamed to [TaskFlow](/automation/taskflow). See [TaskFlow](/automation/taskflow) for the current documentation.
ClawFlow was renamed to [Task Flow](/automation/taskflow). See [Task Flow](/automation/taskflow) for the current documentation.

File diff suppressed because it is too large Load Diff

View File

@@ -1,299 +1,8 @@
---
summary: "Guidance for choosing between heartbeat and cron jobs for automation"
read_when:
- Deciding how to schedule recurring tasks
- Setting up background monitoring or notifications
- Optimizing token usage for periodic checks
summary: "Redirect to /automation"
title: "Cron vs Heartbeat"
---
# Cron vs Heartbeat: When to Use Each
# Cron vs Heartbeat
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
One important distinction:
- **Heartbeat** is a scheduled **main-session turn** — no task record created.
- **Cron (main)** is a scheduled **system event into the main session** — creates a task record with `silent` notify policy.
- **Cron (isolated)** is a scheduled **background run** — creates a task record tracked in `openclaw tasks`.
All cron job executions (main and isolated) create [task records](/automation/tasks). Heartbeat turns do not. Main-session cron tasks use `silent` notify policy by default so they do not generate notifications.
## Quick Decision Guide
| Use Case | Recommended | Why |
| ------------------------------------ | ------------------- | ---------------------------------------- |
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
| Background project health check | Heartbeat | Piggybacks on existing cycle |
## Heartbeat: Periodic Awareness
Heartbeats run in the **main session** at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important.
### When to use heartbeat
- **Multiple periodic checks**: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these.
- **Context-aware decisions**: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait.
- **Conversational continuity**: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally.
- **Low-overhead monitoring**: One heartbeat replaces many small polling tasks.
### Heartbeat advantages
- **Batches multiple checks**: One agent turn can review inbox, calendar, and notifications together.
- **Reduces API calls**: A single heartbeat is cheaper than 5 isolated cron jobs.
- **Context-aware**: The agent knows what you've been working on and can prioritize accordingly.
- **Smart suppression**: If nothing needs attention, the agent replies `HEARTBEAT_OK` and no message is delivered.
- **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring.
- **No task record**: heartbeat turns stay in main-session history (see [Background Tasks](/automation/tasks)).
### Heartbeat example: HEARTBEAT.md checklist
```md
# Heartbeat checklist
- Check email for urgent messages
- Review calendar for events in next 2 hours
- If a background task finished, summarize results
- If idle for 8+ hours, send a brief check-in
```
The agent reads this on each heartbeat and handles all items in one turn.
### Configuring heartbeat
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m", // interval
target: "last", // explicit alert delivery target (default is "none")
activeHours: { start: "08:00", end: "22:00" }, // optional
},
},
},
}
```
See [Heartbeat](/gateway/heartbeat) for full configuration.
## Cron: Precise Scheduling
Cron jobs run at precise times and can run in isolated sessions without affecting main context.
Recurring top-of-hour schedules are automatically spread by a deterministic
per-job offset in a 0-5 minute window.
### When to use cron
- **Exact timing required**: "Send this at 9:00 AM every Monday" (not "sometime around 9").
- **Standalone tasks**: Tasks that don't need conversational context.
- **Different model/thinking**: Heavy analysis that warrants a more powerful model.
- **One-shot reminders**: "Remind me in 20 minutes" with `--at`.
- **Noisy/frequent tasks**: Tasks that would clutter main session history.
- **External triggers**: Tasks that should run independently of whether the agent is otherwise active.
### Cron advantages
- **Precise timing**: 5-field or 6-field (seconds) cron expressions with timezone support.
- **Built-in load spreading**: recurring top-of-hour schedules are staggered by up to 5 minutes by default.
- **Per-job control**: override stagger with `--stagger <duration>` or force exact timing with `--exact`.
- **Session isolation**: Runs in `cron:<jobId>` without polluting main history.
- **Model overrides**: Use a cheaper or more powerful model per job.
- **Delivery control**: Isolated jobs default to `announce` (summary); choose `none` as needed.
- **Immediate delivery**: Announce mode posts directly without waiting for heartbeat.
- **No agent context needed**: Runs even if main session is idle or compacted.
- **One-shot support**: `--at` for precise future timestamps.
- **Task tracking**: isolated jobs create [background task](/automation/tasks) records visible in `openclaw tasks` and `openclaw tasks audit`.
### Cron example: Daily morning briefing
```bash
openclaw cron add \
--name "Morning briefing" \
--cron "0 7 * * *" \
--tz "America/New_York" \
--session isolated \
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
--model opus \
--announce \
--channel whatsapp \
--to "+15551234567"
```
This runs at exactly 7:00 AM New York time, uses Opus for quality, and announces a summary directly to WhatsApp.
### Cron example: One-shot reminder
```bash
openclaw cron add \
--name "Meeting reminder" \
--at "20m" \
--session main \
--system-event "Reminder: standup meeting starts in 10 minutes." \
--wake now \
--delete-after-run
```
See [Cron jobs](/automation/cron-jobs) for full CLI reference.
## Decision Flowchart
```
Does the task need to run at an EXACT time?
YES -> Use cron
NO -> Continue...
Does the task need isolation from main session?
YES -> Use cron (isolated)
NO -> Continue...
Can this task be batched with other periodic checks?
YES -> Use heartbeat (add to HEARTBEAT.md)
NO -> Use cron
Is this a one-shot reminder?
YES -> Use cron with --at
NO -> Continue...
Does it need a different model or thinking level?
YES -> Use cron (isolated) with --model/--thinking
NO -> Use heartbeat
```
## Combining Both
The most efficient setup uses **both**:
1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
### Example: Efficient automation setup
**HEARTBEAT.md** (checked every 30 min):
```md
# Heartbeat checklist
- Scan inbox for urgent emails
- Check calendar for events in next 2h
- Review any pending tasks
- Light check-in if quiet for 8+ hours
```
**Cron jobs** (precise timing):
```bash
# Daily morning briefing at 7am
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --announce
# Weekly project review on Mondays at 9am
openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
# One-shot reminder
openclaw cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
```
## Lobster: Deterministic workflows with approvals
Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals.
Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.
### When Lobster fits
- **Multi-step automation**: You need a fixed pipeline of tool calls, not a one-off prompt.
- **Approval gates**: Side effects should pause until you approve, then resume.
- **Resumable runs**: Continue a paused workflow without re-running earlier steps.
### How it pairs with heartbeat and cron
- **Heartbeat/cron** decide _when_ a run happens.
- **Lobster** defines _what steps_ happen once the run starts.
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster.
For ad-hoc workflows, call Lobster directly.
### Operational notes (from the code)
- Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
- If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
- The tool is an **optional plugin**; enable it additively via `tools.alsoAllow: ["lobster"]` (recommended).
- Lobster expects the `lobster` CLI to be available on `PATH`.
See [Lobster](/tools/lobster) for full usage and examples.
## Main Session vs Isolated Session
Both heartbeat and cron can interact with the main session, but differently:
| | Heartbeat | Cron (main) | Cron (isolated) |
| -------------------------- | ------------------------------- | ------------------------ | ----------------------------------------------- |
| Session | Main | Main (via system event) | `cron:<jobId>` or custom session |
| History | Shared | Shared | Fresh each run (isolated) / Persistent (custom) |
| Context | Full | Full | None (isolated) / Cumulative (custom) |
| Model | Main session model | Main session model | Can override |
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Announce summary (default) |
| [Tasks](/automation/tasks) | No task record | Task record (silent) | Task record (visible in `openclaw tasks`) |
### When to use main session cron
Use `--session main` with `--system-event` when you want:
- The reminder/event to appear in main session context
- The agent to handle it during the next heartbeat with full context
- No separate isolated run
```bash
openclaw cron add \
--name "Check project" \
--every "4h" \
--session main \
--system-event "Time for a project health check" \
--wake now
```
### When to use isolated cron
Use `--session isolated` when you want:
- A clean slate without prior context
- Different model or thinking settings
- Announce summaries directly to a channel
- History that doesn't clutter main session
```bash
openclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 0" \
--session isolated \
--message "Weekly codebase analysis..." \
--model opus \
--thinking high \
--announce
```
## Cost Considerations
| Mechanism | Cost Profile |
| --------------- | ------------------------------------------------------- |
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
| Cron (isolated) | Full agent turn per job; can use cheaper model |
**Tips**:
- Keep `HEARTBEAT.md` small to minimize token overhead.
- Batch similar checks into heartbeat instead of multiple cron jobs.
- Use `target: "none"` on heartbeat if you only want internal processing.
- Use isolated cron with a cheaper model for routine tasks.
## Related
- [Automation Overview](/automation) — all automation mechanisms at a glance
- [Heartbeat](/gateway/heartbeat) — full heartbeat configuration
- [Cron jobs](/automation/cron-jobs) — full cron CLI and API reference
- [Background Tasks](/automation/tasks) — task ledger, audit, and lifecycle
- [System](/cli/system) — system events + heartbeat controls
This page moved to [Automation & Tasks](/automation). See [Automation & Tasks](/automation) for the decision guide comparing cron and heartbeat.

View File

@@ -1,256 +1,8 @@
---
summary: "Gmail Pub/Sub push wired into OpenClaw webhooks via gogcli"
read_when:
- Wiring Gmail inbox triggers to OpenClaw
- Setting up Pub/Sub push for agent wake
summary: "Redirect to /automation/cron-jobs"
title: "Gmail PubSub"
---
# Gmail Pub/Sub -> OpenClaw
# Gmail PubSub
Goal: Gmail watch -> Pub/Sub push -> `gog gmail watch serve` -> OpenClaw webhook.
## Prereqs
- `gcloud` installed and logged in ([install guide](https://docs.cloud.google.com/sdk/docs/install-sdk)).
- `gog` (gogcli) installed and authorized for the Gmail account ([gogcli.sh](https://gogcli.sh/)).
- OpenClaw hooks enabled (see [Webhooks](/automation/webhook)).
- `tailscale` logged in ([tailscale.com](https://tailscale.com/)). Supported setup uses Tailscale Funnel for the public HTTPS endpoint.
Other tunnel services can work, but are DIY/unsupported and require manual wiring.
Right now, Tailscale is what we support.
Example hook config (enable Gmail preset mapping):
```json5
{
hooks: {
enabled: true,
token: "OPENCLAW_HOOK_TOKEN",
path: "/hooks",
presets: ["gmail"],
},
}
```
To deliver the Gmail summary to a chat surface, override the preset with a mapping
that sets `deliver` + optional `channel`/`to`:
```json5
{
hooks: {
enabled: true,
token: "OPENCLAW_HOOK_TOKEN",
presets: ["gmail"],
mappings: [
{
match: { path: "gmail" },
action: "agent",
wakeMode: "now",
name: "Gmail",
sessionKey: "hook:gmail:{{messages[0].id}}",
messageTemplate: "New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
model: "openai/gpt-5.2-mini",
deliver: true,
channel: "last",
// to: "+15551234567"
},
],
},
}
```
If you want a fixed channel, set `channel` + `to`. Otherwise `channel: "last"`
uses the last delivery route (falls back to WhatsApp).
To force a cheaper model for Gmail runs, set `model` in the mapping
(`provider/model` or alias). If you enforce `agents.defaults.models`, include it there.
To set a default model and thinking level specifically for Gmail hooks, add
`hooks.gmail.model` / `hooks.gmail.thinking` in your config:
```json5
{
hooks: {
gmail: {
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off",
},
},
}
```
Notes:
- Per-hook `model`/`thinking` in the mapping still overrides these defaults.
- Fallback order: `hooks.gmail.model``agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts).
- If `agents.defaults.models` is set, the Gmail model must be in the allowlist.
- Gmail hook content is wrapped with external-content safety boundaries by default.
To disable (dangerous), set `hooks.gmail.allowUnsafeExternalContent: true`.
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
under `~/.openclaw/hooks/transforms` (see [Webhooks](/automation/webhook)).
## Wizard (recommended)
Use the OpenClaw helper to wire everything together (installs deps on macOS via brew):
```bash
openclaw webhooks gmail setup \
--account openclaw@gmail.com
```
Defaults:
- Uses Tailscale Funnel for the public push endpoint.
- Writes `hooks.gmail` config for `openclaw webhooks gmail run`.
- Enables the Gmail hook preset (`hooks.presets: ["gmail"]`).
Path note: when `tailscale.mode` is enabled, OpenClaw automatically sets
`hooks.gmail.serve.path` to `/` and keeps the public path at
`hooks.gmail.tailscale.path` (default `/gmail-pubsub`) because Tailscale
strips the set-path prefix before proxying.
If you need the backend to receive the prefixed path, set
`hooks.gmail.tailscale.target` (or `--tailscale-target`) to a full URL like
`http://127.0.0.1:8788/gmail-pubsub` and match `hooks.gmail.serve.path`.
Want a custom endpoint? Use `--push-endpoint <url>` or `--tailscale off`.
Platform note: on macOS the wizard installs `gcloud`, `gogcli`, and `tailscale`
via Homebrew; on Linux install them manually first.
Gateway auto-start (recommended):
- When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
`gog gmail watch serve` on boot and auto-renews the watch.
- Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out (useful if you run the daemon yourself).
- Do not run the manual daemon at the same time, or you will hit
`listen tcp 127.0.0.1:8788: bind: address already in use`.
Manual daemon (starts `gog gmail watch serve` + auto-renew):
```bash
openclaw webhooks gmail run
```
## One-time setup
1. Select the GCP project **that owns the OAuth client** used by `gog`.
```bash
gcloud auth login
gcloud config set project <project-id>
```
Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client.
2. Enable APIs:
```bash
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
```
3. Create a topic:
```bash
gcloud pubsub topics create gog-gmail-watch
```
4. Allow Gmail push to publish:
```bash
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
```
## Start the watch
```bash
gog gmail watch start \
--account openclaw@gmail.com \
--label INBOX \
--topic projects/<project-id>/topics/gog-gmail-watch
```
Save the `history_id` from the output (for debugging).
## Run the push handler
Local example (shared token auth):
```bash
gog gmail watch serve \
--account openclaw@gmail.com \
--bind 127.0.0.1 \
--port 8788 \
--path /gmail-pubsub \
--token <shared> \
--hook-url http://127.0.0.1:18789/hooks/gmail \
--hook-token OPENCLAW_HOOK_TOKEN \
--include-body \
--max-bytes 20000
```
Notes:
- `--token` protects the push endpoint (`x-gog-token` or `?token=`).
- `--hook-url` points to OpenClaw `/hooks/gmail` (mapped; isolated run + summary to main).
- `--include-body` and `--max-bytes` control the body snippet sent to OpenClaw.
Recommended: `openclaw webhooks gmail run` wraps the same flow and auto-renews the watch.
## Expose the handler (advanced, unsupported)
If you need a non-Tailscale tunnel, wire it manually and use the public URL in the push
subscription (unsupported, no guardrails):
```bash
cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate
```
Use the generated URL as the push endpoint:
```bash
gcloud pubsub subscriptions create gog-gmail-watch-push \
--topic gog-gmail-watch \
--push-endpoint "https://<public-url>/gmail-pubsub?token=<shared>"
```
Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run:
```bash
gog gmail watch serve --verify-oidc --oidc-email <svc@...>
```
## Test
Send a message to the watched inbox:
```bash
gog gmail send \
--account openclaw@gmail.com \
--to openclaw@gmail.com \
--subject "watch test" \
--body "ping"
```
Check watch state and history:
```bash
gog gmail watch status --account openclaw@gmail.com
gog gmail history --account openclaw@gmail.com --since <historyId>
```
## Troubleshooting
- `Invalid topicName`: project mismatch (topic not in the OAuth client project).
- `User not authorized`: missing `roles/pubsub.publisher` on the topic.
- Empty messages: Gmail push only provides `historyId`; fetch via `gog gmail history`.
## Cleanup
```bash
gog gmail watch stop --account openclaw@gmail.com
gcloud pubsub subscriptions delete gog-gmail-watch-push
gcloud pubsub topics delete gog-gmail-watch
```
This page moved to [Scheduled Tasks](/automation/cron-jobs#gmail-pubsub-integration). See [Scheduled Tasks](/automation/cron-jobs#gmail-pubsub-integration) for Gmail PubSub documentation.

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +1,115 @@
---
summary: "Overview of all automation mechanisms: heartbeat, cron, tasks, hooks, webhooks, and more"
summary: "Overview of automation mechanisms: tasks, cron, hooks, standing orders, and Task Flow"
read_when:
- Deciding how to automate work with OpenClaw
- Choosing between heartbeat, cron, hooks, and webhooks
- Choosing between heartbeat, cron, hooks, and standing orders
- Looking for the right automation entry point
title: "Automation Overview"
title: "Automation & Tasks"
---
# Automation
# Automation & Tasks
OpenClaw provides several automation mechanisms, each suited to different use cases. This page helps you choose the right one.
OpenClaw runs work in the background through tasks, scheduled jobs, event hooks, and standing instructions. This page helps you choose the right mechanism and understand how they fit together.
## Quick decision guide
```mermaid
flowchart TD
A{Run on a schedule?} -->|Yes| B{Exact timing needed?}
A -->|No| C{React to events?}
B -->|Yes| D[Cron]
B -->|No| E[Heartbeat]
C -->|Yes| F[Hooks]
C -->|No| G[Standing Orders]
START([What do you need?]) --> Q1{Schedule work?}
START --> Q2{Track detached work?}
START --> Q3{Orchestrate multi-step flows?}
START --> Q4{React to lifecycle events?}
START --> Q5{Give the agent persistent instructions?}
Q1 -->|Yes| Q1a{Exact timing or flexible?}
Q1a -->|Exact| CRON["Scheduled Tasks (Cron)"]
Q1a -->|Flexible| HEARTBEAT[Heartbeat]
Q2 -->|Yes| TASKS[Background Tasks]
Q3 -->|Yes| FLOW[Task Flow]
Q4 -->|Yes| HOOKS[Hooks]
Q5 -->|Yes| SO[Standing Orders]
```
## Mechanisms at a glance
| Use case | Recommended | Why |
| --------------------------------------- | ---------------------- | ------------------------------------------------ |
| Send daily report at 9 AM sharp | Scheduled Tasks (Cron) | Exact timing, isolated execution |
| Remind me in 20 minutes | Scheduled Tasks (Cron) | One-shot with precise timing (`--at`) |
| Run weekly deep analysis | Scheduled Tasks (Cron) | Standalone task, can use different model |
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
| Inspect status of a subagent or ACP run | Background Tasks | Tasks ledger tracks all detached work |
| Audit what ran and when | Background Tasks | `openclaw tasks list` and `openclaw tasks audit` |
| Multi-step research then summarize | Task Flow | Durable orchestration with revision tracking |
| Run a script on session reset | Hooks | Event-driven, fires on lifecycle events |
| Execute code on every tool call | Hooks | Hooks can filter by event type |
| Always check compliance before replying | Standing Orders | Injected into every session automatically |
| Mechanism | What it does | Runs in | Creates task record |
| ---------------------------------------------- | -------------------------------------------------------- | ------------------------ | ------------------- |
| [Heartbeat](/gateway/heartbeat) | Periodic main-session turn — batches multiple checks | Main session | No |
| [Cron](/automation/cron-jobs) | Scheduled jobs with precise timing | Main or isolated session | Yes (all types) |
| [Background Tasks](/automation/tasks) | Tracks detached work (cron, ACP, subagents, CLI) | N/A (ledger) | N/A |
| [Hooks](/automation/hooks) | Event-driven scripts triggered by agent lifecycle events | Hook runner | No |
| [Standing Orders](/automation/standing-orders) | Persistent instructions injected into the system prompt | Main session | No |
| [Webhooks](/automation/webhook) | Receive inbound HTTP events and route to the agent | Gateway HTTP | No |
### Scheduled Tasks (Cron) vs Heartbeat
### Specialized automation
| Dimension | Scheduled Tasks (Cron) | Heartbeat |
| --------------- | ----------------------------------- | ------------------------------------- |
| Timing | Exact (cron expressions, one-shot) | Approximate (default every 30 min) |
| Session context | Fresh (isolated) or shared | Full main-session context |
| Task records | Always created | Never created |
| Delivery | Channel, webhook, or silent | Inline in main session |
| Best for | Reports, reminders, background jobs | Inbox checks, calendar, notifications |
| Mechanism | What it does |
| ---------------------------------------------- | ----------------------------------------------- |
| [Gmail PubSub](/automation/gmail-pubsub) | Real-time Gmail notifications via Google PubSub |
| [Polling](/automation/poll) | Periodic data source checks (RSS, APIs, etc.) |
| [Auth Monitoring](/automation/auth-monitoring) | Credential health and expiry alerts |
Use Scheduled Tasks (Cron) when you need precise timing or isolated execution. Use Heartbeat when the work benefits from full session context and approximate timing is fine.
## Core concepts
### Scheduled tasks (cron)
Cron is the Gateway's built-in scheduler for precise timing. It persists jobs, wakes the agent at the right time, and can deliver output to a chat channel or webhook endpoint. Supports one-shot reminders, recurring expressions, and inbound webhook triggers.
See [Scheduled Tasks](/automation/cron-jobs).
### Tasks
The background task ledger tracks all detached work: ACP runs, subagent spawns, isolated cron executions, and CLI operations. Tasks are records, not schedulers. Use `openclaw tasks list` and `openclaw tasks audit` to inspect them.
See [Background Tasks](/automation/tasks).
### Task Flow
Task Flow is the flow orchestration substrate above background tasks. It manages durable multi-step flows with managed and mirrored sync modes, revision tracking, and `openclaw tasks flow list|show|cancel` for inspection.
See [Task Flow](/automation/taskflow).
### Standing orders
Standing orders grant the agent permanent operating authority for defined programs. They live in workspace files (typically `AGENTS.md`) and are injected into every session. Combine with cron for time-based enforcement.
See [Standing Orders](/automation/standing-orders).
### Hooks
Hooks are event-driven scripts triggered by agent lifecycle events (`/new`, `/reset`, `/stop`), session compaction, gateway startup, message flow, and tool calls. Hooks are automatically discovered from directories and can be managed with `openclaw hooks`.
See [Hooks](/automation/hooks).
### Heartbeat
Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records. Use `HEARTBEAT.md` for a small checklist, or a `tasks:` block when you want due-only periodic checks inside heartbeat itself. Empty heartbeat files skip as `empty-heartbeat-file`; due-only task mode skips as `no-tasks-due`.
See [Heartbeat](/gateway/heartbeat).
## How they work together
The most effective setups combine multiple mechanisms:
1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
3. **Hooks** react to specific events (tool calls, session resets, compaction) with custom scripts.
4. **Standing Orders** give the agent persistent context ("always check the project board before replying").
5. **Background Tasks** automatically track all detached work so you can inspect and audit it.
See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for a detailed comparison of the two scheduling mechanisms.
## TaskFlow
[TaskFlow](/automation/taskflow) is the flow orchestration substrate above background tasks. It manages durable multi-step flows with managed and mirrored sync modes, and exposes `openclaw flows list|show|cancel` for inspection and recovery. See [TaskFlow](/automation/taskflow) for details.
- **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders. All cron executions create task records.
- **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
- **Hooks** react to specific events (tool calls, session resets, compaction) with custom scripts.
- **Standing orders** give the agent persistent context and authority boundaries.
- **Task Flow** coordinates multi-step flows above individual tasks.
- **Tasks** automatically track all detached work so you can inspect and audit it.
## Related
- [Cron vs Heartbeat](/automation/cron-vs-heartbeat) — detailed comparison guide
- [TaskFlow](/automation/taskflow) — flow orchestration above tasks
- [Troubleshooting](/automation/troubleshooting) — debugging automation issues
- [Scheduled Tasks](/automation/cron-jobs) — precise scheduling and one-shot reminders
- [Background Tasks](/automation/tasks) — task ledger for all detached work
- [Task Flow](/automation/taskflow) — durable multi-step flow orchestration
- [Hooks](/automation/hooks) — event-driven lifecycle scripts
- [Standing Orders](/automation/standing-orders) — persistent agent instructions
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
- [Configuration Reference](/gateway/configuration-reference) — all config keys

View File

@@ -1,86 +1,8 @@
---
summary: "Poll sending via gateway + CLI"
read_when:
- Adding or modifying poll support
- Debugging poll sends from the CLI or gateway
summary: "Redirect to /tools/message"
title: "Polls"
---
# Polls
## Supported channels
- Telegram
- WhatsApp (web channel)
- Discord
- Microsoft Teams (Adaptive Cards)
## CLI
```bash
# Telegram
openclaw message poll --channel telegram --target 123456789 \
--poll-question "Ship it?" --poll-option "Yes" --poll-option "No"
openclaw message poll --channel telegram --target -1001234567890:topic:42 \
--poll-question "Pick a time" --poll-option "10am" --poll-option "2pm" \
--poll-duration-seconds 300
# WhatsApp
openclaw message poll --target +15555550123 \
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
openclaw message poll --target 123456789@g.us \
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
# Discord
openclaw message poll --channel discord --target channel:123456789 \
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
openclaw message poll --channel discord --target channel:123456789 \
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
# Microsoft Teams
openclaw message poll --channel msteams --target conversation:19:abc@thread.tacv2 \
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
```
Options:
- `--channel`: `whatsapp` (default), `telegram`, `discord`, or `msteams`
- `--poll-multi`: allow selecting multiple options
- `--poll-duration-hours`: Discord-only (defaults to 24 when omitted)
- `--poll-duration-seconds`: Telegram-only (5-600 seconds)
- `--poll-anonymous` / `--poll-public`: Telegram-only poll visibility
## Gateway RPC
Method: `poll`
Params:
- `to` (string, required)
- `question` (string, required)
- `options` (string[], required)
- `maxSelections` (number, optional)
- `durationHours` (number, optional)
- `durationSeconds` (number, optional, Telegram-only)
- `isAnonymous` (boolean, optional, Telegram-only)
- `channel` (string, optional, default: `whatsapp`)
- `idempotencyKey` (string, required)
## Channel differences
- Telegram: 2-10 options. Supports forum topics via `threadId` or `:topic:` targets. Uses `durationSeconds` instead of `durationHours`, limited to 5-600 seconds. Supports anonymous and public polls.
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
- Microsoft Teams: Adaptive Card polls (OpenClaw-managed). No native poll API; `durationHours` is ignored.
## Agent tool (Message)
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `channel`).
For Telegram, the tool also accepts `pollDurationSeconds`, `pollAnonymous`, and `pollPublic`.
Use `action: "poll"` for poll creation. Poll fields passed with `action: "send"` are rejected.
Note: Discord has no “pick exactly N” mode; `pollMulti` maps to multi-select.
Teams polls are rendered as Adaptive Cards and require the gateway to stay online
to record votes in `~/.openclaw/msteams-polls.json`.
This page moved to [Message tool](/cli/message). See [Message tool](/cli/message) for poll documentation.

View File

@@ -247,8 +247,8 @@ Each program should have:
## Related
- [Automation Overview](/automation) — all automation mechanisms at a glance
- [Automation & Tasks](/automation) — all automation mechanisms at a glance
- [Cron Jobs](/automation/cron-jobs) — schedule enforcement for standing orders
- [Hooks](/automation/hooks) — event-driven scripts for agent lifecycle events
- [Webhooks](/automation/webhook) — inbound HTTP event triggers
- [Webhooks](/automation/cron-jobs#webhooks) — inbound HTTP event triggers
- [Agent Workspace](/concepts/agent-workspace) — where standing orders live, including the full list of auto-injected bootstrap files (AGENTS.md, SOUL.md, etc.)

View File

@@ -1,51 +1,82 @@
---
summary: "TaskFlow flow orchestration layer above background tasks"
summary: "Task Flow flow orchestration layer above background tasks"
read_when:
- You want to understand how TaskFlow relates to background tasks
- You encounter TaskFlow or openclaw flows in release notes or docs
- You want to understand how Task Flow relates to background tasks
- You encounter Task Flow or openclaw tasks flow in release notes or docs
- You want to inspect or manage durable flow state
title: "TaskFlow"
title: "Task Flow"
---
# TaskFlow
# Task Flow
TaskFlow is the flow orchestration substrate that sits above [background tasks](/automation/tasks). It manages durable multi-step flows with their own state, revision tracking, and sync semantics while individual tasks remain the unit of detached work.
Task Flow is the flow orchestration substrate that sits above [background tasks](/automation/tasks). It manages durable multi-step flows with their own state, revision tracking, and sync semantics while individual tasks remain the unit of detached work.
## When to use Task Flow
Use Task Flow when work spans multiple sequential or branching steps and you need durable progress tracking across gateway restarts. For single background operations, a plain [task](/automation/tasks) is sufficient.
| Scenario | Use |
| ------------------------------------- | -------------------- |
| Single background job | Plain task |
| Multi-step pipeline (A then B then C) | Task Flow (managed) |
| Observe externally created tasks | Task Flow (mirrored) |
| One-shot reminder | Cron job |
## Sync modes
TaskFlow supports two sync modes:
### Managed mode
- **Managed** — TaskFlow owns the lifecycle end-to-end, creating and driving tasks as flow steps progress.
- **Mirrored** — TaskFlow observes externally created tasks and keeps flow state in sync without taking ownership of task creation.
Task Flow owns the lifecycle end-to-end. It creates tasks as flow steps, drives them to completion, and advances the flow state automatically.
Example: a weekly report flow that (1) gathers data, (2) generates the report, and (3) delivers it. Task Flow creates each step as a background task, waits for completion, then moves to the next step.
```
Flow: weekly-report
Step 1: gather-data → task created → succeeded
Step 2: generate-report → task created → succeeded
Step 3: deliver → task created → running
```
### Mirrored mode
Task Flow observes externally created tasks and keeps flow state in sync without taking ownership of task creation. This is useful when tasks originate from cron jobs, CLI commands, or other sources and you want a unified view of their progress as a flow.
Example: three independent cron jobs that together form a "morning ops" routine. A mirrored flow tracks their collective progress without controlling when or how they run.
## Durable state and revision tracking
Each flow persists its own state and tracks revisions so progress survives gateway restarts. Revision tracking enables conflict detection when multiple sources attempt to advance the same flow.
Each flow persists its own state and tracks revisions so progress survives gateway restarts. Revision tracking enables conflict detection when multiple sources attempt to advance the same flow concurrently.
## Cancel behavior
`openclaw tasks flow cancel` sets a sticky cancel intent on the flow. Active tasks within the flow are cancelled, and no new steps are started. The cancel intent persists across restarts, so a cancelled flow stays cancelled even if the gateway restarts before all child tasks have terminated.
## CLI commands
```bash
# List active and recent flows
openclaw flows list
openclaw tasks flow list
# Show details for a specific flow
openclaw flows show <lookup>
openclaw tasks flow show <lookup>
# Cancel a running flow
openclaw flows cancel <lookup>
# Cancel a running flow and its active tasks
openclaw tasks flow cancel <lookup>
```
- `openclaw flows list` — shows tracked flows with status and sync mode
- `openclaw flows show <lookup>` — inspect one flow by flow id or lookup key
- `openclaw flows cancel <lookup>` — cancel a running flow and its active tasks
| Command | Description |
| --------------------------------- | --------------------------------------------- |
| `openclaw tasks flow list` | Shows tracked flows with status and sync mode |
| `openclaw tasks flow show <id>` | Inspect one flow by flow id or lookup key |
| `openclaw tasks flow cancel <id>` | Cancel a running flow and its active tasks |
## How flows relate to tasks
Flows coordinate tasks, not replace them. A single flow may drive multiple background tasks over its lifetime. Use `openclaw tasks` to inspect individual task records and `openclaw flows` to inspect the orchestrating flow.
Flows coordinate tasks, not replace them. A single flow may drive multiple background tasks over its lifetime. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow.
## Related
- [Background Tasks](/automation/tasks) — the detached work ledger that flows coordinate
- [CLI: flows](/cli/flows) — CLI command reference for `openclaw flows`
- [CLI: tasks](/cli/index#tasks) — CLI command reference for `openclaw tasks flow`
- [Automation Overview](/automation) — all automation mechanisms at a glance
- [Cron Jobs](/automation/cron-jobs) — scheduled jobs that may feed into flows

View File

@@ -9,7 +9,7 @@ title: "Background Tasks"
# Background Tasks
> **Cron vs Heartbeat vs Tasks?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for choosing the right scheduling mechanism. This page covers **tracking** background work, not scheduling it.
> **Looking for scheduling?** See [Automation & Tasks](/automation) for choosing the right mechanism. This page covers **tracking** background work, not scheduling it.
Background tasks track work that runs **outside your main conversation session**:
ACP runs, subagent spawns, isolated cron job executions, and CLI-initiated operations.
@@ -25,6 +25,14 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
- Tasks are **records**, not schedulers — cron and heartbeat decide _when_ work runs, tasks track _what happened_.
- ACP, subagents, all cron jobs, and CLI operations create tasks. Heartbeat turns do not.
- Each task moves through `queued → running → terminal` (succeeded, failed, timed_out, cancelled, or lost).
- Cron tasks stay live while the cron runtime still owns the job; chat-backed CLI tasks stay live only while their owning run context is still active.
- Completion is push-driven: detached work can notify directly or wake the
requester session/heartbeat when it finishes, so status polling loops are
usually the wrong shape.
- Isolated cron runs and subagent completions best-effort clean up tracked browser tabs/processes for their child session before final cleanup bookkeeping.
- Isolated cron delivery suppresses stale interim parent replies while
descendant subagent work is still draining, and it prefers final descendant
output when that arrives before delivery.
- Completion notifications are delivered directly to a channel or queued for the next heartbeat.
- `openclaw tasks list` shows all tasks; `openclaw tasks audit` surfaces issues.
- Terminal records are kept for 7 days, then automatically pruned.
@@ -50,6 +58,15 @@ openclaw tasks notify <lookup> state_changes
# Run a health audit
openclaw tasks audit
# Preview or apply maintenance
openclaw tasks maintenance
openclaw tasks maintenance --apply
# Inspect TaskFlow state
openclaw tasks flow list
openclaw tasks flow show <lookup>
openclaw tasks flow cancel <lookup>
```
## What creates a task
@@ -59,7 +76,7 @@ openclaw tasks audit
| ACP background runs | `acp` | Spawning a child ACP session | `done_only` |
| Subagent orchestration | `subagent` | Spawning a subagent via `sessions_spawn` | `done_only` |
| Cron jobs (all types) | `cron` | Every cron execution (main-session and isolated) | `silent` |
| CLI operations | `cli` | `openclaw agent` commands that run through the gateway | `done_only` |
| CLI operations | `cli` | `openclaw agent` commands that run through the gateway | `silent` |
Main-session cron tasks use `silent` notify policy by default — they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
@@ -91,15 +108,22 @@ stateDiagram-v2
| `failed` | Completed with an error |
| `timed_out` | Exceeded the configured timeout |
| `cancelled` | Stopped by the operator via `openclaw tasks cancel` |
| `lost` | Backing child session disappeared (detected after a 5-minute grace period) |
| `lost` | The runtime lost authoritative backing state after a 5-minute grace period |
Transitions happen automatically — when the associated agent run ends, the task status updates to match.
`lost` is runtime-aware:
- ACP tasks: backing ACP child session metadata disappeared.
- Subagent tasks: backing child session disappeared from the target agent store.
- Cron tasks: the cron runtime no longer tracks the job as active.
- CLI tasks: isolated child-session tasks use the child session; chat-backed CLI tasks use the live run context instead, so lingering channel/group/direct session rows do not keep them alive.
## Delivery and notifications
When a task reaches a terminal state, OpenClaw notifies you. There are two delivery paths:
**Direct delivery** — if the task has a channel target (the `requesterOrigin`), the completion message goes straight to that channel (Telegram, Discord, Slack, etc.).
**Direct delivery** — if the task has a channel target (the `requesterOrigin`), the completion message goes straight to that channel (Telegram, Discord, Slack, etc.). For subagent completions, OpenClaw also preserves bound thread/topic routing when available and can fill a missing `to` / account from the requester session's stored route (`lastChannel` / `lastTo` / `lastAccountId`) before giving up on direct delivery.
**Session-queued delivery** — if direct delivery fails or no origin is set, the update is queued as a system event in the requester's session and surfaces on the next heartbeat.
@@ -107,6 +131,10 @@ When a task reaches a terminal state, OpenClaw notifies you. There are two deliv
Task completion triggers an immediate heartbeat wake so you see the result quickly — you do not have to wait for the next scheduled heartbeat tick.
</Tip>
That means the usual workflow is push-based: start detached work once, then let
the runtime wake or notify you on completion. Poll task state only when you
need debugging, intervention, or an explicit audit.
### Notification policies
Control how much you hear about each task:
@@ -167,11 +195,47 @@ Surfaces operational issues. Findings also appear in `openclaw status` when issu
| ------------------------- | -------- | ----------------------------------------------------- |
| `stale_queued` | warn | Queued for more than 10 minutes |
| `stale_running` | error | Running for more than 30 minutes |
| `lost` | error | Backing session is gone |
| `lost` | error | Runtime-backed task ownership disappeared |
| `delivery_failed` | warn | Delivery failed and notify policy is not `silent` |
| `missing_cleanup` | warn | Terminal task with no cleanup timestamp |
| `inconsistent_timestamps` | warn | Timeline violation (for example ended before started) |
### `tasks maintenance`
```bash
openclaw tasks maintenance [--json]
openclaw tasks maintenance --apply [--json]
```
Use this to preview or apply reconciliation, cleanup stamping, and pruning for
tasks and Task Flow state.
Reconciliation is runtime-aware:
- ACP/subagent tasks check their backing child session.
- Cron tasks check whether the cron runtime still owns the job.
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
Completion cleanup is also runtime-aware:
- Subagent completion best-effort closes tracked browser tabs/processes for the child session before announce cleanup continues.
- Isolated cron completion best-effort closes tracked browser tabs/processes for the cron session before the run fully tears down.
- Isolated cron delivery waits out descendant subagent follow-up when needed and
suppresses stale parent acknowledgement text instead of announcing it.
- Subagent completion delivery prefers the latest visible assistant text; if that is empty it falls back to sanitized latest tool/toolResult text, and timeout-only tool-call runs can collapse to a short partial-progress summary.
- Cleanup failures do not mask the real task outcome.
### `tasks flow list|show|cancel`
```bash
openclaw tasks flow list [--status <status>] [--json]
openclaw tasks flow show <lookup> [--json]
openclaw tasks flow cancel <lookup>
```
Use these when the orchestrating Task Flow is the thing you care about rather
than one individual background task record.
## Chat task board (`/tasks`)
Use `/tasks` in any chat session to see background tasks linked to that session. The board shows
@@ -216,7 +280,7 @@ The registry loads into memory at gateway start and syncs writes to SQLite for d
A sweeper runs every **60 seconds** and handles three things:
1. **Reconciliation** — checks if active tasks' backing sessions still exist. If a child session has been gone for more than 5 minutes, the task is marked `lost`.
1. **Reconciliation** — checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and chat-backed CLI tasks use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
2. **Cleanup stamping** — sets a `cleanupAfter` timestamp on terminal tasks (endedAt + 7 days).
3. **Pruning** — deletes records past their `cleanupAfter` date.
@@ -224,11 +288,11 @@ A sweeper runs every **60 seconds** and handles three things:
## How tasks relate to other systems
### Tasks and TaskFlow
### Tasks and Task Flow
[TaskFlow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw flows` to inspect the orchestrating flow.
[Task Flow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow.
See [TaskFlow](/automation/taskflow) and [CLI: flows](/cli/flows) for details.
See [Task Flow](/automation/taskflow) for details.
### Tasks and cron
@@ -252,10 +316,8 @@ A task's `runId` links to the agent run doing the work. Agent lifecycle events (
## Related
- [Automation Overview](/automation) — all automation mechanisms at a glance
- [TaskFlow](/automation/taskflow) — flow orchestration above tasks
- [Cron Jobs](/automation/cron-jobs) — scheduling background work
- [Cron vs Heartbeat](/automation/cron-vs-heartbeat) — choosing the right mechanism
- [Automation & Tasks](/automation) — all automation mechanisms at a glance
- [Task Flow](/automation/taskflow) — flow orchestration above tasks
- [Scheduled Tasks](/automation/cron-jobs) — scheduling background work
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
- [CLI: flows](/cli/flows) — CLI reference for `openclaw flows`
- [CLI: Tasks](/cli/index#tasks) — CLI command reference

View File

@@ -1,122 +1,8 @@
---
summary: "Troubleshoot cron and heartbeat scheduling and delivery"
read_when:
- Cron did not run
- Cron ran but no message was delivered
- Heartbeat seems silent or skipped
summary: "Redirect to /automation/cron-jobs"
title: "Automation Troubleshooting"
---
# Automation troubleshooting
# Automation Troubleshooting
Use this page for scheduler and delivery issues (`cron` + `heartbeat`).
## Command ladder
```bash
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
openclaw channels status --probe
```
Then run automation checks:
```bash
openclaw cron status
openclaw cron list
openclaw system heartbeat last
```
## Cron not firing
```bash
openclaw cron status
openclaw cron list
openclaw cron runs --id <jobId> --limit 20
openclaw logs --follow
```
Good output looks like:
- `cron status` reports enabled and a future `nextWakeAtMs`.
- Job is enabled and has a valid schedule/timezone.
- `cron runs` shows `ok` or explicit skip reason.
Common signatures:
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled in config/env.
- `cron: timer tick failed` → scheduler tick crashed; inspect surrounding stack/log context.
- `reason: not-due` in run output → manual run called without `--force` and job not due yet.
## Cron fired but no delivery
```bash
openclaw cron runs --id <jobId> --limit 20
openclaw cron list
openclaw channels status --probe
openclaw logs --follow
```
Good output looks like:
- Run status is `ok`.
- Delivery mode/target are set for isolated jobs.
- Channel probe reports target channel connected.
Common signatures:
- Run succeeded but delivery mode is `none` → no external message is expected.
- Delivery target missing/invalid (`channel`/`to`) → run may succeed internally but skip outbound.
- Channel auth errors (`unauthorized`, `missing_scope`, `Forbidden`) → delivery blocked by channel credentials/permissions.
## Heartbeat suppressed or skipped
```bash
openclaw system heartbeat last
openclaw logs --follow
openclaw config get agents.defaults.heartbeat
openclaw channels status --probe
```
Good output looks like:
- Heartbeat enabled with non-zero interval.
- Last heartbeat result is `ran` (or skip reason is understood).
Common signatures:
- `heartbeat skipped` with `reason=quiet-hours` → outside `activeHours`.
- `requests-in-flight` → main lane busy; heartbeat deferred.
- `empty-heartbeat-file` → interval heartbeat skipped because `HEARTBEAT.md` has no actionable content and no tagged cron event is queued.
- `alerts-disabled` → visibility settings suppress outbound heartbeat messages.
## Timezone and activeHours gotchas
```bash
openclaw config get agents.defaults.heartbeat.activeHours
openclaw config get agents.defaults.heartbeat.activeHours.timezone
openclaw config get agents.defaults.userTimezone || echo "agents.defaults.userTimezone not set"
openclaw cron list
openclaw logs --follow
```
Quick rules:
- `Config path not found: agents.defaults.userTimezone` means the key is unset; heartbeat falls back to host timezone (or `activeHours.timezone` if set).
- Cron without `--tz` uses gateway host timezone.
- Heartbeat `activeHours` uses configured timezone resolution (`user`, `local`, or explicit IANA tz).
- Cron `at` schedules treat ISO timestamps without timezone as UTC unless you used CLI `--at "<offset-less-iso>" --tz <iana>`.
Common signatures:
- Jobs run at the wrong wall-clock time after host timezone changes.
- Heartbeat always skipped during your daytime because `activeHours.timezone` is wrong.
Related:
- [/automation/cron-jobs](/automation/cron-jobs)
- [/gateway/heartbeat](/gateway/heartbeat)
- [/automation/cron-vs-heartbeat](/automation/cron-vs-heartbeat)
- [/concepts/timezone](/concepts/timezone)
This page moved to [Scheduled Tasks](/automation/cron-jobs#troubleshooting). See [Scheduled Tasks](/automation/cron-jobs#troubleshooting) for troubleshooting documentation.

View File

@@ -1,217 +1,8 @@
---
summary: "Webhook ingress for wake and isolated agent runs"
read_when:
- Adding or changing webhook endpoints
- Wiring external systems into OpenClaw
summary: "Redirect to /automation/cron-jobs"
title: "Webhooks"
---
# Webhooks
Gateway can expose a small HTTP webhook endpoint for external triggers.
## Enable
```json5
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
// Optional: restrict explicit `agentId` routing to this allowlist.
// Omit or include "*" to allow any agent.
// Set [] to deny all explicit `agentId` routing.
allowedAgentIds: ["hooks", "main"],
},
}
```
Notes:
- `hooks.token` is required when `hooks.enabled=true`.
- `hooks.path` defaults to `/hooks`.
## Auth
Every request must include the hook token. Prefer headers:
- `Authorization: Bearer <token>` (recommended)
- `x-openclaw-token: <token>`
- Query-string tokens are rejected (`?token=...` returns `400`).
- Treat `hooks.token` holders as full-trust callers for the hook ingress surface on that gateway. Hook payload content is still untrusted, but this is not a separate non-owner auth boundary.
## Endpoints
### `POST /hooks/wake`
Payload:
```json
{ "text": "System line", "mode": "now" }
```
- `text` **required** (string): The description of the event (e.g., "New email received").
- `mode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
Effect:
- Enqueues a system event for the **main** session
- If `mode=now`, triggers an immediate heartbeat
### `POST /hooks/agent`
Payload:
```json
{
"message": "Run this",
"name": "Email",
"agentId": "hooks",
"sessionKey": "hook:email:msg-123",
"wakeMode": "now",
"deliver": true,
"channel": "last",
"to": "+15551234567",
"model": "openai/gpt-5.2-mini",
"thinking": "low",
"timeoutSeconds": 120
}
```
- `message` **required** (string): The prompt or message for the agent to process.
- `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.
- `agentId` optional (string): Route this hook to a specific agent. Unknown IDs fall back to the default agent. When set, the hook runs using the resolved agent's workspace and configuration.
- `sessionKey` optional (string): The key used to identify the agent's session. By default this field is rejected unless `hooks.allowRequestSessionKey=true`.
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
- `channel` optional (string): The messaging channel for delivery. Use `last` or any configured channel or plugin id, for example `discord`, `matrix`, `telegram`, or `whatsapp`. Defaults to `last`.
- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for Microsoft Teams). Defaults to the last recipient in the main session.
- `model` optional (string): Model override (e.g., `anthropic/claude-sonnet-4-6` or an alias). Must be in the allowed model list if restricted.
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.
Effect:
- Runs an **isolated** agent turn (own session key)
- Always posts a summary into the **main** session
- If `wakeMode=now`, triggers an immediate heartbeat
## Session key policy (breaking change)
`/hooks/agent` payload `sessionKey` overrides are disabled by default.
- Recommended: set a fixed `hooks.defaultSessionKey` and keep request overrides off.
- Optional: allow request overrides only when needed, and restrict prefixes.
Recommended config:
```json5
{
hooks: {
enabled: true,
token: "${OPENCLAW_HOOKS_TOKEN}",
defaultSessionKey: "hook:ingress",
allowRequestSessionKey: false,
allowedSessionKeyPrefixes: ["hook:"],
},
}
```
Compatibility config (legacy behavior):
```json5
{
hooks: {
enabled: true,
token: "${OPENCLAW_HOOKS_TOKEN}",
allowRequestSessionKey: true,
allowedSessionKeyPrefixes: ["hook:"], // strongly recommended
},
}
```
### `POST /hooks/<name>` (mapped)
Custom hook names are resolved via `hooks.mappings` (see configuration). A mapping can
turn arbitrary payloads into `wake` or `agent` actions, with optional templates or
code transforms.
Mapping options (summary):
- `hooks.presets: ["gmail"]` enables the built-in Gmail mapping.
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
- `hooks.transformsDir` (if set) must stay within the transforms root under your OpenClaw config directory (typically `~/.openclaw/hooks/transforms`).
- `transform.module` must resolve within the effective transforms directory (traversal/escape paths are rejected).
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
- TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime.
- Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface
(`channel` defaults to `last` and falls back to WhatsApp).
- `agentId` routes the hook to a specific agent; unknown IDs fall back to the default agent.
- `hooks.allowedAgentIds` restricts explicit `agentId` routing. Omit it (or include `*`) to allow any agent. Set `[]` to deny explicit `agentId` routing.
- `hooks.defaultSessionKey` sets the default session for hook agent runs when no explicit key is provided.
- `hooks.allowRequestSessionKey` controls whether `/hooks/agent` payloads may set `sessionKey` (default: `false`).
- `hooks.allowedSessionKeyPrefixes` optionally restricts explicit `sessionKey` values from request payloads and mappings.
- `allowUnsafeExternalContent: true` disables the external content safety wrapper for that hook
(dangerous; only for trusted internal sources).
- `openclaw webhooks gmail setup` writes `hooks.gmail` config for `openclaw webhooks gmail run`.
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
## Responses
- `200` for `/hooks/wake`
- `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
- `413` on oversized payloads
## Examples
```bash
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
```
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-openclaw-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}'
```
### Use a different model
Add `model` to the agent payload (or mapping) to override the model for that run:
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-openclaw-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
```
If you enforce `agents.defaults.models`, make sure the override model is included there.
```bash
curl -X POST http://127.0.0.1:18789/hooks/gmail \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}'
```
## Security
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
- Use a dedicated hook token; do not reuse gateway auth tokens.
- Prefer a dedicated hook agent with strict `tools.profile` and sandboxing so hook ingress has a narrower blast radius.
- Repeated auth failures are rate-limited per client address to slow brute-force attempts.
- If you use multi-agent routing, set `hooks.allowedAgentIds` to limit explicit `agentId` selection.
- Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions.
- If you enable request `sessionKey`, restrict `hooks.allowedSessionKeyPrefixes` (for example, `["hook:"]`).
- Avoid including sensitive raw payloads in webhook logs.
- Hook payloads are treated as untrusted and wrapped with safety boundaries by default.
If you must disable this for a specific hook, set `allowUnsafeExternalContent: true`
in that hook's mapping (dangerous).
This page moved to [Scheduled Tasks](/automation/cron-jobs#webhooks). See [Scheduled Tasks](/automation/cron-jobs#webhooks) for webhook documentation.

View File

@@ -26,6 +26,7 @@ OpenClaw supports Brave Search API as a `web_search` provider.
config: {
webSearch: {
apiKey: "BRAVE_API_KEY_HERE",
mode: "web", // or "llm-context"
},
},
},
@@ -46,6 +47,11 @@ OpenClaw supports Brave Search API as a `web_search` provider.
Provider-specific Brave search settings now live under `plugins.entries.brave.config.webSearch.*`.
Legacy `tools.web.search.apiKey` still loads through the compatibility shim, but it is no longer the canonical config path.
`webSearch.mode` controls the Brave transport:
- `web` (default): normal Brave web search with titles, URLs, and snippets
- `llm-context`: Brave LLM Context API with pre-extracted text chunks and sources for grounding
## Tool parameters
| Parameter | Description |
@@ -54,6 +60,7 @@ Legacy `tools.web.search.apiKey` still loads through the compatibility shim, but
| `count` | Number of results to return (1-10, default: 5) |
| `country` | 2-letter ISO country code (e.g., "US", "DE") |
| `language` | ISO 639-1 language code for search results (e.g., "en", "de", "fr") |
| `search_lang` | Brave search-language code (e.g., `en`, `en-gb`, `zh-hans`) |
| `ui_lang` | ISO language code for UI elements |
| `freshness` | Time filter: `day` (24h), `week`, `month`, or `year` |
| `date_after` | Only results published after this date (YYYY-MM-DD) |
@@ -88,6 +95,9 @@ await web_search({
- OpenClaw uses the Brave **Search** plan. If you have a legacy subscription (e.g. the original Free plan with 2,000 queries/month), it remains valid but does not include newer features like LLM Context or higher rate limits.
- Each Brave plan includes **\$5/month in free credit** (renewing). The Search plan costs \$5 per 1,000 requests, so the credit covers 1,000 queries/month. Set your usage limit in the Brave dashboard to avoid unexpected charges. See the [Brave API portal](https://brave.com/search/api/) for current plans.
- The Search plan includes the LLM Context endpoint and AI inference rights. Storing results to train or tune models requires a plan with explicit storage rights. See the Brave [Terms of Service](https://api-dashboard.search.brave.com/terms-of-service).
- `llm-context` mode returns grounded source entries instead of the normal web-search snippet shape.
- `llm-context` mode does not support `ui_lang`, `freshness`, `date_after`, or `date_before`.
- `ui_lang` must include a region subtag like `en-US`.
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`).
See [Web tools](/tools/web) for the full web_search configuration.

View File

@@ -11,6 +11,11 @@ title: "BlueBubbles"
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
## Bundled plugin
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not
need a separate `openclaw plugins install` step.
## Overview
- Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)).
@@ -404,9 +409,9 @@ Prefer `chat_guid` for stable routing:
## Security
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`.
- Keep the API password and webhook endpoint secret (treat them like credentials).
- Localhost trust means a same-host reverse proxy can unintentionally bypass the password. If you proxy the gateway, require auth at the proxy and configure `gateway.trustedProxies`. See [Gateway security](/gateway/security#reverse-proxy-configuration).
- There is no localhost bypass for BlueBubbles webhook auth. If you proxy webhook traffic, keep the BlueBubbles password on the request end-to-end. `gateway.trustedProxies` does not replace `channels.bluebubbles.password` here. See [Gateway security](/gateway/security#reverse-proxy-configuration).
- Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
## Troubleshooting

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