Compare commits

..

158 Commits

Author SHA1 Message Date
Peter Steinberger
1f82f46085 docs: sync codex review skill 2026-05-14 14:38:56 +01:00
Peter Steinberger
82fd88dbf4 feat: add gateway token scopes 2026-05-14 12:12:27 +01:00
Ayaan Zaidi
d25bece9f6 refactor(codex): tighten status rate-limit formatting 2026-05-14 15:45:33 +05:30
Matthew Schleder
724868cec8 docs: add Codex rate-limit changelog entry 2026-05-14 15:45:33 +05:30
Matthew Schleder
6b6538bd13 fix(codex): format status rate limits like usage 2026-05-14 15:45:33 +05:30
Vincent Koc
dbabfc550f fix(telnyx): validate webhook client state base64 2026-05-14 18:08:16 +08:00
Vincent Koc
c822824503 fix(qqbot): validate cron payload base64 2026-05-14 18:04:32 +08:00
Vincent Koc
fe97f1fa4f fix(voice-call): validate realtime media frame base64 2026-05-14 18:00:30 +08:00
Gio Della-Libera
b9dc6d86b8 test(config): refresh plugin schema fixtures 2026-05-14 17:58:27 +08:00
Gio Della-Libera
13c2e245aa fix(config): use plugin channel schemas in dry-run 2026-05-14 17:58:26 +08:00
Vincent Koc
f3f6a866ca fix(msteams): validate inline image base64 2026-05-14 17:57:53 +08:00
joshavant
6ae9c8bead note telegram worker dist fix 2026-05-14 04:56:14 -05:00
joshavant
8ba7927f6e fix telegram ingress worker dist entry 2026-05-14 04:55:39 -05:00
Vincent Koc
84ec355af8 fix(qa-channel): reject malformed inline attachment data 2026-05-14 17:54:55 +08:00
Ayaan Zaidi
23ed804657 fix(telegram): keep plugin slash commands on native path 2026-05-14 15:22:53 +05:30
Vincent Koc
23cfc81bcd fix(file-transfer): validate inline write base64 2026-05-14 17:51:20 +08:00
Vincent Koc
92524fcf98 fix(proxy): reject malformed debug proxy targets 2026-05-14 17:46:10 +08:00
Vincent Koc
a47132734b fix(agents): skip continuation bootstrap preload 2026-05-14 17:42:36 +08:00
Val Alexander
b405c6e640 fix(mac): verify launchd stop releases gateway port
Fixes #73132.

Summary:
- Verify macOS LaunchAgent stop/restart port postconditions before reporting success.
- Resolve the effective gateway port from launchd args, persisted service environment, then caller env.
- Delay degraded fallback success output until the listener port is confirmed released.

Verification:
- node scripts/run-vitest.mjs src/daemon/launchd.test.ts src/cli/daemon-cli/lifecycle.test.ts src/cli/daemon-cli/lifecycle-core.test.ts src/cli/daemon-cli/restart-health.test.ts
- pnpm exec oxfmt --check --threads=1 src/daemon/launchd.ts src/daemon/launchd.test.ts CHANGELOG.md
- git diff --check
- Testbox tbx_01krjxf8vrbjwxv3xfdx4770xr: pnpm check:changed
2026-05-14 04:41:45 -05:00
Vincent Koc
c70adb8528 fix(plugins): wrap malformed node proxy payloads 2026-05-14 17:40:38 +08:00
Vincent Koc
6b3998aa40 fix(gateway): ignore malformed host on session routes 2026-05-14 17:35:57 +08:00
Peter Steinberger
365c986a5b docs(models): clarify cli runtime alias comment 2026-05-14 10:35:35 +01:00
Peter Steinberger
ac5674b32c fix(web): keep legacy Brave search fallback provider-owned
- Keep doctor migration as canonical for legacy Brave web-search config.
- Move legacy runtime support into Brave-owned provider config handling.
- Preserve legacy config precedence over ambient BRAVE_API_KEY.

Verification:
- node scripts/run-vitest.mjs run src/secrets/runtime-web-tools.test.ts --maxWorkers=1
- pnpm test extensions/brave -- --maxWorkers=1
- pnpm check:changed via Blacksmith Testbox tbx_01krjwy2gc4d2sxb3hqxcbhhtk / https://github.com/openclaw/openclaw/actions/runs/25852532246
2026-05-14 10:32:55 +01:00
Peter Steinberger
731c8843ff test: update lint suppression allowlist 2026-05-14 10:27:12 +01:00
Peter Steinberger
554dfbd017 docs(ui): add i18n report changelog 2026-05-14 10:27:12 +01:00
Peter Steinberger
4feb4e6623 docs(ui): document i18n report usage 2026-05-14 10:27:12 +01:00
Peter Steinberger
266722500c fix(ui): avoid noisy i18n report locale warnings 2026-05-14 10:27:12 +01:00
samzong
4e76d6e427 fix(ui): harden i18n report filters
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-14 10:27:12 +01:00
samzong
ee9d471865 feat(ui): add i18n baseline report
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-14 10:27:12 +01:00
Vincent Koc
d5abbd29cc changelog: cover @sjf Codex marketplace handling and @scotthuang weixin 2.4.3
- (#81625) Codex migrate delayed marketplace + warning/next-step glyph cleanup. Thanks @sjf.
- (#81730) Weixin bundled catalog bump 2.4.1 -> 2.4.3. Thanks @scotthuang.
2026-05-14 17:26:45 +08:00
Mariano
a5c1956ca1 feat(codex): bind CLI sessions from nodes
Adds node-backed Codex CLI session listing and resume binding for paired nodes, including Windows shim-safe Codex resume spawning, docs, changelog, and focused Codex coverage.

Verification:
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/plugins/codex-harness.md extensions/codex/index.ts extensions/codex/src/command-formatters.ts extensions/codex/src/command-handlers.ts extensions/codex/src/commands.test.ts extensions/codex/src/conversation-binding-data.ts extensions/codex/src/conversation-binding.test.ts extensions/codex/src/conversation-binding.ts extensions/codex/src/node-cli-sessions.ts extensions/codex/src/node-cli-sessions.test.ts
- pnpm run lint:tmp:no-random-messaging
- pnpm run lint:extensions:bundled
- OPENCLAW_VITEST_MAX_WORKERS=4 pnpm test extensions/codex/src/node-cli-sessions.test.ts extensions/codex/src/conversation-binding.test.ts extensions/codex/src/commands.test.ts
- pnpm tsgo:extensions
- git diff --check
- AWS Crabbox focused proof run_a901a61e006f
2026-05-14 11:24:30 +02:00
Ayaan Zaidi
2268ce3a14 docs(changelog): note telegram cron html fix 2026-05-14 14:39:23 +05:30
Ayaan Zaidi
e28d66d531 fix(cli): preserve lazy sender formatting 2026-05-14 14:39:23 +05:30
Ayaan Zaidi
41aee75cd1 test(cli): prove lazy sender preserves html formatting 2026-05-14 14:39:23 +05:30
Peter Steinberger
04605f1670 docs: allow maintainer proof override 2026-05-14 10:08:54 +01:00
Peter Steinberger
a0f35574d0 Remove codex-cli backend and migrate to Codex runtime
Remove the bundled codex-cli backend, migrate legacy codex-cli refs and runtime pins to the Codex app-server runtime, and update live/backend workflow coverage for the supported CLI lanes.
2026-05-14 10:07:18 +01:00
Peter Steinberger
66b98b7294 chore: sync codex review skill 2026-05-14 10:02:14 +01:00
Peter Steinberger
a582fc2d5c chore: tighten codex review skill 2026-05-14 09:58:58 +01:00
Peter Steinberger
beea866a53 chore: add codex review skill 2026-05-14 09:56:13 +01:00
samzong
1d121c1f08 chore(gateway): add startup trace attribution (#81738)
Adds owner-level startup trace attribution for gateway auth, plugin loading, lookup counts, and plugin sidecar services.

Verification:
- node scripts/run-vitest.mjs src/plugins/startup-trace-segment.test.ts src/plugins/services.test.ts src/plugins/loader.test.ts src/gateway/server-startup-config.secrets.test.ts
- pnpm build
- pnpm check

CI override:
- Red checks are unrelated baseline noise. The failed CI shard is src/cli/plugins-install-persist.test.ts, which fails on origin/main 336ba2a2b3 with the same missing resolveIsNixMode mock export. PR #81738 touches gateway/plugin startup trace files and CHANGELOG.md, not the failing CLI plugin install test.

Thanks @samzong.

Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
2026-05-14 16:50:08 +08:00
Vincent Koc
12b8db34ee fix(browser): handle malformed node proxy payloads 2026-05-14 16:48:28 +08:00
Josh Avant
fd244fd76d Fix Telegram polling ingress under event-loop stalls (#81746)
* fix telegram polling ingress under event-loop stalls

* add changelog for telegram ingress fix
2026-05-14 03:35:06 -05:00
scotthuang
c35634c729 Fix/weixin catalog update 2.4.3 (#81730)
* fix(weixin): upgrade catalog to 2.4.3

* fix(weixin): update catalog integrity for 2.4.3

---------

Co-authored-by: scotthuang <scotthuang@tencent.com>
2026-05-14 03:32:38 -05:00
Vincent Koc
0668f1e003 fix(web): resolve explicit global search providers 2026-05-14 16:29:55 +08:00
Vincent Koc
94f3ecae9a perf(cli): route plugin list json directly 2026-05-14 16:29:21 +08:00
Vincent Koc
641ad418c9 fix(clickclack): skip malformed websocket frames 2026-05-14 16:26:02 +08:00
Eduardo Buitrago
336ba2a2b3 fix(agents): forward explicit per-run timeout to LLM idle watchdog (#79426)
Merged via squash.

Prepared head SHA: 0e6cf9b4d5
Co-authored-by: legolaz8451 <18042830+legolaz8451@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Reviewed-by: @joshavant
2026-05-14 03:24:01 -05:00
Vincent Koc
8717525fbc test(cli): cover parent startup budgets 2026-05-14 16:18:26 +08:00
Sarah Fortune
2f2563314a fix(codex): handle delayed plugin marketplace (#81625) 2026-05-14 01:17:58 -07:00
Jack Storment
b79c41d252 fix(memory): discover slugged daily memory files alongside date-only files
Widen daily memory filename discovery so slugged session-memory files flow through Dreaming, rem-backfill, rem-harness, doctor, and short-term promotion.

Preserve exact slugged source paths during historical seeding and rem-backfill attribution, including multiple files for the same day.

Add regression coverage for slugged ingestion, rem-backfill, rem-harness preview paths, and doctor backfill day extraction.

Fixes #69536.

Co-authored-by: Jack Storment <crazycoder131@gmail.com>
2026-05-14 09:17:44 +01:00
Vincent Koc
31a28eb5ba fix(media): reject malformed redirect locations 2026-05-14 16:16:56 +08:00
Kaspre
f71df80522 perf(plugins): memoize metadata snapshots narrowly 2026-05-14 16:15:50 +08:00
Vincent Koc
f6d0cc6cc3 perf(cli): keep channel option help lightweight 2026-05-14 16:13:28 +08:00
Vincent Koc
db743e4918 fix(voice-call): ignore malformed host for webhook paths 2026-05-14 16:11:29 +08:00
Vincent Koc
462a056210 fix(gateway): ignore malformed host on json routes 2026-05-14 16:06:23 +08:00
Vincent Koc
fe25ed214e refactor(cli): lazy-load devices runtime 2026-05-14 16:02:42 +08:00
Peter Steinberger
3fa9658b39 fix: carry transcript update sequence 2026-05-14 08:59:31 +01:00
samzong
c20224688c fix(gateway): carry transcript update sequence
Signed-off-by: samzong <samzong.lu@gmail.com>
2026-05-14 08:59:31 +01:00
Vincent Koc
a012411d5e refactor(cli): lazy-load plugins runtime 2026-05-14 15:57:07 +08:00
anagnorisis2peripeteia
bcb9fa42be fix(models): keep CLI runtime providers in /models picker (#81239)
Merged via squash.

Prepared head SHA: 294741d1db
Co-authored-by: anagnorisis2peripeteia <129746152+anagnorisis2peripeteia@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
2026-05-14 10:52:57 +03:00
Peter Steinberger
3bd47a95a8 test: align release check with external slack package 2026-05-14 08:49:48 +01:00
Peter Steinberger
e17f628a75 test(release): skip externalized slack pack paths 2026-05-14 08:49:00 +01:00
Peter Steinberger
7edcfabf51 fix(build): keep slack dependencies extension-owned 2026-05-14 08:49:00 +01:00
Peter Steinberger
e7ae306aa1 refactor(auth): use fs-safe stale lock recovery 2026-05-14 08:49:00 +01:00
Peter Steinberger
04afd114bb fix(auth): reclaim zombie-owned stale locks 2026-05-14 08:49:00 +01:00
Peter Steinberger
c499ef1a6b fix(auth): preserve non-signalable lock owners 2026-05-14 08:49:00 +01:00
Peter Steinberger
f84d031d38 fix(auth): fail closed on unreadable stale locks 2026-05-14 08:49:00 +01:00
Peter Steinberger
3048ad4731 refactor(infra): centralize stale lock cleanup 2026-05-14 08:49:00 +01:00
Peter Steinberger
ceb3092493 fix(auth): reclaim stale file locks 2026-05-14 08:49:00 +01:00
Vincent Koc
babd25c6b7 refactor(cli): lazy-load models runtime 2026-05-14 15:48:11 +08:00
Peter Steinberger
bfc798bd0b fix(ci): restore main build 2026-05-14 08:38:49 +01:00
Peter Steinberger
32f89760e3 docs: clarify landing recap requirement 2026-05-14 08:37:30 +01:00
Peter Steinberger
11017c93cf build: keep external provider deps out of core dist 2026-05-14 08:29:10 +01:00
Peter Steinberger
643dea2455 fix(mattermost): gate delivery success log 2026-05-14 08:29:07 +01:00
kinjitakabe
6789fe248b fix(mattermost): diagnose silent final-delivery completions
`deliverMattermostReplyPayload` accepted a substantive (non-reasoning) reply
payload, called the shared `deliverTextOrMediaReply`, and dropped its
`"empty"|"text"|"media"` return value on the floor. When the underlying chunker
or media-resolution produced no text and no media to send, the function
returned `Promise<void>` and the caller in `monitor.ts` unconditionally logged
`delivered reply to <channel>` — masking a silent completion where no
Mattermost API send ever happened (the symptom in #80501).

Thread the outcome through the helper, evaluate it against the original
payload to distinguish intentional reasoning suppression from a substantive
payload that vanished, and log a structured `mattermost no-visible-reply`
diagnostic for the substantive-vanished case. The misleading "delivered
reply to" log now only fires on actual visible delivery; reasoning-skipped
payloads correctly stay silent.

No behavior change: visible-delivery decisions, preview-finalization, and the
existing reasoning-suppression contract are untouched. Operators can now grep
the new diagnostic to detect the failure class instead of seeing the agent
appear to go silent.

Fixes #80501.
2026-05-14 08:29:07 +01:00
Josh Avant
10d2f41c83 fix(browser): request admin scope for CLI control (#81716)
* fix(browser): request admin scope for CLI control

* chore(changelog): note browser CLI scope fix
2026-05-14 02:20:14 -05:00
Vincent Koc
b1fc55fded fix(web): honor legacy search api key selection 2026-05-14 15:14:27 +08:00
Vincent Koc
8962d35264 refactor(cli): mark parent help in descriptors 2026-05-14 15:11:33 +08:00
Vincent Koc
d41907a5cb fix(slack): ignore malformed media redirects 2026-05-14 15:10:40 +08:00
Peter Steinberger
b8dccbf310 ci: run package patch guard in pr checks 2026-05-14 08:09:28 +01:00
Mariano Belinky
949012797b fix: reference codex watchdog changelog entry 2026-05-14 08:07:47 +01:00
Mariano Belinky
1aef36b60d fix(codex): keep post-tool watchdog armed 2026-05-14 08:07:47 +01:00
Vincent Koc
1e5641ba82 changelog: cover @sjf migrate trailing-period cleanup (#81705) 2026-05-14 15:03:30 +08:00
Peter Steinberger
0916a19cb5 ci: block new package patches 2026-05-14 07:57:59 +01:00
Vincent Koc
1d5f01500d fix(matrix): tolerate malformed location params 2026-05-14 14:55:52 +08:00
Peter Steinberger
625713091e docs: clarify plugin externalization guidance 2026-05-14 07:53:25 +01:00
Sarah Fortune
d9e999cf4f fix(migrate): drop trailing periods from migrate item messages (#81705) 2026-05-13 23:52:19 -07:00
Peter Steinberger
81b239dc98 build: externalize slack openshell vertex plugins 2026-05-14 07:46:58 +01:00
Vincent Koc
7f05ea60fa changelog: cover Windows sandbox bind, env-marker inference, bodyless media
- (#63074) Security/sandbox: include Windows USERPROFILE in blocked home roots. Thanks @luoyanglang.
- Models config/auth: stop inferring providers from broad env-var name patterns; use structured SecretRefs only. Thanks @sallyom.
- Media fetch: avoid buffering bodyless responses. Thanks @shakkernerd.
2026-05-14 14:46:06 +08:00
Vincent Koc
8ec9bfb31e fix(ci): authenticate performance report publishing 2026-05-14 14:40:20 +08:00
Josh Avant
4e1f59010e fix(gateway): suppress startup liveness warnings (#81699)
* fix(gateway): suppress startup liveness warnings

* docs(changelog): note diagnostic startup grace fix
2026-05-14 01:39:46 -05:00
Vincent Koc
25eef1203a fix(plugins): prefer installed memory tool owners 2026-05-14 14:35:45 +08:00
Vincent Koc
d656cda46d fix(process): normalize Windows child env keys 2026-05-14 14:34:00 +08:00
Peter Steinberger
5479b6b32c build(deps): consume fs-safe 0.2.3 2026-05-14 07:31:40 +01:00
Peter Steinberger
36755e4057 [codex] externalize amazon bedrock providers (#81687)
* build: externalize amazon bedrock providers

* build: skip external plugins in root dist graph

* test: update managed npm override expectation

* build: mark amazon providers external-only
2026-05-14 07:27:40 +01:00
Vincent Koc
6bd19bffd6 changelog: cover @sjf onboarding wizard provider-flag forwarding (#81669) 2026-05-14 14:23:47 +08:00
Vincent Koc
31de033590 fix(hooks): allow dot-prefixed handler paths 2026-05-14 14:23:13 +08:00
Vincent Koc
e064cc98f0 fix(ci): skip locale refresh on invalid provider auth 2026-05-14 14:13:39 +08:00
Sarah Fortune
0a42afae3a fix(onboard): forward provider auth flags through wizard (#81669)
The wizard's applyAuthChoice call dropped provider-specific flag values
like --openai-api-key, only forwarding token/tokenProvider. As a result,
maybeApplyApiKeyFromOption could not honor the flag and onboarding still
prompted "Use existing OPENAI_API_KEY?" when the operator already
passed --openai-api-key alongside an existing env var (e.g. onboard-fast
harnesses that pre-seed --openai-api-key "$OPENAI_API_KEY").

Spread opts into the inner opts bag so provider-specific flag values
reach the provider auth method via ctx.opts. When no flag is passed the
env-confirm prompt still fires unchanged.
2026-05-13 23:12:40 -07:00
pashpashpash
da1ccd3077 fix(replies): preserve rich coalesced block replies (#81689) 2026-05-14 15:10:58 +09:00
Vincent Koc
2ab08c8a19 fix(cli): keep plugin parent help lightweight 2026-05-14 14:09:53 +08:00
Vincent Koc
c635f0087e fix(plugins): preserve dot-prefixed package metadata 2026-05-14 14:08:53 +08:00
Vincent Koc
3b7d01b63f fix(ci): prefer valid locale refresh provider 2026-05-14 14:03:06 +08:00
Vincent Koc
8f612787a8 fix(ci): restore control ui locale refresh 2026-05-14 13:58:27 +08:00
WhatsSkiLL
eefa6ecea0 fix(plugins): discover setup provider env vars (#81542)
Discover provider plugins from setup.providers[].envVars credentials during provider discovery while keeping the deprecated providerAuthEnvVars fallback.

Co-authored-by: JARVIS-Glasses <whatsskilll@gmail.com>
2026-05-14 06:58:05 +01:00
Vincent Koc
9518d12e13 fix(codex): remap dot-prefixed bootstrap context 2026-05-14 13:55:26 +08:00
Peter Steinberger
65ea6fdb49 docs: clarify Codex home isolation 2026-05-14 06:51:57 +01:00
Vincent Koc
af3d9333aa fix(agents): remap dot-prefixed context paths 2026-05-14 13:40:25 +08:00
Vincent Koc
cd42df45d6 fix(telegram): allow dot-prefixed local media 2026-05-14 13:32:29 +08:00
Vincent Koc
3485a907d1 fix(auth): stop Codex OAuth refresh spam
Treat high-confidence app-server OAuth refresh invalidation as terminal auth-profile failure, while keeping entitlement and rate-limit payloads out of re-auth classification.
2026-05-14 13:28:47 +08:00
Vincent Koc
6cac228a0c changelog: cover @sjf migrate selection hint cleanup (#8da06d46f8f) 2026-05-14 13:24:20 +08:00
Vincent Koc
b73d01f13b fix(canvas): reject malformed document paths 2026-05-14 13:23:26 +08:00
samzong
bb8aa0cfe2 [Fix] Throttle agent event fanout (#80335)
Merged via squash.

Prepared head SHA: 5dddb405ad
Co-authored-by: samzong <13782141+samzong@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-13 22:21:46 -07:00
Vincent Koc
1fc92ddfb1 fix(cli): preserve multiline table colors 2026-05-14 13:18:08 +08:00
Vincent Koc
284dcc51b8 fix(replies): preserve rich outbound content 2026-05-14 13:17:06 +08:00
Vincent Koc
6cfb62d5aa fix(canvas): harden asset path resolution 2026-05-14 13:15:32 +08:00
Vincent Koc
bc0def52af fix(ci): read sparse package manifests from index 2026-05-14 13:11:42 +08:00
Vincent Koc
ce63b9ca46 fix(plugin-sdk): classify memory core alias 2026-05-14 13:11:42 +08:00
Sarah Fortune
8da06d46f8 fix(migrate): hide per-item hints in Codex skill/plugin selection prompts 2026-05-13 22:05:42 -07:00
Ayaan Zaidi
e44b915dbf docs(changelog): note oauthRef runtime auth fix (#81633) 2026-05-14 10:31:16 +05:30
Ayaan Zaidi
df4aac8f96 fix(auth): accept oauthRef profiles for runtime auth 2026-05-14 10:31:16 +05:30
Vincent Koc
c04bbd3cbb fix(agents): allow dot-prefixed sandbox paths 2026-05-14 12:52:24 +08:00
Vincent Koc
fe89243c3b fix(plugin-sdk): restore memory core alias 2026-05-14 12:50:22 +08:00
Val Alexander
6db2ee6583 fix(ios): restore privacy permission prompts
Restores first-use iOS authorization prompts for Contacts, Calendar, and Reminders by adding the missing usage descriptions, requesting access from `.notDetermined` in the service paths, and adding Settings Privacy & Access status/actions.

Verification:
- `plutil -lint apps/ios/Sources/Info.plist apps/ios/Tests/Info.plist apps/ios/ShareExtension/Info.plist apps/ios/ActivityWidget/Info.plist apps/ios/WatchApp/Info.plist apps/ios/WatchExtension/Info.plist`
- `swiftformat --lint apps/ios/Sources/Permissions/PermissionRequestBridge.swift apps/ios/Sources/Contacts/ContactsService.swift apps/ios/Sources/Calendar/CalendarService.swift apps/ios/Sources/Reminders/RemindersService.swift apps/ios/Sources/Settings/PrivacyAccessSectionView.swift apps/ios/Sources/Settings/SettingsTab.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift --config config/swiftformat`
- `swiftlint lint --config apps/ios/.swiftlint.yml apps/ios/Sources/Permissions/PermissionRequestBridge.swift apps/ios/Sources/Contacts/ContactsService.swift apps/ios/Sources/Calendar/CalendarService.swift apps/ios/Sources/Reminders/RemindersService.swift apps/ios/Sources/Settings/PrivacyAccessSectionView.swift apps/ios/Sources/Settings/SettingsTab.swift apps/ios/Sources/Onboarding/GatewayOnboardingView.swift apps/ios/Tests/PermissionRequestBridgeTests.swift`
- `git diff --check origin/main...HEAD`
- `rg '<<<<<<<|=======|>>>>>>>' CHANGELOG.md apps/ios apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift`
- `pnpm ios:build`
- `xcodebuild test -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 17' -configuration Debug -only-testing:OpenClawTests/PermissionRequestBridgeTests`
- Fresh-erased iPhone 17 simulator proof for Contacts denial/Open Settings, Calendar add-only/full-access upgrade, and Reminders authorization prompts.

Not tested: physical device, or a paired gateway command invocation after onboarding.
2026-05-13 23:45:35 -05:00
Vincent Koc
ca7349b585 fix(media): normalize cross-platform media paths 2026-05-14 12:43:15 +08:00
Val Alexander
dd4c68b525 fix(agents): suppress aborted assistant output
Summary:
- Suppress aborted embedded-run assistant partials, reasoning text, reply directives, and stale previous-assistant fallback output.
- Preserve clean timeout/error payloads, tool/media payloads, and compaction bookkeeping for non-aborted delivery paths.
- Add focused regressions for aborted partial text, reasoning text, stale fallback, and timeout delivery.

Verification:
- git diff --check HEAD~1..HEAD
- PATH=/Users/buns/.nvm/versions/node/v22.22.2/bin:$PATH pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/agents/pi-embedded-runner/run.ts src/agents/pi-embedded-runner/run/payloads.errors.test.ts src/agents/pi-embedded-runner/run/payloads.ts
- PATH=/Users/buns/.nvm/versions/node/v22.22.2/bin:$PATH pnpm test src/agents/pi-embedded-runner/run/payloads.errors.test.ts src/agents/pi-embedded-runner/run/payloads.test.ts -- --reporter=verbose
- PATH=/Users/buns/.nvm/versions/node/v22.22.2/bin:$PATH pnpm test src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts src/auto-reply/reply/get-reply-run-queue.test.ts src/auto-reply/reply/abort.test.ts -- --reporter=verbose
- PATH=/Users/buns/.nvm/versions/node/v22.22.2/bin:$PATH pnpm test src/auto-reply/inbound.test.ts -- --reporter=verbose
- PATH=/Users/buns/.nvm/versions/node/v22.22.2/bin:$PATH pnpm check:changed
- GitHub CI run 25841945093 passed, including checks-node-auto-reply-core-top-level, checks-node-core, and build-artifacts.
- Real behavior proof run 25841947282 passed.

Fixes #48241
Thanks @BunsDev
2026-05-13 23:41:59 -05:00
Vincent Koc
89987df3d5 test(matrix): guard externalized runtime deps 2026-05-14 12:38:17 +08:00
Vincent Koc
b97b5d6f22 changelog: cover three @sjf codex-migrate UX fixes
- humanize MIGRATION_REASON_* codes in conflict-status messaging
- swap migrate glyphs for manual-review (🔍) and archive (📖) items
- split codex-migrate output into preview + result phases
2026-05-14 12:36:45 +08:00
Bek
aa39107261 docs: align Slack docs for socket mode and troubleshooting (#81647) 2026-05-14 00:34:30 -04:00
Vincent Koc
117d2c9f2e changelog: cover Codex MCP, migration binary, and subagent maintenance fixes
- (#81551) Codex app-server MCP server projection, thread rotation, resume resend (jalehman)
- (#81582) Codex migration uses managed codex binary (fuller-stack-dev)
- (#81498) Subagent registry sessions preserved through maintenance (ai-hpc)
2026-05-14 12:31:58 +08:00
Vincent Koc
f5ebe63ecd fix(auto-reply): preserve debounce ordering 2026-05-14 12:29:30 +08:00
Sarah Fortune
2d231cef27 fix(migrate): humanize conflict-status messaging across migrate UI
Replace internal `MIGRATION_REASON_*` codes with natural sentences across the migrate UI.

| Surface | Before | After |
| --- | --- | --- |
| Selection prompt (skill) | `(Codex CLI skill; conflict: target exists)` | `(Codex skill already installed in workspace)` |
| Selection prompt (plugin) | `(openai-curated; conflict: plugin exists)` | `(openai-curated plugin already installed in workspace)` |
| Plan/result row (skill conflict) | `• conflict: gh-address-comments (Copy Codex skill into OpenClaw)` | `⚠️ gh-address-comments (Already installed in workspace.)` |
| Plan/result row (plugin conflict) | `• conflict: <name> (Install Codex plugin into OpenClaw)` | `⚠️ <name> (Already installed in workspace.)` |
2026-05-13 21:25:58 -07:00
rolandrscheel
e4cee2eb69 perf(gateway): cache session list resolver lookups
Refs #75839.\n\nRebases and lands the sessions.list resolver-cache fix from #77187 after maintainer conflict repair. The change keeps cache state scoped to a single sessions.list call and memoizes deterministic per-row resolver work for repeated provider/model tuples.\n\nVerification:\n- pnpm test src/gateway/session-utils.perf.test.ts src/gateway/session-utils.test.ts\n- pnpm exec oxfmt --check --threads=1 src/gateway/session-utils.ts src/gateway/session-utils.perf.test.ts scripts/github/real-behavior-proof-policy.mjs\n- git diff --check HEAD -- CHANGELOG.md scripts/github/real-behavior-proof-policy.mjs src/gateway/session-utils.perf.test.ts src/gateway/session-utils.ts\n- GitHub PR checks: 87 passing, CodeQL neutral, 21 skipped\n\nCo-authored-by: OpenClaw Agent <openclaw-agent@users.noreply.github.com>
2026-05-13 23:20:40 -05:00
Vincent Koc
5b418c3c4f fix(channels): preserve Telegram ordering without blocking follow-ups 2026-05-14 12:19:02 +08:00
Val Alexander
c722ae6a65 fix(control-ui): prevent iOS input zoom
Fixes #64651. Supersedes #64673.

Keeps shared form, config, and usage Control UI text-entry controls at 16px on touch-primary devices while preserving chat composer input sizing, so iOS Safari no longer auto-zooms focused fields.

Verification:
- pnpm exec oxfmt --check --threads=1 CHANGELOG.md ui/src/styles/components.css ui/src/styles/config.css ui/src/styles/usage.css ui/src/styles/chat/layout.test.ts ui/src/styles/components.test.ts ui/src/styles/config.test.ts ui/src/styles/usage.test.ts
- git diff --check
- pnpm test ui/src/styles/chat/layout.test.ts ui/src/styles/components.test.ts ui/src/styles/config.test.ts ui/src/styles/usage.test.ts
- pnpm check:changed
- Playwright WebKit iPhone 12 computed-style proof for all targeted controls at 16px
- GitHub Real behavior proof, CI, and workflow sanity on exact PR head fa0d44a8fd
2026-05-13 23:17:34 -05:00
Val Alexander
5d4a8b0072 fix(agents): make trajectory cleanup timeout configurable
Refs #75839.\n\nAdds OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS and OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS for agent cleanup steps while preserving the 10s default. Includes focused timeout precedence tests, trajectory docs, and changelog coverage.\n\nVerification:\n- pnpm test src/agents/run-cleanup-timeout.test.ts\n- pnpm exec oxfmt --check --threads=1 src/agents/run-cleanup-timeout.ts src/agents/run-cleanup-timeout.test.ts\n- pnpm format:docs:check docs/tools/trajectory.md\n- git diff --check\n- pnpm check:changed\n- GitHub PR checks: 88 passing, CodeQL neutral, 21 skipped
2026-05-13 23:09:56 -05:00
Vincent Koc
5496c0d5b7 docs(testing): clarify pnpm proof routing 2026-05-14 12:09:17 +08:00
Josh Lehman
1ee0d51e92 fix(codex): preserve MCP servers in app-server harness (#81551)
* Plumb bundle MCP config into Codex app server

* fix: align codex mcp thread config with pi

* fix: rotate codex mcp threads when disabled

* fix: scope codex bundle mcp to bundled servers

* fix(codex): resend user MCP config on resume

---------

Co-authored-by: Josh Lehman <phaedrus@Mac.hsd1.ca.comcast.net>
2026-05-13 21:05:20 -07:00
Val Alexander
4935e24c7a fix: reconcile control ui run status cleanup
Fix stale Control UI active-run cleanup across terminal, reconnect, reset, and session-switch paths. Adds shared run lifecycle cleanup, stale compaction/fallback reconciliation, focused tests, and the compact composer run-status chip. Fixes #76874 and #64220; refs #71630. Validated with green PR CI on head 141f07158f and focused local UI tests.
2026-05-13 23:03:45 -05:00
Josh Lehman
aac216d699 fix: route plugin LLM completions through Codex runtime (#81511)
* fix: route plugin LLM completions through Codex runtime

* fix: preserve OpenRouter completion model ids

* fix: allow registry config compat guards
2026-05-13 21:02:28 -07:00
Vincent Koc
3b8ac38ae9 fix(codex): classify app-server auth refresh failures
Classify Codex native/app-server auth refresh logout failures and preserve app-server relogin detail in RPC errors.
2026-05-14 11:56:18 +08:00
Kevin Lin
78644bc6de fix: carry codex migration config through onboarding 2026-05-13 20:50:07 -07:00
Vincent Koc
3da9027770 fix(update): allow update config size drops 2026-05-14 11:47:17 +08:00
Val Alexander
256377c029 feat(ui): add WebChat auto-scroll mode selector
Add a persisted Control UI/WebChat auto-scroll mode setting with near-bottom, always, and off modes. The implementation preserves the current near-bottom behavior by default, keeps manual scroll-to-bottom available when automatic scrolling is off, exposes the selector in desktop and mobile chat controls, syncs i18n fallbacks, and adds focused storage/render/scroll coverage.

Verification:
- pnpm test ui/src/ui/app-settings.test.ts ui/src/ui/views/chat.test.ts ui/src/ui/app-render.helpers.node.test.ts ui/src/ui/app-render.helpers.browser.test.ts ui/src/ui/storage.node.test.ts ui/src/ui/app-scroll.test.ts -- --reporter=verbose
- pnpm check:changed
- pnpm ui:i18n:check
- pnpm ui:build
- PR CI green on head 1b8859c8ba

Fixes #7648.
Fixes #81287.
2026-05-13 22:24:53 -05:00
NVIDIAN
7c5222a195 fix: preserve pending subagent sessions during maintenance (#81498) 2026-05-13 23:19:18 -04:00
pashpashpash
78eb92e622 Route Codex message tool replies back to WebChat and TUI (#81586)
* fix: route internal ui message tool replies

* docs: document reserved codex sdk helpers

* test(gateway): stabilize sessions send agent assertion

* fix(agents): preserve rich internal source replies

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-05-14 11:55:54 +09:00
Val Alexander
faa443a452 fix(chat/ios): downscale image attachments before send
Resize iOS chat PhotosPicker image attachments through the shared JPEG transcoder before staging/sending. Cap long edge and payload bytes, strip source metadata, preserve previews from processed data, and add focused processor/view-model regression tests.\n\nFixes #68524.\nSupersedes #73710.
2026-05-13 21:44:05 -05:00
Jerry-Xin
61ae9b7193 fix(update): preserve config during update repair
Preserve update-time config state by snapshotting before repair/restart writes, keeping plugin install records available for migration, and blocking unsafe update-time config size drops.

Also documents the Codex reserved SDK subpaths needed by the plugin contract guardrail.

Fixes #80077.

Thanks @Jerry-Xin and @vincentkoc.

Co-authored-by: Jerry-Xin <3401616+Jerry-Xin@users.noreply.github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-05-14 10:43:33 +08:00
Jason
3fd64a281f fix: use managed codex binary for source migration (#81582)
* fix: use managed codex binary for source migration

* docs: document codex reserved sdk subpaths
2026-05-13 19:40:54 -07:00
Vincent Koc
c95ccf43c1 fix: sync codex cli package pin 2026-05-14 10:35:18 +08:00
Val Alexander
6a41a54212 fix(macos): harden direct gateway TLS pinning
Summary:
- Require macOS system trust before saving and accepting first-use direct `wss://` gateway TLS pins.
- Honor `gateway.remote.tlsFingerprint` in macOS direct node-mode TLS params.
- Add focused Swift coverage and update remote gateway docs/changelog.

Verification:
- Local: swiftformat --lint on touched Swift files.
- Local: git diff --check HEAD~1..HEAD.
- Local: swift test --package-path apps/shared/OpenClawKit --filter GatewayTLSPinningTests.
- Local: swift test --package-path apps/macos --filter 'MacNodeModeCoordinatorTests|GatewayEndpointStoreTests'.
- Local: PATH=/Users/buns/.nvm/versions/node/v24.13.0/bin:$PATH pnpm docs:list.
- CI: macos-node, macos-swift, check-docs, security-fast, security-scm-fast, security-dependency-audit, Opengrep OSS, and changed-path checks passed on PR head cf383fc047.

Fixes #50642.
Supersedes #50643.
2026-05-13 21:30:22 -05:00
Eduardo Piva
983064f5f8 fix(sessions): report ACP-runtime metadata for ACP-keyed sessions
Report ACP control-plane session runtime metadata from persisted ACP session metadata/backend, and keep ACP-shaped bridge sessions on normal configured model/runtime metadata.

Proof: focused sessions runtime/model-display tests, core prod/test typechecks, touched-file format check, seeded openclaw sessions --json behavior proof, and passing relevant CI. Known unrelated red check: checks-fast-contracts-plugins-d plugin SDK documentation contract for codex helper subpaths.
2026-05-13 19:03:50 -07:00
Sarah Fortune
bce56bacc7 fix(migrate): swap glyphs on manual-review and archive item rows
Manual-review items are kind:"manual" with status:"skipped" so they were rendering with ⏭️, which reads like "done, ignored" — exactly the wrong signal for items that still need user attention. Render with 🔍 instead so the row says "look closer here".

Archive items end up status:"migrated" once written to the report dir, so they were rendering with , which overstates what happened — the file was saved aside, not imported. Render with 📖 so the row reads "filed away".

Skill/plugin/secret/memory rows continue to render with their status glyphs (  ⏭️ ⚠️) unchanged. JSON output (--json) is unaffected.
2026-05-13 18:58:17 -07:00
Vincent Koc
e774b25b2f fix(agents): preserve reply metadata through tool media 2026-05-14 09:54:31 +08:00
pashpashpash
3ce922437f fix: load Codex for selectable OpenAI agent models
Treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair.\n\nPR: https://github.com/openclaw/openclaw/pull/81591
2026-05-14 09:51:15 +08:00
Vincent Koc
97ed9b2d82 test(agents): fix live profile lint 2026-05-14 09:19:04 +08:00
Vincent Koc
5923d9e807 fix(plugin-sdk): export codex runtime helpers 2026-05-14 09:19:04 +08:00
Vincent Koc
a504cd0190 test: make root permission assertions deterministic 2026-05-14 08:52:41 +08:00
598 changed files with 20801 additions and 3078 deletions

View File

@@ -0,0 +1,115 @@
---
name: codex-review
description: "Codex code review closeout: local dirty changes, PR branch vs main, parallel tests."
---
# Codex Review
Run Codex's built-in code review as a closeout check. This is code review (`codex review`), not Guardian `auto_review` approval routing.
Use when:
- user asks for Codex review / autoreview / second-model review
- after non-trivial code edits, before final/commit/ship
- reviewing a local branch or PR branch after fixes
## Contract
- Treat review output as advisory. Never blindly apply it.
- Verify every finding by reading the real code path and adjacent files.
- Read dependency docs/source/types when the finding depends on external behavior.
- Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
- Keep going until Codex review returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun Codex review.
- Stop as soon as the review command/helper exits 0 with no accepted/actionable findings. Do not run an extra direct `codex review` just to get a nicer "clean" line, a second opinion, or clearer closeout wording.
- Treat the helper's successful exit plus absence of actionable findings as the clean review result, even if the underlying Codex CLI output is terse.
- If rejecting a finding as intentional/not worth fixing, add a brief inline code comment only when it explains a real invariant or ownership decision that future reviewers should know.
- Do not push just to review. Push only when the user requested push/ship/PR update.
## Pick Target
Dirty local work:
```bash
codex review --uncommitted
```
Use this only when the patch is actually unstaged/staged/untracked in the
current checkout. For committed, pushed, or PR work, review the branch against
its base instead; do not force `--mode local` / `--uncommitted` just because the
helper docs mention dirty work first. A clean `--uncommitted` review only proves
there is no local patch.
Branch/PR work:
```bash
git fetch origin
codex review --base origin/main
```
Do not pass an inline prompt with `--base`; current CLI rejects `--base` + `[PROMPT]` even though help text is ambiguous. If custom instructions are needed, run the plain base review first, then do a local/manual follow-up pass.
If an open PR exists, use its actual base:
```bash
base=$(gh pr view --json baseRefName --jq .baseRefName)
codex review --base "origin/$base"
```
Committed single change:
```bash
codex review --commit HEAD
```
## Parallel Closeout
Format first if formatting can change line locations. Then it is OK to run tests and review in parallel:
```bash
scripts/codex-review --parallel-tests "<focused test command>"
```
Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain. Once that rerun exits cleanly, stop; do not spend another long review cycle on redundant confirmation.
## Context Efficiency
Codex review is usually noisy. Default to a subagent filter when subagents are available. Ask it to run the review and return only:
- actionable findings it accepts
- findings it rejects, with one-line reason
- exact files/tests to rerun
Run inline only for tiny changes or when subagents are unavailable.
## Helper
Bundled helper:
```bash
~/.codex/skills/codex-review/scripts/codex-review --help
```
If installed from `agent-scripts`, path is:
```bash
/Users/steipete/Projects/agent-scripts/skills/codex-review/scripts/codex-review --help
```
The helper:
- chooses dirty `--uncommitted` first
- otherwise uses current PR base if `gh pr view` works
- otherwise uses `origin/main` for non-main branches
- should be left in `--mode auto` or forced to `--mode branch` for committed/PR work; do not force `--mode local` after committing
- writes only to stdout unless `--output` or `CODEX_REVIEW_OUTPUT` is set
- supports `--dry-run` and `--parallel-tests`
- prints `codex-review clean: no accepted/actionable findings reported` when the selected review command exits 0
## Final Report
Include:
- review command used
- tests/proof run
- findings accepted/rejected, briefly why
- the clean review result from the final helper/review run, or why a remaining finding was consciously rejected
Do not run another Codex review solely to improve the final report wording. If the final helper run exited 0 and produced no accepted/actionable findings, report that exact run as clean.

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: codex-review [options]
Options:
--mode auto|local|branch Target selection. Default: auto.
--base REF Base ref for branch review. Default: PR base or origin/main.
--codex-bin PATH Codex binary. Default: codex.
--output FILE Also save output to file.
--parallel-tests CMD Run review and test command concurrently.
--dry-run Print selected commands, do not run.
-h, --help Show help.
Modes:
local codex review --uncommitted
branch codex review --base <base>
auto dirty tree -> local, else PR/current branch -> branch
EOF
}
mode=auto
base_ref=
codex_bin=${CODEX_BIN:-codex}
output=${CODEX_REVIEW_OUTPUT:-}
parallel_tests=
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
mode=${2:-}
shift 2
;;
--base)
base_ref=${2:-}
shift 2
;;
--codex-bin)
codex_bin=${2:-}
shift 2
;;
--output)
output=${2:-}
shift 2
;;
--parallel-tests)
parallel_tests=${2:-}
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
usage >&2
exit 2
;;
esac
done
case "$mode" in
auto|local|branch) ;;
*)
echo "invalid --mode: $mode" >&2
exit 2
;;
esac
git rev-parse --show-toplevel >/dev/null
current_branch=$(git branch --show-current 2>/dev/null || true)
dirty=false
if [[ -n "$(git status --porcelain)" ]]; then
dirty=true
fi
pr_url=
if [[ -z "$base_ref" && "$mode" != local ]] && command -v gh >/dev/null 2>&1; then
if pr_lines=$(gh pr view --json baseRefName,url --jq '[.baseRefName, .url] | @tsv' 2>/dev/null); then
base_name=${pr_lines%%$'\t'*}
pr_url=${pr_lines#*$'\t'}
if [[ -n "$base_name" ]]; then
base_ref="origin/$base_name"
fi
fi
fi
if [[ -z "$base_ref" ]]; then
base_ref=origin/main
fi
review_kind=
if [[ "$mode" == local || ( "$mode" == auto && "$dirty" == true ) ]]; then
review_kind=local
elif [[ "$mode" == branch || ( "$mode" == auto && -n "$current_branch" && "$current_branch" != "main" ) ]]; then
review_kind=branch
else
echo "no review target: clean main checkout and no forced mode" >&2
exit 1
fi
if [[ "$review_kind" == local ]]; then
review_cmd=("$codex_bin" review --uncommitted)
else
review_cmd=("$codex_bin" review --base "$base_ref")
fi
printf 'codex-review target: %s\n' "$review_kind"
printf 'branch: %s\n' "${current_branch:-detached}"
if [[ -n "$pr_url" ]]; then
printf 'pr: %s\n' "$pr_url"
fi
printf 'review:'
printf ' %q' "${review_cmd[@]}"
printf '\n'
if [[ -n "$parallel_tests" ]]; then
printf 'tests: %s\n' "$parallel_tests"
fi
if [[ "$review_kind" == branch ]]; then
printf 'fetch: git fetch origin --quiet\n'
fi
if [[ -n "$output" ]]; then
printf 'output: %s\n' "$output"
fi
if [[ "$dry_run" == true ]]; then
exit 0
fi
if [[ "$review_kind" == branch ]]; then
git fetch origin --quiet || {
echo "warning: git fetch origin failed; reviewing with existing refs" >&2
}
fi
run_review() {
if [[ -n "$output" ]]; then
mkdir -p "$(dirname "$output")"
"${review_cmd[@]}" 2>&1 | tee "$output"
else
"${review_cmd[@]}"
fi
}
if [[ -z "$parallel_tests" ]]; then
run_review
review_status=$?
if [[ "$review_status" == 0 ]]; then
printf 'codex-review clean: no accepted/actionable findings reported\n'
fi
exit "$review_status"
fi
review_status_file=$(mktemp)
tests_status_file=$(mktemp)
(
set +e
run_review
status=$?
printf '%s\n' "$status" > "$review_status_file"
) &
review_pid=$!
(
set +e
bash -lc "$parallel_tests"
status=$?
printf '%s\n' "$status" > "$tests_status_file"
) &
tests_pid=$!
wait "$review_pid" || true
wait "$tests_pid" || true
review_status=$(cat "$review_status_file")
tests_status=$(cat "$tests_status_file")
rm -f "$review_status_file" "$tests_status_file"
printf 'codex-review exit: %s\n' "$review_status"
printf 'tests exit: %s\n' "$tests_status"
if [[ "$review_status" != 0 || "$tests_status" != 0 ]]; then
exit 1
fi
printf 'codex-review clean: no accepted/actionable findings reported\n'

View File

@@ -19,9 +19,11 @@ or validating a change without wasting hours.
Prove the touched surface first. Do not reflexively run the whole suite.
1. Inspect the diff and classify the touched surface:
- source: `pnpm changed:lanes --json`, then `pnpm check:changed`
- tests only: `pnpm test:changed`
- one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- normal source checkout, source change: `pnpm changed:lanes --json`, then `pnpm check:changed`
- normal source checkout, tests only: `pnpm test:changed`
- normal source checkout, one failing file: `pnpm test <path-or-filter> -- --reporter=verbose`
- Codex worktree or linked/sparse checkout, one/few explicit files: `node scripts/run-vitest.mjs <path-or-filter>`
- Codex worktree or linked/sparse checkout, changed gates or anything broad: `node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox ... --shell -- "pnpm check:changed"`
- workflow-only: `git diff --check`, workflow syntax/lint (`actionlint` when available)
- docs-only: `pnpm docs:list`, docs formatter/lint only if docs tooling changed or requested
2. Reproduce narrowly before fixing.
@@ -36,6 +38,12 @@ Prove the touched surface first. Do not reflexively run the whole suite.
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files.
- If deps are missing, run `pnpm install`, retry once, then report the first actionable error.
- In a Codex worktree or linked/sparse checkout, do not run direct local
`pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, or `scripts/committer` until
you have verified pnpm will not reconcile or reinstall dependencies. Use
`node scripts/run-vitest.mjs` for tiny local proof, `node
scripts/crabbox-wrapper.mjs` for Testbox, and `git commit --no-verify` only
after the relevant remote or node-wrapper proof is already clean.
- For Blacksmith Testbox proof, use Crabbox first. `pnpm crabbox:run -- --provider
blacksmith-testbox --timing-json -- <command...>` warms, claims, syncs, runs,
reports, and cleans up one-shot boxes. Reuse only an id/slug created in this
@@ -55,6 +63,14 @@ OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
`pnpm test` wrapper so project routing, workers, and setup stay correct.
When the checkout is a Codex worktree, prefer the direct node harness instead:
```bash
node scripts/run-vitest.mjs <path-or-filter>
```
That keeps the test scoped without giving pnpm a chance to run dependency
status checks or install reconciliation in a linked worktree.
## Command Semantics

View File

@@ -1398,6 +1398,7 @@ jobs:
pnpm tool-display:check
pnpm check:host-env-policy:swift
pnpm dup:check:coverage
pnpm deps:patches:check
;;
prod-types)
pnpm tsgo:prod

View File

@@ -137,8 +137,10 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_PROVIDER: ${{ secrets.ANTHROPIC_API_KEY != '' && 'anthropic' || 'openai' }}
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-6' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
OPENCLAW_CONTROL_UI_I18N_THINKING: low
OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "1"
LOCALE: ${{ matrix.locale }}
run: node --import tsx scripts/control-ui-i18n.ts sync --locale "${LOCALE}" --write

View File

@@ -2154,27 +2154,11 @@ jobs:
fi
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
# Keep the release-blocking CI lane on Codex API-key auth. The
# staged auth-file path remains supported for local maintainer
# reruns, but it can hang on stale subscription/session state in
# an otherwise healthy release run.
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
# Replace the staged config.toml with a minimal CI-safe config so
# the repo stays trusted for MCP/tool use without inheriting
# maintainer-local provider/profile overrides that do not exist
# inside CI.
# Codex's workspace-write sandbox relies on user namespaces that
# this Docker lane does not provide, so run Codex unsandboxed
# inside the already-isolated container to keep MCP cron/tool
# execution representative instead of failing on nested sandbox
# setup.
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
;;
live-codex-harness-docker)
# Keep CI on the API-key path for now. The staged Codex auth secret
@@ -2395,14 +2379,11 @@ jobs:
fi
case "${{ matrix.suite_id }}" in
live-cli-backend-docker)
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
echo "OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1" >> "$GITHUB_ENV"
;;
live-codex-harness-docker)
echo "OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key" >> "$GITHUB_ENV"

View File

@@ -489,9 +489,7 @@ jobs:
reports_root=".artifacts/clawgrit-reports"
mkdir -p "$reports_root"
git -C "$reports_root" init -b main
git -C "$reports_root" remote add origin https://github.com/openclaw/clawgrit-reports.git
auth_header="$(printf 'x-access-token:%s' "$CLAWGRIT_REPORTS_TOKEN" | base64 -w0)"
git -C "$reports_root" config http.https://github.com/.extraheader "AUTHORIZATION: basic ${auth_header}"
git -C "$reports_root" remote add origin "https://x-access-token:${CLAWGRIT_REPORTS_TOKEN}@github.com/openclaw/clawgrit-reports.git"
if git -C "$reports_root" ls-remote --exit-code --heads origin main >/dev/null 2>&1; then
git -C "$reports_root" fetch --depth=1 origin main
git -C "$reports_root" checkout -B main FETCH_HEAD
@@ -501,10 +499,13 @@ jobs:
- name: Publish to clawgrit reports
if: ${{ steps.kova.outputs.report_json != '' && steps.clawgrit.outputs.present == 'true' }}
env:
CLAWGRIT_REPORTS_TOKEN: ${{ secrets.CLAWGRIT_REPORTS_TOKEN }}
shell: bash
run: |
set -euo pipefail
reports_root=".artifacts/clawgrit-reports"
git -C "$reports_root" remote set-url origin "https://x-access-token:${CLAWGRIT_REPORTS_TOKEN}@github.com/openclaw/clawgrit-reports.git"
ref_slug="$(printf '%s' "${TESTED_REF}" | tr -c 'A-Za-z0-9._-' '-')"
run_slug="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
dest="${reports_root}/openclaw-performance/${ref_slug}/${run_slug}/${LANE_ID}"

View File

@@ -31,6 +31,9 @@ Skills own workflows; root owns hard policy and routing.
- Core/tests: no deep plugin internals (`extensions/*/src/**`, `onboard.js`). Use public barrels, SDK facade, generic contracts.
- Owner boundary: owner-specific repair/detection/onboarding/auth/defaults/provider behavior lives in owner plugin. Shared/core gets generic seams only.
- Dependency ownership follows runtime ownership: plugin-only deps stay plugin-local; root deps only for core imports or intentionally internalized bundled plugin runtime.
- Internal bundled plugins ship in core dist; bundled-only facade loader ok only for them.
- External official plugins own package/deps and are excluded from core dist; core uses registry-aware `facade-runtime` or generic contracts.
- Externalizing a bundled plugin: update package excludes, official catalogs, docs, tests, and prove core runtime paths resolve installed plugin roots before root-dep removal.
- Legacy config repair belongs in `openclaw doctor --fix`, not startup/load-time core migrations. Runtime paths use canonical contracts.
- New seams: backward-compatible, documented, versioned. Third-party plugins exist.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
@@ -47,8 +50,10 @@ Skills own workflows; root owns hard policy and routing.
- Package manager/runtime: repo defaults only. No swaps without approval.
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
- Tests: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- Checks: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); never add `tsc --noEmit`, `typecheck`, `check:types`.
- Formatting: `oxfmt`, not Prettier. Use repo wrappers (`pnpm format:*`, `pnpm lint:*`, `scripts/run-oxlint.mjs`).
@@ -57,7 +62,8 @@ Skills own workflows; root owns hard policy and routing.
## Validation
- Use `$openclaw-testing` for test/CI choice and `$crabbox` for remote/full/E2E proof.
- Small/narrow tests, lints, format checks, and type probes are fine locally.
- Small/narrow tests, lints, format checks, and type probes are fine locally only in a healthy normal checkout.
- In Codex worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
- Full suites, broad changed gates, Docker/package/E2E/live/cross-OS proof, or anything that bogs down the Mac: Crabbox/Testbox.
- One/few files local. If a local command fans out, stop and move broad proof to Crabbox/Testbox.
- Before handoff/push: prove touched surface. Before landing to `main`: issue proof plus appropriate full/broad proof unless scope is clearly narrow.
@@ -79,13 +85,14 @@ Skills own workflows; root owns hard policy and routing.
- PR verification: before merge, post exact local commands, CI/Testbox run IDs, before/after proof when used, and known proof gaps.
- Issue fixed on `main` with proof: comment proof + commit/PR, then close.
- After landing or requested close/sweep: search duplicates; comment proof + canonical commit/PR/release before closing.
- After landing/ship final: include 2-5 sentence recap of what landed: behavior change, key files/surface, proof run, issue/PR state. Do not answer with only status/links.
- `ship` that fixes an issue: after push, comment proof + commit link, then close the issue.
- GH comments with backticks, `$`, or shell snippets: use heredoc/body file, not inline double-quoted `--body`.
- PR create: real body required. Include Summary + Verification; mention refs, behavior, and proof.
- Real behavior proof section is parsed. Use exact `field: value` labels: `Behavior addressed`, `Real environment tested`, `Exact steps or command run after this patch`, `Evidence after fix`, `Observed result after fix`, `What was not tested`.
- PR artifacts/screenshots: attach to PR/comment/external artifact store. Do not commit `.github/pr-assets`.
- CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need.
- Maintainers: ignore `Real behavior proof` failures that only say PR body lacks real after-fix evidence.
- Maintainers: may skip/ignore `Real behavior proof` when local tests or Crabbox verified behavior; record proof in PR verification.
- `/landpr`: use `~/.codex/prompts/landpr.md`; do not idle on `auto-response` or `check-docs`.
## Code

View File

@@ -6,15 +6,111 @@ Docs: https://docs.openclaw.ai
### Changes
- Gateway auth: add `gateway.auth.tokenScopes` for trusted headless shared-token WebSocket clients to receive static operator scopes such as `operator.read`, with privileged scopes bounded to loopback binds unless `gateway.auth.allowPrivilegedTokenScopes` is explicitly enabled. Fixes #80836 and #81775. Thanks @jchoi-atn and @vivekchand.
- Control UI/i18n: add a `pnpm ui:i18n:report` baseline report for hardcoded-copy focus areas and locale fallback metadata. (#81320) Thanks @samzong.
- Maintainer tooling: add a repo-local `codex-review` skill for Codex closeout reviews, including local dirty-work and PR-branch review helpers that rerun until no accepted/actionable findings remain and avoid unsupported inline prompts with `--base`.
- Maintainer tooling: fail CI when pull requests add package patch files or pnpm patched dependencies, preserving the upstream-and-bump dependency workflow.
- Amazon Bedrock: externalize the Bedrock and Bedrock Mantle provider packages so core installs no longer pull AWS SDK dependencies unless those providers are installed.
- Plugins: externalize Slack, OpenShell sandbox, and Anthropic Vertex so their runtime dependency cones install only when those plugins are installed.
- Codex migration: remove the bundled `codex-cli` backend and repair legacy `codex-cli/*` model refs to the Codex app-server route on `openai/*`.
- Control UI/WebChat: add a persisted auto-scroll mode selector so users can keep the current near-bottom behavior, always follow streaming output, or turn automatic streaming scroll off and use the New messages button manually. Fixes #7648 and #81287. Thanks @BunsDev.
- ACP: add `acp.fallbacks` so ACP turns can try configured backup runtime backends when the primary backend is unavailable before any output is emitted. (#69542) Thanks @kaseonedge.
- Gateway/startup: add owner-level startup trace attribution for auth, plugin loading, lookup counts, and plugin sidecar services. (#81738) Thanks @samzong.
### Fixes
- CLI/migrate: handle delayed Codex plugin marketplace responses so warnings, next-steps, and conflict states render with ⚠️ glyphs and post-install migration retries the marketplace fetch instead of silently skipping plugin items. (#81625) Thanks @sjf.
- Channels/Weixin: bump the bundled `@tencent-weixin/openclaw-weixin` external entry to `2.4.3` (from `2.4.1`) so onboarding and `openclaw channels add` install the current Tencent Weixin (personal WeChat) plugin release. (#81730) Thanks @scotthuang.
- CLI: lazy-load model, plugin, and device runtime helpers and keep channel option help on generated startup metadata or generic fallback text so parent/help output renders without importing those runtime paths.
- CLI: route `plugins list --json` through the parsed command fast path and cover it in response budgets so plugin JSON inventory avoids full CLI registration work.
- Gateway/session history: carry monotonic transcript message sequence through live updates and refresh SSE history when stale sequence input would otherwise append bad incremental state. (#81474) Thanks @samzong.
- Memory/daily-files: widen the daily-memory file matcher used by Dreaming, rem-backfill, rem-harness, the doctor sweep, and short-term promotion so `memory/YYYY-MM-DD-<slug>.md` files written by the bundled session-memory hook (and any future slugged variants) are discovered alongside the date-only `memory/YYYY-MM-DD.md` shape. Date extraction still uses the leading `YYYY-MM-DD` capture group, so per-day ingestion/promotion semantics are unchanged for existing date-only files; slugged files now flow through the same paths instead of being silently skipped. Fixes #69536. Thanks @jack-stormentswe.
- macOS/Gateway: fail managed LaunchAgent stop and restart when the configured gateway port remains busy after cleanup instead of reporting success while a listener survives. Fixes #73132. Thanks @BunsDev.
- Telegram: ship the isolated polling worker at the root dist path used by the bundled worker loader, avoiding startup failures looking for `dist/telegram-ingress-worker.runtime.js`.
- Security/sandbox: include Windows `USERPROFILE` in the sandbox blocked home roots so credential-bearing binds (such as `.codex`, `.openclaw`, or `.ssh` under the Windows user profile) are denied even when `HOME` points at a different shell home. (#63074) Thanks @luoyanglang.
- Gateway/OpenAI-compatible HTTP: parse shared JSON endpoint paths without trusting malformed Host headers, avoiding 500s before `/v1/chat/completions`, `/v1/responses`, and `/v1/embeddings` request handling.
- Telegram: keep Bot API polling alive during main event-loop stalls by moving ingress to an isolated worker with a durable local spool. Fixes #81132. (#81746) Thanks @joshavant.
- Telegram: resolve plugin native commands with the active runtime config so commands like `/codex ...` stay on the native command path.
- Telegram: preserve rendered HTML formatting through lazy cron announce delivery so Markdown links stay clickable instead of falling back to literal anchor tags. Fixes #81742. (#81758)
- Voice-call webhooks: parse webhook and realtime upgrade paths without trusting malformed Host headers, avoiding 500s before provider signature checks or path rejection.
- Media store: reject malformed redirect `Location` headers as media-download failures instead of letting URL parsing escape the async response callback.
- ClickClack: skip malformed realtime websocket frames instead of stopping the channel monitor on a single bad JSON event.
- Browser tool: treat malformed node proxy `payloadJSON` responses as browser proxy failures instead of leaking raw JSON parser errors.
- Gateway HTTP: match models, session kill, and session history route paths without trusting malformed Host headers, avoiding pre-auth 500s on those endpoints.
- Google Meet/Codex: report malformed node proxy `payloadJSON` responses with plugin-owned errors instead of leaking raw JSON parser failures.
- Debug proxy: reject malformed relative-form proxy targets with a controlled 400 response instead of letting URL parsing escape the request handler.
- File transfer: reject malformed inline `file_write` base64 before computing hashes or invoking paired nodes, avoiding Node's lenient base64 decoder.
- QA channel: skip malformed inline inbound attachment base64 instead of staging silently corrupted media for agent turns.
- Microsoft Teams: reject malformed inline HTML image base64 padding instead of decoding corrupted `data:` image attachments.
- Voice-call realtime: ignore malformed provider media-frame base64 before forwarding audio into bridge and transcription paths.
- QQBot: reject malformed stored cron payload base64 before JSON decoding structured reminder data.
- Telnyx voice-call: use the raw `client_state` fallback when webhook state is malformed base64 instead of using silently corrupted decoded text.
- Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom.
- Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd.
- CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf.
- CLI/migrate: drop trailing periods from Codex migrate item messages and `REASON_CODE_MESSAGES` strings so plan/result rows read as labels instead of sentence fragments. (#81705) Thanks @sjf.
- Slack: treat malformed private-file redirect `Location` headers as unfollowable redirects instead of failing Slack media downloads.
- Matrix: ignore malformed percent-encoding in optional location URI parameters instead of letting a bad `geo:` event abort inbound message handling.
- Web search: auto-detect Brave through its legacy `tools.web.search.apiKey` compatibility fallback while keeping doctor migration to `plugins.entries.brave.config.webSearch.apiKey` as the canonical repair, so allowlisted isolated cron runs do not report `web_search` unavailable before migration. Fixes #81538. Thanks @atomicmonk.
- Plugins: memoize repeated in-process plugin metadata snapshots and keep vanished managed-install residue from forcing full derived discovery, reducing gateway/status startup scans under large plugin sets. Fixes #81143 and #79806. (#81570) Thanks @Kaspre, @holgergruenhagen, @JanPlessow, and @mjamiv.
- Plugins: discover provider plugins from `setup.providers[].envVars` credentials during provider discovery while keeping the deprecated `providerAuthEnvVars` fallback. (#81542) Thanks @JARVIS-Glasses.
- Docs/Codex harness: clarify that per-agent `CODEX_HOME` isolates `~/.codex` while inherited `HOME` intentionally keeps `.agents` discovery and subprocess user-home state available.
- CLI/plugins: keep bare plugin and parent-command help on the lightweight path, avoiding plugin registry discovery before rendering help.
- Auth: reclaim dead-owner stale file locks before retrying locked writes, so crashed OAuth refreshes no longer wedge `auth-profiles.json` until manual cleanup.
- CLI tables: preserve muted/color styling on wrapped continuation lines after multiline cells, keeping `openclaw plugins list` descriptions readable.
- Process execution: collapse case-insensitive duplicate child environment keys on Windows so caller-provided overrides such as `PATH` cannot be shadowed by host `Path`.
- Browser CLI: request the existing `operator.admin` gateway scope explicitly for browser control commands, avoiding unnecessary scope-upgrade approval loops. Fixes #81555. (#81716) Thanks @joshavant.
- Web: honor explicitly configured global `web_search` providers during provider ownership resolution while keeping sandboxed `web_fetch` limited to bundled providers.
- Agents: skip bootstrap file and hook preload work on completed `continuation-skip` turns when no workspace bootstrap is pending, reducing isolated-agent prep latency without changing first-turn bootstrap behavior. Fixes #81548. Thanks @delizaran-unpa.
- Config: validate JSON dry-runs against plugin-owned channel schemas, so external channel fields are not rejected by stale bundled schemas. Fixes #77887. (#81504) Thanks @giodl73-repo.
- iOS: restore first-use Contacts, Calendar, and Reminders permission prompts and add Privacy & Access status/actions in Settings. Thanks @BunsDev.
- Canvas: return not found for malformed percent-encoded Canvas/A2UI/document asset paths and keep decoded parent traversal blocked before path normalization.
- Telegram: allow trusted local Bot API media files whose filenames start with dots instead of falling back to remote download.
- Agents/Codex app-server: remap injected context files under dot-dot-prefixed workspace directories when a run switches to an effective sandbox workspace.
- Control UI/i18n: use the installed workspace pi runtime for locale refreshes, update the fallback package pin, and skip scheduled refreshes with invalid provider credentials instead of failing main.
- CI/performance: authenticate the clawgrit report repository remote during both checkout and publish so performance report pushes do not fail after benchmarks complete.
- Hooks: load workspace-relative legacy hook modules from dot-dot-prefixed directories without treating the filename prefix as parent traversal.
- Plugins: preserve installed package metadata and persisted registry freshness checks for plugin package paths under dot-dot-prefixed directories.
- Agents: allow dot-dot-prefixed filenames such as `..note.txt` through sandbox FS bridge, remote sandbox reads, and apply_patch summaries without mistaking the name for parent traversal.
- Gateway/diagnostics: suppress cold-start liveness warnings during the startup grace window while still sampling liveness metrics. Fixes #79915. (#81699) Thanks @joshavant.
- CLI/migrate: hide per-item source/plugin hints on non-conflicting Codex skill and plugin selection prompts, keeping the hint text reserved for rows that actually need attention. Thanks @sjf.
- Codex harness: treat high-confidence app-server OAuth refresh invalidation as a terminal auth-profile failure, stopping repeated raw token-refresh errors without turning entitlement or usage-limit payloads into re-auth prompts.
- CLI/migrate: humanize Codex conflict-status messaging across the migrate UI so selection prompts and plan/result rows say "Codex skill already installed in workspace" instead of surfacing internal `MIGRATION_REASON_*` codes. Thanks @sjf.
- CLI/migrate: render migrate result rows with distinct glyphs for manual-review (🔍) and archive (📖) items instead of the misleading "skipped" and "migrated" checkmarks, so users can see which entries still need attention versus which were filed away. Thanks @sjf.
- CLI/migrate: split Codex migrate output into separate preview and result phases so the Before plan and After result render through clack with independently tunable copy. Thanks @sjf.
- Codex app-server: project bundle and user MCP servers into Codex threads, rotate threads when an MCP server is disabled, scope bundle MCP injection to bundled servers, and resend user MCP config on resume so MCP changes take effect mid-session without restarting the agent. (#81551) Thanks @jalehman.
- Codex migration: invoke the managed Codex binary instead of a stale system `codex` for source-config migration plans, so users running the bundled Codex runtime get plan output that matches the binary the gateway will actually use. (#81582) Thanks @fuller-stack-dev.
- Subagents/maintenance: preserve pending subagent registry sessions during session-store cleanup, pruning, and disk-budget enforcement so in-flight subagent runs are not deleted by background maintenance before they complete. (#81498) Thanks @ai-hpc.
- Plugin SDK: restore the deprecated `openclaw/plugin-sdk/memory-core` package subpath as an alias of `memory-host-core`, so published memory companion plugins that still import it resolve on current hosts.
- Control UI/chat: reconcile terminal and reconnect run cleanup with cached session activity, stale compaction/fallback indicators, and a compact composer run-status chip so completed or interrupted turns do not leave Stop active. Fixes #76874 and #64220; refs #71630. Thanks @BunsDev.
- Maintainer tooling: clarify which pnpm test/check commands are safe locally versus inside Codex worktrees, routing linked-worktree gates through node wrappers and Crabbox/Testbox.
- Auto-reply: preserve same-key ordering when debounced inbound work falls back to immediate flushes, so follow-up turns cannot overtake an active buffered flush.
- Telegram/WhatsApp: keep Telegram same-chat replies ordered behind active no-delay turns without blocking WhatsApp follow-up message dispatch.
- Codex migration: avoid duplicate cached plugin bundle warnings when app-server plugin inventory is available.
- Agents: suppress aborted embedded assistant partials, reasoning text, reply directives, and stale prior replies before user-facing delivery while preserving clean timeout/error payloads. Fixes #48241. Thanks @BunsDev, @andyliu, and @yassinebkr.
- Agents: allow dot-dot-prefixed filenames such as `..file.txt` inside workspace and sandbox path policy while still rejecting real parent traversal.
- Native image input: detect Windows drive image paths in plain prompts so `C:\...\screenshot.png` references are not missed.
- Media: normalize Windows-style filename hints before staging attachments, remote media, audio transcodes, and saved-media display names, so POSIX hosts do not preserve drive or directory text in generated filenames.
- Media references: resolve first-level inbound media files whose IDs start with dots instead of treating names like `..photo.png` as parent traversal.
- iOS/chat: resize PhotosPicker image attachments to capped JPEGs before staging and sending, stripping source metadata and keeping oversized camera photos under the chat upload budget. Fixes #68524. Thanks @BunsDev.
- Control UI: keep shared form, config, and usage text-entry controls at 16px on touch-primary devices while preserving chat composer input sizing, so iOS Safari no longer auto-zooms focused fields. Fixes #64651; carries forward #64673. Thanks @NianJiuZst and @BunsDev.
- Codex harness: classify native app-server token-refresh logout and relogin failures as authentication refresh errors, so users get re-authentication guidance instead of a raw runtime failure.
- Agents/trajectory: make the trajectory flush cleanup timeout configurable with `OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS`, preserving the 10s default while slower stores drain. Refs #75839. Thanks @BunsDev.
- Codex startup: treat selectable configured OpenAI agent models as Codex runtime requirements during plugin auto-enable, startup planning, and doctor install repair, so Anthropic-primary configs can still switch to OpenAI/Codex cleanly.
- Agents: preserve source-reply delivery metadata when merging tool-returned media into the final reply, keeping message-tool-only replies deliverable and mirrored. Thanks @pashpashpash and @vincentkoc.
- Replies: treat rich presentation, interactive controls, and channel-native payload data as outbound content across follow-up, heartbeat, cron, ACP, and block-streaming delivery paths, preventing card/button-only replies from being dropped as empty.
- Replies: deliver rich-only block replies even when block-streaming coalescing is enabled, keeping card and button payloads from being dropped by the text coalescer. Thanks @pashpashpash.
- macOS/companion: require system TLS trust before pinning a first-use direct `wss://` gateway certificate and honor `gateway.remote.tlsFingerprint` as the explicit pin for remote node-mode sessions, so fresh endpoints fail closed when macOS cannot trust the certificate unless configured out of band. Fixes #50642. Thanks @BunsDev.
- Update: snapshot config before update-time repair and restart writes, preserve plugin install records through doctor cleanup, and keep update-time config size drops from blocking the update while pointing users to the pre-update backup. Fixes #80077. (#80257) Thanks @Jerry-Xin and @vincentkoc.
- WebChat/TUI: route Codex `tools.message` source replies to the active internal UI turn and mirror them to session history, so message-tool-only harness replies, including rich presentation and button-only replies, no longer disappear while WebChat and TUI remain non-targetable outbound channels. (#81586) Thanks @pashpashpash.
- Codex auth: accept OAuth profiles backed by `oauthRef` during runtime auth selection, so official Codex OAuth logins are used by app-server agent runs. (#81633) Thanks @obviyus.
- Sessions/status: classify ACP spawn-child sessions as `kind: "spawn-child"` instead of `"direct"` in `openclaw sessions` and status output; extract the duplicated session-kind classifier into a shared helper (`src/sessions/classify-session-kind.ts`) so both surfaces stay in sync. Fixes catalog #19. (#79544)
- Sessions/Gateway: report `agentRuntime.id: "acpx"` (or stored backend id) with `source: "session-key"` for ACP control-plane session rows in `openclaw sessions --json`, `openclaw status`, and Gateway session RPC responses instead of the incorrect `"auto"` / `"pi"` implicit fallback. Fixes catalog #18. (#79550)
- Telegram: delete tool-progress-only draft bubbles before rotating to the real answer, preventing orphaned progress messages in streamed replies.
- Codex app-server: keep per-agent `CODEX_HOME` isolation without rewriting `HOME` by default, so Codex-run subprocesses can still find normal user-home config, tokens, and CLI state unless the launch explicitly overrides `HOME`. Thanks @pashpashpash.
- ACP: preserve redacted numeric JSON-RPC `RequestError` details in runtime failure text, so backend diagnostics are visible instead of only `Internal error`. Fixes #81126. (#81188) Thanks @vyctorbrzezowski.
- Agents: cache unchanged PI model discovery stores and model lookups, reducing repeated model-resolution startup latency under large model configs. Fixes #78851.
- Onboarding: carry returned Codex plugin migration config through the OpenAI model wizard so accepted plugin migrations are saved with the final config write.
- Security/Windows ACL audit: classify Anonymous Logon, Guests, Interactive, Local, and Network SIDs as world-equivalent principals so broadly writable paths stay critical instead of being downgraded to group-writable. Fixes #74350. (#74383) Thanks @dwc1997.
- Media-understanding: retry transient remote attachment fetch failures before audio or vision processing, so Discord voice notes are not lost after one network/CDN blip. Fixes #74316. Thanks @vyctorbrzezowski and @gabrielexito-stack.
- Control UI: order timestamped live stream and tool items before untimestamped history fallbacks, keeping chat history in visible time order. Fixes #80759. (#81016) Thanks @akrimm702.
@@ -31,6 +127,7 @@ Docs: https://docs.openclaw.ai
- Config: serialize and retry semantic config mutations centrally, so concurrent commands can rebase safe changes instead of clobbering or hand-rolling command-local retry loops. (#76601)
- Require approval for setup-code device pairing [AI]. (#81292) Thanks @pgondhi987.
- Plugins/install: preserve third-party peer dependencies in the managed npm root when later plugin installs or updates recalculate the shared dependency tree. Thanks @shakkernerd.
- Plugins/memory: prefer the npm-installed memory-lancedb plugin over the bundled fallback during duplicate resolution, keeping Active Memory's `memory_recall` tool visible after managed installs. Fixes #81193. Thanks @julio-arcila.
- Plugins/uninstall: prune managed third-party peer dependencies after their owning npm plugin is removed, without blocking plugin cleanup on peer-prune failures.
- Docker: pin setup-time container paths so stale host `.env` OpenClaw paths cannot leak into Linux containers. Fixes #80381. (#81105) Thanks @brokemac79.
- Channels/WeCom: refresh the official onboarding install to `@wecom/wecom-openclaw-plugin@2026.5.7` and update existing managed npm installs instead of failing on the package directory. Fixes #79884. (#80390) Thanks @brokemac79.
@@ -57,6 +154,7 @@ Docs: https://docs.openclaw.ai
- Gateway: keep active reply runs visible to stuck-session diagnostics and clear no-active-work recovery state, preventing stale queued lanes after compaction or tool failures. Fixes #80677. (#81302)
- Codex app-server: rotate incompatible context-engine-managed native threads so Lossless-managed sessions do not resume stale hidden Codex history. (#81223) Thanks @jalehman.
- Codex cron: execute scheduled command-style automation payloads before workspace bootstrap or memory review, preserving existing isolated cron jobs after Codex harness migration. (#81510) Thanks @jalehman.
- Plugin LLM completions: honor Codex agent-runtime policy for canonical OpenAI model refs, so context-engine summarizers can use Codex OAuth instead of requiring direct `OPENAI_API_KEY` auth. (#81511) Thanks @jalehman.
- Gateway/OpenAI HTTP: return OpenAI-compatible 400 errors for invalid sampling params and provider validation failures instead of collapsing them to 500s. (#81275) Thanks @Lellansin.
- Telegram: publish plugin and skill command description localizations to native command menus while filtering unsupported locale codes and preserving Telegram command limits. (#81351) Thanks @jzakirov.
- Limit hook CLI tool authority [AI]. (#81065) Thanks @pgondhi987.
@@ -75,6 +173,7 @@ Docs: https://docs.openclaw.ai
- Reject truncated exec approval commands [AI]. (#81001) Thanks @pgondhi987.
- Enforce inline shell wrapper payload matching [AI]. (#80978) Thanks @pgondhi987.
- fix(node-pairing): replace changed pending requests [AI]. (#80894) Thanks @pgondhi987.
- Codex/status: align `/codex status` rate-limit wording with `/status` by showing remaining quota and compact reset durations instead of used quota and raw ISO timestamps. Thanks @MatthewSchleder.
- Rate limit Google Chat webhook requests [AI]. (#80974) Thanks @pgondhi987.
- Docker: mount the auth-profile secret key directory so OAuth-backed auth profiles survive container rebuilds. (#80991)
- Onboarding: accept Codex auth profiles for canonical OpenAI model checks, avoiding false missing-auth warnings. (#80913) Thanks @rubencu.
@@ -88,6 +187,7 @@ Docs: https://docs.openclaw.ai
- Telegram/groups: in single-account setups, treat an explicit empty `accounts.<id>.groups: {}` map the same as undefined so the root `channels.telegram.groups` allowlist still applies, instead of silently dropping every group update under the default `groupPolicy: "allowlist"`. Multi-account semantics are unchanged so per-account explicit-empty groups still scope-disable a single account without affecting siblings; the explicit way to block all groups for any account remains `groupPolicy: "disabled"`. Fixes #79427. (#81030) Thanks @kinjitakabe.
- Codex (app-server): project user-configured `mcp.servers` into new Codex thread configs, matching the codex-cli runtime's existing `-c mcp_servers=...` behavior so app-server-runtime agents see the same user MCP servers the CLI runtime already exposes. Plugin-curated apps remain attached via the separate `apps` config patch. Fixes #80814. Thanks @kinjitakabe.
- Enforce Slack plugin approval button authorization [AI]. (#80899) Thanks @pgondhi987.
- Mattermost: log a structured `mattermost no-visible-reply` diagnostic when a substantive (non-reasoning) final reply payload reaches `deliverMattermostReplyPayload` but the underlying `deliverTextOrMediaReply` returns `"empty"` — previously the run completed with a misleading `delivered reply to <channel>` log even though no Mattermost API send happened, masking silent completions in channel/thread contexts. No behavior change; the diagnostic surfaces the failure so operators can detect it instead of seeing the agent appear to go silent. Fixes #80501. Thanks @robbyproc87.
- Recognize PowerShell -ec inline commands [AI]. (#80893) Thanks @pgondhi987.
- fix(qqbot): authorize approval button callbacks [AI]. (#80892) Thanks @pgondhi987.
- Telegram: render supported HTML tags in streamed and durable replies instead of showing literal markup. (#80977)
@@ -114,6 +214,10 @@ Docs: https://docs.openclaw.ai
- Plugins doctor: report stale plugin config warnings and avoid claiming full plugin health when config warnings remain. (#81515) Thanks @BKF-Gitty.
- Sessions: display `model: "<agentId>-acp"` / `modelProvider: "acpx"` (ACP-runtime sentinel) for ACP control-plane sessions in `openclaw sessions` output, instead of the agent's configured model which was misleading. Catalog finding 20. (#79543)
- Slack: normalize message read `before` and `after` timestamp bounds before calling Slack history or thread reply APIs. Fixes #80835. (#81338) Thanks @honor2030.
- Gateway: throttle assistant/thinking agent event fanout during streaming bursts without dropping buffered deltas. (#80335) Thanks @samzong.
- Codex app-server: keep the short post-tool completion watchdog armed across dynamic tool completion bookkeeping so embedded Codex runs fail fast and release their session lane when Codex goes quiet after a tool result. (#81697) Thanks @mbelinky.
- Models: restore authenticated CLI runtime providers in the `/models` picker while keeping legacy runtime aliases hidden from setup/default model choices. Closes #81212. (#81239) Thanks @anagnorisis2peripeteia.
- Agents/cron: honor a cron payload's explicit `timeoutSeconds` for the LLM idle watchdog even when it numerically equals `agents.defaults.timeoutSeconds`, preserving explicit per-run timeout intent and preventing stalled streaming replies from being cut to the implicit 120s cap. (#79426) Thanks @legolaz8451.
### Changes
@@ -203,6 +307,7 @@ Docs: https://docs.openclaw.ai
- Codex app-server: mirror native Codex subagent spawn lifecycle events into Task Registry so app-server child agents appear in task/status surfaces without relying on transcript text. (#79512) Thanks @mbelinky.
- Gateway: expose optional `isHeartbeat` metadata on agent event payloads so clients can distinguish scheduled heartbeat runs from ordinary chat runs. (#80610) Thanks @medns.
- Agents: add `agents.defaults.runRetries` and `agents.list[].runRetries` config for embedded Pi runner retry loop limits. (#80661) Thanks @medns.
- Codex: add node-backed Codex CLI session listing and binding so an OpenClaw conversation can continue an existing Codex CLI session running on a paired node.
### Fixes
@@ -573,6 +678,7 @@ Docs: https://docs.openclaw.ai
- CLI/migrate: show native Codex plugin names before truncated plan items and prompt for plugin activation explicitly during interactive Codex migration instead of silently keeping every planned plugin. Thanks @kevinslin.
- CLI/migrate: leave already configured target Codex plugins unchecked in the interactive plugin selector and show a `plugin exists` conflict hint while keeping new plugin activations selected by default. Thanks @kevinslin.
- CLI/migrate: return cleanly without apply confirmation when interactive Codex migration leaves both skill copies and native plugin activations unselected. Thanks @kevinslin.
- Gateway/sessions: extend the per-call sessions-list `rowContext` cache with memoization for `resolveSessionDisplayModelIdentityRef`, thinking metadata, and `resolveModelCostConfig` so deterministic per-row resolvers run once per unique `(provider, model[, agentId])` tuple instead of once per session. Cuts CPU on `sessions.list` for stores with many sessions sharing a small set of model tuples; behavior is unchanged for callers that pass no `rowContext`. Thanks @rolandrscheel.
- Cron CLI: add `openclaw cron list --agent <id>`, normalize the requested agent id, and include jobs without a stored agent id under the configured default agent while keeping `cron list` unfiltered when no agent is supplied. Fixes #77118. Thanks @zhanggttry.
- Slack/performance: reduce message preparation, stream recipient lookup, and thread-context allocation overhead on Slack reply hot paths. Thanks @vincentkoc.
- Control UI/chat: strip untrusted sender metadata from live streams and transcript display, preserve canvas preview anchors, and stop operator UI clients from injecting their internal client id as sender identity. Fixes #78739. Thanks @tmimmanuel, @guguangxin-eng, @hclsys, and @BunsDev.
@@ -637,62 +743,6 @@ Docs: https://docs.openclaw.ai
### Fixes
- Models/auth: keep `agents.defaults.model` when `openclaw models auth login` runs without `--set-default`, so provider onboarding patches add models without silently switching the primary. Fixes #78162. (#78241) Thanks @neeravmakwana.
- Control UI/chat: localize the remaining chat welcome, composer, run-control, session/model/thinking selector, and zh-CN Skills labels through the Control UI i18n pipeline so non-English browser locales no longer see those chat controls in English. Fixes #79937. Thanks @BunsDev.
- Control UI: surface browser-blocked WebSocket security failures with wss:// and loopback dashboard guidance instead of leaving the connection on a dead security error. Thanks @BunsDev.
- Gateway/diagnostics: keep active-only transient event-loop max-delay samples as info-level stability telemetry instead of warning-level liveness diagnostics. Thanks @BunsDev.
- Google/Gemini: default new API-key onboarding to stable `google/gemini-2.5-flash` instead of the preview Pro route, reducing surprise daily quota exhaustion. Fixes #79670. Thanks @HugeBunny.
- Amazon Bedrock: expose Claude thinking profiles through the lightweight provider policy surface so `/think:adaptive` validates before the Bedrock runtime plugin is loaded. Fixes #79754. Thanks @phoenixyy and @hclsys.
- Codex/transcripts: mirror dynamic tool calls and outputs into Codex app-server transcripts so tool activity is visible alongside assistant text instead of being elided, with per-item output capped at 12,000 characters. (#79952) Thanks @scoootscooob.
- Memory: close temp SQLite handles before failed atomic reindex cleanup and retry Windows EBUSY/EPERM/EACCES temp file removals, so `memory index --force` does not abort or leave temp sidecars on locked filesystems. Fixes #79708. Thanks @LobsterFarmerAmp and @hclsys.
- Agents/CLI: add an explicit `reseedFromRawTranscriptWhenUncompacted` backend opt-in so safe invalidated CLI sessions can reseed from a bounded raw OpenClaw transcript tail before compaction while auth-boundary resets remain no-raw. Fixes #79713. (#79764) Thanks @hclsys.
- Agents/CLI: handle resumed CLI JSONL output and bound supervisor output buffering so resumed runs stay readable without letting noisy child output grow unbounded.
- Codex app-server: honor per-call `timeoutMs`, configured `image_generate` timeouts, and media image-understanding timeouts for dynamic tool calls, capped at 600000 ms, so slow image generation and image analysis no longer fail at the 30s bridge default. Fixes #79810. Thanks @omarshahine.
- Agents/sandbox: include the container workspace path hint in sandbox-root escape errors while preserving shortened host workspace roots. Fixes #79712. Thanks @haumanto and @hclsys.
- macOS/device pairing: let the native app read CLI PEM device identities and let the TypeScript loader migrate legacy Swift raw-key identities without generating a new device id, preventing repeated pairing prompts when `OPENCLAW_STATE_DIR` is shared. Fixes #76815. Thanks @BunsDev.
- Image generation: honor configured web-fetch SSRF policy across OpenAI, Google, MiniMax, OpenRouter, and Vydra provider requests so RFC2544 fake-IP proxy opt-ins reach generation calls. Fixes #79716. (#79765) Thanks @hclsys.
- Telegram: persist reply-chain message cache records as a compact append log instead of rewriting the full cache on every inbound message, reducing large-group turn latency.
- Telegram/CLI-backend: mirror outbound replies to the session transcript so CLI-backend agent responses create `.jsonl` session files, preventing `sessionId=unknown` on subsequent runs. Fixes #75991.
- Gateway/nodes: allow approved chat-channel macOS node exec replays to cross transient agent WebSocket reconnects only when node, agent session, and channel target metadata still match, restoring Telegram/WeCom host=node approvals without opening a general backend replay bypass. Fixes #77656. Thanks @BunsDev.
- QQBot: route gateway WebSocket connections through the ambient proxy agent so deployments with `https_proxy`, `HTTPS_PROXY`, or `HTTP_PROXY` can reach the QQ gateway. (#72961) Thanks @xialonglee.
- Agents/subagents: treat `sessions_spawn` `model: "default"` as the default-model fallback and ignore ACP-only stream targets for native sub-agent spawns. Fixes #72078. (#72101) Thanks @xialonglee.
- Agents/failover: stop retrying assistant-prefill format rejections across auth profiles or model fallbacks, surfacing the deterministic provider error instead of requeueing the lane. Fixes #79688. (#79728) Thanks @hclsys.
- Google/Gemini: resolve missing Gemini 3 Flash catalog rows through the Google provider template path so image-capable media-understanding models keep `input: ["text", "image"]` instead of falling back to text-only metadata. Fixes #79750. (#79759) Thanks @fenglanhua and @hclsys.
- Memory/QMD: warn with a manual stale collection removal hint when QMD reports a path/pattern conflict but `collection list` lacks verifiable metadata, avoiding unsafe stderr-only rebinds. Refs #71783. (#72297) Thanks @MonkeyLeeT.
- Models/auth: make `openclaw models status --check` and dashboard auth health honor effective auth profile order while keeping stale profiles visible. (#79685) Thanks @nimbleenigma.
- Agents/failover: classify bare `stream_read_error` streaming failures as transient timeouts so configured model fallback runs instead of surfacing the raw transport error. Fixes #79689. (#79692) Thanks @hekunwang.
- Agents/failover: persist overloaded auth-profile cooldown marks before exhausted fallback summaries surface, so immediate fallback retries honor the recorded cooldown state.
- Docs/Subagents: correct the listed sub-agent bootstrap context files to include `SOUL.md`, `IDENTITY.md`, and `USER.md`. (#79470) Thanks @lastguru-net.
- Backup: keep live backup archives from copying current agent session transcripts, cron run logs, and delivery queues while preserving workspace lock/temp files and keeping `--json` output parseable when volatile files are skipped. Fixes #72249. (#72251) Thanks @abnershang.
- Backup: place the temp manifest outside every backed-up asset so `backup create --verify` still passes when `TMPDIR` resolves inside a source path (for example `~/.openclaw/tmp`), avoiding the duplicate root manifest that otherwise tripped `Expected exactly one backup manifest entry, found 2`. Fixes #75007. Thanks @YaanFPV.
- OpenAI/Codex: install the Codex runtime plugin from npm during OpenAI onboarding and load it automatically for implicit OpenAI model routes, while preserving manual PI runtime overrides. Fixes #79358.
- OpenAI/realtime voice: defer `response.create` while a realtime response is still active, retry after `response.done`/`response.cancelled`, and align GA input transcription/noise-reduction defaults with the Codex realtime reference so Discord/Voice Call consult results can resume speaking instead of tripping the active-response race.
- OpenAI/realtime voice: avoid duplicate barge-in cancellation requests, log realtime model interruption/cutoff events in Discord voice logs, and treat OpenAI's no-active-response cancellation reply as a completed cancel so Discord voice sessions do not wedge pending speech after fast interruptions.
- Agents/runtime: strip trailing assistant prefill for Claude-family OpenAI Responses routes, persist prompt/assistant profile cooldown marks before fallback, and show the configured container root in sandbox escape diagnostics. Fixes #79688 and #79712. Thanks @stainlu and @mushuiyu886.
- Gateway: avoid false degraded event-loop health during rapid health/readiness/status probes unless sustained load has delay co-evidence, while keeping hard delay detection immediate. (#77028) Thanks @rubencu.
- Markdown: keep blockquote spans off trailing paragraph separators. Fixes #79646.
- Plugin SDK/LM Studio: recover Harmony plain-text tool calls from LM Studio streams. Fixes #78326.
- Control UI: refresh the model cache after `session_status(model=...)` changes a session model. Fixes #79613.
- Agents/context-engine: share loop-hook checkpoints with the after-turn finalizer so messages are not replayed. Fixes #79630.
- Codex app-server: keep native hook relays alive for long-running turns so shell and file approvals stay reachable until the configured run window finishes. (#77533) Thanks @rubencu.
- Gateway/macOS: clear ignored SIGUSR1 restart state, skip redundant package-update restarts when the refreshed LaunchAgent already serves the expected version, and give launchd a 10s throttle plus 20s shutdown window so update restarts do not leave old gateways alive or fight supervisor recovery. Fixes #79577; refs #78699 and #60885. Thanks @BunsDev.
- Status/Codex: route Codex-harness `openai/*` usage through the OpenAI Codex quota provider and scope CLI status usage to the default agent auth store so `/status` and `openclaw status --usage` show Codex quota windows again. Fixes #79312. Thanks @keshavbotagent.
- Matrix: keep joined strict DM rooms discoverable when stale `m.direct` mappings already point at an older strict room, and let `dm.sessionScope: "per-room"` promote safe unmapped strict rooms through the existing unnamed/unaliased room gate. Fixes #79514. Thanks @stainlu.
- Gateway/agent: pass the session-key agent id into inline image attachment validation so the first image in a fresh per-agent session uses the agent's vision-capable model override instead of the text-only system default. Fixes #79407. Thanks @pandadev66.
- Gateway/maintenance: prune dedupe overflow against a stable excess count and keep active agent retries from starting duplicate runs after cache eviction. (#73841) Thanks @thesomewhatyou.
- Control UI/subagents: suppress internal `subagent_announce` handoff prompts from requester transcripts and hide legacy inter-session wrapper rows so completed subagent results no longer surface runtime context in WebChat history. (#79618) Thanks @joshavant.
- Discord: preserve username target resolution for Discord outbound sends. (#79076) Thanks @vincentkoc.
- Gateway/sessions: rotate generated transcript paths when gateway sessions reset, complementing the daily-rollover transcript persistence. (#79076) Thanks @vincentkoc.
- Dependencies: pin the transitive `fast-uri` production dependency to `3.1.2` so the production dependency audit no longer resolves the vulnerable `<=3.1.1` range. Thanks @shakkernerd.
- Plugins/install: fail managed npm plugin installs when OpenClaw cannot repair a required plugin-local `node_modules/openclaw` peer link, preventing that peer-link failure mode from producing unusable `@openclaw/codex` installs. Refs #79462. Thanks @ai-hpc.
- xAI/tools: register and execute `x_search` and `code_execution` when the xAI API key comes from an auth profile, keeping the plugin tool gate aligned with `openclaw onboard --auth-choice xai-api-key`. Fixes #79353. Thanks @dbernaltbn.
- Cron/agents: recognize same-target `edit``write` recovery in `isSameToolMutationAction`, so a successful `write` to a path clears an earlier failed `edit` on the same path. Stops cron from reporting fatal failures when an agent self-heals across `edit` and `write`, while preserving same-tool fingerprint matching, blocking different-target writes, and excluding tools (including `apply_patch`) whose real call args do not produce a stable `path` fingerprint segment. Fixes #79024. Thanks @RenzoMXD.
- Gateway/Tailscale: add opt-in `gateway.tailscale.preserveFunnel` so when `tailscale.mode = "serve"` and an externally configured Tailscale Funnel route already covers the gateway port, OpenClaw skips re-applying `tailscale serve` on startup and skips the `resetOnExit` teardown for that run, keeping operator-managed Funnel exposure alive across gateway restarts. Fixes #57241. Thanks @RenzoMXD.
- CLI/router: when `openclaw <name>` does not match a CLI subcommand, check plugin tool manifests first so names like `lcm_recent` get an agent-tool diagnostic instead of the misleading suggestion to add the tool name to `plugins.allow`. Fixes #77214. Thanks @100yenadmin.
- QA-lab/parity: bump the live mock-openai parity baseline from `claude-opus-4-6`/`claude-sonnet-4-6` to `claude-opus-4-7`/`claude-sonnet-4-7` and the candidate alt from `gpt-5.4-alt` to `gpt-5.5-alt` in `openclaw-release-checks.yml` and `qa-live-transports-convex.yml`, matching the active Opus 4.7 / GPT-5.5 defaults already used elsewhere on main. Carries forward the surface-bump portion of #74290. Thanks @100yenadmin.
- QA-lab/scenarios: raise the `approval-turn-tool-followthrough` per-turn fallback timeouts from 20s/30s to 60s so cold mock-gateway parity runs do not flake on the approval-turn chain. Carries forward the timeout-bump portion of #74290. Thanks @100yenadmin.
- Gateway/restart continuation: treat routed post-reboot agent turns as trusted internal continuations while preserving the original Telegram topic route, and retry briefly when the previous run is still shutting down, so owner-only tools remain available for chained restart workflows after reboot.
- MS Teams: normalize pre-thread-qualified route session keys before deriving channel-thread lanes so cached route reuse cannot create malformed mixed `:thread:OLD:thread:NEW` sessions. Fixes #66771. (#78850) Thanks @harrisali0101.
- Agents/compaction: keep the recent tail after manual `/compact` when Pi returns an empty or no-op compaction summary, preventing blank checkpoints from replacing the live context.
- Native commands: handle slash commands before workspace and agent-reply bootstrap so Telegram `/status` and other command-only native replies do not wait behind full agent turn setup.
- Telegram/groups: include the recent local chat window and nearby reply-target window as generic inbound context so stale reply ancestry does not overshadow the live group conversation.
@@ -710,6 +760,7 @@ Docs: https://docs.openclaw.ai
- Gateway/macOS: `repairLaunchAgentBootstrap` no longer kickstarts an already-running LaunchAgent, preventing unnecessary service restarts and session disconnects when repair runs against a healthy gateway. Fixes #77428. Thanks @ramitrkar-hash.
- Gateway/macOS: `openclaw gateway stop --disable` now persists the LaunchAgent disable bit even after a previous bootout left the service not loaded, keeping the explicit stay-down path reliable. (#78412) Thanks @wdeveloper16.
- CLI/status: keep lean `openclaw status --json` off manifest-backed channel discovery so configured-channel checks do not repeatedly rescan plugin metadata. Fixes #79129.
- Gateway/Tailscale: add opt-in `gateway.tailscale.preserveFunnel` so when `tailscale.mode = "serve"` and an externally configured Tailscale Funnel route already covers the gateway port, OpenClaw skips re-applying `tailscale serve` on startup and skips the `resetOnExit` teardown for that run, keeping operator-managed Funnel exposure alive across gateway restarts. Fixes #57241. Thanks @RenzoMXD.
- Control UI/chat: hide retired and non-public Google Gemini model IDs from chat model catalogs and route the bare `gemini-3-pro` alias to Gemini 3.1 Pro Preview instead of the shut-down Gemini 3 Pro Preview. Thanks @BunsDev.
- CLI/infer: canonicalize case-only catalog model refs in `infer model run --model` so mixed-case provider/model strings resolve to the canonical catalog entry instead of failing with `Unknown model`. (#78940) Thanks @ai-hpc.
- CLI/infer: allow explicit local `infer model run --model <provider/model>` probes to use exact bundled static catalog rows before the provider is written to config, surfacing missing credentials as auth errors instead of `Unknown model`.

View File

@@ -4,15 +4,19 @@ import OpenClawKit
final class CalendarService: CalendarServicing {
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .event)
let authorized = EventKitAuthorization.allowsRead(status: status)
let authorized: Bool = if status == .notDetermined || status == .writeOnly {
await Self.requestFullEventAccess()
} else {
EventKitAuthorization.allowsRead(status: status)
}
guard authorized else {
throw NSError(domain: "Calendar", code: 1, userInfo: [
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
])
}
let store = EKEventStore()
let (start, end) = Self.resolveRange(
startISO: params.startISO,
endISO: params.endISO)
@@ -37,15 +41,19 @@ final class CalendarService: CalendarServicing {
}
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .event)
let authorized = EventKitAuthorization.allowsWrite(status: status)
let authorized: Bool = if status == .notDetermined {
await Self.requestWriteOnlyEventAccess()
} else {
EventKitAuthorization.allowsWrite(status: status)
}
guard authorized else {
throw NSError(domain: "Calendar", code: 2, userInfo: [
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
])
}
let store = EKEventStore()
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty else {
throw NSError(domain: "Calendar", code: 3, userInfo: [
@@ -95,6 +103,24 @@ final class CalendarService: CalendarServicing {
return OpenClawCalendarAddPayload(event: payload)
}
private static func requestFullEventAccess() async -> Bool {
await PermissionRequestBridge.awaitRequest { completion in
let store = EKEventStore()
store.requestFullAccessToEvents { granted, _ in
completion(granted)
}
}
}
private static func requestWriteOnlyEventAccess() async -> Bool {
await PermissionRequestBridge.awaitRequest { completion in
let store = EKEventStore()
store.requestWriteOnlyAccessToEvents { granted, _ in
completion(granted)
}
}
}
private static func resolveCalendar(
store: EKEventStore,
calendarId: String?,

View File

@@ -97,14 +97,17 @@ final class ContactsService: ContactsServicing {
return OpenClawContactsAddPayload(contact: Self.payload(from: persisted))
}
private static func ensureAuthorization(store: CNContactStore, status: CNAuthorizationStatus) async -> Bool {
private static func ensureAuthorization(status: CNAuthorizationStatus) async -> Bool {
switch status {
case .authorized, .limited:
return true
case .notDetermined:
// Dont prompt during node.invoke; the caller should instruct the user to grant permission.
// Prompts block the invoke and lead to timeouts in headless flows.
return false
return await PermissionRequestBridge.awaitRequest { completion in
let store = CNContactStore()
store.requestAccess(for: .contacts) { granted, _ in
completion(granted)
}
}
case .restricted, .denied:
return false
@unknown default:
@@ -113,15 +116,14 @@ final class ContactsService: ContactsServicing {
}
private static func authorizedStore() async throws -> CNContactStore {
let store = CNContactStore()
let status = CNContactStore.authorizationStatus(for: .contacts)
let authorized = await Self.ensureAuthorization(store: store, status: status)
let authorized = await Self.ensureAuthorization(status: status)
guard authorized else {
throw NSError(domain: "Contacts", code: 1, userInfo: [
NSLocalizedDescriptionKey: "CONTACTS_PERMISSION_REQUIRED: grant Contacts permission",
])
}
return store
return CNContactStore()
}
private static func normalizeStrings(_ values: [String]?, lowercased: Bool = false) -> [String] {

View File

@@ -52,6 +52,14 @@
</array>
<key>NSCameraUsageDescription</key>
<string>OpenClaw can capture photos or short video clips when requested via the gateway.</string>
<key>NSCalendarsUsageDescription</key>
<string>OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.</string>
<key>NSCalendarsFullAccessUsageDescription</key>
<string>OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.</string>
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string>OpenClaw uses your calendars to add events when you enable calendar access.</string>
<key>NSContactsUsageDescription</key>
<string>OpenClaw uses your contacts so you can search and reference people while using the assistant.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>OpenClaw discovers and connects to your OpenClaw gateway on the local network.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
@@ -64,6 +72,8 @@
<string>OpenClaw may use motion data to support device-aware interactions and automations.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>OpenClaw needs photo library access when you choose existing photos to share with your assistant.</string>
<key>NSRemindersFullAccessUsageDescription</key>
<string>OpenClaw uses your reminders to list, add, and complete tasks when you enable reminders access.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>OpenClaw uses on-device speech recognition for voice wake.</string>
<key>NSSupportsLiveActivities</key>

View File

@@ -1,4 +1,5 @@
import Foundation
import OpenClawKit
import SwiftUI
struct GatewayOnboardingView: View {

View File

@@ -0,0 +1,64 @@
import Foundation
enum PermissionRequestBridge {
final class Box: @unchecked Sendable {
private let lock = NSLock()
private var continuation: CheckedContinuation<Bool, Never>?
private var hasResumed = false
func install(_ continuation: CheckedContinuation<Bool, Never>) -> Bool {
self.lock.lock()
if self.hasResumed {
self.lock.unlock()
continuation.resume(returning: false)
return false
}
self.continuation = continuation
self.lock.unlock()
return true
}
func resume(_ value: Bool) {
self.lock.lock()
guard !self.hasResumed else {
self.lock.unlock()
return
}
self.hasResumed = true
let continuation = self.continuation
self.continuation = nil
self.lock.unlock()
continuation?.resume(returning: value)
}
func canStartRequest() -> Bool {
self.lock.lock()
let canStart = !self.hasResumed
self.lock.unlock()
return canStart
}
}
static func awaitRequest(
_ start: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool
{
let box = Box()
return await withTaskCancellationHandler {
await withCheckedContinuation(isolation: nil) { continuation in
guard !Task.isCancelled else {
continuation.resume(returning: false)
return
}
guard box.install(continuation) else { return }
Task { @MainActor in
guard box.canStartRequest() else { return }
start { granted in
box.resume(granted)
}
}
}
} onCancel: {
box.resume(false)
}
}
}

View File

@@ -4,15 +4,19 @@ import OpenClawKit
final class RemindersService: RemindersServicing {
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .reminder)
let authorized = EventKitAuthorization.allowsRead(status: status)
let authorized: Bool = if status == .notDetermined || status == .writeOnly {
await Self.requestFullReminderAccess()
} else {
EventKitAuthorization.allowsRead(status: status)
}
guard authorized else {
throw NSError(domain: "Reminders", code: 1, userInfo: [
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
])
}
let store = EKEventStore()
let limit = max(1, min(params.limit ?? 50, 500))
let statusFilter = params.status ?? .incomplete
@@ -48,15 +52,19 @@ final class RemindersService: RemindersServicing {
}
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload {
let store = EKEventStore()
let status = EKEventStore.authorizationStatus(for: .reminder)
let authorized = EventKitAuthorization.allowsWrite(status: status)
let authorized: Bool = if status == .notDetermined {
await Self.requestFullReminderAccess()
} else {
EventKitAuthorization.allowsWrite(status: status)
}
guard authorized else {
throw NSError(domain: "Reminders", code: 2, userInfo: [
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
])
}
let store = EKEventStore()
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty else {
throw NSError(domain: "Reminders", code: 3, userInfo: [
@@ -100,6 +108,15 @@ final class RemindersService: RemindersServicing {
return OpenClawRemindersAddPayload(reminder: payload)
}
private static func requestFullReminderAccess() async -> Bool {
await PermissionRequestBridge.awaitRequest { completion in
let store = EKEventStore()
store.requestFullAccessToReminders { granted, _ in
completion(granted)
}
}
}
private static func resolveList(
store: EKEventStore,
listId: String?,

View File

@@ -0,0 +1,298 @@
import Contacts
import EventKit
import SwiftUI
import UIKit
struct PrivacyAccessSectionView: View {
@State private var contactsStatus: CNAuthorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
@State private var calendarStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .event)
@State private var remindersStatus: EKAuthorizationStatus = EKEventStore.authorizationStatus(for: .reminder)
@Environment(\.scenePhase) private var scenePhase
var body: some View {
DisclosureGroup("Privacy & Access") {
self.permissionRow(
title: "Contacts",
icon: "person.crop.circle",
status: self.statusText(for: self.contactsStatus),
detail: "Search and add contacts from the assistant.",
actionTitle: self.actionTitle(for: self.contactsStatus),
action: self.handleContactsAction)
self.permissionRow(
title: "Calendar (Add Events)",
icon: "calendar.badge.plus",
status: self.calendarWriteStatusText,
detail: "Add events with least privilege.",
actionTitle: self.calendarWriteActionTitle,
action: self.handleCalendarWriteAction)
self.permissionRow(
title: "Calendar (View Events)",
icon: "calendar",
status: self.calendarReadStatusText,
detail: "List and read calendar events.",
actionTitle: self.calendarReadActionTitle,
action: self.handleCalendarReadAction)
self.permissionRow(
title: "Reminders",
icon: "checklist",
status: self.remindersStatusText,
detail: "List, add, and complete reminders.",
actionTitle: self.remindersActionTitle,
action: self.handleRemindersAction)
}
.onAppear { self.refreshAll() }
.onChange(of: self.scenePhase) { _, phase in
if phase == .active {
self.refreshAll()
}
}
}
private func permissionRow(
title: String,
icon: String,
status: String,
detail: String,
actionTitle: String?,
action: (() -> Void)?) -> some View
{
VStack(alignment: .leading, spacing: 6) {
HStack {
Label(title, systemImage: icon)
Spacer()
Text(status)
.font(.footnote.weight(.medium))
.foregroundStyle(self.statusColor(for: status))
}
Text(detail)
.font(.footnote)
.foregroundStyle(.secondary)
if let actionTitle, let action {
Button(actionTitle, action: action)
.font(.footnote)
.buttonStyle(.bordered)
}
}
.padding(.vertical, 2)
}
private func statusColor(for status: String) -> Color {
switch status {
case "Allowed":
.green
case "Not Set":
.orange
case "Add-Only":
.yellow
default:
.red
}
}
private func statusText(for cnStatus: CNAuthorizationStatus) -> String {
switch cnStatus {
case .authorized, .limited:
"Allowed"
case .notDetermined:
"Not Set"
case .denied, .restricted:
"Not Allowed"
@unknown default:
"Unknown"
}
}
private func actionTitle(for cnStatus: CNAuthorizationStatus) -> String? {
switch cnStatus {
case .notDetermined:
"Request Access"
case .denied, .restricted:
"Open Settings"
default:
nil
}
}
private func handleContactsAction() {
switch self.contactsStatus {
case .notDetermined:
Task {
_ = await PermissionRequestBridge.awaitRequest { completion in
let store = CNContactStore()
store.requestAccess(for: .contacts) { granted, _ in
completion(granted)
}
}
await MainActor.run { self.refreshAll() }
}
case .denied, .restricted:
self.openSettings()
default:
break
}
}
private var calendarWriteStatusText: String {
switch self.calendarStatus {
case .authorized, .fullAccess, .writeOnly:
"Allowed"
case .notDetermined:
"Not Set"
case .denied, .restricted:
"Not Allowed"
@unknown default:
"Unknown"
}
}
private var calendarWriteActionTitle: String? {
switch self.calendarStatus {
case .notDetermined:
"Request Access"
case .denied, .restricted:
"Open Settings"
default:
nil
}
}
private func handleCalendarWriteAction() {
switch self.calendarStatus {
case .notDetermined:
Task {
_ = await self.requestCalendarWriteOnly()
await MainActor.run { self.refreshAll() }
}
case .denied, .restricted:
self.openSettings()
default:
break
}
}
private var calendarReadStatusText: String {
switch self.calendarStatus {
case .authorized, .fullAccess:
"Allowed"
case .writeOnly:
"Add-Only"
case .notDetermined:
"Not Set"
case .denied, .restricted:
"Not Allowed"
@unknown default:
"Unknown"
}
}
private var calendarReadActionTitle: String? {
switch self.calendarStatus {
case .notDetermined:
"Request Full Access"
case .writeOnly:
"Upgrade to Full Access"
case .denied, .restricted:
"Open Settings"
default:
nil
}
}
private func handleCalendarReadAction() {
switch self.calendarStatus {
case .notDetermined, .writeOnly:
Task {
_ = await self.requestCalendarFull()
await MainActor.run { self.refreshAll() }
}
case .denied, .restricted:
self.openSettings()
default:
break
}
}
private var remindersStatusText: String {
switch self.remindersStatus {
case .authorized, .fullAccess:
"Allowed"
case .writeOnly:
"Add-Only"
case .notDetermined:
"Not Set"
case .denied, .restricted:
"Not Allowed"
@unknown default:
"Unknown"
}
}
private var remindersActionTitle: String? {
switch self.remindersStatus {
case .notDetermined:
"Request Access"
case .writeOnly:
"Upgrade to Full Access"
case .denied, .restricted:
"Open Settings"
default:
nil
}
}
private func handleRemindersAction() {
switch self.remindersStatus {
case .notDetermined, .writeOnly:
Task {
_ = await self.requestRemindersFull()
await MainActor.run { self.refreshAll() }
}
case .denied, .restricted:
self.openSettings()
default:
break
}
}
private func refreshAll() {
self.contactsStatus = CNContactStore.authorizationStatus(for: .contacts)
self.calendarStatus = EKEventStore.authorizationStatus(for: .event)
self.remindersStatus = EKEventStore.authorizationStatus(for: .reminder)
}
private func requestCalendarWriteOnly() async -> Bool {
await PermissionRequestBridge.awaitRequest { completion in
let store = EKEventStore()
store.requestWriteOnlyAccessToEvents { granted, _ in
completion(granted)
}
}
}
private func requestCalendarFull() async -> Bool {
await PermissionRequestBridge.awaitRequest { completion in
let store = EKEventStore()
store.requestFullAccessToEvents { granted, _ in
completion(granted)
}
}
}
private func requestRemindersFull() async -> Bool {
await PermissionRequestBridge.awaitRequest { completion in
let store = EKEventStore()
store.requestFullAccessToReminders { granted, _ in
completion(granted)
}
}
}
private func openSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
}

View File

@@ -405,6 +405,8 @@ struct SettingsTab: View {
}
}
AnyView(PrivacyAccessSectionView())
DisclosureGroup("Device Info") {
TextField("Name", text: self.$displayName)
Text(self.instanceId)
@@ -419,16 +421,7 @@ struct SettingsTab: View {
}
}
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
self.dismiss()
} label: {
Image(systemName: "xmark")
}
.accessibilityLabel("Close")
}
}
.modifier(SettingsCloseToolbar())
.sheet(isPresented: self.$showGatewayProblemDetails) {
if let gatewayProblem = self.appModel.lastGatewayProblem {
GatewayProblemDetailsSheet(
@@ -488,90 +481,91 @@ struct SettingsTab: View {
Text(self.scannerError ?? "")
}
.onAppear {
self.lastLocationModeRaw = self.locationEnabledModeRaw
self.syncManualPortText()
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedInstanceId.isEmpty {
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
self.lastLocationModeRaw = self.locationEnabledModeRaw
self.syncManualPortText()
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedInstanceId.isEmpty {
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
self.gatewayPassword = GatewaySettingsStore
.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
}
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
self.appModel.refreshLastShareEventFromRelay()
// Keep setup front-and-center when disconnected; keep things compact once connected.
self.gatewayExpanded = !self.isGatewayConnected
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
if self.isGatewayConnected {
self.appModel.reloadTalkConfig()
}
}
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
self.appModel.refreshLastShareEventFromRelay()
// Keep setup front-and-center when disconnected; keep things compact once connected.
self.gatewayExpanded = !self.isGatewayConnected
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
if self.isGatewayConnected {
self.appModel.reloadTalkConfig()
.onChange(of: self.selectedAgentPickerId) { _, newValue in
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
self.appModel.setSelectedAgentId(trimmed.isEmpty ? nil : trimmed)
}
}
.onChange(of: self.selectedAgentPickerId) { _, newValue in
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
self.appModel.setSelectedAgentId(trimmed.isEmpty ? nil : trimmed)
}
.onChange(of: self.appModel.selectedAgentId ?? "") { _, newValue in
if newValue != self.selectedAgentPickerId {
self.selectedAgentPickerId = newValue
.onChange(of: self.appModel.selectedAgentId ?? "") { _, newValue in
if newValue != self.selectedAgentPickerId {
self.selectedAgentPickerId = newValue
}
}
}
.onChange(of: self.preferredGatewayStableID) { _, newValue in
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
GatewaySettingsStore.savePreferredGatewayStableID(trimmed)
}
.onChange(of: self.gatewayToken) { _, newValue in
guard !self.suppressCredentialPersist else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId)
}
.onChange(of: self.gatewayPassword) { _, newValue in
guard !self.suppressCredentialPersist else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
}
.onChange(of: self.defaultShareInstruction) { _, newValue in
ShareToAgentSettings.saveDefaultInstruction(newValue)
}
.onChange(of: self.manualGatewayPort) { _, _ in
self.syncManualPortText()
}
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
if newValue != nil {
self.setupCode = ""
self.setupStatusText = nil
return
.onChange(of: self.preferredGatewayStableID) { _, newValue in
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
GatewaySettingsStore.savePreferredGatewayStableID(trimmed)
}
if self.manualGatewayEnabled {
self.setupStatusText = self.appModel.gatewayStatusText
.onChange(of: self.gatewayToken) { _, newValue in
guard !self.suppressCredentialPersist else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId)
}
}
.onChange(of: self.appModel.gatewayStatusText) { _, newValue in
guard self.manualGatewayEnabled || self.connectingGatewayID == "manual" else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.setupStatusText = trimmed
}
.onChange(of: self.locationEnabledModeRaw) { _, newValue in
let previous = self.lastLocationModeRaw
self.lastLocationModeRaw = newValue
guard let mode = OpenClawLocationMode(rawValue: newValue) else { return }
Task {
let granted = await self.appModel.requestLocationPermissions(mode: mode)
if !granted {
await MainActor.run {
self.locationEnabledModeRaw = previous
self.lastLocationModeRaw = previous
}
.onChange(of: self.gatewayPassword) { _, newValue in
guard !self.suppressCredentialPersist else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
}
.onChange(of: self.defaultShareInstruction) { _, newValue in
ShareToAgentSettings.saveDefaultInstruction(newValue)
}
.onChange(of: self.manualGatewayPort) { _, _ in
self.syncManualPortText()
}
.onChange(of: self.appModel.gatewayServerName) { _, newValue in
if newValue != nil {
self.setupCode = ""
self.setupStatusText = nil
return
}
await MainActor.run {
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
if self.manualGatewayEnabled {
self.setupStatusText = self.appModel.gatewayStatusText
}
}
.onChange(of: self.appModel.gatewayStatusText) { _, newValue in
guard self.manualGatewayEnabled || self.connectingGatewayID == "manual" else { return }
let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.setupStatusText = trimmed
}
.onChange(of: self.locationEnabledModeRaw) { _, newValue in
let previous = self.lastLocationModeRaw
self.lastLocationModeRaw = newValue
guard let mode = OpenClawLocationMode(rawValue: newValue) else { return }
Task {
let granted = await self.appModel.requestLocationPermissions(mode: mode)
if !granted {
await MainActor.run {
self.locationEnabledModeRaw = previous
self.lastLocationModeRaw = previous
}
return
}
await MainActor.run {
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
}
}
}
}
}
.gatewayTrustPromptAlert()
}
@@ -1138,4 +1132,21 @@ struct SettingsTab: View {
}
}
private struct SettingsCloseToolbar: ViewModifier {
@Environment(\.dismiss) private var dismiss
func body(content: Content) -> some View {
content.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
self.dismiss()
} label: {
Image(systemName: "xmark")
}
.accessibilityLabel("Close")
}
}
}
}
// swiftlint:enable type_body_length

View File

@@ -40,6 +40,7 @@ Sources/Onboarding/OnboardingStateStore.swift
Sources/Onboarding/OnboardingWizardView.swift
Sources/Onboarding/QRScannerView.swift
Sources/OpenClawApp.swift
Sources/Permissions/PermissionRequestBridge.swift
Sources/Push/ExecApprovalNotificationBridge.swift
Sources/Push/BackgroundAliveBeacon.swift
Sources/Push/PushBuildConfig.swift
@@ -60,6 +61,7 @@ Sources/Services/WatchConnectivityTransport.swift
Sources/Services/WatchMessagingPayloadCodec.swift
Sources/Services/WatchMessagingService.swift
Sources/SessionKey.swift
Sources/Settings/PrivacyAccessSectionView.swift
Sources/Settings/SettingsNetworkingHelpers.swift
Sources/Settings/SettingsTab.swift
Sources/Settings/VoiceWakeWordsSettingsView.swift

View File

@@ -0,0 +1,26 @@
import Testing
@testable import OpenClaw
@Suite(.serialized) struct PermissionRequestBridgeTests {
@Test func `box resumes immediately when cancelled before install`() async {
let box = PermissionRequestBridge.Box()
box.resume(false)
let granted: Bool = await withCheckedContinuation { continuation in
_ = box.install(continuation)
}
#expect(granted == false)
#expect(box.canStartRequest() == false)
}
@Test func `box resumes installed continuation once`() async {
let box = PermissionRequestBridge.Box()
let granted: Bool = await withCheckedContinuation { continuation in
_ = box.install(continuation)
box.resume(true)
box.resume(false)
}
#expect(granted == true)
}
}

View File

@@ -136,11 +136,16 @@ targets:
NSBonjourServices:
- _openclaw-gw._tcp
NSCameraUsageDescription: OpenClaw can capture photos or short video clips when requested via the gateway.
NSCalendarsUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
NSCalendarsFullAccessUsageDescription: OpenClaw uses your calendars to show events and scheduling context when you enable calendar access.
NSCalendarsWriteOnlyAccessUsageDescription: OpenClaw uses your calendars to add events when you enable calendar access.
NSContactsUsageDescription: OpenClaw uses your contacts so you can search and reference people while using the assistant.
NSLocationWhenInUseUsageDescription: OpenClaw uses your location when you allow location sharing.
NSLocationAlwaysAndWhenInUseUsageDescription: OpenClaw can share your location in the background when you enable Always.
NSMicrophoneUsageDescription: OpenClaw needs microphone access for voice wake.
NSMotionUsageDescription: OpenClaw may use motion data to support device-aware interactions and automations.
NSPhotoLibraryUsageDescription: OpenClaw needs photo library access when you choose existing photos to share with your assistant.
NSRemindersFullAccessUsageDescription: OpenClaw uses your reminders to list, add, and complete tasks when you enable reminders access.
NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for voice wake.
NSSupportsLiveActivities: true
ITSAppUsesNonExemptEncryption: false

View File

@@ -69,6 +69,17 @@ enum GatewayRemoteConfig {
}
}
static func resolveTLSFingerprint(root: [String: Any]) -> String? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["tlsFingerprint"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
static func resolveGatewayUrl(root: [String: Any]) -> URL? {
guard let raw = self.resolveUrlString(root: root) else { return nil }
return self.normalizeGatewayUrl(raw)

View File

@@ -83,7 +83,9 @@ final class MacNodeModeCoordinator {
clientId: "openclaw-macos",
clientMode: "node",
clientDisplayName: InstanceIdentity.displayName)
let sessionBox = self.buildSessionBox(url: config.url)
let sessionBox = self.buildSessionBox(
url: config.url,
connectionMode: AppStateStore.shared.connectionMode)
try await self.session.connect(
url: config.url,
@@ -243,15 +245,35 @@ final class MacNodeModeCoordinator {
return true
}
private func buildSessionBox(url: URL) -> WebSocketSessionBox? {
nonisolated static func tlsParams(
for url: URL,
connectionMode: AppState.ConnectionMode,
root: [String: Any],
storedFingerprint: String?) -> GatewayTLSParams?
{
guard url.scheme?.lowercased() == "wss" else { return nil }
let stableID = Self.tlsPinStoreKey(for: url)
let configuredFingerprint = connectionMode == .remote
? GatewayRemoteConfig.resolveTLSFingerprint(root: root)
: nil
let expectedFingerprint = configuredFingerprint ?? storedFingerprint
return GatewayTLSParams(
required: true,
expectedFingerprint: expectedFingerprint,
allowTOFU: expectedFingerprint == nil,
storeKey: stableID)
}
private func buildSessionBox(url: URL, connectionMode: AppState.ConnectionMode) -> WebSocketSessionBox? {
guard url.scheme?.lowercased() == "wss" else { return nil }
let stableID = Self.tlsPinStoreKey(for: url)
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
let params = GatewayTLSParams(
required: true,
expectedFingerprint: stored,
allowTOFU: stored == nil,
storeKey: stableID)
guard let params = Self.tlsParams(
for: url,
connectionMode: connectionMode,
root: OpenClawConfigFile.loadDict(),
storedFingerprint: stored)
else { return nil }
let session = GatewayTLSPinningSession(params: params)
return WebSocketSessionBox(session: session)
}

View File

@@ -287,4 +287,36 @@ struct GatewayEndpointStoreTests {
let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://127.attacker.example")
#expect(url == nil)
}
@Test func `resolve tls fingerprint trims remote config value`() {
let root: [String: Any] = [
"gateway": [
"remote": [
"tlsFingerprint": " sha256:ABC123 ",
],
],
]
#expect(GatewayRemoteConfig.resolveTLSFingerprint(root: root) == "sha256:ABC123")
}
@Test func `resolve tls fingerprint ignores blank or non string values`() {
let blank: [String: Any] = [
"gateway": [
"remote": [
"tlsFingerprint": " ",
],
],
]
let nonString: [String: Any] = [
"gateway": [
"remote": [
"tlsFingerprint": 123,
],
],
]
#expect(GatewayRemoteConfig.resolveTLSFingerprint(root: blank) == nil)
#expect(GatewayRemoteConfig.resolveTLSFingerprint(root: nonString) == nil)
}
}

View File

@@ -35,6 +35,60 @@ struct MacNodeModeCoordinatorTests {
#expect(MacNodeModeCoordinator.tlsPinStoreKey(for: url) == "gateway.example.ts.net:443")
}
@Test func `remote tls params prefer configured fingerprint over stored pin`() throws {
let url = try #require(URL(string: "wss://gateway.example.com"))
let root: [String: Any] = [
"gateway": [
"remote": [
"tlsFingerprint": "sha256:configured",
],
],
]
let params = try #require(MacNodeModeCoordinator.tlsParams(
for: url,
connectionMode: .remote,
root: root,
storedFingerprint: "stored"))
#expect(params.expectedFingerprint == "sha256:configured")
#expect(params.allowTOFU == false)
#expect(params.storeKey == "gateway.example.com:443")
}
@Test func `remote tls params allow first use only when no configured or stored pin exists`() throws {
let url = try #require(URL(string: "wss://gateway.example.com"))
let params = try #require(MacNodeModeCoordinator.tlsParams(
for: url,
connectionMode: .remote,
root: [:],
storedFingerprint: nil))
#expect(params.expectedFingerprint == nil)
#expect(params.allowTOFU == true)
}
@Test func `local tls params ignore remote configured fingerprint`() throws {
let url = try #require(URL(string: "wss://127.0.0.1:18789"))
let root: [String: Any] = [
"gateway": [
"remote": [
"tlsFingerprint": "sha256:remote",
],
],
]
let params = try #require(MacNodeModeCoordinator.tlsParams(
for: url,
connectionMode: .local,
root: root,
storedFingerprint: "stored-local"))
#expect(params.expectedFingerprint == "stored-local")
#expect(params.allowTOFU == false)
}
@Test func `auto repairs trusted tailscale serve pin mismatch`() throws {
let url = try #require(URL(string: "wss://gateway.example.ts.net"))
let failure = GatewayTLSValidationFailure(

View File

@@ -17,6 +17,7 @@ private let chatUILogger = Logger(subsystem: "ai.openclaw", category: "OpenClawC
// swiftlint:disable:next type_body_length
public final class OpenClawChatViewModel {
public static let defaultModelSelectionID = "__default__"
private static let maxAttachmentBytes = 5_000_000
public private(set) var messages: [OpenClawChatMessage] = []
public var input: String = ""
@@ -1298,11 +1299,6 @@ public final class OpenClawChatViewModel {
}
private func addImageAttachment(url: URL?, data: Data, fileName: String, mimeType: String) async {
if data.count > 5_000_000 {
self.errorText = "Attachment \(fileName) exceeds 5 MB limit"
return
}
let uti: UTType = {
if let url {
return UTType(filenameExtension: url.pathExtension) ?? .data
@@ -1314,13 +1310,33 @@ public final class OpenClawChatViewModel {
return
}
let preview = Self.previewImage(data: data)
let processed: Data
do {
processed = try await Task.detached(priority: .userInitiated) {
try ChatImageProcessor.processForUpload(data: data)
}.value
} catch {
self.errorText = "Could not process \(fileName): \(error.localizedDescription)"
return
}
if processed.count > Self.maxAttachmentBytes {
self.errorText = "Attachment \(fileName) exceeds 5 MB limit after resizing"
return
}
let outputFileName: String = {
let baseName = (fileName as NSString).deletingPathExtension
return baseName.isEmpty ? "image.jpg" : "\(baseName).jpg"
}()
let preview = Self.previewImage(data: processed)
self.attachments.append(
OpenClawPendingAttachment(
url: url,
data: data,
fileName: fileName,
mimeType: mimeType,
data: processed,
fileName: outputFileName,
mimeType: "image/jpeg",
preview: preview))
}

View File

@@ -0,0 +1,44 @@
import Foundation
/// Chat-specific image upload policy built on the shared JPEG transcoder.
public enum ChatImageProcessor {
public static let maxLongEdgePx = 1600
public static let jpegQuality = 0.8
public static let maxPayloadBytes = 3_500_000
public enum ProcessError: Error, LocalizedError, Sendable {
case notAnImage
case decodeFailed
case encodeFailed
public var errorDescription: String? {
switch self {
case .notAnImage:
"The data is not a recognizable image."
case .decodeFailed:
"The image could not be decoded."
case .encodeFailed:
"The image could not be resized to fit the chat upload limit."
}
}
}
public static func processForUpload(data: Data) throws -> Data {
do {
let result = try JPEGTranscoder.transcodeToJPEG(
imageData: data,
maxLongEdgePx: self.maxLongEdgePx,
quality: self.jpegQuality,
maxBytes: self.maxPayloadBytes)
return result.data
} catch JPEGTranscodeError.decodeFailed {
throw ProcessError.notAnImage
} catch JPEGTranscodeError.propertiesMissing {
throw ProcessError.decodeFailed
} catch JPEGTranscodeError.sizeLimitExceeded {
throw ProcessError.encodeFailed
} catch {
throw ProcessError.encodeFailed
}
}
}

View File

@@ -57,7 +57,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
{
return link
}
return fromGatewayURLString(
return self.fromGatewayURLString(
trimmed,
bootstrapToken: nil,
token: nil,
@@ -89,7 +89,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
{
return link
}
for candidate in setupCodeCandidates(in: trimmed) where candidate != trimmed {
for candidate in self.setupCodeCandidates(in: trimmed) where candidate != trimmed {
if let data = decodeBase64Url(candidate),
let link = decodeSetupPayload(from: data)
{
@@ -104,7 +104,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable {
if let urlString = payload.url?.trimmingCharacters(in: .whitespacesAndNewlines),
!urlString.isEmpty
{
return fromGatewayURLString(
return self.fromGatewayURLString(
urlString,
bootstrapToken: payload.bootstrapToken,
token: payload.token,

View File

@@ -79,6 +79,12 @@ public protocol GatewayDeviceTokenRetryTrustProviding: AnyObject {
var allowsDeviceTokenRetryAuth: Bool { get }
}
enum GatewayTLSFirstUsePolicy {
static func allowsFirstUsePin(systemTrustOk: Bool) -> Bool {
systemTrustOk
}
}
public enum GatewayTLSStore {
private static let keychainService = "ai.openclaw.tls-pinning"
@@ -159,7 +165,8 @@ public enum GatewayTLSStore {
}
}
public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLSessionDelegate, GatewayTLSFailureProviding, GatewayDeviceTokenRetryTrustProviding, @unchecked Sendable {
public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLSessionDelegate,
GatewayTLSFailureProviding, GatewayDeviceTokenRetryTrustProviding, @unchecked Sendable {
private let params: GatewayTLSParams
private let failureLock = NSLock()
private var lastTLSFailure: GatewayTLSValidationFailure?
@@ -238,12 +245,14 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS
return
}
if self.params.allowTOFU {
if let storeKey = params.storeKey {
GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey)
if GatewayTLSFirstUsePolicy.allowsFirstUsePin(systemTrustOk: systemTrustOk) {
if let storeKey = params.storeKey {
GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey)
}
self.clearTLSFailure()
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
self.clearTLSFailure()
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
}

View File

@@ -37,6 +37,26 @@ public struct JPEGTranscoder: Sendable {
maxWidthPx: Int?,
quality: Double,
maxBytes: Int? = nil) throws -> (data: Data, widthPx: Int, heightPx: Int)
{
try self.transcodeToJPEG(
imageData: imageData,
maxWidthPx: maxWidthPx,
maxLongEdgePx: nil,
quality: quality,
maxBytes: maxBytes)
}
/// Re-encodes image data to JPEG, optionally downscaling so the *oriented* longest edge is <= `maxLongEdgePx`.
///
/// When `maxLongEdgePx` is provided it takes precedence over `maxWidthPx`.
/// - Important: This normalizes EXIF orientation (the output pixels are rotated if needed; orientation tag is not
/// relied on).
public static func transcodeToJPEG(
imageData: Data,
maxWidthPx: Int? = nil,
maxLongEdgePx: Int?,
quality: Double,
maxBytes: Int? = nil) throws -> (data: Data, widthPx: Int, heightPx: Int)
{
guard let src = CGImageSourceCreateWithData(imageData as CFData, nil) else {
throw JPEGTranscodeError.decodeFailed
@@ -63,6 +83,10 @@ public struct JPEGTranscoder: Sendable {
let maxDim = max(orientedWidth, orientedHeight)
var targetMaxPixelSize: Int = {
if let maxLongEdgePx, maxLongEdgePx > 0 {
guard maxDim > maxLongEdgePx else { return maxDim } // never upscale
return maxLongEdgePx
}
guard let maxWidthPx, maxWidthPx > 0 else { return maxDim }
guard orientedWidth > maxWidthPx else { return maxDim } // never upscale
@@ -81,6 +105,7 @@ public struct JPEGTranscoder: Sendable {
guard let img = CGImageSourceCreateThumbnailAtIndex(src, 0, thumbOpts as CFDictionary) else {
throw JPEGTranscodeError.decodeFailed
}
let opaqueImage = Self.flattenAlphaIfNeeded(img)
let out = NSMutableData()
guard let dest = CGImageDestinationCreateWithData(out, UTType.jpeg.identifier as CFString, 1, nil) else {
@@ -88,12 +113,12 @@ public struct JPEGTranscoder: Sendable {
}
let q = self.clampQuality(quality)
let encodeProps = [kCGImageDestinationLossyCompressionQuality: q] as CFDictionary
CGImageDestinationAddImage(dest, img, encodeProps)
CGImageDestinationAddImage(dest, opaqueImage, encodeProps)
guard CGImageDestinationFinalize(dest) else {
throw JPEGTranscodeError.encodeFailed
}
return (out as Data, img.width, img.height)
return (out as Data, opaqueImage.width, opaqueImage.height)
}
guard let maxBytes, maxBytes > 0 else {
@@ -132,4 +157,34 @@ public struct JPEGTranscoder: Sendable {
return best
}
/// JPEG cannot store alpha. Flatten transparent sources over white before encoding so ImageIO does not composite
/// transparent pixels onto black by default.
private static func flattenAlphaIfNeeded(_ image: CGImage) -> CGImage {
switch image.alphaInfo {
case .none, .noneSkipFirst, .noneSkipLast:
return image
default:
break
}
guard
let context = CGContext(
data: nil,
width: image.width,
height: image.height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
else {
return image
}
let rect = CGRect(x: 0, y: 0, width: image.width, height: image.height)
context.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1))
context.fill(rect)
context.draw(image, in: rect)
return context.makeImage() ?? image
}
}

View File

@@ -0,0 +1,187 @@
import CoreGraphics
import Foundation
import ImageIO
import Testing
import UniformTypeIdentifiers
@testable import OpenClawKit
struct ChatImageProcessorTests {
private func syntheticJPEG(width: Int, height: Int) throws -> Data {
guard
let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
else {
throw NSError(domain: "ChatImageProcessorTests", code: 1)
}
context.setFillColor(CGColor(red: 0.8, green: 0.2, blue: 0.4, alpha: 1))
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
context.setFillColor(CGColor(red: 0.1, green: 0.7, blue: 0.3, alpha: 1))
context.fill(CGRect(x: 0, y: 0, width: width / 2, height: height / 2))
guard let image = context.makeImage() else {
throw NSError(domain: "ChatImageProcessorTests", code: 2)
}
let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil)
else {
throw NSError(domain: "ChatImageProcessorTests", code: 3)
}
let properties: [CFString: Any] = [
kCGImageDestinationLossyCompressionQuality: 0.95,
kCGImagePropertyExifDictionary: [
kCGImagePropertyExifDateTimeOriginal: "2026:04:20 16:30:00",
kCGImagePropertyExifLensModel: "Leaky Lens 50mm f/1.4",
] as CFDictionary,
kCGImagePropertyGPSDictionary: [
kCGImagePropertyGPSLatitude: 60.02,
kCGImagePropertyGPSLatitudeRef: "N",
kCGImagePropertyGPSLongitude: 10.95,
kCGImagePropertyGPSLongitudeRef: "E",
] as CFDictionary,
kCGImagePropertyTIFFDictionary: [
kCGImagePropertyTIFFMake: "LeakCorp",
kCGImagePropertyTIFFModel: "Privacy-Leaker-1",
] as CFDictionary,
]
CGImageDestinationAddImage(destination, image, properties as CFDictionary)
guard CGImageDestinationFinalize(destination) else {
throw NSError(domain: "ChatImageProcessorTests", code: 4)
}
return data as Data
}
private func syntheticPNGWithAlpha(width: Int, height: Int) throws -> Data {
guard
let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
else {
throw NSError(domain: "ChatImageProcessorTests", code: 5)
}
context.clear(CGRect(x: 0, y: 0, width: width, height: height))
context.setFillColor(CGColor(red: 1, green: 0, blue: 0, alpha: 1))
context.fill(CGRect(x: width / 4, y: height / 4, width: width / 2, height: height / 2))
guard let image = context.makeImage() else {
throw NSError(domain: "ChatImageProcessorTests", code: 6)
}
let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data, UTType.png.identifier as CFString, 1, nil)
else {
throw NSError(domain: "ChatImageProcessorTests", code: 7)
}
CGImageDestinationAddImage(destination, image, nil)
guard CGImageDestinationFinalize(destination) else {
throw NSError(domain: "ChatImageProcessorTests", code: 8)
}
return data as Data
}
private func properties(for data: Data) -> [CFString: Any] {
guard
let source = CGImageSourceCreateWithData(data as CFData, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
else {
return [:]
}
return properties
}
private func dimensions(for data: Data) -> (width: Int, height: Int)? {
let properties = self.properties(for: data)
guard
let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
let height = properties[kCGImagePropertyPixelHeight] as? NSNumber
else {
return nil
}
return (width.intValue, height.intValue)
}
@Test func `resizes landscape long edge to upload limit`() throws {
let source = try self.syntheticJPEG(width: 4000, height: 3000)
let output = try ChatImageProcessor.processForUpload(data: source)
let dimensions = try #require(self.dimensions(for: output))
#expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
#expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (4000.0 / 3000.0)) <= 0.02)
}
@Test func `resizes portrait long edge to upload limit`() throws {
let source = try self.syntheticJPEG(width: 3000, height: 4000)
let output = try ChatImageProcessor.processForUpload(data: source)
let dimensions = try #require(self.dimensions(for: output))
#expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
#expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (3000.0 / 4000.0)) <= 0.02)
}
@Test func `resizes narrow tall long edge to upload limit`() throws {
let source = try self.syntheticJPEG(width: 1080, height: 2400)
let output = try ChatImageProcessor.processForUpload(data: source)
let dimensions = try #require(self.dimensions(for: output))
#expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
#expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (1080.0 / 2400.0)) <= 0.02)
}
@Test func `small image is not upscaled`() throws {
let source = try self.syntheticJPEG(width: 400, height: 300)
let output = try ChatImageProcessor.processForUpload(data: source)
let dimensions = try #require(self.dimensions(for: output))
#expect(max(dimensions.width, dimensions.height) <= 400)
}
@Test func `output fits payload budget`() throws {
let source = try self.syntheticJPEG(width: 4000, height: 3000)
let output = try ChatImageProcessor.processForUpload(data: source)
#expect(output.count <= ChatImageProcessor.maxPayloadBytes)
}
@Test func `rejects non image data`() {
let garbage = Data("not an image".utf8)
#expect(throws: ChatImageProcessor.ProcessError.self) {
_ = try ChatImageProcessor.processForUpload(data: garbage)
}
}
@Test func `strips source metadata from output`() throws {
let source = try self.syntheticJPEG(width: 3000, height: 2000)
let output = try ChatImageProcessor.processForUpload(data: source)
let properties = self.properties(for: output)
let gps = properties[kCGImagePropertyGPSDictionary] as? [CFString: Any] ?? [:]
#expect(gps.isEmpty)
for needle in ["Leaky Lens", "LeakCorp", "Privacy-Leaker", "2026:04:20"] {
#expect(output.range(of: Data(needle.utf8)) == nil)
}
}
@Test func `flattens transparent sources to opaque JPEG`() throws {
let source = try self.syntheticPNGWithAlpha(width: 800, height: 600)
let output = try ChatImageProcessor.processForUpload(data: source)
let imageSource = try #require(CGImageSourceCreateWithData(output as CFData, nil))
let image = try #require(CGImageSourceCreateImageAtIndex(imageSource, 0, nil))
#expect([.none, .noneSkipFirst, .noneSkipLast].contains(image.alphaInfo))
}
}

View File

@@ -0,0 +1,109 @@
import CoreGraphics
import Foundation
import ImageIO
import OpenClawKit
import UniformTypeIdentifiers
import XCTest
@testable import OpenClawChatUI
private struct AttachmentProcessingTransport: OpenClawChatTransport {
func requestHistory(sessionKey _: String) async throws -> OpenClawChatHistoryPayload {
throw NSError(domain: "ChatViewModelAttachmentTests", code: 1)
}
func sendMessage(
sessionKey _: String,
message _: String,
thinking _: String,
idempotencyKey _: String,
attachments _: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse
{
throw NSError(domain: "ChatViewModelAttachmentTests", code: 2)
}
func requestHealth(timeoutMs _: Int) async throws -> Bool {
true
}
func events() -> AsyncStream<OpenClawChatTransportEvent> {
AsyncStream { _ in }
}
}
private func makeChatAttachmentJPEG(width: Int, height: Int) throws -> Data {
guard
let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
else {
throw NSError(domain: "ChatViewModelAttachmentTests", code: 3)
}
context.setFillColor(CGColor(red: 0.2, green: 0.4, blue: 0.8, alpha: 1))
context.fill(CGRect(x: 0, y: 0, width: width, height: height))
context.setFillColor(CGColor(red: 0.9, green: 0.5, blue: 0.1, alpha: 1))
context.fill(CGRect(x: 0, y: 0, width: width / 2, height: height / 2))
guard let image = context.makeImage() else {
throw NSError(domain: "ChatViewModelAttachmentTests", code: 4)
}
let data = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil) else {
throw NSError(domain: "ChatViewModelAttachmentTests", code: 5)
}
CGImageDestinationAddImage(destination, image, [kCGImageDestinationLossyCompressionQuality: 0.95] as CFDictionary)
guard CGImageDestinationFinalize(destination) else {
throw NSError(domain: "ChatViewModelAttachmentTests", code: 6)
}
return data as Data
}
private func chatAttachmentDimensions(for data: Data) -> (width: Int, height: Int)? {
guard
let source = CGImageSourceCreateWithData(data as CFData, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
let height = properties[kCGImagePropertyPixelHeight] as? NSNumber
else {
return nil
}
return (width.intValue, height.intValue)
}
final class ChatViewModelAttachmentTests: XCTestCase {
func testImageAttachmentsAreProcessedBeforeStaging() async throws {
let imageData = try makeChatAttachmentJPEG(width: 3000, height: 4000)
let viewModel = await MainActor.run {
OpenClawChatViewModel(sessionKey: "main", transport: AttachmentProcessingTransport())
}
await MainActor.run {
viewModel.addImageAttachment(data: imageData, fileName: "camera.heic", mimeType: "image/jpeg")
}
try await waitUntil("attachment processed") {
await MainActor.run { !viewModel.attachments.isEmpty || viewModel.errorText != nil }
}
let attachment = try await MainActor.run {
guard let attachment = viewModel.attachments.first else {
throw NSError(domain: "ChatViewModelAttachmentTests", code: 7)
}
return (attachment.fileName, attachment.mimeType, attachment.data)
}
let dimensions = try XCTUnwrap(chatAttachmentDimensions(for: attachment.2))
XCTAssertEqual(attachment.0, "camera.jpg")
XCTAssertEqual(attachment.1, "image/jpeg")
XCTAssertLessThanOrEqual(attachment.2.count, ChatImageProcessor.maxPayloadBytes)
XCTAssertLessThanOrEqual(max(dimensions.width, dimensions.height), ChatImageProcessor.maxLongEdgePx)
let errorText = await MainActor.run { viewModel.errorText }
XCTAssertNil(errorText)
}
}

View File

@@ -0,0 +1,9 @@
import Testing
@testable import OpenClawKit
struct GatewayTLSPinningTests {
@Test func `first use pinning requires system trust`() {
#expect(GatewayTLSFirstUsePolicy.allowsFirstUsePin(systemTrustOk: true))
#expect(!GatewayTLSFirstUsePolicy.allowsFirstUsePin(systemTrustOk: false))
}
}

View File

@@ -1,4 +1,4 @@
c311205806d0eaa3631788dc2c489ece999b70430021ff91b365ce7ccfcba23c config-baseline.json
2e27b71c9ed109767a227f5163917a4468a1969079fc3457a3df7fe74c1fa2b7 config-baseline.core.json
90e4b700ae744fa81dd1986e4db16aadbb7d1dc4dfe695e03a35e75ea39bfde4 config-baseline.json
8edde78f6986926b50613931d5588e4278768ca3003e5ece0edb3110ab9118b0 config-baseline.core.json
2aa997d48549bd321a478485126a4bd5065ba47333a80e7eb07a0ef6ad75b0a6 config-baseline.channel.json
0dac8944a0d51ae96f97e3809907f8a04d08413434a1a1190240f7e13bb11c4d config-baseline.plugin.json
1ab5b65a94d84f59bae5e6bbe310057fae0a645f2538ab00f1f37b7f8b371e6f config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
e8c15cff96a0a869cfe3de29679d4296603a16bfa4676940845a484c23db8e56 plugin-sdk-api-baseline.json
a6cbb8dc21b3ed16e0abd23c60a817ddedd65f336427c9fa565a43ca5dcc9a85 plugin-sdk-api-baseline.jsonl
1cf7ca2ee1db3bf44682c487c780c6b1c47bbce27e74fb6f455cef445544c84f plugin-sdk-api-baseline.json
24b8e3e4773579e5a184dd5f91a5ad2f8e92519b6fe314820a94d7a64bd1141e plugin-sdk-api-baseline.jsonl

View File

@@ -23,17 +23,17 @@ Production-ready for DMs and channels via Slack app integrations. Default mode i
Both transports are production-ready and reach feature parity for messaging, slash commands, App Home, and interactivity. Pick by deployment shape, not features.
| Concern | Socket Mode (default) | HTTP Request URLs |
| ---------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| Public Gateway URL | Not required | Required (DNS, TLS, reverse proxy or tunnel) |
| Outbound network | Outbound WSS to `wss-primary.slack.com` must be reachable | No outbound WS; inbound HTTPS only |
| Tokens needed | Bot token (`xoxb-...`) + App-Level Token (`xapp-...`) with `connections:write` | Bot token (`xoxb-...`) + Signing Secret |
| Dev laptop / behind firewall | Works as-is | Needs a public tunnel (ngrok, Cloudflare Tunnel, Tailscale Funnel) or staging Gateway |
| Horizontal scaling | One Socket Mode session per app per host; multiple Gateways need separate Slack apps | Stateless POST handler; multiple Gateway replicas can share one app behind a load balancer |
| Multi-account on one Gateway | Supported; each account opens its own WS | Supported; each account needs a unique `webhookPath` (default `/slack/events`) so registrations do not collide |
| Slash command transport | Delivered over the WS connection; `slash_commands[].url` is ignored | Slack POSTs to `slash_commands[].url`; field is required for the command to dispatch |
| Request signing | Not used (auth is the App-Level Token) | Slack signs every request; OpenClaw verifies with `signingSecret` |
| Recovery on connection drop | Slack SDK auto-reconnects; the gateway's pong-timeout transport tuning applies | No persistent connection to drop; retries are per-request from Slack |
| Concern | Socket Mode (default) | HTTP Request URLs |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Public Gateway URL | Not required | Required (DNS, TLS, reverse proxy or tunnel) |
| Outbound network | Outbound WSS to `wss-primary.slack.com` must be reachable | No outbound WS; inbound HTTPS only |
| Tokens needed | Bot token (`xoxb-...`) + App-Level Token (`xapp-...`) with `connections:write` | Bot token (`xoxb-...`) + Signing Secret |
| Dev laptop / behind firewall | Works as-is | Needs a public tunnel (ngrok, Cloudflare Tunnel, Tailscale Funnel) or staging Gateway |
| Horizontal scaling | One Socket Mode session per app per host; multiple Gateways need separate Slack apps | Stateless POST handler; multiple Gateway replicas can share one app behind a load balancer |
| Multi-account on one Gateway | Supported; each account opens its own WS | Supported; each account needs a unique `webhookPath` (default `/slack/events`) so registrations do not collide |
| Slash command transport | Delivered over the WS connection; `slash_commands[].url` is ignored | Slack POSTs to `slash_commands[].url`; field is required for the command to dispatch |
| Request signing | Not used (auth is the App-Level Token) | Slack signs every request; OpenClaw verifies with `signingSecret` |
| Recovery on connection drop | Slack SDK auto-reconnect is enabled; OpenClaw also restarts failed Socket Mode sessions with bounded backoff. Pong-timeout transport tuning applies. | No persistent connection to drop; retries are per-request from Slack |
<Note>
**Pick Socket Mode** for single-Gateway hosts, dev laptops, and on-prem networks that can reach `*.slack.com` outbound but cannot accept inbound HTTPS.
@@ -41,6 +41,16 @@ Both transports are production-ready and reach feature parity for messaging, sla
**Pick HTTP Request URLs** when running multiple Gateway replicas behind a load balancer, when outbound WSS is blocked but inbound HTTPS is allowed, or when you already terminate Slack webhooks at a reverse proxy.
</Note>
## Install
Install Slack before configuring the channel:
```bash
openclaw plugins install @openclaw/slack
```
`plugins install` registers and enables the plugin. The plugin still does nothing until you configure the Slack app and channel settings below. See [Plugins](/tools/plugin) for general plugin behavior and install rules.
## Quick setup
<Tabs>
@@ -181,7 +191,7 @@ Both transports are production-ready and reach feature parity for messaging, sla
</CodeGroup>
<Note>
**Recommended** matches the bundled Slack plugin's full feature set: App Home, slash commands, files, reactions, pins, group DMs, and emoji/usergroup reads. Pick **Minimal** when workspace policy restricts scopes — it covers DMs, channel/group history, mentions, and slash commands but drops files, reactions, pins, group-DM (`mpim:*`), `emoji:read`, and `usergroups:read`. See [Manifest and scope checklist](#manifest-and-scope-checklist) for per-scope rationale and additive options like extra slash commands.
**Recommended** matches the Slack plugin's full feature set: App Home, slash commands, files, reactions, pins, group DMs, and emoji/usergroup reads. Pick **Minimal** when workspace policy restricts scopes — it covers DMs, channel/group history, mentions, and slash commands but drops files, reactions, pins, group-DM (`mpim:*`), `emoji:read`, and `usergroups:read`. See [Manifest and scope checklist](#manifest-and-scope-checklist) for per-scope rationale and additive options like extra slash commands.
</Note>
After Slack creates the app:
@@ -383,7 +393,7 @@ openclaw gateway
</CodeGroup>
<Note>
**Recommended** matches the bundled Slack plugin's full feature set; **Minimal** drops files, reactions, pins, group-DM (`mpim:*`), `emoji:read`, and `usergroups:read` for restrictive workspaces. See [Manifest and scope checklist](#manifest-and-scope-checklist) for per-scope rationale.
**Recommended** matches the Slack plugin's full feature set; **Minimal** drops files, reactions, pins, group-DM (`mpim:*`), `emoji:read`, and `usergroups:read` for restrictive workspaces. See [Manifest and scope checklist](#manifest-and-scope-checklist) for per-scope rationale.
</Note>
<Info>
@@ -462,6 +472,13 @@ OpenClaw sets the Slack SDK client pong timeout to 15 seconds by default for Soc
Use this only for Socket Mode workspaces that log Slack websocket pong/server-ping timeouts or run on hosts with known event-loop starvation. `clientPingTimeout` is the pong wait after the SDK sends a client ping; `serverPingTimeout` is the wait for Slack server pings. App messages and events remain application state, not transport liveness signals.
Notes:
- `socketMode` is ignored in HTTP Request URL mode.
- Base `channels.slack.socketMode` settings apply to all Slack accounts unless overridden. Per-account overrides use `channels.slack.accounts.<accountId>.socketMode`; because this is an object override, include every socket tuning field you want for that account.
- Only `clientPingTimeout` has an OpenClaw default (`15000`). `serverPingTimeout` and `pingPongLoggingEnabled` are passed to the Slack SDK only when configured.
- Socket Mode restart backoff starts around 2 seconds and caps around 30 seconds. Consecutive recoverable start/start-wait failures stop after 12 attempts; after a successful connection, later recoverable disconnects start a fresh retry cycle. Non-recoverable Slack auth errors such as `invalid_auth`, revoked tokens, or missing scopes fail fast instead of retrying forever.
## Manifest and scope checklist
The base Slack app manifest is the same for Socket Mode and HTTP Request URLs. Only the `settings` block (and the slash command `url`) differs.
@@ -931,8 +948,9 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
- Slack route bindings accept raw peer IDs plus Slack target forms such as `channel:C12345678`, `user:U12345678`, and `<@U12345678>`.
- With default `session.dmScope=main`, Slack DMs collapse to agent main session.
- Channel sessions: `agent:<agentId>:slack:channel:<channelId>`.
- Thread replies can create thread session suffixes (`:thread:<threadTs>`) when applicable.
- In channels where OpenClaw handles top-level messages without requiring an explicit mention, non-`off` `replyToMode` routes each handled root into `agent:<agentId>:slack:channel:<channelId>:thread:<rootTs>` so the visible Slack thread maps to one OpenClaw session from the first turn.
- Ordinary top-level channel messages stay on the per-channel session, even when `replyToMode` is non-`off`.
- Slack thread replies use the parent Slack `thread_ts` for session suffixes (`:thread:<threadTs>`), even when outbound reply threading is disabled with `replyToMode="off"`.
- OpenClaw seeds an eligible top-level channel root into `agent:<agentId>:slack:channel:<channelId>:thread:<rootTs>` when that root is expected to start a visible Slack thread, so the root and later thread replies share one OpenClaw session. This applies to `app_mention` events, explicit bot or configured mention-pattern matches, and `requireMention: false` channels with non-`off` `replyToMode`.
- `channels.slack.thread.historyScope` default is `thread`; `thread.inheritParent` default is `false`.
- `channels.slack.thread.initialHistoryLimit` controls how many existing thread messages are fetched when a new thread session starts (default `20`; set `0` to disable).
- `channels.slack.thread.requireExplicitMention` (default `false`): when `true`, suppress implicit thread mentions so the bot only responds to explicit `@bot` mentions inside threads, even when the bot already participated in the thread. Without this, replies in a bot-participated thread bypass `requireMention` gating.
@@ -953,7 +971,7 @@ For explicit Slack thread replies from the `message` tool, set `replyBroadcast:
When a `message` tool call runs inside a Slack thread and targets the same channel, OpenClaw normally inherits the current Slack thread according to `replyToMode`. Set `topLevel: true` on `action: "send"` or `action: "upload-file"` to force a new parent-channel message instead. `threadId: null` is accepted as the same top-level opt-out.
<Note>
`replyToMode="off"` disables **all** reply threading in Slack, including explicit `[[reply_to_*]]` tags. This differs from Telegram, where explicit tags are still honored in `"off"` mode. Slack threads hide messages from the channel while Telegram replies stay visible inline.
`replyToMode="off"` disables outbound Slack reply threading, including explicit `[[reply_to_*]]` tags. It does not flatten inbound Slack thread sessions: messages already posted inside a Slack thread still route to the `:thread:<threadTs>` session. This differs from Telegram, where explicit tags are still honored in `"off"` mode. Slack threads hide messages from the channel while Telegram replies stay visible inline.
</Note>
## Ack reactions
@@ -1258,6 +1276,17 @@ Primary reference: [Configuration reference - Slack](/gateway/config-channels#sl
- channel allowlist (`channels.slack.channels`) — **keys must be channel IDs** (`C12345678`), not names (`#channel-name`). Name-based keys silently fail under `groupPolicy: "allowlist"` because channel routing is ID-first by default. To find an ID: right-click the channel in Slack → **Copy link** — the `C...` value at the end of the URL is the channel ID.
- `requireMention`
- per-channel `users` allowlist
- `messages.groupChat.visibleReplies`: if it is `"message_tool"` and logs show assistant text with no `message(action=send)` call, the turn was processed but the final answer was kept private. Set it to `"automatic"` if you want normal assistant final replies posted back to Slack channels.
```json5
{
messages: {
groupChat: {
visibleReplies: "automatic",
},
},
}
```
Useful commands:
@@ -1274,7 +1303,8 @@ openclaw doctor
- `channels.slack.dm.enabled`
- `channels.slack.dmPolicy` (or legacy `channels.slack.dm.policy`)
- pairing approvals / allowlist entries
- pairing approvals / allowlist entries (`dmPolicy: "open"` still requires `channels.slack.allowFrom: ["*"]`)
- group DMs use MPIM handling; enable `channels.slack.dm.groupEnabled` and, if configured, include the MPIM in `channels.slack.dm.groupChannels`
- Slack Assistant DM events: verbose logs mentioning `drop message_changed`
usually mean Slack sent an edited Assistant-thread event without a
recoverable human sender in message metadata
@@ -1287,12 +1317,19 @@ openclaw pairing list slack
<Accordion title="Socket mode not connecting">
Validate bot + app tokens and Socket Mode enablement in Slack app settings.
The `xapp-...` App-Level Token needs `connections:write`, and the `xoxb-...`
bot token must belong to the same Slack app/workspace as the app token.
If `openclaw channels status --probe --json` shows `botTokenStatus` or
`appTokenStatus: "configured_unavailable"`, the Slack account is
configured but the current runtime could not resolve the SecretRef-backed
value.
Logs such as `slack socket mode failed to start; retry ...` are recoverable
start failures. Missing scopes, revoked tokens, and invalid auth fail fast
instead. A `slack token mismatch ...` log means the bot token and app token
appear to belong to different Slack apps; fix the Slack app credentials.
</Accordion>
<Accordion title="HTTP mode not receiving events">
@@ -1302,11 +1339,16 @@ openclaw pairing list slack
- webhook path
- Slack Request URLs (Events + Interactivity + Slash Commands)
- unique `webhookPath` per HTTP account
- the public URL terminates TLS and forwards requests to the Gateway path
- the Slack app `request_url` path exactly matches `channels.slack.webhookPath` (default `/slack/events`)
If `signingSecretStatus: "configured_unavailable"` appears in account
snapshots, the HTTP account is configured but the current runtime could not
resolve the SecretRef-backed signing secret.
A repeated `slack: webhook path ... already registered` log means two HTTP
accounts are using the same `webhookPath`; give each account a distinct path.
</Accordion>
<Accordion title="Native/slash commands not firing">
@@ -1315,7 +1357,14 @@ openclaw pairing list slack
- native command mode (`channels.slack.commands.native: true`) with matching slash commands registered in Slack
- or single slash command mode (`channels.slack.slashCommand.enabled: true`)
Also check `commands.useAccessGroups` and channel/user allowlists.
Slack does not create or remove slash commands automatically. `commands.native: "auto"` does not enable Slack native commands; use `true` and create the matching commands in the Slack app. In HTTP mode, every Slack slash command must include the Gateway URL. In Socket Mode, command payloads arrive over the websocket and Slack ignores `slash_commands[].url`.
Also check `commands.useAccessGroups`, DM authorization, channel allowlists,
and per-channel `users` allowlists. Slack returns ephemeral errors for
blocked slash-command senders, including:
- `This channel is not allowed.`
- `You are not authorized to use this command here.`
</Accordion>
</AccordionGroup>

View File

@@ -517,7 +517,11 @@ Before a first run, check the wrapper from the repo root:
pnpm crabbox:run -- --help | sed -n '1,120p'
```
The repo wrapper refuses a stale Crabbox binary that does not advertise `blacksmith-testbox`. Pass the provider explicitly even though `.crabbox.yaml` has owned-cloud defaults.
The repo wrapper refuses a stale Crabbox binary that does not advertise `blacksmith-testbox`. Pass the provider explicitly even though `.crabbox.yaml` has owned-cloud defaults. In Codex worktrees or linked/sparse checkouts, avoid the local `pnpm crabbox:run` script because pnpm may reconcile dependencies before Crabbox starts; invoke the node wrapper directly instead:
```bash
node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox --timing-json --shell -- "pnpm test <path-or-filter>"
```
Changed gate:

View File

@@ -155,7 +155,7 @@ order and tells you what it chose:
- `OPENAI_API_KEY` -> `openai/gpt-5.5`
- `ANTHROPIC_API_KEY` -> `anthropic/claude-opus-4-7`
- Claude Code CLI -> `claude-cli/claude-opus-4-7`
- Codex CLI -> `codex-cli/gpt-5.5`
- Codex -> `openai/gpt-5.5` through the Codex app-server harness
If none are available, setup still writes the default workspace and leaves the
model unset. Install or log into Codex/Claude Code, or expose
@@ -171,7 +171,6 @@ back to local runtimes already present on the machine:
- Claude Code CLI: `claude-cli/claude-opus-4-7`
- Codex app-server harness: `openai/gpt-5.5`
- Codex CLI: `codex-cli/gpt-5.5`
The model-assisted planner cannot mutate config directly. It must translate the
request into one of Crestodian's typed commands, then the normal approval and

View File

@@ -123,9 +123,10 @@ inventory a specific Codex home.
Use this provider when moving to the OpenClaw Codex harness and you want to
promote useful personal Codex CLI assets deliberately. Local Codex app-server
launches use a per-agent `CODEX_HOME`, so they do not read your personal Codex
CLI state by default, while subprocesses still inherit the normal process
`HOME` unless the app-server launch explicitly overrides it.
launches use a per-agent `CODEX_HOME`, so they do not read your personal
`~/.codex` by default. The normal process `HOME` is still inherited, so Codex
can see shared `$HOME/.agents/*` skills/plugin marketplace entries and
subprocesses can find user-home config and tokens.
Running `openclaw migrate codex` in an interactive terminal previews the full
plan, then opens checkbox selectors before the final apply confirmation. Skill

View File

@@ -97,6 +97,8 @@ This is the agent-facing decision tree:
`openai/<model>` with `openclaw doctor --fix`; doctor keeps the Codex auth
route by adding provider/model-scoped `agentRuntime.id: "codex"` where the
old model ref implied it.
Legacy **`codex-cli/*` model refs** repair to the same `openai/<model>` Codex
app-server route; OpenClaw no longer keeps a bundled Codex CLI backend.
5. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
ACP with `runtime: "acp"` and `agentId: "codex"`.
6. If the request is for **Claude Code, Gemini CLI, OpenCode, Cursor, Droid, or
@@ -180,6 +182,10 @@ Legacy refs such as `claude-cli/claude-opus-4-7` remain supported for
compatibility, but new config should keep the provider/model canonical and put
the execution backend in provider/model runtime policy.
Legacy `codex-cli/*` refs are different: doctor migrates them to `openai/*` so
they run through the Codex app-server harness instead of preserving a Codex CLI
backend.
`auto` mode is intentionally conservative for most providers. OpenAI agent
models are the exception: unset runtime and `auto` both resolve to the Codex
harness. Explicit PI runtime config remains an opt-in compatibility route for

View File

@@ -16,8 +16,11 @@ Your agent has three memory-related files:
- **`MEMORY.md`** — long-term memory. Durable facts, preferences, and
decisions. Loaded at the start of every DM session.
- **`memory/YYYY-MM-DD.md`** — daily notes. Running context and observations.
Today and yesterday's notes are loaded automatically.
- **`memory/YYYY-MM-DD.md`** (or **`memory/YYYY-MM-DD-<slug>.md`**) — daily notes.
Running context and observations. Today and yesterday's notes are loaded
automatically, and slugged variants such as those written by the bundled
session-memory hook on `/new` or `/reset` are now picked up alongside the
date-only file.
- **`DREAMS.md`** (optional) — Dream Diary and dreaming sweep
summaries for human review, including grounded historical backfill entries.

View File

@@ -41,9 +41,9 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
</Accordion>
<Accordion title="CLI runtimes">
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*`, `google/gemini-*`, or `openai/gpt-*`, then set provider/model runtime policy to `claude-cli`, `google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*` or `google/gemini-*`, then set provider/model runtime policy to `claude-cli` or `google-gemini-cli` when you want a local CLI backend.
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate back to canonical provider refs with the runtime recorded separately.
Legacy `claude-cli/*` and `google-gemini-cli/*` refs migrate back to canonical provider refs with the runtime recorded separately. Legacy `codex-cli/*` refs migrate to `openai/*` and use the Codex app-server route; OpenClaw no longer keeps a bundled Codex CLI backend.
</Accordion>
</AccordionGroup>

View File

@@ -2,7 +2,7 @@
summary: "CLI backends: local AI CLI fallback with optional MCP tool bridge"
read_when:
- You want a reliable fallback when API providers fail
- You are running Codex CLI or other local AI CLIs and want to reuse them
- You are running local AI CLIs and want to reuse them
- You want to understand the MCP loopback bridge for CLI backend tool access
title: "CLI backends"
---
@@ -31,11 +31,11 @@ thread/conversation binding, and persistent external coding sessions, use
## Beginner-friendly quick start
You can use Codex CLI **without any config** (the bundled OpenAI plugin
You can use Claude Code CLI **without any config** (the bundled Anthropic plugin
registers a default backend):
```bash
openclaw agent --message "hi" --model codex-cli/gpt-5.5
openclaw agent --message "hi" --model claude-cli/claude-sonnet-4-6
```
If your gateway runs under launchd/systemd and PATH is minimal, add just the
@@ -46,8 +46,8 @@ command path:
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "/opt/homebrew/bin/codex",
"claude-cli": {
command: "/opt/homebrew/bin/claude",
},
},
},
@@ -72,11 +72,11 @@ Add a CLI backend to your fallback list so it only runs when primary models fail
defaults: {
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["codex-cli/gpt-5.5"],
fallbacks: ["claude-cli/claude-sonnet-4-6"],
},
models: {
"anthropic/claude-opus-4-6": { alias: "Opus" },
"codex-cli/gpt-5.5": {},
"claude-cli/claude-sonnet-4-6": {},
},
},
},
@@ -97,7 +97,7 @@ All CLI backends live under:
agents.defaults.cliBackends
```
Each entry is keyed by a **provider id** (e.g. `codex-cli`, `my-cli`).
Each entry is keyed by a **provider id** (e.g. `claude-cli`, `my-cli`).
The provider id becomes the left side of your model ref:
```
@@ -111,9 +111,6 @@ The provider id becomes the left side of your model ref:
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "/opt/homebrew/bin/codex",
},
"my-cli": {
command: "my-cli",
args: ["--json"],
@@ -149,7 +146,7 @@ The provider id becomes the left side of your model ref:
## How it works
1. **Selects a backend** based on the provider prefix (`codex-cli/...`).
1. **Selects a backend** based on the provider prefix (`claude-cli/...`).
2. **Builds a system prompt** using the same OpenClaw prompt + workspace context.
3. **Executes the CLI** with a session id (if supported) so history stays consistent.
The bundled `claude-cli` backend keeps a Claude stdio process alive per
@@ -164,12 +161,6 @@ told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats
a new policy.
</Note>
The bundled OpenAI `codex-cli` backend passes OpenClaw's system prompt through
Codex's `model_instructions_file` config override (`-c
model_instructions_file="..."`). Codex does not expose a Claude-style
`--append-system-prompt` flag, so OpenClaw writes the assembled prompt to a
temporary file for each fresh Codex CLI session.
The bundled Anthropic `claude-cli` backend receives the OpenClaw skills snapshot
two ways: the compact OpenClaw skills catalog in the appended system prompt, and
a temporary Claude Code plugin passed with `--plugin-dir`. The plugin contains
@@ -292,7 +283,7 @@ load local files from plain paths.
- `output: "json"` (default) tries to parse JSON and extract text + session id.
- For Gemini CLI JSON output, OpenClaw reads reply text from `response` and
usage from `stats` when `usage` is missing or empty.
- `output: "jsonl"` parses JSONL streams (for example Codex CLI `--json`) and extracts the final agent message plus session
- `output: "jsonl"` parses JSONL streams and extracts the final agent message plus session
identifiers when present.
- `output: "text"` treats stdout as the final response.
@@ -304,16 +295,19 @@ Input modes:
## Defaults (plugin-owned)
The bundled OpenAI plugin also registers a default for `codex-cli`:
Bundled CLI backend defaults live with their owning plugin. For example,
Anthropic owns `claude-cli` and Google owns `google-gemini-cli`. OpenAI Codex
agent runs use the Codex app-server harness through `openai/*`; OpenClaw no
longer registers a bundled `codex-cli` backend.
- `command: "codex"`
- `args: ["exec","--json","--color","never","--sandbox","workspace-write","--skip-git-repo-check"]`
- `resumeArgs: ["exec","resume","{sessionId}","-c","sandbox_mode=\"workspace-write\"","--skip-git-repo-check"]`
The bundled Anthropic plugin registers a default for `claude-cli`:
- `command: "claude"`
- `args: ["-p","--output-format","stream-json","--include-partial-messages","--verbose", ...]`
- `output: "jsonl"`
- `resumeOutput: "text"`
- `input: "stdin"`
- `modelArg: "--model"`
- `imageArg: "--image"`
- `sessionMode: "existing"`
- `sessionMode: "always"`
The bundled Google plugin also registers a default for `google-gemini-cli`:
@@ -383,9 +377,6 @@ opt into a generated MCP config overlay with `bundleMcp: true`.
Current bundled behavior:
- `claude-cli`: generated strict MCP config file
- `codex-cli`: inline config overrides for `mcp_servers`; the generated
OpenClaw loopback server is marked with Codex's per-server tool approval mode
so MCP calls cannot stall on local approval prompts
- `google-gemini-cli`: generated Gemini system settings file
When bundle MCP is enabled, OpenClaw:
@@ -414,16 +405,13 @@ children and Streamable HTTP/SSE streams do not outlive the run.
- **Streaming is backend-specific.** Some backends stream JSONL; others buffer
until exit.
- **Structured outputs** depend on the CLI's JSON format.
- **Codex CLI sessions** resume via text output (no JSONL), which is less
structured than the initial `--json` run. OpenClaw sessions still work
normally.
## Troubleshooting
- **CLI not found**: set `command` to a full path.
- **Wrong model name**: use `modelAliases` to map `provider/model` → CLI model.
- **No session continuity**: ensure `sessionArg` is set and `sessionMode` is not
`none` (Codex CLI currently cannot resume with JSON output).
`none`.
- **Images ignored**: set `imageArg` (and verify CLI supports file paths).
## Related

View File

@@ -461,8 +461,8 @@ Optional CLI backends for text-only fallback runs (no tool calls). Useful as a b
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "/opt/homebrew/bin/codex",
"claude-cli": {
command: "/opt/homebrew/bin/claude",
},
"my-cli": {
command: "my-cli",

View File

@@ -476,7 +476,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
- **Socket mode** requires both `botToken` and `appToken` (`SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` for default account env fallback).
- **HTTP mode** requires `botToken` plus `signingSecret` (at root or per-account).
- `socketMode` passes Slack SDK Socket Mode transport tuning through to the public Bolt receiver API. Use it only when investigating ping/pong timeout or stale websocket behavior.
- `socketMode` passes Slack SDK Socket Mode transport tuning through to the public Bolt receiver API. Use it only when investigating ping/pong timeout or stale websocket behavior. `clientPingTimeout` defaults to `15000`; `serverPingTimeout` and `pingPongLoggingEnabled` are passed only when configured.
- `botToken`, `appToken`, `signingSecret`, and `userToken` accept plaintext
strings or SecretRef objects.
- Slack account snapshots expose per-credential source/status fields such as

View File

@@ -444,6 +444,8 @@ See [Inferred commitments](/concepts/commitments).
auth: {
mode: "token", // none | token | password | trusted-proxy
token: "your-token",
// tokenScopes: ["operator.read"], // optional static scopes for trusted device-less token clients
// allowPrivilegedTokenScopes: false, // safe default; set true only to allow write/admin tokenScopes on non-loopback binds
// password: "your-password", // or OPENCLAW_GATEWAY_PASSWORD
// trustedProxy: { userHeader: "x-forwarded-user" }, // for mode=trusted-proxy; see /gateway/trusted-proxy-auth
allowTailscale: true,
@@ -515,6 +517,8 @@ See [Inferred commitments](/concepts/commitments).
- **Auth**: required by default. Non-loopback binds require gateway auth. In practice that means a shared token/password or an identity-aware reverse proxy with `gateway.auth.mode: "trusted-proxy"`. Onboarding wizard generates a token by default.
- If both `gateway.auth.token` and `gateway.auth.password` are configured (including SecretRefs), set `gateway.auth.mode` explicitly to `token` or `password`. Startup and service install/repair flows fail when both are configured and mode is unset.
- `gateway.auth.mode: "none"`: explicit no-auth mode. Use only for trusted local loopback setups; this is intentionally not offered by onboarding prompts.
- `gateway.auth.tokenScopes`: optional static operator scopes for device-less WebSocket clients that authenticate with `gateway.auth.token`. Use this for trusted headless observers that cannot complete device pairing, for example `["operator.read"]` to allow `sessions.messages.subscribe`, `sessions.subscribe`, `logs.tail`, and `channels.status`. Missing or empty keeps device-less token auth scope-less; clients cannot self-grant scopes with `params.scopes`.
- `gateway.auth.allowPrivilegedTokenScopes`: permits `gateway.auth.tokenScopes` values beyond `operator.read` when the Gateway is bound to a non-loopback address. Keep this unset unless an external trust boundary protects the shared token.
- `gateway.auth.mode: "trusted-proxy"`: delegate browser/user auth to an identity-aware reverse proxy and trust identity headers from `gateway.trustedProxies` (see [Trusted Proxy Auth](/gateway/trusted-proxy-auth)). This mode expects a **non-loopback** proxy source by default; same-host loopback reverse proxies require explicit `gateway.auth.trustedProxy.allowLoopback = true`. Internal same-host callers can use `gateway.auth.password` as a local direct fallback; `gateway.auth.token` remains mutually exclusive with trusted-proxy mode.
- `gateway.auth.allowTailscale`: when `true`, Tailscale Serve identity headers can satisfy Control UI/WebSocket auth (verified via `tailscale whois`). HTTP API endpoints do **not** use that Tailscale header auth; they follow the gateway's normal HTTP auth mode instead. This tokenless flow assumes the gateway host is trusted. Defaults to `true` when `tailscale.mode = "serve"`.
- `gateway.auth.rateLimit`: optional failed-auth limiter. Applies per client IP and per auth scope (shared-secret and device-token are tracked independently). Blocked attempts return `429` + `Retry-After`.

View File

@@ -18,6 +18,7 @@ and an optional `mirror` workspace mode.
## Prerequisites
- OpenShell plugin installed (`openclaw plugins install @openclaw/openshell-sandbox`)
- The `openshell` CLI installed and on `PATH` (or set a custom path via
`plugins.entries.openshell.config.command`)
- An OpenShell account with sandbox access
@@ -25,7 +26,11 @@ and an optional `mirror` workspace mode.
## Quick start
1. Enable the plugin and set the sandbox backend:
1. Install and enable the plugin, then set the sandbox backend:
```bash
openclaw plugins install @openclaw/openshell-sandbox
```
```json5
{

View File

@@ -102,9 +102,20 @@ own `system.run` exec approval policy.
## Shared-secret auth
Shared gateway token/password auth is treated as trusted operator access for
that Gateway. OpenAI-compatible HTTP surfaces and `/tools/invoke` restore the
normal full operator default scope set for shared-secret bearer auth, even if a
caller sends narrower declared scopes.
that Gateway. Device-less WebSocket clients cannot self-grant scopes with
`params.scopes`; token-auth clients receive `gateway.auth.tokenScopes` when that
field is configured, otherwise they connect with no operator scopes until they
use device pairing. Configure `["operator.read"]` for trusted headless observer
clients that need `sessions.messages.subscribe` without an interactive pairing
flow.
OpenAI-compatible HTTP surfaces and `/tools/invoke` restore the normal full
operator default scope set for shared-secret bearer auth, even if a caller sends
narrower declared scopes.
Privileged `gateway.auth.tokenScopes` values such as `operator.write` or
`operator.admin` are accepted on loopback binds. Non-loopback binds require the
explicit `gateway.auth.allowPrivilegedTokenScopes` opt-in.
Identity-bearing modes, such as trusted proxy auth or private-ingress `none`,
can still honor explicit declared scopes. Use separate Gateways for real trust

View File

@@ -233,6 +233,10 @@ Common scopes:
`talk.config` with `includeSecrets: true` requires `operator.talk.secrets`
(or `operator.admin`).
Device-less shared-token WebSocket clients do not receive scopes from
`params.scopes`. Configure `gateway.auth.tokenScopes` on the Gateway when a
trusted headless token client needs static access such as `operator.read`.
Plugin-registered gateway RPC methods may request their own operator scope, but
reserved core admin prefixes (`config.*`, `exec.approvals.*`, `wizard.*`,
`update.*`) always resolve to `operator.admin`.
@@ -414,7 +418,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
<Accordion title="Session control">
- `sessions.list` returns the current session index, including per-row `agentRuntime` metadata when an agent runtime backend is configured.
- `sessions.subscribe` and `sessions.unsubscribe` toggle session change event subscriptions for the current WS client.
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session.
- `sessions.messages.subscribe` and `sessions.messages.unsubscribe` toggle transcript/message event subscriptions for one session. Requires `operator.read`; trusted headless token clients can obtain that scope through `gateway.auth.tokenScopes`.
- `sessions.preview` returns bounded transcript previews for specific session keys.
- `sessions.describe` returns one Gateway session row for an exact session key.
- `sessions.resolve` resolves or canonicalizes a session target.

View File

@@ -148,7 +148,7 @@ Short version: **keep the Gateway loopback-only** unless you're sure you need a
- `gateway.remote.token` / `.password` are client credential sources. They do **not** configure server auth by themselves.
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`, including macOS direct mode. Without a configured or previously stored pin, macOS only pins a first-use certificate after normal system trust passes; self-signed or private-CA gateways that macOS does not already trust need an explicit fingerprint or Remote over SSH.
- **Tailscale Serve** can authenticate Control UI/WebSocket traffic via identity
headers when `gateway.auth.allowTailscale: true`; HTTP API endpoints do not
use that Tailscale header auth and instead follow the gateway's normal HTTP

View File

@@ -867,6 +867,27 @@ Set a token so **all** WS clients must authenticate:
}
```
Device-less token-auth WebSocket clients do not inherit scopes from their
handshake request. If a trusted local/headless observer needs read-only
subscriptions without device pairing, configure:
```json5
{
gateway: {
bind: "loopback",
auth: {
mode: "token",
token: "your-token",
tokenScopes: ["operator.read"],
},
},
}
```
Only use privileged token scopes such as `operator.write` or `operator.admin`
on loopback binds, or set `gateway.auth.allowPrivilegedTokenScopes: true` after
you have put an external trust boundary around the shared token.
Doctor can generate one for you: `openclaw doctor --generate-gateway-token`.
<Note>

View File

@@ -133,7 +133,7 @@ openclaw models list --json
</Tip>
## Live: CLI backend smoke (Claude, Codex, Gemini, or other local CLIs)
## Live: CLI backend smoke (Claude, Gemini, or other local CLIs)
- Test: `src/gateway/gateway-cli-backend.live.test.ts`
- Goal: validate the Gateway + agent pipeline using a local CLI backend, without touching your default config.
@@ -145,9 +145,9 @@ openclaw models list --json
- Default provider/model: `claude-cli/claude-sonnet-4-6`
- Command/args/image behavior come from the owning CLI backend plugin metadata.
- Overrides (optional):
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.5"`
- `OPENCLAW_LIVE_CLI_BACKEND_COMMAND="/full/path/to/codex"`
- `OPENCLAW_LIVE_CLI_BACKEND_ARGS='["exec","--json","--color","never","--sandbox","read-only","--skip-git-repo-check"]'`
- `OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-sonnet-4-6"`
- `OPENCLAW_LIVE_CLI_BACKEND_COMMAND="/full/path/to/claude"`
- `OPENCLAW_LIVE_CLI_BACKEND_ARGS='["-p","--output-format","json"]'`
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_PROBE=1` to send a real image attachment (paths are injected into the prompt). Docker recipes default this off unless explicitly requested.
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_ARG="--image"` to pass image file paths as CLI args instead of prompt injection.
- `OPENCLAW_LIVE_CLI_BACKEND_IMAGE_MODE="repeat"` (or `"list"`) to control how image args are passed when `IMAGE_ARG` is set.
@@ -158,8 +158,8 @@ openclaw models list --json
Example:
```bash
OPENCLAW_LIVE_CLI_BACKEND=1 \
OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.5" \
OPENCLAW_LIVE_CLI_BACKEND=1 \
OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-sonnet-4-6" \
pnpm test:live src/gateway/gateway-cli-backend.live.test.ts
```
@@ -186,7 +186,6 @@ Single-provider Docker recipes:
```bash
pnpm test:docker:live-cli-backend:claude
pnpm test:docker:live-cli-backend:claude-subscription
pnpm test:docker:live-cli-backend:codex
pnpm test:docker:live-cli-backend:gemini
```
@@ -194,9 +193,9 @@ Notes:
- The Docker runner lives at `scripts/test-live-cli-backend-docker.sh`.
- It runs the live CLI-backend smoke inside the repo Docker image as the non-root `node` user.
- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code` or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
- `pnpm test:docker:live-cli-backend:claude-subscription` requires portable Claude Code subscription OAuth through either `~/.claude/.credentials.json` with `claudeAiOauth.subscriptionType` or `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`. It first proves direct `claude -p` in Docker, then runs two Gateway CLI-backend turns without preserving Anthropic API-key env vars. This subscription lane disables the Claude MCP/tool and image probes by default because Claude currently routes third-party app usage through extra-usage billing instead of normal subscription plan limits.
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude, Codex, and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
- Claude's default smoke also patches the session from Sonnet to Opus and verifies the resumed session still remembers an earlier note.
## Live: APNs HTTP/2 proxy reachability

View File

@@ -81,7 +81,7 @@ node.
- **Health probe failed**: check SSH reachability, PATH, and that Baileys is logged in (`openclaw status --json`).
- **Web Chat stuck**: confirm the gateway is running on the remote host and the forwarded port matches the gateway WS port; the UI requires a healthy WS connection.
- **Node IP shows 127.0.0.1**: expected with the SSH tunnel. Switch **Transport** to **Direct (ws/wss)** if you want the gateway to see the real client IP.
- **Dashboard works but Mac capabilities are offline**: this means the app's operator/control connection is healthy, but the companion node connection is not connected or is missing its command surface. Open the menu bar device section and check whether the Mac is `paired · disconnected`. For `wss://*.ts.net` Tailscale Serve endpoints, the app detects stale legacy TLS leaf pins after certificate rotation, clears the stale pin when macOS trusts the new certificate, and retries automatically. If the certificate is not system-trusted or the host is not a Tailscale Serve name, review the certificate or switch to **Remote over SSH**.
- **Dashboard works but Mac capabilities are offline**: this means the app's operator/control connection is healthy, but the companion node connection is not connected or is missing its command surface. Open the menu bar device section and check whether the Mac is `paired · disconnected`. For `wss://*.ts.net` Tailscale Serve endpoints, the app detects stale legacy TLS leaf pins after certificate rotation, clears the stale pin when macOS trusts the new certificate, and retries automatically. If the certificate is not system-trusted or the host is not a Tailscale Serve name, set `gateway.remote.tlsFingerprint` to the expected certificate fingerprint, review the certificate, or switch to **Remote over SSH**.
- **Voice Wake**: trigger phrases are forwarded automatically in remote mode; no separate forwarder is needed.
## Notification sounds

View File

@@ -166,18 +166,23 @@ login instead of inherited child-process env. WebSocket app-server connections
do not receive Gateway env API-key fallback; use an explicit auth profile or the
remote app-server's own account.
Stdio app-server launches inherit OpenClaw's process environment by default, but
OpenClaw owns the Codex app-server account bridge and sets both `CODEX_HOME` and
`HOME` to per-agent directories under that agent's OpenClaw state. Codex's own
skill loader reads `$CODEX_HOME/skills` and `$HOME/.agents/skills`, so both
values are isolated for local app-server launches. That keeps Codex-native
skills, plugins, config, accounts, and thread state scoped to the OpenClaw agent
instead of leaking in from the operator's personal Codex CLI home.
Stdio app-server launches inherit OpenClaw's process environment by default.
OpenClaw owns the Codex app-server account bridge and sets `CODEX_HOME` to a
per-agent directory under that agent's OpenClaw state. That keeps Codex config,
accounts, plugin cache/data, and thread state scoped to the OpenClaw agent
instead of leaking in from the operator's personal `~/.codex` home.
OpenClaw does not rewrite `HOME` for normal local app-server launches. Codex-run
subprocesses such as `openclaw`, `gh`, `git`, cloud CLIs, and shell commands see
the normal process home and can find user-home config and tokens. Codex may also
discover `$HOME/.agents/skills` and `$HOME/.agents/plugins/marketplace.json`;
that `.agents` discovery is intentionally shared with the operator home and is
separate from isolated `~/.codex` state.
OpenClaw plugins and OpenClaw skill snapshots still flow through OpenClaw's own
plugin registry and skill loader. Personal Codex CLI assets do not. If you have
useful Codex CLI skills or plugins that should become part of an OpenClaw agent,
inventory them explicitly:
plugin registry and skill loader. Personal Codex `~/.codex` assets do not. If
you have useful Codex CLI skills or plugins from a Codex home that should become
part of an OpenClaw agent, inventory them explicitly:
```bash
openclaw migrate codex --dry-run
@@ -205,8 +210,9 @@ If a deployment needs additional environment isolation, add those variables to
```
`appServer.clearEnv` only affects the spawned Codex app-server child process.
`CODEX_HOME` and `HOME` remain reserved for OpenClaw's per-agent Codex
isolation on local launches.
OpenClaw removes `CODEX_HOME` and `HOME` from this list during local launch
normalization: `CODEX_HOME` stays per-agent, and `HOME` stays inherited so
subprocesses can use normal user-home state.
## Dynamic tools

View File

@@ -177,13 +177,14 @@ Keep provider refs and runtime policy separate:
Common command routing:
| User intent | Use |
| ------------------------------- | --------------------------------------- |
| Attach the current chat | `/codex bind [--cwd <path>]` |
| Resume an existing Codex thread | `/codex resume <thread-id>` |
| List or filter Codex threads | `/codex threads [filter]` |
| Send Codex feedback only | `/codex diagnostics [note]` |
| Start an ACP/acpx task | ACP/acpx session commands, not `/codex` |
| User intent | Use |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Attach the current chat | `/codex bind [--cwd <path>]` |
| Resume an existing Codex thread | `/codex resume <thread-id>` |
| List or filter Codex threads | `/codex threads [filter]` |
| Attach an existing Codex CLI session on a paired node | `/codex sessions --host <node> [filter]`, then `/codex resume <session-id> --host <node> --bind here` |
| Send Codex feedback only | `/codex diagnostics [note]` |
| Start an ACP/acpx task | ACP/acpx session commands, not `/codex` |
| Use case | Configure | Verify | Notes |
| ---------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------- | ---------------------------------- |
@@ -424,6 +425,13 @@ time when Codex reports one and tries the next ordered auth profile for the same
Codex run. When the reset time passes, the subscription profile becomes eligible
again without changing the selected `openai/gpt-*` model or Codex runtime.
For local stdio app-server launches, OpenClaw sets `CODEX_HOME` to a per-agent
directory so Codex config, auth/account files, plugin cache/data, and native
thread state do not read or write the operator's personal `~/.codex` by
default. OpenClaw preserves the normal process `HOME`; Codex-run subprocesses
can still find user-home config and tokens, and Codex may discover shared
`$HOME/.agents/skills` and `$HOME/.agents/plugins/marketplace.json` entries.
If a deployment needs additional environment isolation, add those variables to
`appServer.clearEnv`:
@@ -445,6 +453,9 @@ If a deployment needs additional environment isolation, add those variables to
```
`appServer.clearEnv` only affects the spawned Codex app-server child process.
OpenClaw removes `CODEX_HOME` and `HOME` from this list during local launch
normalization: `CODEX_HOME` stays per-agent, and `HOME` stays inherited so
subprocesses can use normal user-home state.
Codex dynamic tools default to `searchable` loading. OpenClaw does not expose
dynamic tools that duplicate Codex-native workspace operations: `read`, `write`,
@@ -480,7 +491,7 @@ Supported `appServer` fields:
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. `CODEX_HOME` and `HOME` are reserved for OpenClaw's per-agent Codex isolation on local launches. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped Codex app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |

View File

@@ -388,7 +388,7 @@ Prefer the narrowest metadata that already describes ownership. Use
when those fields express the relationship. Use `activation` for extra planner
hints that cannot be represented by those ownership fields.
Use top-level `cliBackends` for CLI runtime aliases such as `claude-cli`,
`codex-cli`, or `google-gemini-cli`; `activation.onAgentHarnesses` is only for
`my-cli`, or `google-gemini-cli`; `activation.onAgentHarnesses` is only for
embedded agent harness ids that do not already have an ownership field.
This block is metadata only. It does not register runtime behavior, and it does

View File

@@ -52,10 +52,7 @@ commands.
| Plugin | Description | Distribution | Surface |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [alibaba](/plugins/reference/alibaba) | Adds video generation provider support. | `@openclaw/alibaba-provider`<br />included in OpenClaw | contracts: videoGenerationProviders |
| [amazon-bedrock](/plugins/reference/amazon-bedrock) | Adds Amazon Bedrock model provider support to OpenClaw. | `@openclaw/amazon-bedrock-provider`<br />included in OpenClaw | providers: amazon-bedrock; contracts: memoryEmbeddingProviders |
| [amazon-bedrock-mantle](/plugins/reference/amazon-bedrock-mantle) | Adds Amazon Bedrock Mantle model provider support to OpenClaw. | `@openclaw/amazon-bedrock-mantle-provider`<br />included in OpenClaw | providers: amazon-bedrock-mantle |
| [anthropic](/plugins/reference/anthropic) | Adds Anthropic model provider support to OpenClaw. | `@openclaw/anthropic-provider`<br />included in OpenClaw | providers: anthropic; contracts: mediaUnderstandingProviders |
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | Adds Anthropic Vertex model provider support to OpenClaw. | `@openclaw/anthropic-vertex-provider`<br />included in OpenClaw | providers: anthropic-vertex |
| [arcee](/plugins/reference/arcee) | Adds Arcee model provider support to OpenClaw. | `@openclaw/arcee-provider`<br />included in OpenClaw | providers: arcee |
| [azure-speech](/plugins/reference/azure-speech) | Azure AI Speech text-to-speech (MP3, native Ogg/Opus voice notes, PCM telephony). | `@openclaw/azure-speech`<br />included in OpenClaw | contracts: speechProviders |
| [bonjour](/plugins/reference/bonjour) | Advertise the local OpenClaw gateway over Bonjour/mDNS. | `@openclaw/bonjour`<br />included in OpenClaw | plugin |
@@ -110,7 +107,6 @@ commands.
| [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`<br />included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders |
| [opencode-go](/plugins/reference/opencode-go) | Adds OpenCode Go model provider support to OpenClaw. | `@openclaw/opencode-go-provider`<br />included in OpenClaw | providers: opencode-go; contracts: mediaUnderstandingProviders |
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`<br />included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, speechProviders, videoGenerationProviders |
| [openshell](/plugins/reference/openshell) | Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. | `@openclaw/openshell-sandbox`<br />included in OpenClaw | plugin |
| [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [qianfan](/plugins/reference/qianfan) | Adds Qianfan model provider support to OpenClaw. | `@openclaw/qianfan-provider`<br />included in OpenClaw | providers: qianfan |
| [qwen](/plugins/reference/qwen) | Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenClaw. | `@openclaw/qwen-provider`<br />included in OpenClaw | providers: qwen, qwencloud, modelstudio, dashscope; contracts: mediaUnderstandingProviders, videoGenerationProviders |
@@ -120,7 +116,6 @@ commands.
| [sglang](/plugins/reference/sglang) | Adds SGLang model provider support to OpenClaw. | `@openclaw/sglang-provider`<br />included in OpenClaw | providers: sglang |
| [signal](/plugins/reference/signal) | Adds the Signal channel surface for sending and receiving OpenClaw messages. | `@openclaw/signal`<br />included in OpenClaw | channels: signal |
| [skill-workshop](/plugins/reference/skill-workshop) | Captures repeatable workflows as workspace skills, with pending review, safe writes, and skill prompt refresh. | `@openclaw/skill-workshop`<br />included in OpenClaw | contracts: tools |
| [slack](/plugins/reference/slack) | Adds the Slack channel surface for sending and receiving OpenClaw messages. | `@openclaw/slack`<br />included in OpenClaw | channels: slack |
| [stepfun](/plugins/reference/stepfun) | Adds StepFun, StepFun Plan model provider support to OpenClaw. | `@openclaw/stepfun-provider`<br />included in OpenClaw | providers: stepfun, stepfun-plan |
| [synthetic](/plugins/reference/synthetic) | Adds Synthetic model provider support to OpenClaw. | `@openclaw/synthetic-provider`<br />included in OpenClaw | providers: synthetic |
| [tavily](/plugins/reference/tavily) | Adds agent-callable tools. Adds web search provider support. | `@openclaw/tavily-plugin`<br />included in OpenClaw | contracts: tools, webSearchProviders; skills |
@@ -143,33 +138,38 @@ commands.
## Official external packages
| Plugin | Description | Distribution | Surface |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />npm; ClawHub | skills |
| [brave](/plugins/reference/brave) | Adds web search provider support. | `@openclaw/brave-plugin`<br />npm; ClawHub | contracts: webSearchProviders |
| [codex](/plugins/reference/codex) | Codex app-server harness and Codex-managed GPT model catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord |
| [feishu](/plugins/reference/feishu) | Adds the Feishu channel surface for sending and receiving OpenClaw messages. | `@openclaw/feishu`<br />npm; ClawHub | channels: feishu; contracts: tools; skills |
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />npm; ClawHub | contracts: tools |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />npm; ClawHub | channels: googlechat |
| [line](/plugins/reference/line) | Adds the LINE channel surface for sending and receiving OpenClaw messages. | `@openclaw/line`<br />npm; ClawHub | channels: line |
| [lobster](/plugins/reference/lobster) | Typed workflow tool with resumable approvals. | `@openclaw/lobster`<br />npm; ClawHub | contracts: tools |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />ClawHub: `clawhub:@openclaw/matrix`; npm | channels: matrix |
| [memory-lancedb](/plugins/reference/memory-lancedb) | Adds agent-callable tools. | `@openclaw/memory-lancedb`<br />npm; ClawHub | contracts: tools |
| [msteams](/plugins/reference/msteams) | Adds the Microsoft Teams channel surface for sending and receiving OpenClaw messages. | `@openclaw/msteams`<br />npm; ClawHub | channels: msteams |
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | Adds the Nextcloud Talk channel surface for sending and receiving OpenClaw messages. | `@openclaw/nextcloud-talk`<br />npm; ClawHub | channels: nextcloud-talk |
| [nostr](/plugins/reference/nostr) | Adds the Nostr channel surface for sending and receiving OpenClaw messages. | `@openclaw/nostr`<br />npm; ClawHub | channels: nostr |
| [qqbot](/plugins/reference/qqbot) | Adds the QQ Bot channel surface for sending and receiving OpenClaw messages. | `@openclaw/qqbot`<br />npm; ClawHub | channels: qqbot; contracts: tools; skills |
| [synology-chat](/plugins/reference/synology-chat) | Adds the Synology Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/synology-chat`<br />npm; ClawHub | channels: synology-chat |
| [tlon](/plugins/reference/tlon) | Adds the Tlon channel surface for sending and receiving OpenClaw messages. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; contracts: tools; skills |
| [twitch](/plugins/reference/twitch) | Adds the Twitch channel surface for sending and receiving OpenClaw messages. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [voice-call](/plugins/reference/voice-call) | Adds agent-callable tools. | `@openclaw/voice-call`<br />npm; ClawHub | contracts: tools |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [zalo](/plugins/reference/zalo) | Adds the Zalo channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalo`<br />npm; ClawHub | channels: zalo |
| [zalouser](/plugins/reference/zalouser) | Adds the Zalo Personal channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalouser`<br />npm; ClawHub | channels: zalouser; contracts: tools |
| Plugin | Description | Distribution | Surface |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />npm; ClawHub | skills |
| [amazon-bedrock](/plugins/reference/amazon-bedrock) | Adds Amazon Bedrock model provider support to OpenClaw. | `@openclaw/amazon-bedrock-provider`<br />npm | providers: amazon-bedrock; contracts: memoryEmbeddingProviders |
| [amazon-bedrock-mantle](/plugins/reference/amazon-bedrock-mantle) | Adds Amazon Bedrock Mantle model provider support to OpenClaw. | `@openclaw/amazon-bedrock-mantle-provider`<br />npm | providers: amazon-bedrock-mantle |
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | Adds Anthropic Vertex model provider support to OpenClaw. | `@openclaw/anthropic-vertex-provider`<br />npm; ClawHub | providers: anthropic-vertex |
| [brave](/plugins/reference/brave) | Adds web search provider support. | `@openclaw/brave-plugin`<br />npm; ClawHub | contracts: webSearchProviders |
| [codex](/plugins/reference/codex) | Codex app-server harness and Codex-managed GPT model catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | Read-only diff viewer and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
| [discord](/plugins/reference/discord) | Adds the Discord channel surface for sending and receiving OpenClaw messages. | `@openclaw/discord`<br />npm; ClawHub | channels: discord |
| [feishu](/plugins/reference/feishu) | Adds the Feishu channel surface for sending and receiving OpenClaw messages. | `@openclaw/feishu`<br />npm; ClawHub | channels: feishu; contracts: tools; skills |
| [google-meet](/plugins/reference/google-meet) | Join Google Meet calls through Chrome or Twilio transports. | `@openclaw/google-meet`<br />npm; ClawHub | contracts: tools |
| [googlechat](/plugins/reference/googlechat) | Adds the Google Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/googlechat`<br />npm; ClawHub | channels: googlechat |
| [line](/plugins/reference/line) | Adds the LINE channel surface for sending and receiving OpenClaw messages. | `@openclaw/line`<br />npm; ClawHub | channels: line |
| [lobster](/plugins/reference/lobster) | Typed workflow tool with resumable approvals. | `@openclaw/lobster`<br />npm; ClawHub | contracts: tools |
| [matrix](/plugins/reference/matrix) | Adds the Matrix channel surface for sending and receiving OpenClaw messages. | `@openclaw/matrix`<br />ClawHub: `clawhub:@openclaw/matrix`; npm | channels: matrix |
| [memory-lancedb](/plugins/reference/memory-lancedb) | Adds agent-callable tools. | `@openclaw/memory-lancedb`<br />npm; ClawHub | contracts: tools |
| [msteams](/plugins/reference/msteams) | Adds the Microsoft Teams channel surface for sending and receiving OpenClaw messages. | `@openclaw/msteams`<br />npm; ClawHub | channels: msteams |
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | Adds the Nextcloud Talk channel surface for sending and receiving OpenClaw messages. | `@openclaw/nextcloud-talk`<br />npm; ClawHub | channels: nextcloud-talk |
| [nostr](/plugins/reference/nostr) | Adds the Nostr channel surface for sending and receiving OpenClaw messages. | `@openclaw/nostr`<br />npm; ClawHub | channels: nostr |
| [openshell](/plugins/reference/openshell) | Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. | `@openclaw/openshell-sandbox`<br />npm; ClawHub | plugin |
| [qqbot](/plugins/reference/qqbot) | Adds the QQ Bot channel surface for sending and receiving OpenClaw messages. | `@openclaw/qqbot`<br />npm; ClawHub | channels: qqbot; contracts: tools; skills |
| [slack](/plugins/reference/slack) | Adds the Slack channel surface for sending and receiving OpenClaw messages. | `@openclaw/slack`<br />npm; ClawHub | channels: slack |
| [synology-chat](/plugins/reference/synology-chat) | Adds the Synology Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/synology-chat`<br />npm; ClawHub | channels: synology-chat |
| [tlon](/plugins/reference/tlon) | Adds the Tlon channel surface for sending and receiving OpenClaw messages. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; contracts: tools; skills |
| [twitch](/plugins/reference/twitch) | Adds the Twitch channel surface for sending and receiving OpenClaw messages. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [voice-call](/plugins/reference/voice-call) | Adds agent-callable tools. | `@openclaw/voice-call`<br />npm; ClawHub | contracts: tools |
| [whatsapp](/plugins/reference/whatsapp) | Adds the WhatsApp channel surface for sending and receiving OpenClaw messages. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [zalo](/plugins/reference/zalo) | Adds the Zalo channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalo`<br />npm; ClawHub | channels: zalo |
| [zalouser](/plugins/reference/zalouser) | Adds the Zalo Personal channel surface for sending and receiving OpenClaw messages. | `@openclaw/zalouser`<br />npm; ClawHub | channels: zalouser; contracts: tools |
## Source checkout only

View File

@@ -19,10 +19,10 @@ pnpm plugins:inventory:gen
| ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [acpx](/plugins/reference/acpx) | Embedded ACP runtime backend with plugin-owned session and transport management. | `@openclaw/acpx`<br />npm; ClawHub | skills |
| [alibaba](/plugins/reference/alibaba) | Adds video generation provider support. | `@openclaw/alibaba-provider`<br />included in OpenClaw | contracts: videoGenerationProviders |
| [amazon-bedrock](/plugins/reference/amazon-bedrock) | Adds Amazon Bedrock model provider support to OpenClaw. | `@openclaw/amazon-bedrock-provider`<br />included in OpenClaw | providers: amazon-bedrock; contracts: memoryEmbeddingProviders |
| [amazon-bedrock-mantle](/plugins/reference/amazon-bedrock-mantle) | Adds Amazon Bedrock Mantle model provider support to OpenClaw. | `@openclaw/amazon-bedrock-mantle-provider`<br />included in OpenClaw | providers: amazon-bedrock-mantle |
| [amazon-bedrock](/plugins/reference/amazon-bedrock) | Adds Amazon Bedrock model provider support to OpenClaw. | `@openclaw/amazon-bedrock-provider`<br />npm | providers: amazon-bedrock; contracts: memoryEmbeddingProviders |
| [amazon-bedrock-mantle](/plugins/reference/amazon-bedrock-mantle) | Adds Amazon Bedrock Mantle model provider support to OpenClaw. | `@openclaw/amazon-bedrock-mantle-provider`<br />npm | providers: amazon-bedrock-mantle |
| [anthropic](/plugins/reference/anthropic) | Adds Anthropic model provider support to OpenClaw. | `@openclaw/anthropic-provider`<br />included in OpenClaw | providers: anthropic; contracts: mediaUnderstandingProviders |
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | Adds Anthropic Vertex model provider support to OpenClaw. | `@openclaw/anthropic-vertex-provider`<br />included in OpenClaw | providers: anthropic-vertex |
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | Adds Anthropic Vertex model provider support to OpenClaw. | `@openclaw/anthropic-vertex-provider`<br />npm; ClawHub | providers: anthropic-vertex |
| [arcee](/plugins/reference/arcee) | Adds Arcee model provider support to OpenClaw. | `@openclaw/arcee-provider`<br />included in OpenClaw | providers: arcee |
| [azure-speech](/plugins/reference/azure-speech) | Azure AI Speech text-to-speech (MP3, native Ogg/Opus voice notes, PCM telephony). | `@openclaw/azure-speech`<br />included in OpenClaw | contracts: speechProviders |
| [bonjour](/plugins/reference/bonjour) | Advertise the local OpenClaw gateway over Bonjour/mDNS. | `@openclaw/bonjour`<br />included in OpenClaw | plugin |
@@ -93,7 +93,7 @@ pnpm plugins:inventory:gen
| [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`<br />included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders |
| [opencode-go](/plugins/reference/opencode-go) | Adds OpenCode Go model provider support to OpenClaw. | `@openclaw/opencode-go-provider`<br />included in OpenClaw | providers: opencode-go; contracts: mediaUnderstandingProviders |
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`<br />included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, speechProviders, videoGenerationProviders |
| [openshell](/plugins/reference/openshell) | Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. | `@openclaw/openshell-sandbox`<br />included in OpenClaw | plugin |
| [openshell](/plugins/reference/openshell) | Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-based command execution. | `@openclaw/openshell-sandbox`<br />npm; ClawHub | plugin |
| [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [qa-channel](/plugins/reference/qa-channel) | Adds the QA Channel surface for sending and receiving OpenClaw messages. | `@openclaw/qa-channel`<br />source checkout only | channels: qa-channel |
| [qa-lab](/plugins/reference/qa-lab) | OpenClaw QA lab plugin with private debugger UI and scenario runner. | `@openclaw/qa-lab`<br />source checkout only | plugin |
@@ -107,7 +107,7 @@ pnpm plugins:inventory:gen
| [sglang](/plugins/reference/sglang) | Adds SGLang model provider support to OpenClaw. | `@openclaw/sglang-provider`<br />included in OpenClaw | providers: sglang |
| [signal](/plugins/reference/signal) | Adds the Signal channel surface for sending and receiving OpenClaw messages. | `@openclaw/signal`<br />included in OpenClaw | channels: signal |
| [skill-workshop](/plugins/reference/skill-workshop) | Captures repeatable workflows as workspace skills, with pending review, safe writes, and skill prompt refresh. | `@openclaw/skill-workshop`<br />included in OpenClaw | contracts: tools |
| [slack](/plugins/reference/slack) | Adds the Slack channel surface for sending and receiving OpenClaw messages. | `@openclaw/slack`<br />included in OpenClaw | channels: slack |
| [slack](/plugins/reference/slack) | Adds the Slack channel surface for sending and receiving OpenClaw messages. | `@openclaw/slack`<br />npm; ClawHub | channels: slack |
| [stepfun](/plugins/reference/stepfun) | Adds StepFun, StepFun Plan model provider support to OpenClaw. | `@openclaw/stepfun-provider`<br />included in OpenClaw | providers: stepfun, stepfun-plan |
| [synology-chat](/plugins/reference/synology-chat) | Adds the Synology Chat channel surface for sending and receiving OpenClaw messages. | `@openclaw/synology-chat`<br />npm; ClawHub | channels: synology-chat |
| [synthetic](/plugins/reference/synthetic) | Adds Synthetic model provider support to OpenClaw. | `@openclaw/synthetic-provider`<br />included in OpenClaw | providers: synthetic |

View File

@@ -12,7 +12,7 @@ Adds Amazon Bedrock Mantle model provider support to OpenClaw.
## Distribution
- Package: `@openclaw/amazon-bedrock-mantle-provider`
- Install route: included in OpenClaw
- Install route: npm
## Surface

View File

@@ -12,7 +12,7 @@ Adds Amazon Bedrock model provider support to OpenClaw.
## Distribution
- Package: `@openclaw/amazon-bedrock-provider`
- Install route: included in OpenClaw
- Install route: npm
## Surface

View File

@@ -12,7 +12,7 @@ Adds Anthropic Vertex model provider support to OpenClaw.
## Distribution
- Package: `@openclaw/anthropic-vertex-provider`
- Install route: included in OpenClaw
- Install route: npm; ClawHub
## Surface

View File

@@ -12,7 +12,7 @@ Sandbox backend powered by OpenShell with mirrored local workspaces and SSH-base
## Distribution
- Package: `@openclaw/openshell-sandbox`
- Install route: included in OpenClaw
- Install route: npm; ClawHub
## Surface

View File

@@ -12,7 +12,7 @@ Adds the Slack channel surface for sending and receiving OpenClaw messages.
## Distribution
- Package: `@openclaw/slack`
- Install route: included in OpenClaw
- Install route: npm; ClawHub
## Surface

View File

@@ -320,9 +320,9 @@ descriptor-backed placeholders for parse-time lazy loading.
### CLI backend registration
`api.registerCliBackend(...)` lets a plugin own the default config for a local
AI CLI backend such as `codex-cli`.
AI CLI backend such as `claude-cli` or `my-cli`.
- The backend `id` becomes the provider prefix in model refs like `codex-cli/gpt-5`.
- The backend `id` becomes the provider prefix in model refs like `my-cli/gpt-5`.
- The backend `config` uses the same shape as `agents.defaults.cliBackends.<id>`.
- User config still wins. OpenClaw merges `agents.defaults.cliBackends.<id>` over the
plugin default before running the CLI.

View File

@@ -41,6 +41,13 @@ but new code should not add imports from them: `agent-runtime-test-contracts`,
`text-runtime`, and `zod`. Import `zod` directly from `zod` in new plugin code.
`plugin-test-runtime` is still an active focused test helper subpath.
### Reserved bundled plugin helper subpaths
These subpaths are plugin-owned compatibility surfaces reserved for their owning
bundled plugin, not general SDK APIs: `plugin-sdk/codex-mcp-projection` and
`plugin-sdk/codex-native-task-runtime`. Cross-owner extension imports are blocked
by package contract guardrails.
### Deprecated unused public subpaths
These public subpaths existed for at least one month and currently have no
@@ -215,6 +222,8 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`,
| `plugin-sdk/runtime` | Broad runtime/logging/backup/plugin-install helpers |
| `plugin-sdk/runtime-env` | Narrow runtime env, logger, timeout, retry, and backoff helpers |
| `plugin-sdk/browser-config` | Supported browser config facade for normalized profile/defaults, CDP URL parsing, and browser-control auth helpers |
| `plugin-sdk/codex-mcp-projection` | Reserved bundled Codex helper for projecting user MCP server config into Codex thread config; not for third-party plugins |
| `plugin-sdk/codex-native-task-runtime` | Reserved bundled Codex helper for native task mirror/runtime wiring; not for third-party plugins |
| `plugin-sdk/channel-runtime-context` | Generic channel runtime-context registration and lookup helpers |
| `plugin-sdk/matrix` | Deprecated Matrix compatibility facade for older third-party channel packages; new plugins should import `plugin-sdk/run-command` directly |
| `plugin-sdk/mattermost` | Deprecated Mattermost compatibility facade for older third-party channel packages; new plugins should import generic SDK subpaths directly |
@@ -361,10 +370,18 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`,
</Accordion>
<Accordion title="Reserved bundled-helper subpaths">
There are currently no reserved bundled-helper SDK subpaths. Owner-specific
helpers live inside the owning plugin package, while reusable host contracts
use generic SDK subpaths such as `plugin-sdk/gateway-runtime`,
`plugin-sdk/security-runtime`, and `plugin-sdk/plugin-config-runtime`.
Reserved bundled-helper SDK subpaths are narrow owner-specific surfaces for
bundled plugin code. They are tracked in the SDK inventory so package
builds and aliasing stay deterministic, but they are not general plugin
authoring APIs. New reusable host contracts should use generic SDK subpaths
such as `plugin-sdk/gateway-runtime`, `plugin-sdk/security-runtime`, and
`plugin-sdk/plugin-config-runtime`.
| Subpath | Owner and purpose |
| --- | --- |
| `plugin-sdk/codex-mcp-projection` | Bundled Codex plugin helper for projecting user MCP server config into Codex app-server thread config |
| `plugin-sdk/codex-native-task-runtime` | Bundled Codex plugin helper for mirroring Codex app-server native subagents into OpenClaw task state |
</Accordion>
</AccordionGroup>

View File

@@ -49,9 +49,9 @@ model as `provider/model`.
- [xAI](/providers/xai)
- [Z.AI](/providers/zai)
## Additional bundled provider variants
## Additional provider variants
- `anthropic-vertex` - implicit Anthropic on Google Vertex support when Vertex credentials are available; no separate onboarding auth choice
- `anthropic-vertex` - install `@openclaw/anthropic-vertex-provider` for implicit Anthropic on Google Vertex support when Vertex credentials are available; no separate onboarding auth choice
- `copilot-proxy` - local VS Code Copilot Proxy bridge; use `openclaw onboard --auth-choice copilot-proxy`
- `google-gemini-cli` - unofficial Gemini CLI OAuth flow; requires a local `gemini` install (`brew install gemini-cli` or `npm install -g @google/gemini-cli`); default model `google-gemini-cli/gemini-3-flash-preview`; use `openclaw onboard --auth-choice google-gemini-cli` or `openclaw models auth login --provider google-gemini-cli --set-default`

View File

@@ -75,7 +75,7 @@ PI runtime config remains available as an opt-in compatibility route. When PI is
explicitly selected with an `openai-codex` auth profile, OpenClaw keeps the
public model ref as `openai/*` and routes PI internally through the legacy
Codex-auth transport. Run `openclaw doctor --fix` to repair stale
`openai-codex/*` model refs or old PI session pins that do not come from
`openai-codex/*`, `codex-cli/*`, or old PI session pins that do not come from
explicit runtime config.
</Note>
@@ -85,7 +85,7 @@ explicit runtime config.
| ------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------ |
| Chat / Responses | `openai/<model>` model provider | Yes |
| Codex subscription models | `openai/<model>` with `openai-codex` OAuth | Yes |
| Legacy Codex model refs | `openai-codex/<model>` | Repaired by doctor to `openai/<model>` |
| Legacy Codex model refs | `openai-codex/<model>` or `codex-cli/<model>` | Repaired by doctor to `openai/<model>` |
| Codex app-server harness | `openai/<model>` with omitted runtime or provider/model `agentRuntime.id: codex` | Yes |
| Server-side web search | Native OpenAI Responses tool | Yes, when web search is enabled and no provider pinned |
| Images | `image_generate` | Yes |
@@ -245,6 +245,7 @@ Choose your preferred auth method and follow the setup steps.
| `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in or ordered `openai` auth profile |
| `openai/gpt-5.5` | provider/model `agentRuntime.id: "pi"` | PI embedded runtime with internal Codex-auth transport | Selected `openai-codex` profile |
| `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Existing `openai-codex` profile |
| `codex-cli/gpt-5.5` | repaired by doctor | Legacy CLI route rewritten to `openai/gpt-5.5` | Codex app-server auth |
<Warning>
Do not configure older `openai-codex/gpt-5.1*`, `openai-codex/gpt-5.2*`, or

View File

@@ -15,6 +15,7 @@ title: "Tests"
- `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed`: explicit broad changed test run. Use it when a test harness/config/package edit should fall back to Vitest's broader changed-test behavior.
- `pnpm changed:lanes`: shows the architectural lanes triggered by the diff against `origin/main`.
- `pnpm check:changed`: runs the smart changed check gate for the diff against `origin/main`. It runs typecheck, lint, and guard commands for the affected architectural lanes, but does not run Vitest tests. Use `pnpm test:changed` or explicit `pnpm test <target>` for test proof.
- Codex worktrees and linked/sparse checkouts: avoid direct local `pnpm test*`, `pnpm check*`, and `pnpm crabbox:run` unless you have verified pnpm will not reconcile dependencies. For tiny explicit-file proof use `node scripts/run-vitest.mjs <path-or-filter>`; for changed gates or broad proof use `node scripts/crabbox-wrapper.mjs run --provider blacksmith-testbox ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox.
- `OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree <local-heavy-check command>`: keeps heavy-check serialization inside the current worktree instead of the Git common dir for commands such as `pnpm check:changed` and targeted `pnpm test ...`. Use it only on high-capacity local hosts when you intentionally run independent checks across linked worktrees.
- `pnpm test`: routes explicit file/directory targets through scoped Vitest lanes. Untargeted runs use fixed shard groups and expand to leaf configs for local parallel execution; the extension group always expands to the per-extension shard configs instead of one giant root-project process.
- Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` summary. Vitest's own duration line stays the per-shard detail.
@@ -42,7 +43,7 @@ title: "Tests"
- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan for selected lanes, image kinds, package/live-image needs, state scenarios, and credential checks without building or running Docker. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. If one lane exceeds the effective weight or resource cap on a low-parallelism host, it can still start from an empty pool and will run alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs, `summary.json`, `failures.json`, and phase timings are written under `.artifacts/docker-tests/<run-id>/`; use `pnpm test:docker:timings <summary.json>` to inspect slow lanes and `pnpm test:docker:rerun <run-id|summary.json|failures.json>` to print cheap targeted rerun commands.
- `pnpm test:docker:browser-cdp-snapshot`: Builds a Chromium-backed source E2E container, starts raw CDP plus an isolated Gateway, runs `browser doctor --deep`, and verifies CDP role snapshots include link URLs, cursor-promoted clickables, iframe refs, and frame metadata.
- `pnpm test:docker:skill-install`: Installs the packed OpenClaw tarball in a bare Docker runner, disables `skills.install.allowUploadedArchives`, resolves a current skill slug from live ClawHub search, installs it through `openclaw skills install`, and verifies `SKILL.md`, `.clawhub/origin.json`, `.clawhub/lock.json`, and `skills info --json`.
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:codex`, `pnpm test:docker:live-cli-backend:codex:resume`, or `pnpm test:docker:live-cli-backend:codex:mcp`. Claude and Gemini have matching `:resume` and `:mcp` aliases.
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:claude`, `pnpm test:docker:live-cli-backend:claude:resume`, or `pnpm test:docker:live-cli-backend:claude:mcp`. Gemini has matching `:resume` and `:mcp` aliases.
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key, pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.

View File

@@ -77,7 +77,7 @@ Onboarding starts with **QuickStart** (defaults) vs **Advanced** (full control).
3. **Gateway** — Port, bind address, auth mode, Tailscale exposure.
In interactive token mode, choose default plaintext token storage or opt into SecretRef.
Non-interactive token SecretRef path: `--gateway-token-ref-env <ENV_VAR>`.
4. **Channels** — built-in and bundled chat channels such as iMessage, Discord, Feishu, Google Chat, Mattermost, Microsoft Teams, QQ Bot, Signal, Slack, Telegram, WhatsApp, and more.
4. **Channels** — built-in and official plugin chat channels such as iMessage, Discord, Feishu, Google Chat, Mattermost, Microsoft Teams, QQ Bot, Signal, Slack, Telegram, WhatsApp, and more.
5. **Daemon** — Installs a LaunchAgent (macOS), systemd user unit (Linux/WSL2), or native Windows Scheduled Task with per-user Startup-folder fallback.
If token auth requires a token and `gateway.auth.token` is SecretRef-managed, daemon install validates it but does not persist the resolved token into supervisor service environment metadata.
If token auth requires a token and the configured token SecretRef is unresolved, daemon install is blocked with actionable guidance.

View File

@@ -31,9 +31,12 @@ If a skill name conflicts, the highest source wins.
Codex CLI's native `$CODEX_HOME/skills` directory is not one of these OpenClaw
skill roots. In Codex harness mode, local app-server launches use isolated
per-agent Codex homes, so personal Codex CLI skills are not loaded implicitly.
Use `openclaw migrate codex --dry-run` to inventory them and
`openclaw migrate codex` to choose skill directories with an interactive
per-agent Codex homes, so skills in the operator's personal `~/.codex/skills`
are not loaded implicitly. Codex-native `.agents` discovery uses inherited
`HOME` separately; OpenClaw's own skill roots above already include
`~/.agents/skills`. Use `openclaw migrate codex --dry-run` to inventory skills
from the Codex home, then `openclaw migrate codex` to choose skill directories
with an interactive
checkbox prompt before copying them into the current OpenClaw agent workspace.
For non-interactive runs, repeat `--skill <name>` for the exact skills to copy.

View File

@@ -168,6 +168,20 @@ This disables runtime trajectory capture. `/export-trajectory` can still export
the transcript branch, but runtime-only files such as compiled context,
provider artifacts, and prompt metadata may be missing.
## Tune flush timeout
OpenClaw flushes runtime trajectory sidecars during agent cleanup. The default
cleanup timeout is 10,000 ms. On slow disks or large stores, set
`OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS` before starting OpenClaw:
```bash
export OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS=30000
```
This controls when OpenClaw logs a `pi-trajectory-flush` timeout and continues.
It does not change the trajectory size caps. To tune all agent cleanup steps
that do not pass an explicit timeout, set `OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS`.
## Privacy and limits
Trajectory bundles are designed for support and debugging, not public posting.

View File

@@ -73,6 +73,7 @@ Notes:
## Sending + delivery
- Messages are sent to the Gateway; delivery to providers is off by default.
- The TUI is an internal source surface like WebChat, not a generic outbound channel. Harnesses that require `tools.message` for visible replies can satisfy the active TUI turn with a targetless `message.send`; explicit provider delivery still uses normal configured channels and never falls back to `lastChannel`.
- Turn delivery on:
- `/deliver on`
- or the Settings panel

View File

@@ -51,6 +51,7 @@ WebChat has two separate data paths:
- The session JSONL file is the durable model/runtime transcript. For normal agent runs, Pi persists model-visible `user`, `assistant`, and `toolResult` messages through its session manager. WebChat does not write arbitrary delivery, status, or helper text into that transcript.
- Gateway `ReplyPayload` events are the live delivery projection. They can be normalized for WebChat/channel display, block streaming, directive tags, media embedding, TTS/audio flags, and UI fallback behavior. They are not themselves the canonical session log.
- Harnesses that require visible replies through `tools.message` still use WebChat as a current-run internal source reply sink. A targetless `message.send` from that active WebChat run is projected into the same chat and mirrored to the session transcript; WebChat does not become a reusable outbound channel and never inherits `lastChannel`.
- WebChat injects assistant transcript entries only when the Gateway owns a displayed message outside a normal Pi assistant turn: `chat.inject`, non-agent command replies, aborted partial output, and WebChat-managed media transcript supplements.
- `chat.history` reads the stored session transcript and applies WebChat display projection. If live assistant text appears during a run but disappears after history reload, first check whether the raw JSONL contains the assistant text, then whether `chat.history` projection stripped it, then whether the Control UI optimistic-tail merge replaced local delivery state with the persisted snapshot.

View File

@@ -116,6 +116,14 @@ describe("active-memory plugin", () => {
config: {
current: () => configFile,
loadConfig: () => configFile,
mutateConfigFile: vi.fn(
async ({ mutate }: { mutate: (draft: Record<string, unknown>) => void }) => {
const draft = structuredClone(configFile);
mutate(draft);
configFile = draft;
return { changed: true, config: configFile };
},
),
replaceConfigFile: vi.fn(
async ({ nextConfig }: { nextConfig: Record<string, unknown> }) => {
configFile = nextConfig;
@@ -476,7 +484,7 @@ describe("active-memory plugin", () => {
});
expect(offResult.text).toBe("Active Memory: off globally.");
expect(api.runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
expect(api.runtime.config.mutateConfigFile).toHaveBeenCalledTimes(1);
expect(
requireRecord(
requireRecord(requireRecord(configFile.plugins, "plugins").entries, "entries")[
@@ -576,7 +584,7 @@ describe("active-memory plugin", () => {
expect(result.text).toContain("global enable/disable changes require operator.admin");
}
expect(api.runtime.config.replaceConfigFile).not.toHaveBeenCalled();
expect(api.runtime.config.mutateConfigFile).not.toHaveBeenCalled();
});
it("allows admin-scoped gateway callers to change global active-memory config", async () => {
@@ -595,7 +603,7 @@ describe("active-memory plugin", () => {
});
expect(result.text).toBe("Active Memory: off globally.");
expect(api.runtime.config.replaceConfigFile).toHaveBeenCalledTimes(1);
expect(api.runtime.config.mutateConfigFile).toHaveBeenCalledTimes(1);
expect(
requireRecord(
requireRecord(requireRecord(configFile.plugins, "plugins").entries, "entries")[

View File

@@ -1,8 +1,11 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.5.12-beta.1",
"private": true,
"description": "OpenClaw Amazon Bedrock Mantle (OpenAI-compatible) provider plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"@anthropic-ai/sdk": "0.95.2",
@@ -15,6 +18,21 @@
"openclaw": {
"extensions": [
"./index.ts"
]
],
"install": {
"npmSpec": "@openclaw/amazon-bedrock-mantle-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"bundledDist": false
},
"release": {
"publishToNpm": true
}
}
}

View File

@@ -1,8 +1,11 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.5.12-beta.1",
"private": true,
"description": "OpenClaw Amazon Bedrock provider plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"@aws-sdk/client-bedrock": "3.1045.0",
@@ -17,6 +20,21 @@
"openclaw": {
"extensions": [
"./index.ts"
]
],
"install": {
"npmSpec": "@openclaw/amazon-bedrock-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"bundledDist": false
},
"release": {
"publishToNpm": true
}
}
}

View File

@@ -1,8 +1,11 @@
{
"name": "@openclaw/anthropic-vertex-provider",
"version": "2026.5.12-beta.1",
"private": true,
"description": "OpenClaw Anthropic Vertex provider plugin",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"dependencies": {
"@anthropic-ai/vertex-sdk": "0.16.0",
@@ -15,6 +18,22 @@
"openclaw": {
"extensions": [
"./index.ts"
]
],
"install": {
"npmSpec": "@openclaw/anthropic-vertex-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.5.12-beta.1"
},
"build": {
"openclawVersion": "2026.5.12-beta.1",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -13,7 +13,11 @@ import { resolveAnthropicVertexClientRegion, resolveAnthropicVertexProjectId } f
type AnthropicVertexEffort = NonNullable<AnthropicOptions["effort"]>;
type AnthropicVertexAdaptiveEffort = AnthropicVertexEffort | "xhigh";
type AnthropicVertexClientOptions = ConstructorParameters<typeof AnthropicVertexSdk>[0];
type AnthropicVertexClientOptions = {
baseURL?: string;
projectId?: string;
region: string;
};
export type AnthropicVertexStreamDeps = {
AnthropicVertex: new (options: AnthropicVertexClientOptions) => unknown;

View File

@@ -0,0 +1,42 @@
import type { ModelCatalogEntry } from "openclaw/plugin-sdk/agent-runtime";
import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS } from "./cli-constants.js";
// Claude CLI auth is subscription-backed, so catalog rows only need picker metadata.
const CLAUDE_CLI_DEFAULT_CONTEXT_WINDOW = 200_000;
const CLAUDE_CLI_MODEL_LABELS: Record<string, string> = {
"claude-opus-4-7": "Claude Opus 4.7 (Claude CLI)",
"claude-opus-4-6": "Claude Opus 4.6 (Claude CLI)",
"claude-opus-4-5": "Claude Opus 4.5 (Claude CLI)",
"claude-sonnet-4-6": "Claude Sonnet 4.6 (Claude CLI)",
"claude-sonnet-4-5": "Claude Sonnet 4.5 (Claude CLI)",
"claude-haiku-4-5": "Claude Haiku 4.5 (Claude CLI)",
};
function extractClaudeCliModelIds(): string[] {
const ids: string[] = [];
const seen = new Set<string>();
for (const ref of CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS) {
if (!ref.startsWith(`${CLAUDE_CLI_BACKEND_ID}/`)) {
continue;
}
const id = ref.slice(CLAUDE_CLI_BACKEND_ID.length + 1);
if (id.length === 0 || seen.has(id)) {
continue;
}
seen.add(id);
ids.push(id);
}
return ids;
}
export function buildClaudeCliCatalogEntries(): ModelCatalogEntry[] {
return extractClaudeCliModelIds().map((id) => ({
id,
name: CLAUDE_CLI_MODEL_LABELS[id] ?? `${id} (Claude CLI)`,
provider: CLAUDE_CLI_BACKEND_ID,
reasoning: true,
input: ["text", "image"],
contextWindow: CLAUDE_CLI_DEFAULT_CONTEXT_WINDOW,
}));
}

View File

@@ -28,6 +28,7 @@ import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import * as claudeCliAuth from "./cli-auth-seam.js";
import { buildAnthropicCliBackend } from "./cli-backend.js";
import { buildClaudeCliCatalogEntries } from "./cli-catalog.js";
import { buildAnthropicCliMigrationResult } from "./cli-migration.js";
import {
CLAUDE_CLI_BACKEND_ID,
@@ -579,6 +580,8 @@ export function buildAnthropicProvider(): ProviderPlugin {
normalizeLowercaseStringOrEmpty(provider) === CLAUDE_CLI_BACKEND_ID
? resolveClaudeCliSyntheticAuth()
: undefined,
// Publish Claude CLI rows through the provider catalog hook.
augmentModelCatalog: () => buildClaudeCliCatalogEntries(),
buildReplayPolicy: buildAnthropicReplayPolicy,
isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId),
resolveReasoningOutputMode: () => "native",

View File

@@ -106,6 +106,34 @@ describe("brave web search provider", () => {
);
});
it("exposes legacy top-level apiKey as a Brave-owned compatibility fallback", () => {
const apiKey = { source: "env", provider: "default", id: "BRAVE_API_KEY" } as const;
const config = {
tools: {
web: {
search: {
apiKey,
},
},
},
};
expect(createBraveWebSearchProvider().getConfiguredCredentialValue?.(config)).toEqual(apiKey);
expect(createBraveWebSearchContractProvider().getConfiguredCredentialValue?.(config)).toEqual(
apiKey,
);
expect(createBraveWebSearchProvider().getConfiguredCredentialFallback?.(config)).toEqual({
path: "tools.web.search.apiKey",
value: apiKey,
});
expect(
createBraveWebSearchContractProvider().getConfiguredCredentialFallback?.(config),
).toEqual({
path: "tools.web.search.apiKey",
value: apiKey,
});
});
it("points missing-key users to fetch/browser alternatives", async () => {
vi.stubEnv("BRAVE_API_KEY", "");
const provider = createBraveWebSearchProvider();

View File

@@ -79,6 +79,28 @@ function resolveProviderWebSearchPluginConfig(
return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : undefined;
}
function resolveLegacyTopLevelBraveCredential(
config: unknown,
): { path: string; value: unknown } | undefined {
if (!isRecord(config)) {
return undefined;
}
const tools = isRecord(config.tools) ? config.tools : undefined;
const web = isRecord(tools?.web) ? tools.web : undefined;
const search = isRecord(web?.search) ? web.search : undefined;
if (!search || !("apiKey" in search)) {
return undefined;
}
return { path: "tools.web.search.apiKey", value: search.apiKey };
}
function resolveConfiguredBraveCredential(config: unknown): unknown {
return (
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey ??
resolveLegacyTopLevelBraveCredential(config)?.value
);
}
function mergeScopedSearchConfig(
searchConfig: Record<string, unknown> | undefined,
key: string,
@@ -148,6 +170,8 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
searchCredential: { type: "top-level" },
configuredCredential: { pluginId: "brave" },
}),
getConfiguredCredentialValue: resolveConfiguredBraveCredential,
getConfiguredCredentialFallback: resolveLegacyTopLevelBraveCredential,
createTool: (ctx) =>
createBraveToolDefinition(
mergeScopedSearchConfig(

View File

@@ -3,6 +3,46 @@ import {
type WebSearchProviderPlugin,
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function resolveLegacyTopLevelBraveCredential(
config: unknown,
): { path: string; value: unknown } | undefined {
if (!isRecord(config)) {
return undefined;
}
const tools = isRecord(config.tools) ? config.tools : undefined;
const web = isRecord(tools?.web) ? tools.web : undefined;
const search = isRecord(web?.search) ? web.search : undefined;
if (!search || !("apiKey" in search)) {
return undefined;
}
return { path: "tools.web.search.apiKey", value: search.apiKey };
}
function resolveProviderWebSearchPluginConfig(
config: unknown,
pluginId: string,
): Record<string, unknown> | undefined {
if (!isRecord(config)) {
return undefined;
}
const plugins = isRecord(config.plugins) ? config.plugins : undefined;
const entries = isRecord(plugins?.entries) ? plugins.entries : undefined;
const entry = isRecord(entries?.[pluginId]) ? entries[pluginId] : undefined;
const pluginConfig = isRecord(entry?.config) ? entry.config : undefined;
return isRecord(pluginConfig?.webSearch) ? pluginConfig.webSearch : undefined;
}
function resolveConfiguredBraveCredential(config: unknown): unknown {
return (
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey ??
resolveLegacyTopLevelBraveCredential(config)?.value
);
}
export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
const credentialPath = "plugins.entries.brave.config.webSearch.apiKey";
@@ -23,6 +63,8 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
searchCredential: { type: "top-level" },
configuredCredential: { pluginId: "brave" },
}),
getConfiguredCredentialValue: resolveConfiguredBraveCredential,
getConfiguredCredentialFallback: resolveLegacyTopLevelBraveCredential,
createTool: () => null,
};
}

View File

@@ -7,6 +7,10 @@ import type {
OpenClawPluginToolContext,
OpenClawPluginToolFactory,
} from "openclaw/plugin-sdk/plugin-entry";
import {
BROWSER_REQUEST_GATEWAY_METHOD,
BROWSER_REQUEST_GATEWAY_SCOPE,
} from "./src/browser-gateway-contract.js";
import { BrowserToolSchema } from "./src/browser-tool.schema.js";
const BROWSER_CLI_DESCRIPTOR = {
@@ -107,13 +111,13 @@ export function registerBrowserPlugin(api: OpenClawPluginApi) {
{ commands: ["browser"], descriptors: [BROWSER_CLI_DESCRIPTOR] },
);
api.registerGatewayMethod(
"browser.request",
BROWSER_REQUEST_GATEWAY_METHOD,
async (opts) => {
const { handleBrowserGatewayRequest } = await import("./register.runtime.js");
return await handleBrowserGatewayRequest(opts);
},
{
scope: "operator.admin",
scope: BROWSER_REQUEST_GATEWAY_SCOPE,
},
);
api.registerService(createLazyBrowserPluginService());

View File

@@ -0,0 +1,3 @@
export const BROWSER_REQUEST_GATEWAY_METHOD = "browser.request" as const;
export const BROWSER_REQUEST_GATEWAY_SCOPE = "operator.admin" as const;
export const BROWSER_REQUEST_GATEWAY_SCOPES = [BROWSER_REQUEST_GATEWAY_SCOPE] as const;

View File

@@ -660,6 +660,20 @@ describe("browser tool snapshot maxChars", () => {
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
});
it("fails node proxy calls cleanly when payloadJSON is malformed", async () => {
mockSingleBrowserProxyNode();
gatewayMocks.callGatewayTool.mockResolvedValueOnce({
ok: true,
payloadJSON: "{not json",
});
const tool = createBrowserTool();
await expect(tool.execute?.("call-1", { action: "status", target: "node" })).rejects.toThrow(
"browser proxy failed",
);
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
});
it("returns a browser doctor report on host", async () => {
const tool = createBrowserTool();
await tool.execute?.("call-1", { action: "doctor" });

View File

@@ -316,17 +316,27 @@ async function callBrowserProxy(params: {
idempotencyKey: crypto.randomUUID(),
},
);
const parsed =
payload?.payload ??
(typeof payload?.payloadJSON === "string" && payload.payloadJSON
? (JSON.parse(payload.payloadJSON) as BrowserProxyResult)
: null);
const parsed = unwrapBrowserProxyPayload(payload);
if (!parsed || typeof parsed !== "object" || !("result" in parsed)) {
throw new Error("browser proxy failed");
}
return parsed;
}
function unwrapBrowserProxyPayload(payload: { payload?: unknown; payloadJSON?: unknown } | null) {
if (payload?.payload !== undefined) {
return payload.payload;
}
if (typeof payload?.payloadJSON !== "string" || !payload.payloadJSON.trim()) {
return null;
}
try {
return JSON.parse(payload.payloadJSON) as BrowserProxyResult;
} catch {
return null;
}
}
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
return await persistBrowserProxyFiles(files);
}

View File

@@ -0,0 +1,34 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { callGatewayFromCli } from "./core-api.js";
type CallGatewayFromCliArgs = Parameters<typeof callGatewayFromCli>;
const gatewayMocks = vi.hoisted(() => ({
callGatewayFromCli: vi.fn(async () => ({ ok: true })),
}));
vi.mock("./core-api.js", () => ({
callGatewayFromCli: gatewayMocks.callGatewayFromCli,
}));
const { callBrowserRequest } = await import("./browser-cli-shared.js");
describe("callBrowserRequest", () => {
beforeEach(() => {
gatewayMocks.callGatewayFromCli.mockClear();
});
it("requests the browser.request admin scope explicitly", async () => {
await callBrowserRequest(
{ json: true },
{ method: "GET", path: "/status", query: { profile: "openclaw" } },
{ progress: true },
);
const call = gatewayMocks.callGatewayFromCli.mock.calls[0] as unknown as
| CallGatewayFromCliArgs
| undefined;
const extra = call?.[3];
expect(extra).toEqual({ progress: true, scopes: ["operator.admin"] });
});
});

View File

@@ -1,4 +1,8 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
BROWSER_REQUEST_GATEWAY_METHOD,
BROWSER_REQUEST_GATEWAY_SCOPES,
} from "../browser-gateway-contract.js";
import { callGatewayFromCli, type GatewayRpcOpts } from "./core-api.js";
export type BrowserParentOpts = GatewayRpcOpts & {
@@ -44,7 +48,7 @@ export async function callBrowserRequest<T>(
: undefined;
const timeout = typeof resolvedTimeout === "number" ? String(resolvedTimeout) : opts.timeout;
const payload = await callGatewayFromCli(
"browser.request",
BROWSER_REQUEST_GATEWAY_METHOD,
{ ...opts, timeout },
{
method: params.method,
@@ -53,7 +57,7 @@ export async function callBrowserRequest<T>(
body: params.body,
timeoutMs: resolvedTimeout,
},
{ progress: extra?.progress },
{ progress: extra?.progress, scopes: [...BROWSER_REQUEST_GATEWAY_SCOPES] },
);
if (payload === undefined) {
throw new Error("Unexpected browser.request response");

View File

@@ -239,4 +239,26 @@ describe("canvas documents", () => {
),
).toBeNull();
});
it("rejects malformed encoded hosted canvas document paths", async () => {
const stateDir = await mkdtemp(path.join(tmpdir(), "openclaw-canvas-documents-"));
tempDirs.push(stateDir);
const documentId = "cv_malformed";
const documentDir = resolveCanvasDocumentDir(documentId, { stateDir });
await mkdir(documentDir, { recursive: true });
await writeFile(path.join(documentDir, "%E0%A4%A.html"), "literal-percent-name", "utf8");
expect(
resolveCanvasHttpPathToLocalPath(
`/__openclaw__/canvas/documents/${documentId}/%E0%A4%A.html`,
{ stateDir },
),
).toBeNull();
expect(
resolveCanvasHttpPathToLocalPath(
`/__openclaw__/canvas/documents/${documentId}/%25E0%25A4%25A.html`,
{ stateDir },
),
).toBe(path.join(documentDir, "%E0%A4%A.html"));
});
});

View File

@@ -153,16 +153,17 @@ export function resolveCanvasHttpPathToLocalPath(
}
const pathWithoutQuery = trimmed.replace(/[?#].*$/, "");
const relative = pathWithoutQuery.slice(prefix.length);
const segments = relative
.split("/")
.map((segment) => {
try {
return decodeURIComponent(segment);
} catch {
return segment;
}
})
.filter(Boolean);
const segments: string[] = [];
for (const segment of relative.split("/")) {
if (!segment) {
continue;
}
try {
segments.push(decodeURIComponent(segment));
} catch {
return null;
}
}
if (segments.length < 2) {
return null;
}

View File

@@ -47,7 +47,15 @@ describe("resolveFileWithinRoot", () => {
it("rejects traversal paths", async () => {
await withCanvasTemp("openclaw-canvas-resolver-", async (root) => {
await fs.writeFile(path.join(root, "outside.txt"), "inside-root", "utf8");
await expect(resolveFileWithinRoot(root, "/../outside.txt")).resolves.toBeNull();
await expect(resolveFileWithinRoot(root, "/%2e%2e%2foutside.txt")).resolves.toBeNull();
});
});
it("rejects malformed URL encoding as a missing file", async () => {
await withCanvasTemp("openclaw-canvas-resolver-", async (root) => {
await expect(resolveFileWithinRoot(root, "/%E0%A4%A")).resolves.toBeNull();
});
});

View File

@@ -9,11 +9,46 @@ export function normalizeUrlPath(rawPath: string): string {
return normalized.startsWith("/") ? normalized : `/${normalized}`;
}
function pathEscapesRoot(decodedPath: string): boolean {
let depth = 0;
for (const segment of decodedPath.split("/")) {
if (segment === "" || segment === ".") {
continue;
}
if (segment === "..") {
if (depth === 0) {
return true;
}
depth--;
continue;
}
depth++;
}
return false;
}
function tryNormalizeUrlPath(rawPath: string): string | null {
let decoded: string;
try {
decoded = decodeURIComponent(rawPath || "/");
} catch {
return null;
}
if (pathEscapesRoot(decoded)) {
return null;
}
const normalized = path.posix.normalize(decoded);
return normalized.startsWith("/") ? normalized : `/${normalized}`;
}
export async function resolveFileWithinRoot(
rootReal: string,
urlPath: string,
): Promise<CanvasOpenResult | null> {
const normalized = normalizeUrlPath(urlPath);
const normalized = tryNormalizeUrlPath(urlPath);
if (normalized === null) {
return null;
}
const rel = normalized.replace(/^\/+/, "");
if (rel.split("/").some((p) => p === "..")) {
return null;

View File

@@ -232,6 +232,10 @@ describe("canvas host", () => {
expect(response.body).toContain("v1");
expect(response.body).toContain(CANVAS_WS_PATH);
const malformed = await captureHandlerResponse(handler, `${CANVAS_HOST_PATH}/%E0%A4%A`);
expect(malformed.status).toBe(404);
expect(malformed.body).toBe("not found");
const miss = await captureHandlerResponse(handler, "/");
expect(miss.handled).toBe(false);
@@ -396,6 +400,9 @@ describe("canvas host", () => {
const traversalRes = await captureA2uiResponse(`${A2UI_PATH}/%2e%2e%2fpackage.json`);
expect(traversalRes.status).toBe(404);
expect(traversalRes.body).toBe("not found");
const malformedRes = await captureA2uiResponse(`${A2UI_PATH}/%E0%A4%A`);
expect(malformedRes.status).toBe(404);
expect(malformedRes.body).toBe("not found");
const symlinkRes = await captureA2uiResponse(`${A2UI_PATH}/${linkName}`);
expect(symlinkRes.status).toBe(404);
expect(symlinkRes.body).toBe("not found");

View File

@@ -0,0 +1,142 @@
import { EventEmitter } from "node:events";
import type { ChannelGatewayContext } from "openclaw/plugin-sdk/channel-contract";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ResolvedClickClackAccount } from "./types.js";
class FakeSocket extends EventEmitter {
close = vi.fn(() => {
this.emit("close");
});
}
const mocks = vi.hoisted(() => ({
client: {
me: vi.fn(),
events: vi.fn(),
websocket: vi.fn(),
channelMessages: vi.fn(),
directMessages: vi.fn(),
thread: vi.fn(),
},
handleClickClackInbound: vi.fn(),
resolveWorkspaceId: vi.fn(),
}));
vi.mock("./http-client.js", () => ({
createClickClackClient: vi.fn(() => mocks.client),
}));
vi.mock("./inbound.js", () => ({
handleClickClackInbound: mocks.handleClickClackInbound,
}));
vi.mock("./resolve.js", () => ({
resolveWorkspaceId: mocks.resolveWorkspaceId,
}));
import { startClickClackGatewayAccount } from "./gateway.js";
function createGatewayContext(
abortSignal: AbortSignal,
): ChannelGatewayContext<ResolvedClickClackAccount> {
const setStatus = vi.fn();
const log = { warn: vi.fn(), info: vi.fn(), error: vi.fn(), debug: vi.fn() };
return {
cfg: {
channels: {
clickclack: {
baseUrl: "https://clickclack.example",
token: "test-token",
workspace: "main",
reconnectMs: 1,
},
},
} as ChannelGatewayContext<ResolvedClickClackAccount>["cfg"],
accountId: "default",
account: {} as ResolvedClickClackAccount,
runtime: {} as ChannelGatewayContext<ResolvedClickClackAccount>["runtime"],
abortSignal,
log,
getStatus: () =>
({ accountId: "default" }) as ReturnType<
ChannelGatewayContext<ResolvedClickClackAccount>["getStatus"]
>,
setStatus,
};
}
describe("ClickClack gateway", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.client.me.mockResolvedValue({
id: "bot-user",
display_name: "Bot",
handle: "bot",
avatar_url: "",
created_at: "2026-01-01T00:00:00.000Z",
});
mocks.client.events.mockResolvedValue([]);
mocks.resolveWorkspaceId.mockResolvedValue("workspace-1");
mocks.client.channelMessages.mockResolvedValue([
{
id: "msg-1",
workspace_id: "workspace-1",
channel_id: "chan-1",
author_id: "human-1",
thread_root_id: "msg-1",
body: "hello",
body_format: "markdown",
created_at: "2026-01-01T00:00:00.000Z",
author: {
id: "human-1",
kind: "human",
display_name: "Human",
handle: "human",
avatar_url: "",
created_at: "2026-01-01T00:00:00.000Z",
},
},
]);
});
it("skips malformed websocket frames without stopping the monitor", async () => {
const socket = new FakeSocket();
mocks.client.websocket.mockReturnValue(socket);
const abort = new AbortController();
const ctx = createGatewayContext(abort.signal);
let runError: unknown;
const run = startClickClackGatewayAccount(ctx).catch((error: unknown) => {
runError = error;
});
await vi.waitFor(() => expect(mocks.client.websocket).toHaveBeenCalledTimes(1));
socket.emit("message", Buffer.from("{not json"));
await new Promise((resolve) => setImmediate(resolve));
expect(runError).toBeUndefined();
expect(ctx.log?.warn).toHaveBeenCalledWith(
"[default] skipped malformed ClickClack websocket event",
);
socket.emit(
"message",
Buffer.from(
JSON.stringify({
id: "evt-1",
cursor: "cursor-1",
type: "message.created",
workspace_id: "workspace-1",
channel_id: "chan-1",
seq: 2,
created_at: "2026-01-01T00:00:00.000Z",
payload: { message_id: "msg-1", author_id: "human-1" },
}),
),
);
await vi.waitFor(() => expect(mocks.handleClickClackInbound).toHaveBeenCalledTimes(1));
abort.abort();
await run;
expect(runError).toBeUndefined();
});
});

View File

@@ -65,6 +65,14 @@ function decodeSocketMessage(data: RawData): string {
return Buffer.concat(data).toString("utf8");
}
function parseSocketEvent(data: RawData): ClickClackEvent | null {
try {
return JSON.parse(decodeSocketMessage(data)) as ClickClackEvent;
} catch {
return null;
}
}
async function processEvent(params: {
account: ResolvedClickClackAccount;
config: CoreConfig;
@@ -146,7 +154,11 @@ export async function startClickClackGatewayAccount(
ctx.abortSignal.addEventListener("abort", abort, { once: true });
socket.on("message", (data) => {
void (async () => {
const event = JSON.parse(decodeSocketMessage(data)) as ClickClackEvent;
const event = parseSocketEvent(data);
if (!event) {
ctx.log?.warn?.(`[${account.accountId}] skipped malformed ClickClack websocket event`);
return;
}
afterCursor = event.cursor || afterCursor;
await processEvent({
account,

View File

@@ -10,6 +10,13 @@ import {
handleCodexConversationInboundClaim,
} from "./src/conversation-binding.js";
import { buildCodexMigrationProvider } from "./src/migration/provider.js";
import {
createCodexCliSessionNodeHostCommands,
createCodexCliSessionNodeInvokePolicies,
listCodexCliSessionsOnNode,
resumeCodexCliSessionOnNode,
resolveCodexCliSessionForBindingOnNode,
} from "./src/node-cli-sessions.js";
export default definePluginEntry({
id: "codex",
@@ -30,10 +37,28 @@ export default definePluginEntry({
buildCodexMediaUnderstandingProvider({ pluginConfig: api.pluginConfig }),
);
api.registerMigrationProvider(buildCodexMigrationProvider({ runtime: api.runtime }));
api.registerCommand(createCodexCommand({ pluginConfig: api.pluginConfig }));
for (const command of createCodexCliSessionNodeHostCommands()) {
api.registerNodeHostCommand(command);
}
for (const policy of createCodexCliSessionNodeInvokePolicies()) {
api.registerNodeInvokePolicy(policy);
}
api.registerCommand(
createCodexCommand({
pluginConfig: api.pluginConfig,
deps: {
listCodexCliSessionsOnNode: (params) =>
listCodexCliSessionsOnNode({ runtime: api.runtime, ...params }),
resolveCodexCliSessionForBindingOnNode: (params) =>
resolveCodexCliSessionForBindingOnNode({ runtime: api.runtime, ...params }),
},
}),
);
api.on("inbound_claim", (event, ctx) =>
handleCodexConversationInboundClaim(event, ctx, {
pluginConfig: resolveCurrentPluginConfig(),
resumeCodexCliSessionOnNode: (params) =>
resumeCodexCliSessionOnNode({ runtime: api.runtime, ...params }),
}),
);
api.onConversationBindingResolved?.(handleCodexConversationBindingResolved);

View File

@@ -12,6 +12,7 @@ import {
bridgeCodexAppServerStartOptions,
refreshCodexAppServerAuthTokens,
resolveCodexAppServerAuthAccountCacheKey,
resolveCodexAppServerAuthProfileId,
resolveCodexAppServerHomeDir,
resolveCodexAppServerNativeHomeDir,
} from "./auth-bridge.js";
@@ -651,6 +652,65 @@ describe("bridgeCodexAppServerStartOptions", () => {
}
});
it("selects an oauthRef-backed Codex profile for app-server login", () => {
expect(
resolveCodexAppServerAuthProfileId({
store: {
version: 1,
profiles: {
"openai-codex:default": {
type: "oauth",
provider: "openai-codex",
access: "",
refresh: "",
expires: Date.now() + 60_000,
oauthRef: {
source: "openclaw-credentials",
provider: "openai-codex",
id: "0123456789abcdef0123456789abcdef",
},
},
},
},
}),
).toBe("openai-codex:default");
});
it("answers refresh requests from a discovered oauthRef-backed Codex profile", async () => {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
access: "refreshed-ref-backed-access-token",
refresh: "refreshed-ref-backed-refresh-token",
expires: Date.now() + 60_000,
accountId: "account-ref-backed-refreshed",
});
try {
upsertAuthProfile({
agentDir,
profileId: "openai-codex:default",
credential: {
type: "oauth",
provider: "openai-codex",
access: "ref-backed-access-token",
refresh: "ref-backed-refresh-token",
expires: Date.now() + 60_000,
accountId: "account-ref-backed",
email: "codex@example.test",
},
});
await expect(refreshCodexAppServerAuthTokens({ agentDir })).resolves.toEqual({
accessToken: "refreshed-ref-backed-access-token",
chatgptAccountId: "account-ref-backed-refreshed",
chatgptPlanType: null,
});
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("ref-backed-refresh-token");
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
it("applies native Codex CLI OAuth when no OpenClaw auth profile exists", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
const agentDir = path.join(root, "agent");

View File

@@ -127,6 +127,42 @@ describe("CodexAppServerClient", () => {
await expect(request).rejects.toHaveProperty("message", "Method not found");
});
it("surfaces relogin details from Codex app-server RPC errors", async () => {
const harness = createClientHarness();
clients.push(harness.client);
const request = harness.client.request("thread/start", {});
const outbound = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
harness.send({
id: outbound.id,
error: {
code: -32602,
message: "failed to load configuration",
data: {
reason: "cloudRequirements",
errorCode: "Auth",
action: "relogin",
statusCode: 401,
detail:
"Your authentication session could not be refreshed automatically. Please log out and sign in again.",
},
},
});
await expect(request).rejects.toHaveProperty(
"message",
"failed to load configuration: Your authentication session could not be refreshed automatically. Please log out and sign in again.",
);
await expect(request).rejects.toHaveProperty("data", {
reason: "cloudRequirements",
errorCode: "Auth",
action: "relogin",
statusCode: 401,
detail:
"Your authentication session could not be refreshed automatically. Please log out and sign in again.",
});
});
it("rejects timed-out requests and ignores late responses", async () => {
vi.useFakeTimers();
const harness = createClientHarness();

View File

@@ -42,13 +42,39 @@ export class CodexAppServerRpcError extends Error {
readonly data?: JsonValue;
constructor(error: { code?: number; message: string; data?: JsonValue }, method: string) {
super(error.message || `${method} failed`);
super(formatCodexAppServerRpcErrorMessage(error, method));
this.name = "CodexAppServerRpcError";
this.code = error.code;
this.data = error.data;
}
}
function formatCodexAppServerRpcErrorMessage(
error: { message: string; data?: JsonValue },
method: string,
): string {
const message = error.message || `${method} failed`;
const detail = readCodexAppServerRpcReloginDetail(error.data);
return detail && !message.includes(detail) ? `${message}: ${detail}` : message;
}
function readCodexAppServerRpcReloginDetail(data: JsonValue | undefined): string | undefined {
const record = isJsonObject(data) ? data : undefined;
const nested = isJsonObject(record?.error) ? record.error : record;
if (!nested) {
return undefined;
}
const isRelogin =
nested.action === "relogin" ||
(nested.reason === "cloudRequirements" && nested.errorCode === "Auth");
const detail = typeof nested.detail === "string" ? nested.detail.trim() : "";
return isRelogin && detail ? detail : undefined;
}
function isJsonObject(value: unknown): value is { [key: string]: JsonValue } {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
export function isCodexAppServerConnectionClosedError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;

View File

@@ -320,6 +320,37 @@ describe("createCodexDynamicToolBridge", () => {
]);
});
it("records internal UI source replies separately from outbound messaging evidence", async () => {
const toolResult = textToolResult("Sent to current chat.", {
status: "ok",
deliveryStatus: "sent",
sourceReplySink: "internal-ui",
sourceReply: {
text: "visible reply",
mediaUrls: ["/tmp/reply.png"],
},
});
const bridge = createBridgeWithToolResult("message", toolResult);
const result = await handleMessageToolCall(bridge, {
action: "send",
message: "<think>private</think>visible reply",
});
expect(result).toEqual(expectInputText("Sent to current chat."));
expect(bridge.telemetry.didSendViaMessagingTool).toBe(true);
expect(bridge.telemetry.messagingToolSentTexts).toEqual([]);
expect(bridge.telemetry.messagingToolSentMediaUrls).toEqual([]);
expect(bridge.telemetry.messagingToolSentTargets).toEqual([]);
expect(bridge.telemetry.messagingToolSourceReplyPayloads).toEqual([
{
text: "visible reply",
mediaUrl: "/tmp/reply.png",
mediaUrls: ["/tmp/reply.png"],
},
]);
});
it("does not record messaging side effects when the send fails", async () => {
const tool = createTool({
name: "message",

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