Compare commits

..

287 Commits

Author SHA1 Message Date
pash
b23ff97ddc Merge remote-tracking branch 'origin/main' into codex/session-transcript-oom-guard
# Conflicts:
#	src/docker-build-cache.test.ts
#	src/scripts/test-projects.test.ts
2026-04-26 17:02:51 -07:00
pash
e705246619 Fix core support boundary test expectations 2026-04-26 17:00:59 -07:00
pash
f936f16cc5 Merge remote-tracking branch 'origin/main' into codex/session-transcript-oom-guard 2026-04-26 16:59:22 -07:00
Peter Steinberger
d2786fb969 test(docker): run observability harness with global tsx 2026-04-27 00:57:55 +01:00
Peter Steinberger
fa0729e145 test: auto-discover vitest suites 2026-04-27 00:55:06 +01:00
pash
fd48faa4ed Fix qa-lab merge CI and compaction review notes 2026-04-26 16:53:15 -07:00
Peter Steinberger
21c51bc140 test(docker): resolve otel decoder from plugin runtime 2026-04-27 00:51:47 +01:00
Vincent Koc
265bc6b6ea test(plugins): guard command cold registry paths
Add command-level sentinel coverage proving channel setup metadata, onboarding auth choices, and models-list provider ownership stay on manifest/registry paths without importing plugin runtime.\n\nLocal verification:\n- pnpm exec oxfmt --check --threads=1 src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm test:serial src/commands/plugin-control-plane-cold-imports.test.ts\n- OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm check:changed\n- clean rebase sanity: git diff --check origin/main...HEAD\n\nPR CI had known unrelated main-red failures matching latest main run 24970053892; the new sentinel test passed in CI.
2026-04-26 16:51:36 -07:00
Peter Steinberger
42db865673 test(docker): run observability on shared image 2026-04-27 00:49:36 +01:00
Vincent Koc
5d7c6e6bda test(docker): add observability smoke
Add Docker aggregate observability coverage for QA-lab OTEL and Prometheus diagnostics.
2026-04-26 16:43:56 -07:00
pash
29f1cae867 Fix qa-lab private qa-channel import 2026-04-26 16:43:55 -07:00
Tak Hoffman
560ddd2f9b Fail package update on unhealthy restart (#72422) 2026-04-26 18:38:23 -05:00
pash
f58dd36a1d Harden session truncation concurrency guards 2026-04-26 16:34:05 -07:00
Peter Steinberger
998e37fcb3 ci: allow installer smoke baseline override 2026-04-27 00:31:30 +01:00
pash
33e3dccbea Fix update CLI service state test mocks 2026-04-26 16:29:55 -07:00
Vincent Koc
3cc52d9050 docs(changelog): note codex usage accounting fix 2026-04-26 16:27:23 -07:00
Vincent Koc
7902c769da fix(codex): normalize cached harness input tokens 2026-04-26 16:27:23 -07:00
Peter Steinberger
9be8d43c31 docs: document installer recovery cleanup 2026-04-27 00:26:02 +01:00
Peter Steinberger
eccb79db99 build: remove private QA package compat shims 2026-04-27 00:26:02 +01:00
pash
6fc954539f Harden session truncation rewrite 2026-04-26 16:20:29 -07:00
pash
fc13a0135e Fix stale e2e Docker cache test 2026-04-26 16:14:58 -07:00
pash
0ced62f512 Fix transcript truncation OOM guard 2026-04-26 16:10:10 -07:00
Peter Steinberger
09a635a28b test: fix main release validation forward-port 2026-04-27 00:07:31 +01:00
Peter Steinberger
5b257cb352 test(qa): drop brittle telegram workflow assertions
(cherry picked from commit b02fdb8264)
2026-04-27 00:07:31 +01:00
Peter Steinberger
efe940e9cb ci(qa): remove telegram beta approval gate
(cherry picked from commit 5e04b0f97a)
2026-04-27 00:07:31 +01:00
Peter Steinberger
8d909ed0da ci(docker): pass beta env to installer e2e
(cherry picked from commit 7677b4ca24)
2026-04-27 00:07:31 +01:00
Peter Steinberger
1bb46ce68a ci(docker): test release installer against beta
(cherry picked from commit d8c4dcb6a4)
2026-04-27 00:07:31 +01:00
Peter Steinberger
54e77a9ec4 ci(docker): use resolved pnpm for scheduled lanes
(cherry picked from commit 61a539a1b7)
2026-04-27 00:07:31 +01:00
Peter Steinberger
43e651db9a ci(docker): preserve pnpm path in scheduler lanes
(cherry picked from commit 2e8a089836)
2026-04-27 00:07:31 +01:00
Peter Steinberger
e7d069edcf test(qa): relax telegram mention reply assertion
(cherry picked from commit 7109251318)
2026-04-27 00:07:31 +01:00
Peter Steinberger
17094640f8 ci(release): trust release branch docker checks
(cherry picked from commit abf0ef9cd3)
2026-04-27 00:07:31 +01:00
Peter Steinberger
16c6a92c53 ci(release): allow npm telegram e2e from release branch
(cherry picked from commit 53f8e9de13)
2026-04-27 00:07:31 +01:00
Peter Steinberger
ef3309a986 fix(release): harden beta validation lanes
(cherry picked from commit 218bceaa14)
2026-04-27 00:07:31 +01:00
Peter Steinberger
95ae3c00bd docs: explain test routing model 2026-04-27 00:05:27 +01:00
Vincent Koc
97e64196a0 fix(hooks): use local timezone for session-memory filenames (#72408) 2026-04-26 16:04:10 -07:00
Peter Steinberger
41ad03dda4 fix(test): allow legacy qa inventory entry 2026-04-27 00:02:33 +01:00
Peter Steinberger
4a578740a2 refactor: deduplicate changed lane detection 2026-04-27 00:02:00 +01:00
Peter Steinberger
20d6daaeaa docs: document automatic bonjour container policy 2026-04-27 00:00:22 +01:00
Peter Steinberger
6018f29dbf ci: keep docker bonjour setting automatic 2026-04-27 00:00:22 +01:00
Peter Steinberger
989cfd1e33 fix(bonjour): auto-disable advertising in containers 2026-04-27 00:00:22 +01:00
Peter Steinberger
89ab39ca64 test: simplify changed test routing 2026-04-26 23:58:13 +01:00
Peter Steinberger
199d5f765f docs(test): explain cheap docker reruns 2026-04-26 23:56:14 +01:00
Peter Steinberger
2fe11020d2 refactor(test): split bundled channel docker scenarios 2026-04-26 23:56:14 +01:00
Peter Steinberger
1ddf6b4e39 ci: skip existing docker e2e images 2026-04-26 23:56:14 +01:00
Peter Steinberger
1a02d00eb4 test: add docker e2e rerun helpers 2026-04-26 23:56:14 +01:00
Peter Steinberger
cfe58387a7 docs: update changelog attribution guidance 2026-04-26 23:51:51 +01:00
Peter Steinberger
6077941d0b fix: restart package updates through updated install 2026-04-26 23:51:51 +01:00
Peter Steinberger
b5714b90ed refactor(test): share docker e2e shell helpers 2026-04-26 23:48:32 +01:00
Peter Steinberger
7a86448a6e ci: reuse docker e2e plan action 2026-04-26 23:48:32 +01:00
Peter Steinberger
6cba12caae test: add docker e2e planner guards 2026-04-26 23:48:32 +01:00
Rubén Cuevas
a08b65a90a fix(telegram): send fresh finals for stale previews (#72038)
* fix(telegram): send fresh finals for stale previews

* test(telegram): cover stale preview send fallback

* fix(telegram): keep stale archived preview fallback

* fix(telegram): clear stale active previews

* fix(telegram): reset preview state after fresh finals
2026-04-26 15:44:30 -07:00
Peter Steinberger
084dde89fd docs: clarify extension ownership boundaries 2026-04-26 23:39:18 +01:00
Peter Steinberger
2efc4a8233 docs(test): document docker e2e layout 2026-04-26 23:36:31 +01:00
Peter Steinberger
cd417f3b68 ci: derive docker e2e artifacts from plan 2026-04-26 23:36:31 +01:00
Peter Steinberger
a2adb05f74 refactor(test): split docker e2e planner 2026-04-26 23:36:31 +01:00
Peter Steinberger
c9c0ab3a44 fix(bonjour): keep ciao failure handling extension-owned 2026-04-26 23:29:40 +01:00
Peter Steinberger
0472b6197a chore: clarify bonjour fatal guard naming 2026-04-26 23:27:35 +01:00
Peter Steinberger
8a60e57846 fix: keep bonjour failures non-fatal 2026-04-26 23:27:08 +01:00
Vincent Koc
c6cf37068c fix(feishu): repair interactive card content extraction (#72397) 2026-04-26 15:26:53 -07:00
Peter Steinberger
ff6044f441 docs(changelog): note Ollama thinking validation fix 2026-04-26 23:25:05 +01:00
Peter Steinberger
5aa3779d8c ci: disable bonjour in install e2e docker 2026-04-26 23:20:08 +01:00
Peter Steinberger
ff9fefb79b fix(agents): validate thinking with model catalog 2026-04-26 23:16:05 +01:00
Peter Steinberger
3746e5b969 ci: cap Telegram E2E build cache 2026-04-26 23:11:21 +01:00
Peter Steinberger
9f5bc5465c style: format codex and loader tests 2026-04-26 23:10:33 +01:00
Peter Steinberger
d108110a89 ci: use packaged tarball for docker e2e 2026-04-26 23:10:33 +01:00
Peter Steinberger
1b1eea238c ci: preserve docker test runner path 2026-04-26 23:04:21 +01:00
Vincent Koc
d9e9e61e77 fix(logging): skip unserializable file log message parts 2026-04-26 15:01:19 -07:00
Vincent Koc
fc0e6e4650 docs(logging): document structured file fields 2026-04-26 15:01:19 -07:00
Vincent Koc
e8df081a1f feat(logging): add file log correlation fields 2026-04-26 15:01:19 -07:00
github-actions[bot]
5c4c33c7de chore(ui): refresh th control ui locale 2026-04-26 22:01:03 +00:00
Vincent Koc
070b55f336 UI: localize command palette labels (#72378) 2026-04-26 14:58:16 -07:00
Vincent Koc
364d49889e fix: allow trusted exec approvals home symlinks (#72377) 2026-04-26 14:57:01 -07:00
Peter Steinberger
baaad52389 ci: split docker e2e images 2026-04-26 22:55:00 +01:00
Peter Steinberger
3a8961af0f test: copy docker build helper in setup e2e 2026-04-26 22:54:27 +01:00
Peter Steinberger
ff570f3a61 fix(ollama): expose native thinking efforts 2026-04-26 22:49:13 +01:00
Peter Steinberger
2cd23957c0 build: use slim docker runtime 2026-04-26 22:47:48 +01:00
Vincent Koc
43a003b8a0 fix: short-circuit live model switch fallback redirects (#72375) 2026-04-26 14:45:02 -07:00
Vincent Koc
fa85e6c26e docs(changelog): note acp stdout fix 2026-04-26 14:42:37 -07:00
Vincent Koc
d46de6cff7 fix(acp): keep server logs off stdout 2026-04-26 14:42:22 -07:00
Peter Steinberger
018f2e78ba build: skip docker apt upgrades 2026-04-26 22:40:44 +01:00
Peter Steinberger
b61954919c ci: verify docker release attestations 2026-04-26 22:40:44 +01:00
Peter Steinberger
5abb717112 docs: add OpenClaw testing skill 2026-04-26 22:40:32 +01:00
Vincent Koc
8226238765 refactor(plugins): share lookup cache eviction 2026-04-26 14:28:15 -07:00
Peter Steinberger
b68b4b9151 ci: add targeted docker lane reruns 2026-04-26 22:27:45 +01:00
Josh Lehman
a3c51f91c5 fix: isolate cron context-engine session keys (#72292) 2026-04-26 14:21:01 -07:00
Vincent Koc
2edbdc42ae refactor(plugins): isolate loader cache state 2026-04-26 14:16:35 -07:00
Peter Steinberger
b28de9a7d9 ci: centralize docker build wrapper 2026-04-26 22:14:36 +01:00
Peter Steinberger
824c3e2b71 ci: enable docker image attestations 2026-04-26 22:14:36 +01:00
Vincent Koc
2194a8c64c docs(logging): document request trace scopes 2026-04-26 14:13:15 -07:00
Vincent Koc
410783c126 fix(diagnostics): chain run traces to request scope 2026-04-26 14:13:15 -07:00
Vincent Koc
3ae6f01d61 feat(logging): propagate request trace scopes 2026-04-26 14:13:14 -07:00
Peter Steinberger
e3cbad4fb6 ci: fix ACPX Docker update repair target 2026-04-26 22:13:00 +01:00
Peter Steinberger
c082cf892a docs: codify formatter tooling 2026-04-26 22:02:31 +01:00
Peter Steinberger
b4a9ac3516 ci: run release Docker chunks through scheduler 2026-04-26 22:02:31 +01:00
Vincent Koc
f0566e410a docs(diagnostics): document model call size timing 2026-04-26 13:43:22 -07:00
Vincent Koc
c6e9849351 feat(diagnostics): capture model call size timing 2026-04-26 13:43:22 -07:00
Vincent Koc
8e1755928c refactor(plugins): split plugin registry facade 2026-04-26 13:43:22 -07:00
Vincent Koc
9eb071c3f1 perf(plugins): reuse persisted registry fallback read 2026-04-26 13:43:22 -07:00
Vincent Koc
522eedc754 refactor(plugins): make provider discovery runtime explicit 2026-04-26 13:43:21 -07:00
Vincent Koc
71e361af8a refactor(plugins): split installed plugin index modules 2026-04-26 13:43:21 -07:00
Peter Steinberger
487f8c5d3a test(gateway): skip codex acp bind when auth is unavailable 2026-04-26 21:42:49 +01:00
Peter Steinberger
7a4574376a fix(ollama): honor native model capabilities 2026-04-26 21:40:22 +01:00
Josh Lehman
8ba82534e6 fix: preserve cron telegram topic delivery after timeout (#72317) 2026-04-26 13:30:54 -07:00
Peter Steinberger
ffa84cdc02 ci: chunk release Docker e2e jobs 2026-04-26 21:23:08 +01:00
pash-openai
67ffa3df8b Add Codex Computer Use setup for Codex mode (#71842)
* Add Codex Computer Use setup

* Tighten Codex Computer Use setup checks

* Handle fresh Codex Computer Use marketplace setup

* Fix channel setup manifest fixture

* Match Codex Computer Use marketplace loading

* Harden plugin manifest test fixtures

* Isolate auth choice legacy manifest test

* Update aggregate shard test expectation

* Improve Codex Computer Use first-run setup

* Harden Codex Computer Use auto-install

* Fix plugin auto-enable test fixture roots
2026-04-26 13:21:56 -07:00
Vincent Koc
df542f75a9 fix(logging): expose trace fields in file logs 2026-04-26 12:52:04 -07:00
Peter Steinberger
edf40ab6c9 test(gateway): retry gemini acp startup warmup timeout 2026-04-26 20:50:06 +01:00
Vincent Koc
406ae72fd2 fix(logging): redact persisted transcript text 2026-04-26 12:12:44 -07:00
Peter Steinberger
f99fb2af86 test(gateway): wait longer for codex harness subagent start 2026-04-26 20:11:16 +01:00
Peter Steinberger
244628f467 docs: clarify PR triage comments 2026-04-26 19:48:22 +01:00
Sally O'Malley
637bd33e69 fix(diagnostics): defer OTEL run span finalization (#72260) 2026-04-26 11:29:05 -07:00
Vincent Koc
e53c068d78 fix: repair skills and memory watcher refresh paths 2026-04-26 11:21:21 -07:00
Peter Steinberger
4e181d30fa test(gateway): classify stream fallback as empty live response 2026-04-26 19:15:00 +01:00
Peter Steinberger
e60cc50dff test(gateway): harden acp bind docker smoke 2026-04-26 19:14:58 +01:00
Peter Steinberger
f2dab9b334 fix(agents): keep responses web search reasoning compatible 2026-04-26 19:14:55 +01:00
Peter Steinberger
fc6cfbd418 fix(agents): honor bundle mcp tool allowlist 2026-04-26 19:14:51 +01:00
Vincent Koc
480a3f66c9 fix: shortcut live session model redirects during fallback 2026-04-26 11:14:05 -07:00
Vincent Koc
19e41a1e69 docs(logging): clarify redaction surfaces 2026-04-26 11:09:56 -07:00
Vincent Koc
b4cdd55f62 fix(discord): escalate repeated health-monitor restarts 2026-04-26 11:09:03 -07:00
Vincent Koc
6b6dcafcee fix(webchat): support non-image file attachments 2026-04-26 10:58:24 -07:00
Vincent Koc
303cde8f60 fix(auto-reply): poison inbound dedupe after partial turn failure
* fix(auto-reply): poison inbound dedupe after replay-unsafe failures

* fix(clownfish): address review for ghcrawl-165980-agentic-merge (1)
2026-04-26 10:58:19 -07:00
Vincent Koc
e672b61417 fix(whatsapp): stop reconnecting quiet sockets
Fixes #70678.\n\nKeeps quiet but healthy WhatsApp linked-device sessions connected by tracking WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Also cleans up transport activity listeners on failed connection-open paths.\n\nCarries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.\n\nValidation:\n- pnpm test:serial extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts extensions/whatsapp/src/connection-controller.test.ts\n- pnpm check:changed\n- codex review --base origin/main
2026-04-26 09:51:41 -07:00
Peter Steinberger
4a3030df9e fix: avoid PowerShell error variable collision 2026-04-26 16:26:31 +01:00
Peter Steinberger
30aa1b5223 fix(release): stabilize beta validation lanes 2026-04-26 16:22:12 +01:00
Peter Steinberger
b438a9cc08 test: align Parallels smoke guards 2026-04-26 16:20:58 +01:00
Peter Steinberger
a87edd732d fix: harden Windows Parallels smoke 2026-04-26 16:13:13 +01:00
Peter Steinberger
79ad635515 fix: pass Linux clock sync as epoch 2026-04-26 16:13:13 +01:00
Peter Steinberger
7e51866d23 fix: sync Parallels Linux clock 2026-04-26 16:13:13 +01:00
Peter Steinberger
73affb491a fix: bound dev update cleanup 2026-04-26 16:13:13 +01:00
Peter Steinberger
ddc2036956 fix: stabilize Parallels plugin smoke paths 2026-04-26 16:13:13 +01:00
Peter Steinberger
631552c554 perf: speed up dispatch-from-config tests 2026-04-26 14:14:12 +01:00
Peter Steinberger
dce35b90fe test(release): wait longer for dashboard smoke 2026-04-26 13:53:59 +01:00
Peter Steinberger
fc666cf42a test(qa): allow slower gateway rpc startup retries 2026-04-26 13:51:40 +01:00
Peter Steinberger
67b9167b80 test(extensions): restore transformed dynamic imports 2026-04-26 13:16:05 +01:00
Peter Steinberger
e97bd70264 perf: speed up slow test imports 2026-04-26 13:10:57 +01:00
Peter Steinberger
9089e6b595 fix(cli): keep channel add plugin install noninteractive
# Conflicts:
#	CHANGELOG.md
2026-04-26 12:59:19 +01:00
Peter Steinberger
7e13f3f514 test(plugin-sdk): tighten channel runtime shim scan 2026-04-26 12:17:49 +01:00
Peter Steinberger
760a1525fb docs(plugin-sdk): refresh api baseline 2026-04-26 12:15:14 +01:00
Peter Steinberger
760dd98ddc fix(ci): repair main type and lint failures 2026-04-26 12:09:35 +01:00
Peter Steinberger
ecf71da888 fix(voice-call): avoid duplicate webhook logs 2026-04-26 12:05:34 +01:00
Vincent Koc
8a63c898c8 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  fix(plugins): satisfy doctor compat lint
  chore(plugins): inventory doctor deprecation compat
  fix(plugins): record crabpot compat deprecations
  docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults
2026-04-26 04:05:11 -07:00
Vincent Koc
efaa66f70d fix(plugins): satisfy doctor compat lint 2026-04-26 04:04:27 -07:00
Vincent Koc
4c40cf8783 chore(plugins): inventory doctor deprecation compat 2026-04-26 04:04:26 -07:00
Vincent Koc
6dfb03ab2e fix(plugins): record crabpot compat deprecations 2026-04-26 04:04:26 -07:00
Vincent Koc
3a54bbb617 fix(plugins): persist synthetic auth refs in index 2026-04-26 04:04:11 -07:00
Vincent Koc
2a5d3ad5b9 docs(dreaming): rewrite with AccordionGroup for phases and backfill, Tabs for quick start and CLI workflow, ParamField for dreaming defaults 2026-04-26 04:04:09 -07:00
Peter Steinberger
a97ee5c1d3 fix(google-meet): recover local chrome tabs 2026-04-26 12:04:00 +01:00
Vincent Koc
647e557869 docs(agent-workspace): rewrite with AccordionGroup for file map, Steps and Tabs for git backup, Warning callouts for sandbox and secret risks 2026-04-26 04:03:00 -07:00
Peter Steinberger
2a26c96000 docs(release): refine beta validation guidance 2026-04-26 12:02:26 +01:00
Vincent Koc
fa4bd05a3a docs(models): rewrite with CardGroup, Steps for selection order, AccordionGroup for picker behavior and merge precedence, ParamField for list/scan flags 2026-04-26 04:01:42 -07:00
Vincent Koc
209522e2e0 docs(model-failover): rewrite with Steps for runtime flow and rotation, AccordionGroup for cooldown buckets and chain rules, Tabs for which errors advance fallback 2026-04-26 03:59:53 -07:00
Vincent Koc
652e8af81e docs(multi-agent): rewrite with Steps for routing tiers, Tabs for common patterns, AccordionGroup for platform examples and tie-breaking 2026-04-26 03:57:19 -07:00
Vincent Koc
c7a0d9b188 Merge branch 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw:
  test(models): stabilize provider index list mocks
  test(cli): cover lazy plugin inspect mocks
  fix(cli): lazy load plugin maintenance paths
  fix(models): keep cold catalog lookup registry indexed
  fix(models): avoid registry for configured list
  fix(cli): lazy load model commands
  fix(ui): remove ineffective dynamic imports
  test: type setup provider mocks
  fix(update): complete channel switch follow-up work
  test(parallels): harden smoke agent model setup
  fix: preserve provider-scoped model options
  fix: keep post-auth model policy cold
  docs: note faster onboarding auth setup
  test: cover setup provider auth selection
  refactor: keep openai setup auth lightweight
  fix: use setup providers for auth choices
  fix: scope provider auth runtime loading
  fix: keep onboarding setup paths cold
  fix: keep onboarding model prompts scoped
2026-04-26 03:51:08 -07:00
Vincent Koc
3013916232 Update docker.md 2026-04-26 03:50:31 -07:00
Vincent Koc
5411f9d217 test(models): stabilize provider index list mocks 2026-04-26 03:49:57 -07:00
Vincent Koc
be388084c2 test(cli): cover lazy plugin inspect mocks 2026-04-26 03:49:57 -07:00
Vincent Koc
e76bac5d14 fix(cli): lazy load plugin maintenance paths 2026-04-26 03:49:56 -07:00
Vincent Koc
aec1bfa0bb fix(models): keep cold catalog lookup registry indexed 2026-04-26 03:49:43 -07:00
Vincent Koc
8740ca7dee fix(models): avoid registry for configured list 2026-04-26 03:49:43 -07:00
Vincent Koc
23710167cd fix(cli): lazy load model commands 2026-04-26 03:49:43 -07:00
Vincent Koc
3a9463edac test(models): stabilize provider index list mocks 2026-04-26 03:47:25 -07:00
Vincent Koc
fc483ef5d0 test(cli): cover lazy plugin inspect mocks 2026-04-26 03:47:24 -07:00
Vincent Koc
38ea99ec74 fix(cli): lazy load plugin maintenance paths 2026-04-26 03:47:23 -07:00
Vincent Koc
9c25c697dd fix(models): keep cold catalog lookup registry indexed 2026-04-26 03:45:46 -07:00
Vincent Koc
b7533f5112 fix(models): avoid registry for configured list 2026-04-26 03:45:45 -07:00
Vincent Koc
c3a81166fc fix(cli): lazy load model commands 2026-04-26 03:45:45 -07:00
Peter Steinberger
ab0d0f677b fix(ui): remove ineffective dynamic imports
(cherry picked from commit b4ff947206)
2026-04-26 11:45:29 +01:00
Peter Steinberger
06fe67d719 test: type setup provider mocks
(cherry picked from commit ea9da71f03)
2026-04-26 11:41:14 +01:00
Peter Steinberger
6a00be5f90 fix(update): complete channel switch follow-up work 2026-04-26 11:38:44 +01:00
Peter Steinberger
cd8187d7ce test(parallels): harden smoke agent model setup 2026-04-26 11:38:33 +01:00
Shakker
8344fae387 fix: preserve provider-scoped model options 2026-04-26 11:36:32 +01:00
Shakker
3fe0718932 fix: keep post-auth model policy cold 2026-04-26 11:36:32 +01:00
Shakker
cd3b871122 docs: note faster onboarding auth setup 2026-04-26 11:36:32 +01:00
Shakker
edcb2326a1 test: cover setup provider auth selection 2026-04-26 11:36:32 +01:00
Shakker
b11dbb49f9 refactor: keep openai setup auth lightweight 2026-04-26 11:36:32 +01:00
Shakker
44183de706 fix: use setup providers for auth choices 2026-04-26 11:36:32 +01:00
Shakker
3fffa78164 fix: scope provider auth runtime loading 2026-04-26 11:36:32 +01:00
Shakker
2f81c5f580 fix: keep onboarding setup paths cold 2026-04-26 11:36:32 +01:00
Shakker
26b203e573 fix: keep onboarding model prompts scoped 2026-04-26 11:36:32 +01:00
Peter Steinberger
c74fb78194 test: harden cron MCP Docker smoke 2026-04-26 11:33:26 +01:00
Peter Steinberger
cd79e01be3 fix: load default memory plugin at startup 2026-04-26 11:32:58 +01:00
Peter Steinberger
0e490a3c26 fix(plugins): serialize bundled runtime mirrors 2026-04-26 11:32:07 +01:00
Peter Steinberger
4506bb2e02 fix: stabilize channel MCP Docker smoke 2026-04-26 11:31:25 +01:00
Peter Steinberger
74a4ff1adc fix: prefer mounted bundled plugin sources 2026-04-26 11:28:41 +01:00
Peter Steinberger
8a52c7b3d9 test: cover ClawHub plugin install uninstall 2026-04-26 11:28:18 +01:00
Peter Steinberger
3979fce4f9 test: satisfy compat registry lint 2026-04-26 11:28:07 +01:00
Peter Steinberger
8f4f33be78 test: keep compat registry guard-safe 2026-04-26 11:25:02 +01:00
Peter Steinberger
46d74c8f09 docs: update changelog for native require loader (#71122) (thanks @Effet) 2026-04-26 11:23:42 +01:00
Effet
75c9b216e5 fixup! perf(plugins): native-require fast path respects tryNative=false
Review feedback from @chatgpt-codex-connector (P1): callers that pass
`tryNative: false` rely on jiti's alias rewriting (e.g.
`bundled-capability-runtime` in Vitest+dist mode narrows the SDK
slice through shim aliases). Route everything through the jiti
loader when `tryNative` is false so those rewrites still apply.

Review feedback from @greptile-apps (P2): forward the full argument
tuple through to the jiti fallback with `...rest` so any future
loader option argument is not silently dropped by the wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:23:42 +01:00
Effet
b40b85c21a perf(plugins): use native require for compiled JS before jiti
Every CLI invocation reads the config snapshot, which pulls bundled
channel doctor contracts and setup surfaces through
`getCachedPluginJitiLoader`. jiti's TS→JS transform pipeline adds
several seconds of per-load overhead on slower hosts (NAS profiling
shows ~78% of `openclaw config get` wall time spent inside the jiti
library), and that overhead is pure waste for the already-compiled
`.js` artifacts shipped in dist/.

Wrap the loader returned by `getCachedPluginJitiLoader` so that
compiled JS targets go through `tryNativeRequireJavaScriptModule`
first. Jiti stays on the hot path for:
- TS/TSX/MTS/CTS sources
- paths the native-require helper declines (Windows by default, or
  module-resolution fallbacks)

This centralises the fast path that already existed — inside
`doctor-contract-registry` and `channel-entry-contract` — and extends
it to every caller that goes through the jiti loader cache.

Benchmark on a modest NAS (Node 22.22, ZFS, telegram + discord
configured):

| command          | before | after |
|------------------|-------:|------:|
| config get X     |    24s |    6s |
| status           |    45s |   18s |
| devices list     |    55s |   26s |
| nodes status     |    55s |   26s |

Fixes the slow config/status/devices/nodes read paths reported in
openclaw#62842. Remaining time is dominated by non-jiti code paths
(config schema validation, eager provider-plugin module eval) that
are out of scope for this patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:23:42 +01:00
Vincent Koc
6d60b035b4 chore(plugins): finish compat registry cleanup 2026-04-26 03:17:25 -07:00
Peter Steinberger
bc49fb1cdf test: fix extension dynamic imports 2026-04-26 11:15:45 +01:00
Peter Steinberger
9694c0611c ci: fix main gate 2026-04-26 11:15:45 +01:00
Peter Steinberger
4b2056fcc1 docs: document plugin package entrypoints 2026-04-26 11:12:09 +01:00
Peter Steinberger
a75c3adc4f refactor: centralize plugin update outcome logging 2026-04-26 11:11:58 +01:00
Peter Steinberger
b7404399ef perf: cache bundled runtime dep manifests 2026-04-26 11:11:58 +01:00
Peter Steinberger
f337c9019c refactor: share plugin package entry resolution 2026-04-26 11:11:58 +01:00
Peter Steinberger
8ba9c9098a fix(agents): avoid provider startup scans 2026-04-26 11:11:37 +01:00
Peter Steinberger
8bc4d4bcd4 fix: prevent duplicate chat attachment send races 2026-04-26 11:10:42 +01:00
Vincent Koc
dc05c93c02 chore(docker): expose diagnostics observability settings 2026-04-26 03:05:10 -07:00
Peter Steinberger
4ed97f7e35 docs: update changelog for plugin fixes 2026-04-26 11:01:10 +01:00
Peter Steinberger
f33a812c07 fix: validate plugin package extension entries 2026-04-26 11:01:10 +01:00
Peter Steinberger
d22d6aed16 fix: respect plugin allowlist for bundled deps 2026-04-26 11:01:10 +01:00
Peter Steinberger
93f2d42259 fix: fail plugin update on update errors 2026-04-26 11:01:10 +01:00
Vincent Koc
861cd026d1 docs(release): add plugin deprecation sweep 2026-04-26 02:59:29 -07:00
Peter Steinberger
9a529ca78b chore: update dependencies 2026-04-26 10:54:58 +01:00
Vincent Koc
9f0cd3514c test(plugins): make compat window guard type-safe 2026-04-26 02:52:45 -07:00
Vincent Koc
bb2425e612 test(plugins): enforce compat removal window 2026-04-26 02:51:48 -07:00
Vincent Koc
5baf90ffef chore(plugins): cap compat removal windows 2026-04-26 02:51:48 -07:00
Vincent Koc
3308347a43 fix(security): keep web search credential checks cold 2026-04-26 02:51:48 -07:00
Vincent Koc
22044af066 fix(config): keep command alias validation cold 2026-04-26 02:51:48 -07:00
Vincent Koc
a9d243327c chore(plugins): complete compat registry inventory 2026-04-26 02:51:47 -07:00
Peter Steinberger
975fd5bc8d docs: add gif asset hygiene guidance 2026-04-26 10:48:06 +01:00
Peter Steinberger
bd95baa4f7 fix(bonjour): suppress ciao process crashes 2026-04-26 10:47:36 +01:00
Peter Steinberger
1be39ac847 fix: increase update step timeout 2026-04-26 10:46:55 +01:00
Peter Steinberger
b67d9bf7f0 fix: propagate update timeout to plugin installs 2026-04-26 10:45:11 +01:00
Vincent Koc
d1f40731e3 chore(ci): tune stale assigned triage 2026-04-26 02:42:09 -07:00
Peter Steinberger
4bc5e183ef fix: avoid CLI startup warmup leaks 2026-04-26 10:41:03 +01:00
Vincent Koc
64af2feda0 docs(context-engine): note that uninstalling the selected context engine plugin resets plugins.slots.contextEngine to the default (c6b7444d16) 2026-04-26 02:39:07 -07:00
Vincent Koc
8314b83f9d docs(agents): scope docs-only validation 2026-04-26 02:35:14 -07:00
Peter Steinberger
2aa375149f test: speed up agent hotspot tests 2026-04-26 10:28:04 +01:00
Peter Steinberger
0b301e9af4 fix: avoid eager channel setup loading 2026-04-26 10:27:35 +01:00
Peter Steinberger
6bc5fe6952 fix: harden plugin install and uninstall transactions 2026-04-26 10:27:23 +01:00
Vincent Koc
893f070560 docs(prometheus): rewrite with Steps quick start, Tabs for enable methods and pull-vs-push, AccordionGroup for label policy and troubleshooting; document the 2048-series cap and trusted-operator scope from the diagnostics-prometheus plugin code 2026-04-26 02:26:08 -07:00
Peter Steinberger
9eb0934492 test: tighten changed test routing 2026-04-26 10:25:04 +01:00
Peter Steinberger
87ac8b0456 refactor(discord): use Carbon request client for proxy fetch 2026-04-26 10:20:49 +01:00
Peter Steinberger
a3483acaab fix: stabilize gpt55 qa lab scenarios 2026-04-26 10:18:42 +01:00
Vincent Koc
0f2e7510cb feat(diagnostics-prometheus): add protected metrics exporter 2026-04-26 02:15:33 -07:00
Peter Steinberger
6cd047e7c2 refactor: clean up update and plugin uninstall helpers 2026-04-26 10:07:39 +01:00
Peter Steinberger
d58ede1b34 docs(changelog): keep discord fix scoped 2026-04-26 10:06:38 +01:00
Peter Steinberger
775c61ef5f fix(discord): ignore stale exec approval clicks 2026-04-26 10:06:38 +01:00
Vincent Koc
57a77ecdf9 docs(multi-agent-sandbox-tools): rewrite with CardGroup, AccordionGroup for examples and troubleshooting, Tabs for restrictions, Steps for filter order 2026-04-26 02:00:56 -07:00
Peter Steinberger
382c554786 docs(release): keep 2026.4.26 changelog marker empty 2026-04-26 09:59:42 +01:00
Peter Steinberger
e6c9123262 docs(release): codify beta train backport scan
(cherry picked from commit b7733c48c0)
2026-04-26 09:59:42 +01:00
Vincent Koc
e400295969 docs(cli-gateway): rewrite with CardGroup, ParamField for run/probe/install flags, AccordionGroup for status semantics and probe interpretation 2026-04-26 01:59:27 -07:00
Vincent Koc
da000ce511 docs(changelog): note subagent completion fallback 2026-04-26 01:58:01 -07:00
Vincent Koc
a911eb748b test(qa): cover subagent completion fallback 2026-04-26 01:58:01 -07:00
Vincent Koc
a1b6567059 fix(agents): fallback subagent completion delivery 2026-04-26 01:58:00 -07:00
Vincent Koc
8741a86f93 docs(broadcast-groups): rewrite with AccordionGroup for use cases and best practices, Tabs for strategy and contexts, Steps for message flow 2026-04-26 01:56:29 -07:00
Vincent Koc
ed537edacf docs(twitch): rewrite with Steps for setup, Tabs for install/auth/access patterns, ParamField for account config, AccordionGroup for troubleshooting 2026-04-26 01:55:13 -07:00
Vincent Koc
91666fe194 docs(cli-plugins): rewrite with CardGroup, AccordionGroup for install/update behavior, ParamField for list flags, Tabs for marketplace sources 2026-04-26 01:53:57 -07:00
Peter Steinberger
c6b7444d16 fix(plugins): reset context engine slot on uninstall 2026-04-26 09:50:34 +01:00
Peter Steinberger
42487d0dac fix(update): retry npm updates without optional deps 2026-04-26 09:50:27 +01:00
Peter Steinberger
832bdbc777 fix(update): repair package config after update 2026-04-26 09:50:19 +01:00
Peter Steinberger
d9c5040fc5 docs(tailscale): clarify Control UI pairing 2026-04-26 09:46:59 +01:00
Peter Steinberger
6f50253a4d fix: clarify install switching 2026-04-26 09:46:41 +01:00
Peter Steinberger
aad7b678b0 fix: pass config to plugin command specs 2026-04-26 09:45:05 +01:00
Peter Steinberger
e29d3516bf fix(gateway): skip Tailscale Control UI pairing 2026-04-26 09:42:25 +01:00
Peter Steinberger
5ab5b75348 fix: update Docker plugin registry smokes 2026-04-26 09:42:14 +01:00
Vincent Koc
2652c9eacf fix(configure): defer web search setup runtime
Keep web-search configure and channel command defaults on cold plugin metadata, harden persisted registry reads, and require active config for manifest command defaults.\n\nThanks @vincentkoc
2026-04-26 01:41:57 -07:00
Peter Steinberger
218636a0ea docs(changelog): split 2026.4.25 and 2026.4.26 notes 2026-04-26 09:40:00 +01:00
Vincent Koc
f164b8b357 docs(webchat): note that reasoning-flagged payloads are excluded from WebChat assistant content, transcript text, and audio blocks (4823288b3b) 2026-04-26 01:39:34 -07:00
Vincent Koc
abd5ec98ab fix(runtime): harden dependency install surfaces (#71997)
* fix(runtime): harden dependency surfaces

* fix(runtime): harden dependency install surfaces

* fix(runtime): address dependency surface review

* fix(runtime): address dependency surface review

* fix(channels): avoid read-only plugin loader cycle

* fix(channels): allow optional read-only loader workspace

* test(commands): refresh current main checks

* test(commands): keep provider metadata mock unique

* test(commands): keep doctor security read-only mock unique
2026-04-26 01:38:21 -07:00
Vincent Koc
eb6b35671a docs(changelog): flatten 27 multi-line bullets into single lines per AGENTS.md rule 2026-04-26 01:35:42 -07:00
Peter Steinberger
3b5463591b chore: bump version to 2026.4.26 2026-04-26 09:28:52 +01:00
Peter Steinberger
4ad8b613c9 test: update npm telegram workflow expectations 2026-04-26 09:24:10 +01:00
Peter Steinberger
1969452c3f fix: hide raw agent failures in group chats 2026-04-26 09:19:27 +01:00
Peter Steinberger
134cc64aff fix: keep host plugin registry out of live Docker state 2026-04-26 09:17:38 +01:00
Peter Steinberger
0c020cdb7a test: update ci expectation drift 2026-04-26 09:16:53 +01:00
Peter Steinberger
2f5e5e9a71 fix: break plugin command spec import cycle 2026-04-26 09:15:47 +01:00
Peter Steinberger
1323683d72 fix: stabilize qa lab capture store cleanup 2026-04-26 09:13:30 +01:00
Ayaan Zaidi
7e376e5aba ci: build npm telegram e2e image after approval 2026-04-26 13:39:18 +05:30
Peter Steinberger
e2ef5e2329 test: keep path alias temp dirs out of repo 2026-04-26 09:09:07 +01:00
Peter Steinberger
c99d72575e fix(release): reject staged runtime deps in packs 2026-04-26 09:08:54 +01:00
Shakker
5c0dc93d1e fix(doctor): keep service repair policy scoped 2026-04-26 09:08:36 +01:00
Shakker
6cf5a5fbcd docs: document external service repair policy 2026-04-26 09:08:36 +01:00
Shakker
0b6ebf3343 fix(doctor): honor external service repair policy 2026-04-26 09:08:35 +01:00
Vincent Koc
d24c6095ce docs(sdk-setup): rewrite with Tabs for package metadata and install paths, ParamField for openclaw fields, AccordionGroup for setup-entry rules and helpers 2026-04-26 01:07:59 -07:00
Vincent Koc
64a7a34c83 docs(trusted-proxy-auth): rewrite with Steps for handshake, Tabs for TLS, AccordionGroup for proxy examples and troubleshooting 2026-04-26 01:04:51 -07:00
Vincent Koc
f2744978a0 docs(slash-commands): rewrite with ParamField for config keys, AccordionGroup for command groups and surface notes 2026-04-26 01:02:55 -07:00
Shakker
5037298d82 test: update channel status label fixtures 2026-04-26 09:01:39 +01:00
Shakker
0a82c819bb fix: keep status channel metadata cold 2026-04-26 09:01:39 +01:00
Peter Steinberger
a434133aac fix: fail update on plugin sync errors 2026-04-26 09:01:18 +01:00
Peter Steinberger
4823288b3b fix(gateway): hide webchat reasoning payloads 2026-04-26 09:00:56 +01:00
Peter Steinberger
164aaa48db style: format gateway imports 2026-04-26 09:00:33 +01:00
Peter Steinberger
878e1a2201 fix(plugins): preload cli backend runtime owners 2026-04-26 08:59:41 +01:00
Vincent Koc
6360e1146f docs(media-understanding): rewrite with Steps for behavior and auto-detect, Tabs for config examples and entries, ParamField for attachments 2026-04-26 00:58:31 -07:00
Peter Steinberger
626313a397 fix: satisfy diagnostic trace lint 2026-04-26 08:57:49 +01:00
Peter Steinberger
606a7dbc75 test: stabilize telegram command pagination retry 2026-04-26 08:57:49 +01:00
Peter Steinberger
7cbe271d08 fix: keep channel command defaults read-only 2026-04-26 08:57:49 +01:00
Vincent Koc
06d409dc27 docs(mattermost): rewrite with Steps for setup and HMAC, Tabs for chatmodes, AccordionGroup for slash commands and troubleshooting 2026-04-26 00:56:05 -07:00
Shakker
295bcde7b8 test: update channel metadata mocks 2026-04-26 08:41:34 +01:00
Peter Steinberger
8d50cd82d3 docs(changelog): finalize 2026.4.25 release notes 2026-04-26 08:41:14 +01:00
Vincent Koc
32d3a820c8 docs(sdk-runtime): rewrite with AccordionGroup for runtime namespaces, Steps for store wiring, ParamField for top-level api fields 2026-04-26 00:40:41 -07:00
Vincent Koc
1dc57d4c31 docs(groups): rewrite with Tabs for sandbox patterns and copy-paste intents, AccordionGroup for per-channel notes, Steps for evaluation order 2026-04-26 00:38:20 -07:00
Vincent Koc
fe69b02951 docs(sandboxing): rewrite with Tabs for modes/backends/workspace, AccordionGroup for SSH/OpenShell details, Steps for image setup 2026-04-26 00:35:52 -07:00
Vincent Koc
3e2e26549a docs(cli-config): rewrite with Tabs for set modes, AccordionGroup for builder flags and dry-run details, Steps for the repair loop 2026-04-26 00:32:59 -07:00
694 changed files with 33455 additions and 13952 deletions

View File

@@ -25,15 +25,36 @@ Use this skill for release and publish-time workflow. Keep ordinary development
- Before release branching, commit any dirty files in coherent groups, push,
pull/rebase, then run `/changelog` on `main` and commit/push/pull that
changelog rewrite immediately before creating the release branch.
- During release planning, inspect both `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` before branching and again
before final publish. For every deprecated or removal-pending compatibility
record whose `removeAfter` date is on or before the release date, either
remove the compatibility path where safe and validate the affected tests, or
write down why removal is blocked and get explicit maintainer approval before
shipping the expired compatibility path.
- When removing deprecated runtime/config compatibility, preserve any doctor
migration, repair, or hint that is still needed by supported upgrade paths.
Doctor-side compatibility should stay tracked in
`src/commands/doctor/shared/deprecation-compat.ts` until maintainers confirm
the repair is no longer needed.
- Revalidate compatibility replacement text during release planning. The
recommended replacement can shift as plugin ownership, externalization, and
config footprint move, so do not blindly copy stale replacement annotations
into release notes.
- Do not delete or rewrite beta tags after they leave the machine. If a
published or pushed beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, run the full pre-npm test roster before publishing
each beta. After a beta is published, run the smaller published-install roster
focused on install/update/Docker/Parallels. If anything fails, fix it on the
release branch, commit/push/pull, increment beta number, and repeat. Operators
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
stop and report.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
the release branch, commit/push/pull, increment beta number, and repeat. Run
the full expensive roster at least once before stable/latest promotion; for
later beta attempts, rerun only lanes whose evidence changed unless the fix
touches broad release, install/update, plugin, Docker, Parallels, or live QA
behavior. After each beta is published, scan current `main` once for critical
fixes that landed after the release branch cut and backport only important
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
after 4 failed beta attempts, stop and report.
- Use `/changelog` before version/tag preparation so the top changelog section
is deduped and ordered by user impact.
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
@@ -75,6 +96,11 @@ Use this skill for release and publish-time workflow. Keep ordinary development
parallel, publish npm from the successful npm preflight, then start published
npm install/update, Docker, and Parallels verification while mac artifacts
continue.
- After a beta is published, overlap remote/manual release rosters where useful,
but avoid piling local Docker, Parallels, and QA-Lab work onto the same host
when it would create system-load noise. Use selective reruns after failures or
fixes, but keep proof that Docker, Parallels, and QA-Lab each passed at least
once before stable/latest promotion.
- Mac packaging may be built from a slight release-branch variation of the
tagged commit when the delta is mac packaging, signing, workflow, or
validation-only release machinery. If mac packaging needs release-branch-only
@@ -107,6 +133,13 @@ Use this skill for release and publish-time workflow. Keep ordinary development
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
or editing a release, extract from `## YYYY.M.D` through the line before the
next level-2 heading and use that complete block as the release notes.
- When preparing release notes, scan `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` for compatibility records
with `warningStarts` or `removeAfter` within 7 days after the release date.
Add an `Upcoming deprecations` note to the release notes when any exist,
including the compatibility code, target date, replacement, and a link to the
record's `docsPath` or `/plugins/compatibility` when no more specific
deprecation page exists.
- When cutting a mac release with a beta GitHub prerelease:
- tag `vYYYY.M.D-beta.N` from the release commit
- create a prerelease titled `openclaw YYYY.M.D-beta.N`
@@ -292,9 +325,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- Docker install/update coverage that exercises the published beta package
- published npm Telegram proof: dispatch Actions > `NPM Telegram Beta E2E`
from `main` with `package_spec=openclaw@<beta-version>` and
`provider_mode=mock-openai`, approve `npm-release`, and require success.
This is the default button path for installed-package onboarding,
Telegram setup, and real Telegram E2E against the published npm package.
`provider_mode=mock-openai`, and require success. This workflow is
maintainer-dispatched and intentionally has no `npm-release` approval gate;
`qa-live-shared` only supplies the shared QA secrets. This is the default
button path for installed-package onboarding, Telegram setup, and real
Telegram E2E against the published npm package.
Use the local `pnpm test:docker:npm-telegram-live` lane with the matching
`OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC` and Convex CI env only as a fallback
or debugging path.
@@ -491,8 +526,10 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
6. Create `release/YYYY.M.D` from that post-changelog `main` commit.
7. Make every repo version location match the beta tag before creating it.
8. Commit release preparation changes on the release branch and push the branch.
9. Run the local build, Docker, and Parallels parts of the full pre-npm beta
test roster from the release branch before any npm preflight or publish.
9. Run the fast local beta preflight from the release branch before any npm
preflight or publish. Keep expensive Docker, Parallels, and published-package
install/update lanes for after the beta is live unless the operator asks to
run them before beta publication.
10. For beta releases, skip mac app build/sign/notarize unless beta scope or a
release blocker specifically requires it. For stable releases, include the
mac app, signing, notarization, and appcast path.
@@ -529,10 +566,16 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
21. Wait for `npm-release` approval from `@openclaw/openclaw-release-managers`.
22. Run postpublish verification:
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>`.
23. Run the post-published beta verification roster. If any lane fails after
the beta tag/package is pushed or published, fix, commit/push/pull,
increment to the next beta tag, and restart at the full pre-npm beta test
roster for the new beta. The roster includes the manual Actions >
23. Run the post-published beta verification roster. First scan current `main`
for critical fixes that landed after the release branch cut; backport only
important low-risk fixes before starting expensive lanes, or increment to
the next beta if the fix must change the already-published package. If any
lane fails after the beta tag/package is pushed or published, fix,
commit/push/pull, increment to the next beta tag, and rerun the affected
beta evidence. Once the beta is live, start remote/manual rosters where they
can overlap safely, but keep local Docker and Parallels load controlled.
Ensure the full expensive roster has passed at least once before
stable/latest promotion. The roster includes the manual Actions >
`NPM Telegram Beta E2E` workflow against the exact published beta package.
If a pre-npm lane fails before any tag/package leaves the machine, fix and
rerun the same intended beta attempt. Repeat up to the operator's

View File

@@ -0,0 +1,244 @@
---
name: openclaw-testing
description: Choose, run, rerun, or debug OpenClaw tests, CI checks, Docker E2E lanes, release validation, and the cheapest safe verification path.
---
# OpenClaw Testing
Use this skill when deciding what to test, debugging failures, rerunning CI,
or validating a change without wasting hours.
## Read First
- `docs/reference/test.md` for local test commands.
- `docs/ci.md` for CI scope, release checks, Docker chunks, and runner behavior.
- Scoped `AGENTS.md` files before editing code under a subtree.
## Default Rule
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`
- 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.
3. Fix root cause.
4. Rerun the same narrow proof.
5. Broaden only when the touched contract demands it.
## Guardrails
- Do not kill unrelated processes or tests. If something is running elsewhere, treat it as owned by the user or another agent.
- Do not run expensive local Docker, full release checks, full `pnpm test`, or full `pnpm check` unless the user asks or the change genuinely requires it.
- 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.
## Local Test Shortcuts
```bash
pnpm changed:lanes --json
pnpm check:changed # changed typecheck/lint/guards; no Vitest
pnpm test:changed # cheap smart changed Vitest targets
OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed
pnpm test <path-or-filter> -- --reporter=verbose
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.
## Command Semantics
- `pnpm check` and `pnpm check:changed` do not run Vitest tests. They are for
typecheck, lint, and guard proof.
- `pnpm test` and `pnpm test:changed` run Vitest tests.
- `pnpm test:changed` is intentionally cheap by default: direct test edits,
sibling tests, explicit source mappings, and import-graph dependents.
- `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` is the explicit broad
fallback for harness/config/package edits that genuinely need it.
- Do not run extension sweeps just because core changed. If a core edit is for a
specific plugin bug, run that plugin's tests explicitly. If a public SDK or
contract change needs consumer proof, choose the smallest representative
plugin/contract tests first, then broaden only when the risk justifies it.
- The test wrapper prints a short `[test] passed|failed|skipped ... in ...`
line. Vitest's own duration is still the per-shard detail.
## Routing Model
- `pnpm changed:lanes --json` answers "which check lanes does this diff touch?"
It is used by `pnpm check:changed` for typecheck/lint/guard selection.
- `pnpm test:changed` answers "which Vitest targets are worth running now?" It
uses the same changed path list, but applies a cheaper test-target resolver.
- Direct test edits run themselves. Source edits prefer explicit mappings,
sibling `*.test.ts`, then import-graph dependents. Shared harness/config/root
edits are skipped by default unless they have precise mapped tests.
- Public SDK or contract edits do not automatically run every plugin test.
`check:changed` proves extension type contracts; the agent chooses the
smallest plugin/contract Vitest proof that matches the actual risk.
- Use `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` only when a harness,
config, package, or unknown-root edit really needs the broad Vitest fallback.
## CI Debugging
Start with current run state, not logs for everything:
```bash
gh run list --branch main --limit 10
gh run view <run-id> --json status,conclusion,headSha,url,jobs
gh run view <run-id> --job <job-id> --log
```
- Check exact SHA. Ignore newer unrelated `main` unless asked.
- For cancelled same-branch runs, confirm whether a newer run superseded it.
- Fetch full logs only for failed or relevant jobs.
## Docker
Docker is expensive. First inspect the scheduler without running Docker:
```bash
OPENCLAW_DOCKER_ALL_DRY_RUN=1 pnpm test:docker:all
OPENCLAW_DOCKER_ALL_DRY_RUN=1 OPENCLAW_DOCKER_ALL_LANES=install-e2e pnpm test:docker:all
OPENCLAW_DOCKER_ALL_LANES=install-e2e node scripts/test-docker-all.mjs --plan-json
```
Run one failed lane locally only when explicitly asked or when GitHub is not
usable:
```bash
OPENCLAW_DOCKER_ALL_LANES=<lane> \
OPENCLAW_DOCKER_ALL_BUILD=0 \
OPENCLAW_DOCKER_ALL_PREFLIGHT=0 \
OPENCLAW_SKIP_DOCKER_BUILD=1 \
OPENCLAW_DOCKER_E2E_BARE_IMAGE='<prepared-bare-image>' \
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE='<prepared-functional-image>' \
pnpm test:docker:all
```
For release validation, prefer the reusable GitHub workflow input:
```yaml
docker_lanes: install-e2e
```
Multiple lanes are allowed:
```yaml
docker_lanes: install-e2e bundled-channel-update-acpx
```
That skips the three chunk matrix and runs one targeted Docker job against the
prepared GHCR images and a fresh OpenClaw npm tarball for the selected ref.
Reruns usually need that new tarball because the fix being tested changed the
package contents even if the SHA-tagged GHCR Docker image can be reused.
Live-only targeted reruns skip the E2E images and build only the live-test
image. Release-path normal mode remains max three Docker chunk jobs:
- `core`
- `package-update`
- `plugins-integrations`
Docker E2E images never copy repo sources as the app under test: the bare image
is a Node/Git runner, and the functional image installs the same prebuilt npm
tarball that bare lanes mount. `scripts/package-openclaw-for-docker.mjs` is the
single packer for local scripts and CI and validates the tarball inventory
before Docker consumes it. `scripts/test-docker-all.mjs --plan-json` is the
scheduler-owned CI plan for image kind, package, live image, lane, and
credential needs. Docker lane definitions live in the single scenario catalog
`scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in
`scripts/lib/docker-e2e-plan.mjs`. `scripts/docker-e2e.mjs` converts plan and
summary JSON into GitHub outputs and step summaries. Every scheduler run writes
`.artifacts/docker-tests/**/summary.json` plus `failures.json`. Read those
before rerunning. Lane entries include `command`, `rerunCommand`, status,
timing, timeout state, image kind, and log file path. The summary also includes
top-level phase timings for preflight, image build, package prep, lane pools,
and cleanup. Use `pnpm test:docker:timings <summary.json>` to rank slow lanes
and phases before deciding whether a broader rerun is justified.
## Cheap Docker Reruns
First derive the smallest rerun command from artifacts:
```bash
pnpm test:docker:rerun <github-run-id>
pnpm test:docker:rerun .artifacts/docker-tests/<run>/failures.json
```
The script downloads Docker E2E artifacts for a GitHub run, reads
`summary.json`/`failures.json`, and prints a combined targeted workflow command
plus per-lane commands. Prefer the combined targeted command when several lanes
failed for the same patch:
```bash
gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
-f ref=<sha> \
-f include_repo_e2e=false \
-f include_release_path_suites=false \
-f include_openwebui=false \
-f docker_lanes='install-e2e bundled-channel-update-acpx' \
-f include_live_suites=false \
-f live_models_only=false
```
That path still runs the prepare job, so it creates a new tarball for `<sha>`.
If the SHA-tagged GHCR bare/functional image already exists, CI skips rebuilding
that image and only uploads the fresh package artifact before the targeted lane
job. Do not rerun the full three-chunk release path unless the failed lane list
or touched surface really requires it.
## Docker Expected Timings
Treat these as ballpark. Blacksmith queue time, GHCR pull speed, provider
latency, npm cache state, and Docker daemon health can dominate.
Current local timing artifact (`.artifacts/docker-tests/lane-timings.json`) has
these rough bands:
- Tiny lanes, seconds to under 1 minute:
`agents-delete-shared-workspace` ~3s, `plugin-update` ~7s,
`config-reload` ~14s, `pi-bundle-mcp-tools` ~15s, `onboard` ~18s,
`session-runtime-context` ~20s, `gateway-network` ~34s, `qr` ~44s.
- Medium deterministic lanes, ~1-5 minutes:
`npm-onboard-channel-agent` ~96s, `openai-image-auth` ~99s,
bundled channel/update lanes usually ~90-300s, `openwebui` ~225s,
`mcp-channels` ~274s.
- Heavy deterministic lanes, ~6-10 minutes:
`bundled-channel-root-owned` ~429s,
`bundled-channel-setup-entry` ~420s,
`bundled-channel-load-failure` ~383s,
`cron-mcp-cleanup` ~567s.
- Live provider lanes, often ~15-20 minutes:
`live-gateway` ~958s, `live-models` ~1054s.
- Installer/release lanes:
`install-e2e` and package-update paths can vary widely with npm, provider,
and package registry behavior. Budget tens of minutes; prefer GitHub targeted
reruns over local repeats.
Default fallback lane timeout is 120 minutes. A timeout usually means debug the
lane log/artifacts first, not “run the whole thing again.”
## Failure Workflow
1. Identify exact failing job, SHA, lane, and artifact path.
2. Read `failures.json`, `summary.json`, and the failed lane log tail.
3. Use `pnpm test:docker:rerun <run-id|failures.json>` to generate targeted
GitHub rerun commands.
4. If the lane has `rerunCommand`, use that only as a local starting point.
5. For Docker release failures, dispatch targeted `docker_lanes=<failed-lane>`
on GitHub before considering local Docker.
6. Patch narrowly, then rerun the failed file/lane only.
7. Broaden to `pnpm check:changed` or CI only after the isolated proof passes.
## When To Escalate
- Public SDK/plugin contract changes: run changed gate plus relevant extension
validation.
- Build output, lazy imports, package boundaries, or published surfaces:
include `pnpm build`.
- Workflow edits: run `pnpm check:workflows`.
- Release branch or tag validation: use release docs and GitHub workflows; avoid
local Docker unless Peter explicitly asks.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "OpenClaw Testing"
short_description: "Choose cheap, targeted OpenClaw validation"
default_prompt: "Use $openclaw-testing to choose the cheapest safe test or CI verification path, inspect failures, and rerun only the relevant OpenClaw lane."

View File

@@ -0,0 +1,145 @@
name: Docker E2E plan and hydrate
description: >
Create a Docker E2E lane plan, expose GitHub outputs, and optionally hydrate
the prebuilt package artifact plus shared Docker images needed by the plan.
inputs:
mode:
description: prepare, chunk, or targeted.
required: true
chunk:
description: Release-path chunk for mode=chunk.
required: false
default: ""
lanes:
description: Comma/space separated lane names for targeted or prepare mode.
required: false
default: ""
include-openwebui:
description: Whether Open WebUI is included when planning release/prepare coverage.
required: false
default: "true"
include-release-path-suites:
description: Whether prepare mode should plan all release-path suites.
required: false
default: "false"
hydrate-artifacts:
description: Whether to download/pull artifacts required by the plan.
required: false
default: "true"
outputs:
credentials:
description: Comma-separated credential groups required by selected lanes.
value: ${{ steps.plan.outputs.credentials }}
needs_bare_image:
description: "1 when selected lanes require the bare Docker E2E image."
value: ${{ steps.plan.outputs.needs_bare_image }}
needs_e2e_image:
description: "1 when selected lanes require any Docker E2E image."
value: ${{ steps.plan.outputs.needs_e2e_image }}
needs_functional_image:
description: "1 when selected lanes require the functional Docker E2E image."
value: ${{ steps.plan.outputs.needs_functional_image }}
needs_live_image:
description: "1 when selected lanes require building the live Docker image."
value: ${{ steps.plan.outputs.needs_live_image }}
needs_package:
description: "1 when selected lanes require the OpenClaw package tarball."
value: ${{ steps.plan.outputs.needs_package }}
plan_json:
description: Path to the generated plan JSON.
value: ${{ steps.plan.outputs.plan_json }}
runs:
using: composite
steps:
- name: Plan Docker E2E lanes
id: plan
shell: bash
env:
MODE: ${{ inputs.mode }}
CHUNK: ${{ inputs.chunk }}
LANES: ${{ inputs.lanes }}
INCLUDE_OPENWEBUI: ${{ inputs.include-openwebui }}
INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include-release-path-suites }}
run: |
set -euo pipefail
mkdir -p .artifacts/docker-tests
case "$MODE" in
prepare)
plan_path=".artifacts/docker-tests/plan.json"
if [[ "$INCLUDE_RELEASE_PATH_SUITES" == "true" ]]; then
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_PLAN_RELEASE_ALL=1
elif [[ -n "$LANES" ]]; then
export OPENCLAW_DOCKER_ALL_LANES="$LANES"
elif [[ "$INCLUDE_OPENWEBUI" == "true" ]]; then
export OPENCLAW_DOCKER_ALL_LANES=openwebui
fi
;;
chunk)
if [[ -z "$CHUNK" ]]; then
echo "chunk input is required for Docker E2E chunk planning." >&2
exit 1
fi
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_CHUNK="$CHUNK"
plan_path=".artifacts/docker-tests/release-${CHUNK}-plan.json"
;;
targeted)
if [[ -z "$LANES" ]]; then
echo "lanes input is required for Docker E2E targeted planning." >&2
exit 1
fi
export OPENCLAW_DOCKER_ALL_LANES="$LANES"
plan_path=".artifacts/docker-tests/targeted-plan.json"
;;
*)
echo "mode must be prepare, chunk, or targeted. Got: $MODE" >&2
exit 1
;;
esac
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="$INCLUDE_OPENWEBUI"
node scripts/test-docker-all.mjs --plan-json > "$plan_path"
node scripts/docker-e2e.mjs github-outputs "$plan_path" >> "$GITHUB_OUTPUT"
echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT"
- name: Download OpenClaw Docker E2E package
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_package == '1'
uses: actions/download-artifact@v8
with:
name: docker-e2e-package
path: .artifacts/docker-e2e-package
- name: Pull shared bare Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_bare_image == '1'
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_BARE_IMAGE}"
- name: Pull shared functional Docker E2E image
if: inputs.hydrate-artifacts == 'true' && steps.plan.outputs.needs_functional_image == '1'
shell: bash
run: |
set -euo pipefail
docker pull "${OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE}"
- name: Validate Docker E2E credentials
if: inputs.hydrate-artifacts == 'true'
shell: bash
env:
CREDENTIALS: ${{ steps.plan.outputs.credentials }}
run: |
set -euo pipefail
credentials=",$CREDENTIALS,"
if [[ "$credentials" == *",openai,"* ]]; then
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
if [[ "$credentials" == *",anthropic,"* && -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
fi

4
.github/labeler.yml vendored
View File

@@ -233,6 +233,10 @@
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-otel/**"
"extensions: diagnostics-prometheus":
- changed-files:
- any-glob-to-any-file:
- "extensions/diagnostics-prometheus/**"
"extensions: llm-task":
- changed-files:
- any-glob-to-any-file:

View File

@@ -1,6 +1,7 @@
name: CI
on:
workflow_dispatch:
push:
branches: [main]
paths-ignore:
@@ -13,8 +14,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha)) }}
cancel-in-progress: true
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -75,6 +76,7 @@ jobs:
submodules: false
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
@@ -82,11 +84,12 @@ jobs:
- name: Detect docs-only changes
id: docs_scope
if: github.event_name != 'workflow_dispatch'
uses: ./.github/actions/detect-docs-changes
- name: Detect changed scopes
id: changed_scope
if: steps.docs_scope.outputs.docs_only != 'true'
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true'
shell: bash
run: |
set -euo pipefail
@@ -101,7 +104,7 @@ jobs:
- name: Detect changed extensions
id: changed_extensions
if: steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true' && steps.changed_scope.outputs.run_node == 'true'
env:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
BASE_REF: ${{ github.event_name == 'push' && github.ref_name || github.event.pull_request.base.ref }}
@@ -125,19 +128,19 @@ jobs:
- name: Build CI manifest
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_DOCS_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }}
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ github.event_name == 'workflow_dispatch' && '{"include":[]}' || steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
run: |
node --input-type=module <<'EOF'

View File

@@ -63,7 +63,7 @@ jobs:
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
# Build amd64 images (default + slim share the build stage cache)
# Build amd64 image. Default and slim tags point to the same slim runtime.
build-amd64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
@@ -74,7 +74,6 @@ jobs:
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -117,12 +116,7 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -159,28 +153,15 @@ jobs:
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
sbom: true
provenance: mode=max
push: true
- name: Build and push amd64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64
cache-from: type=gha,scope=docker-release-amd64
cache-to: type=gha,mode=max,scope=docker-release-amd64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
# Build arm64 images (default + slim share the build stage cache)
# Build arm64 image. Default and slim tags point to the same slim runtime.
build-arm64:
needs: [approve_manual_backfill]
if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
@@ -191,7 +172,6 @@ jobs:
contents: read
outputs:
digest: ${{ steps.build.outputs.digest }}
slim-digest: ${{ steps.build-slim.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -234,12 +214,7 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
@@ -276,25 +251,12 @@ jobs:
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_EXTENSIONS=diagnostics-otel
tags: ${{ steps.tags.outputs.value }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
push: true
- name: Build and push arm64 slim image
id: build-slim
# WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/arm64
cache-from: type=gha,scope=docker-release-arm64
cache-to: type=gha,mode=max,scope=docker-release-arm64
build-args: |
OPENCLAW_VARIANT=slim
tags: ${{ steps.tags.outputs.slim }}
labels: ${{ steps.labels.outputs.value }}
provenance: false
sbom: true
provenance: mode=max
push: true
# Create multi-platform manifests
@@ -351,16 +313,11 @@ jobs:
fi
{
echo "value<<EOF"
printf "%s\n" "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
{
echo "slim<<EOF"
printf "%s\n" "${slim_tags[@]}"
printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create and push default manifest
- name: Create and push manifest
shell: bash
env:
TAGS: ${{ steps.tags.outputs.value }}
@@ -378,20 +335,94 @@ jobs:
"${AMD64_DIGEST}" \
"${ARM64_DIGEST}"
- name: Create and push slim manifest
verify-attestations:
needs: [create-manifest]
if: ${{ always() && needs.create-manifest.result == 'success' }}
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Set up Docker Builder
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image refs
id: refs
shell: bash
env:
SLIM_TAGS: ${{ steps.tags.outputs.slim }}
AMD64_SLIM_DIGEST: ${{ needs.build-amd64.outputs.slim-digest }}
ARM64_SLIM_DIGEST: ${{ needs.build-arm64.outputs.slim-digest }}
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
run: |
set -euo pipefail
mapfile -t tags <<< "${SLIM_TAGS}"
args=()
for tag in "${tags[@]}"; do
[ -z "$tag" ] && continue
args+=("-t" "$tag")
done
docker buildx imagetools create "${args[@]}" \
"${AMD64_SLIM_DIGEST}" \
"${ARM64_SLIM_DIGEST}"
multi_refs=()
slim_multi_refs=()
amd64_refs=()
arm64_refs=()
if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
multi_refs+=("${IMAGE}:main")
slim_multi_refs+=("${IMAGE}:main-slim")
amd64_refs+=("${IMAGE}:main-amd64" "${IMAGE}:main-slim-amd64")
arm64_refs+=("${IMAGE}:main-arm64" "${IMAGE}:main-slim-arm64")
fi
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
version="${SOURCE_REF#refs/tags/v}"
multi_refs+=("${IMAGE}:${version}")
slim_multi_refs+=("${IMAGE}:${version}-slim")
amd64_refs+=("${IMAGE}:${version}-amd64" "${IMAGE}:${version}-slim-amd64")
arm64_refs+=("${IMAGE}:${version}-arm64" "${IMAGE}:${version}-slim-arm64")
if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
multi_refs+=("${IMAGE}:latest")
slim_multi_refs+=("${IMAGE}:slim")
fi
fi
if [[ ${#multi_refs[@]} -eq 0 || ${#amd64_refs[@]} -eq 0 || ${#arm64_refs[@]} -eq 0 ]]; then
echo "::error::No Docker image refs resolved for ref ${SOURCE_REF}"
exit 1
fi
{
echo "multi<<EOF"
printf "%s\n" "${multi_refs[@]}" "${slim_multi_refs[@]}"
echo "EOF"
echo "amd64<<EOF"
printf "%s\n" "${amd64_refs[@]}"
echo "EOF"
echo "arm64<<EOF"
printf "%s\n" "${arm64_refs[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Verify Docker attestations
shell: bash
env:
MULTI_REFS: ${{ steps.refs.outputs.multi }}
AMD64_REFS: ${{ steps.refs.outputs.amd64 }}
ARM64_REFS: ${{ steps.refs.outputs.arm64 }}
run: |
set -euo pipefail
mapfile -t multi_refs <<< "${MULTI_REFS}"
mapfile -t amd64_refs <<< "${AMD64_REFS}"
mapfile -t arm64_refs <<< "${ARM64_REFS}"
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
--platform linux/arm64 \
"${multi_refs[@]}"
node scripts/verify-docker-attestations.mjs \
--platform linux/amd64 \
"${amd64_refs[@]}"
node scripts/verify-docker-attestations.mjs \
--platform linux/arm64 \
"${arm64_refs[@]}"

View File

@@ -10,6 +10,11 @@ on:
required: false
default: false
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
workflow_call:
inputs:
ref:
@@ -21,6 +26,11 @@ on:
required: false
default: true
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
permissions:
contents: read
@@ -103,7 +113,6 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
@@ -218,7 +227,6 @@ jobs:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
@@ -332,7 +340,7 @@ jobs:
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0"
OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: latest
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: ${{ inputs.update_baseline_version || 'latest' }}
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
run: bash scripts/test-install-sh-docker.sh

View File

@@ -34,106 +34,39 @@ env:
PNPM_VERSION: "10.33.0"
jobs:
validate_dispatch_ref:
name: Validate dispatch ref
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Require main workflow ref
env:
WORKFLOW_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "NPM Telegram beta E2E must be dispatched from main so workflow logic stays controlled." >&2
exit 1
fi
approve_release_manager:
name: Approve npm Telegram beta E2E
needs: validate_dispatch_ref
runs-on: ubuntu-latest
environment: npm-release
steps:
- name: Record approval
env:
PACKAGE_SPEC: ${{ inputs.package_spec }}
run: echo "Approved npm Telegram beta E2E for ${PACKAGE_SPEC}"
prepare_docker_e2e_image:
name: Prepare Docker E2E image
needs: validate_dispatch_ref
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout main
- name: Checkout dispatch ref
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Resolve Docker E2E image tag
id: image
shell: bash
env:
SELECTED_SHA: ${{ github.sha }}
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}"
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY"
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
max-cache-size-mb: 800000
- name: Build and push Docker E2E image
- name: Build Docker E2E image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./scripts/e2e/Dockerfile
target: build
platforms: linux/amd64
tags: ${{ steps.image.outputs.image }}
tags: openclaw-docker-e2e:local
load: true
push: false
provenance: false
push: true
run_npm_telegram_beta_e2e:
name: Run published npm Telegram E2E
needs: [approve_release_manager, prepare_docker_e2e_image]
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
permissions:
contents: read
packages: read
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
@@ -178,7 +111,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_SKIP_DOCKER_BUILD: "1"
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_IMAGE: openclaw-docker-e2e:local
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }}
OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }}
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex
@@ -186,6 +119,7 @@ jobs:
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
INPUT_SCENARIO: ${{ inputs.scenario }}
run: |
set -euo pipefail

View File

@@ -23,6 +23,11 @@ on:
required: false
default: true
type: boolean
docker_lanes:
description: Comma/space separated Docker scheduler lane names to run against the prepared image
required: false
default: ""
type: string
include_live_suites:
description: Whether to run live-provider coverage
required: false
@@ -54,6 +59,11 @@ on:
required: false
default: true
type: boolean
docker_lanes:
description: Comma/space separated Docker scheduler lane names to run against the prepared image
required: false
default: ""
type: string
include_live_suites:
description: Whether to run live-provider coverage
required: false
@@ -182,6 +192,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
INPUT_REF: ${{ inputs.ref }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
shell: bash
run: |
set -euo pipefail
@@ -189,9 +200,15 @@ jobs:
trusted_reason=""
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if [[ "${WORKFLOW_REF_NAME}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
fi
if git merge-base --is-ancestor "$selected_sha" refs/remotes/origin/main; then
trusted_reason="main-ancestor"
elif [[ "${WORKFLOW_REF_NAME}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] &&
[[ "$selected_sha" == "$(git rev-parse "refs/remotes/origin/${WORKFLOW_REF_NAME}")" ]]; then
trusted_reason="release-branch-head"
elif git tag --points-at "$selected_sha" | grep -Eq '^v'; then
trusted_reason="release-tag"
else
@@ -208,7 +225,7 @@ jobs:
if [[ -z "$trusted_reason" ]]; then
echo "Ref '${INPUT_REF}' resolved to $selected_sha, which is not trusted for secret-bearing live/E2E checks." >&2
echo "Allowed refs must be on main, point to a release tag, or match an open PR head in ${GITHUB_REPOSITORY}." >&2
echo "Allowed refs must be on main, match the current release branch head, point to a release tag, or match an open PR head in ${GITHUB_REPOSITORY}." >&2
exit 1
fi
@@ -303,7 +320,7 @@ jobs:
requires_live_suites: false
- suite_id: openai-ws-stream-live-e2e
label: OpenAI WebSocket live E2E
command: pnpm test:e2e -- src/agents/openai-ws-stream.e2e.test.ts
command: pnpm test:e2e src/agents/openai-ws-stream.e2e.test.ts
timeout_minutes: 90
requires_repo_e2e: false
requires_live_suites: true
@@ -363,88 +380,23 @@ jobs:
validate_docker_e2e:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_release_path_suites
if: inputs.include_release_path_suites && inputs.docker_lanes == ''
name: Docker E2E (${{ matrix.label }})
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
include:
- suite_id: docker-onboard
label: Onboarding Docker E2E
command: pnpm test:docker:onboard
timeout_minutes: 60
release_path: true
- suite_id: docker-npm-onboard-channel-agent
label: Npm Onboard Channel Agent Docker E2E
command: pnpm test:docker:npm-onboard-channel-agent
timeout_minutes: 90
release_path: true
- suite_id: docker-gateway-network
label: Gateway Network Docker E2E
command: pnpm test:docker:gateway-network
timeout_minutes: 60
release_path: true
- suite_id: docker-openai-web-search-minimal
label: OpenAI Web Search Minimal Docker E2E
command: pnpm test:docker:openai-web-search-minimal
timeout_minutes: 60
release_path: true
- suite_id: docker-mcp-channels
label: MCP Channels Docker E2E
command: pnpm test:docker:mcp-channels
timeout_minutes: 60
release_path: true
- suite_id: docker-pi-bundle-mcp-tools
label: Pi Bundle MCP Tools Docker E2E
command: pnpm test:docker:pi-bundle-mcp-tools
timeout_minutes: 60
release_path: true
- suite_id: docker-cron-mcp-cleanup
label: Cron MCP Cleanup Docker E2E
command: pnpm test:docker:cron-mcp-cleanup
timeout_minutes: 60
release_path: true
- suite_id: docker-plugins
label: Plugins Docker E2E
command: pnpm test:docker:plugins
timeout_minutes: 75
release_path: true
- suite_id: docker-plugin-update
label: Plugin Update Docker E2E
command: pnpm test:docker:plugin-update
timeout_minutes: 60
release_path: true
- suite_id: docker-config-reload
label: Config Reload Docker E2E
command: pnpm test:docker:config-reload
timeout_minutes: 60
release_path: true
- suite_id: docker-bundled-channel-deps
label: Bundled Channel Runtime Deps Docker E2E
command: pnpm test:docker:bundled-channel-deps
timeout_minutes: 75
release_path: true
- suite_id: docker-doctor-switch
label: Doctor Install Switch Docker E2E
command: pnpm test:docker:doctor-switch
timeout_minutes: 60
release_path: true
- suite_id: docker-session-runtime-context
label: Session Runtime Context Docker E2E
command: pnpm test:docker:session-runtime-context
timeout_minutes: 60
release_path: true
- suite_id: docker-qr
label: QR Import Docker E2E
command: pnpm test:docker:qr
timeout_minutes: 60
release_path: true
- suite_id: docker-install-e2e
label: Installer Docker E2E
command: pnpm test:install:e2e
- chunk_id: core
label: core
timeout_minutes: 120
release_path: true
- chunk_id: package-update
label: package/update
timeout_minutes: 180
- chunk_id: plugins-integrations
label: plugins/integrations
timeout_minutes: 180
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -491,7 +443,12 @@ jobs:
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_BARE_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.bare_image }}
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_SKIP_DOCKER_BUILD: "1"
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
DOCKER_E2E_CHUNK: ${{ matrix.chunk_id }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
@@ -516,45 +473,188 @@ jobs:
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Configure suite-specific env
- name: Plan and hydrate Docker E2E chunk
id: plan
uses: ./.github/actions/docker-e2e-plan
with:
mode: chunk
chunk: ${{ matrix.chunk_id }}
include-openwebui: ${{ inputs.include_openwebui }}
- name: Run Docker E2E chunk
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
echo "OPENCLAW_E2E_MODELS=both" >> "$GITHUB_ENV"
;;
esac
export OPENCLAW_DOCKER_ALL_PROFILE=release-path
export OPENCLAW_DOCKER_ALL_CHUNK="${DOCKER_E2E_CHUNK}"
export OPENCLAW_DOCKER_ALL_BUILD=0
export OPENCLAW_DOCKER_ALL_PREFLIGHT=0
export OPENCLAW_DOCKER_ALL_FAIL_FAST=0
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}"
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}"
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json"
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
- name: Validate suite credentials
pnpm test:docker:all
- name: Summarize Docker E2E chunk
if: always()
shell: bash
run: |
set -euo pipefail
case "${{ matrix.suite_id }}" in
docker-install-e2e)
[[ -n "${OPENAI_API_KEY:-}" ]] || {
echo "OPENAI_API_KEY is required for installer Docker E2E." >&2
exit 1
}
if [[ -z "${ANTHROPIC_API_TOKEN:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for installer Docker E2E." >&2
exit 1
fi
;;
esac
summary=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker chunk summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
node scripts/docker-e2e.mjs summary "$summary" "Docker E2E chunk: ${DOCKER_E2E_CHUNK:-unknown}" >> "$GITHUB_STEP_SUMMARY"
- name: Run ${{ matrix.label }}
run: ${{ matrix.command }}
- name: Upload Docker E2E chunk artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: docker-e2e-${{ matrix.chunk_id }}
path: .artifacts/docker-tests/
if-no-files-found: ignore
validate_docker_lanes:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 180
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }}
OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }}
OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }}
OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }}
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }}
OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
FAL_KEY: ${{ secrets.FAL_KEY }}
RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }}
DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }}
BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }}
OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }}
OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }}
OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }}
OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }}
OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_BARE_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.bare_image }}
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_SKIP_DOCKER_BUILD: "1"
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
DOCKER_E2E_LANES: ${{ inputs.docker_lanes }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
with:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Log in to GHCR for shared Docker E2E image
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Hydrate live auth/profile inputs
run: bash scripts/ci-hydrate-live-auth.sh
- name: Plan and hydrate targeted Docker E2E lanes
id: plan
uses: ./.github/actions/docker-e2e-plan
with:
mode: targeted
lanes: ${{ inputs.docker_lanes }}
include-openwebui: ${{ inputs.include_openwebui }}
- name: Run targeted Docker E2E lanes
shell: bash
run: |
set -euo pipefail
export OPENCLAW_DOCKER_ALL_LANES="${DOCKER_E2E_LANES}"
export OPENCLAW_DOCKER_ALL_PREFLIGHT=0
export OPENCLAW_DOCKER_ALL_FAIL_FAST=0
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}"
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted"
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-timings.json"
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
pnpm test:docker:live-build
fi
export OPENCLAW_DOCKER_ALL_BUILD=0
pnpm test:docker:all
- name: Summarize targeted Docker E2E lanes
if: always()
shell: bash
run: |
set -euo pipefail
summary=".artifacts/docker-tests/targeted/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker targeted summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
node scripts/docker-e2e.mjs summary "$summary" "Docker E2E targeted lanes" >> "$GITHUB_STEP_SUMMARY"
- name: Upload targeted Docker E2E artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: docker-e2e-targeted
path: .artifacts/docker-tests/
if-no-files-found: ignore
validate_docker_openwebui:
needs: [validate_selected_ref, prepare_docker_e2e_image]
if: inputs.include_openwebui
if: inputs.include_openwebui && !inputs.include_release_path_suites && inputs.docker_lanes == ''
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 75
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENCLAW_DOCKER_E2E_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.image }}
OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: ${{ needs.prepare_docker_e2e_image.outputs.functional_image }}
OPENCLAW_SKIP_DOCKER_BUILD: "1"
steps:
- name: Checkout selected ref
@@ -591,7 +691,7 @@ jobs:
prepare_docker_e2e_image:
needs: validate_selected_ref
if: inputs.include_release_path_suites || inputs.include_openwebui
if: inputs.include_release_path_suites || inputs.include_openwebui || inputs.docker_lanes != ''
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 90
permissions:
@@ -599,6 +699,13 @@ jobs:
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
bare_image: ${{ steps.image.outputs.bare_image }}
functional_image: ${{ steps.image.outputs.functional_image }}
needs_bare_image: ${{ steps.plan.outputs.needs_bare_image }}
needs_e2e_image: ${{ steps.plan.outputs.needs_e2e_image }}
needs_functional_image: ${{ steps.plan.outputs.needs_functional_image }}
needs_live_image: ${{ steps.plan.outputs.needs_live_image }}
needs_package: ${{ steps.plan.outputs.needs_package }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
@@ -609,7 +716,7 @@ jobs:
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
fetch-depth: 1
- name: Resolve shared Docker E2E image tag
- name: Resolve shared Docker E2E image tags
id: image
shell: bash
env:
@@ -617,31 +724,127 @@ jobs:
run: |
set -euo pipefail
repository="${GITHUB_REPOSITORY,,}"
image="ghcr.io/${repository}-docker-e2e:${SELECTED_SHA}"
bare_image="ghcr.io/${repository}-docker-e2e-bare:${SELECTED_SHA}"
functional_image="ghcr.io/${repository}-docker-e2e-functional:${SELECTED_SHA}"
image="$functional_image"
echo "image=$image" >> "$GITHUB_OUTPUT"
echo "Shared Docker E2E image: \`$image\`" >> "$GITHUB_STEP_SUMMARY"
echo "bare_image=$bare_image" >> "$GITHUB_OUTPUT"
echo "functional_image=$functional_image" >> "$GITHUB_OUTPUT"
echo "Shared Docker E2E bare image: \`$bare_image\`" >> "$GITHUB_STEP_SUMMARY"
echo "Shared Docker E2E functional image: \`$functional_image\`" >> "$GITHUB_STEP_SUMMARY"
- name: Plan Docker E2E images
id: plan
uses: ./.github/actions/docker-e2e-plan
with:
mode: prepare
lanes: ${{ inputs.docker_lanes }}
include-release-path-suites: ${{ inputs.include_release_path_suites }}
include-openwebui: ${{ inputs.include_openwebui }}
hydrate-artifacts: "false"
- name: Setup Node environment
if: steps.plan.outputs.needs_package == '1'
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
install-bun: "true"
- name: Pack OpenClaw package for Docker E2E
if: steps.plan.outputs.needs_package == '1'
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/docker-e2e-package
node scripts/package-openclaw-for-docker.mjs \
--output-dir .artifacts/docker-e2e-package \
--output-name openclaw-current.tgz
- name: Upload OpenClaw Docker E2E package
if: steps.plan.outputs.needs_package == '1'
uses: actions/upload-artifact@v7
with:
name: docker-e2e-package
path: .artifacts/docker-e2e-package/openclaw-current.tgz
if-no-files-found: error
- name: Log in to GHCR
if: steps.plan.outputs.needs_e2e_image == '1'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Check existing shared Docker E2E images
id: image_exists
if: steps.plan.outputs.needs_e2e_image == '1'
shell: bash
run: |
set -euo pipefail
bare_exists=0
functional_exists=0
needs_build=0
if [[ "${{ steps.plan.outputs.needs_bare_image }}" == "1" ]]; then
if docker manifest inspect "${{ steps.image.outputs.bare_image }}" >/dev/null 2>&1; then
bare_exists=1
echo "Shared Docker E2E bare image already exists: ${{ steps.image.outputs.bare_image }}"
else
needs_build=1
fi
fi
if [[ "${{ steps.plan.outputs.needs_functional_image }}" == "1" ]]; then
if docker manifest inspect "${{ steps.image.outputs.functional_image }}" >/dev/null 2>&1; then
functional_exists=1
echo "Shared Docker E2E functional image already exists: ${{ steps.image.outputs.functional_image }}"
else
needs_build=1
fi
fi
echo "bare_exists=$bare_exists" >> "$GITHUB_OUTPUT"
echo "functional_exists=$functional_exists" >> "$GITHUB_OUTPUT"
echo "needs_build=$needs_build" >> "$GITHUB_OUTPUT"
- name: Setup Docker builder
if: steps.image_exists.outputs.needs_build == '1'
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
- name: Build and push shared Docker E2E image
- name: Build and push bare Docker E2E image
if: steps.plan.outputs.needs_bare_image == '1' && steps.image_exists.outputs.bare_exists != '1'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./scripts/e2e/Dockerfile
target: build
target: bare
platforms: linux/amd64
cache-from: type=gha,scope=docker-e2e
cache-to: type=gha,mode=max,scope=docker-e2e
tags: ${{ steps.image.outputs.image }}
provenance: false
cache-from: type=gha,scope=docker-e2e-bare
cache-to: type=gha,mode=max,scope=docker-e2e-bare
tags: ${{ steps.image.outputs.bare_image }}
sbom: true
provenance: mode=max
push: true
- name: Build and push functional Docker E2E image
if: steps.plan.outputs.needs_functional_image == '1' && steps.image_exists.outputs.functional_exists != '1'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ./scripts/e2e/Dockerfile
target: functional
build-contexts: |
openclaw_package=.artifacts/docker-e2e-package
platforms: linux/amd64
cache-from: |
type=gha,scope=docker-e2e-bare
type=gha,scope=docker-e2e-functional
cache-to: type=gha,mode=max,scope=docker-e2e-functional
tags: ${{ steps.image.outputs.functional_image }}
sbom: true
provenance: mode=max
push: true
validate_live_models_docker:

View File

@@ -29,7 +29,7 @@ jobs:
with:
app-id: "2971289"
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
- name: Mark stale issues and pull requests (primary)
- name: Mark stale unassigned issues and pull requests (primary)
id: stale-primary
continue-on-error: true
uses: actions/stale@v10
@@ -56,12 +56,60 @@ jobs:
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (primary)
id: assigned-issue-stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
operations-per-run: 2000
ascending: true
include-only-assigned: true
remove-stale-when-updated: true
stale-issue-message: |
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (primary)
id: assigned-stale-primary
continue-on-error: true
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
ignore-pr-updates: true
remove-stale-when-updated: true
stale-pr-message: |
This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Check stale state cache
id: stale-state
if: always()
@@ -86,7 +134,7 @@ jobs:
core.warning(`Failed to check stale state cache: ${message}`);
core.setOutput("has_state", "false");
}
- name: Mark stale issues and pull requests (fallback)
- name: Mark stale unassigned issues and pull requests (fallback)
if: (steps.stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
@@ -112,12 +160,58 @@ jobs:
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh repro steps.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
- name: Mark stale assigned issues (fallback)
if: (steps.assigned-issue-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: 30
days-before-issue-close: 10
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: stale
exempt-issue-labels: enhancement,maintainer,pinned,security,no-stale
operations-per-run: 2000
ascending: true
include-only-assigned: true
remove-stale-when-updated: true
stale-issue-message: |
This assigned issue has been automatically marked as stale after 30 days of inactivity.
Please add updates or it will be closed.
close-issue-message: |
Closing due to inactivity.
If this is still an issue, please retry on the latest OpenClaw release and share updated details.
If you are absolutely sure it still happens on the latest release, open a new issue with fresh steps to reproduce.
close-issue-reason: not_planned
- name: Mark stale assigned pull requests (fallback)
if: (steps.assigned-stale-primary.outcome == 'failure' || steps.stale-state.outputs.has_state == 'true') && steps.app-token-fallback.outputs.token != ''
uses: actions/stale@v10
with:
repo-token: ${{ steps.app-token-fallback.outputs.token }}
days-before-issue-stale: -1
days-before-issue-close: -1
days-before-pr-stale: 27
days-before-pr-close: 3
stale-pr-label: stale
exempt-pr-labels: maintainer,no-stale,bad-barnacle
operations-per-run: 2000
ascending: true
include-only-assigned: true
ignore-pr-updates: true
remove-stale-when-updated: true
stale-pr-message: |
This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.
close-pr-message: |
Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.
lock-closed-issues:
permissions:

2
.gitignore vendored
View File

@@ -118,6 +118,8 @@ USER.md
!.agents/skills/openclaw-test-heap-leaks/**
!.agents/skills/openclaw-test-performance/
!.agents/skills/openclaw-test-performance/**
!.agents/skills/openclaw-testing/
!.agents/skills/openclaw-testing/**
!.agents/skills/optimizetests/
!.agents/skills/optimizetests/**
!.agents/skills/parallels-discord-roundtrip/

View File

@@ -29,6 +29,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Extension prod code: no core `src/**`, `src/plugin-sdk-internal/**`, other extension `src/**`, or relative outside package.
- Core/tests: no deep plugin internals (`extensions/*/src/**`, `onboard.js`). Use `api.ts`, SDK facade, generic contracts.
- Extension-owned behavior stays extension-owned: repair, detection, onboarding, auth/provider defaults, provider tools/settings.
- Owner boundary: fix owner-specific behavior in the owner module. Shared/core gets generic seams only; no owner ids, dependency strings, defaults, migrations, or recovery policy. If a bug names an extension or its dependency, start in that extension and add a generic core seam only when multiple owners need it.
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
@@ -50,7 +51,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Extension tests: `pnpm test:extensions`, `pnpm test extensions`, `pnpm test extensions/<id>`.
- Targeted tests: `pnpm test <path-or-filter> [vitest args...]`; never raw `vitest`.
- Typecheck: `tsgo` lanes only (`pnpm tsgo*`, `pnpm check:test-types`); do not add `tsc --noEmit`, `typecheck`, `check:types`.
- Format/lint: `pnpm format:check`/`pnpm format`; `pnpm lint*` lanes.
- Formatting: use `oxfmt`, not Prettier. Prefer `pnpm format:check` / `pnpm format`; for targeted files use `pnpm exec oxfmt --check --threads=1 <files...>` or `pnpm exec oxfmt --write --threads=1 <files...>`.
- Linting: use repo wrappers (`pnpm lint:*`, `scripts/run-oxlint.mjs`); do not invoke generic JS formatters/lints unless a repo script uses them.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
- Local first. Use repo `pnpm` lanes before Blacksmith/Testbox. Remote only for parity-only failures, secrets/services, or explicit ask.
@@ -58,6 +60,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without Peter asking.
- PR scan/triage: no unsolicited PR comments/reviews. Report in chat only unless explicitly asked, or a close/duplicate action needs a reason comment.
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20`.
- GitHub search boolean text is fussy. If `OR` queries return empty, split exact terms and search title/body/comments separately before concluding no hits.
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
@@ -85,7 +88,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- extension tests: extension test typecheck/tests
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Before handoff/push for code/test/runtime/config changes: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Docs/changelog-only and CI/workflow metadata-only changes are not changed-gate work by default. Use `git diff --check` plus the relevant formatter/docs/workflow sanity check; escalate to `pnpm check:changed` only when scripts, test config, generated docs/API, package metadata, or runtime/build behavior changed.
- Rebase sanity: after a green `pnpm check:changed`, a clean rebase onto current
`origin/main` does not require rerunning the full changed gate when the rebase
has no conflicts and the branch diff is materially unchanged. Do a quick
@@ -116,6 +120,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
## Tests
- Vitest. Colocated `*.test.ts`; e2e `*.e2e.test.ts`; example models `sonnet-4.6`, `gpt-5.4`.
- Avoid brittle tests that grep workflow/docs strings for operator policy. Prefer executable behavior, parsed config/schema checks, or live run proof; put release/CI policy reminders in AGENTS/docs instead.
- Clean timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` safe.
- Hot tests: avoid per-test `vi.resetModules()` + heavy imports. Measure with `pnpm test:perf:imports <file>` / `pnpm test:perf:hotspots --limit N`.
- Seam depth: pure helper/contract unit tests; one integration smoke per boundary.
@@ -131,7 +136,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
- Changelog user-facing only; pure test/internal usually no entry.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete`.
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete` or `Thanks @codex`.
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
## Git

View File

@@ -4,11 +4,67 @@ Docs: https://docs.openclaw.ai
## Unreleased
## 2026.4.25 (Unreleased)
### Fixes
- Codex harness: normalize cached input tokens before session/context accounting so prompt cache reads are not double-counted in `/status`, `session_status`, or persisted `sessionEntry.totalTokens`. Fixes #69298. Thanks @richardmqq.
- Hooks/session-memory: use the host local timezone for memory filenames, fallback timestamp slugs, and markdown headers instead of UTC dates. Fixes #46703. (#46721) Thanks @Astro-Han.
- Feishu: extract quoted/replied interactive-card text across schema 1.0, schema 2.0, i18n, template-variable, and post-format fallback shapes without carrying broad generated/config churn from related parser experiments. (#38776, #60383, #42218, #45936) Thanks @lishuaigit, @lskun, @just2gooo, and @Br1an67.
- Exec approvals: accept a symlinked `OPENCLAW_HOME` as the trusted approvals root while still rejecting symlinked `.openclaw` path components below it. (#64663) Thanks @FunJim.
- Logging: add top-level `hostname`, flattened `message`, and available `agent_id`, `session_id`, and `channel` fields to file-log JSONL records for multi-agent filtering without removing existing structured log arguments. Fixes #51075. Thanks @stevengonsalvez.
- ACP: route server logs to stderr before Gateway config/bootstrap work so ACP stdout remains JSON-RPC only for IDE integrations. Fixes #49060. Thanks @Hollychou924.
- Logging: propagate internal request trace scopes through Gateway HTTP requests and WebSocket frames so file logs, diagnostic events, agent run traces, model-call traces, OTEL spans, and trusted provider `traceparent` headers share a correlatable `traceId` without logging raw request or model content. Fixes #40353. Thanks @liangruochong44-ui.
- Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.
- Logging: write validated diagnostic trace context as top-level `traceId`, `spanId`, `parentSpanId`, and `traceFlags` fields in file-log JSONL records so traced requests and model calls are easier to correlate in log processors. Refs #40353. Thanks @liangruochong44-ui.
- Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.
- Providers/Ollama: honor `/api/show` capabilities when registering local models so non-tool Ollama models no longer receive the agent tool surface, and keep native Ollama thinking opt-in instead of enabling it by default. Fixes #64710 and duplicate #65343. Thanks @yuan-b, @netherby, @xilopaint, and @Diyforfun2026.
- Providers/Ollama: expose native Ollama thinking effort levels so `/think max` is accepted for reasoning-capable Ollama models and maps to Ollama's highest supported `think` effort. Fixes #71584. Thanks @g0st1n.
- Agents/Ollama: validate explicit `--thinking max` against catalog-discovered Ollama reasoning metadata so local agent runs accept the same native thinking levels shown in the model catalog. Fixes #71584. Thanks @g0st1n.
- Docker/QA: add observability coverage to the normal Docker aggregate so QA-lab OTEL and Prometheus diagnostics run inside Docker. Thanks @vincentkoc.
- Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.
- Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.
- Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @codex.
- Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex.
- Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.
- Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.
- Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.
- Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex.
- Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @codex.
- Plugins: fail `plugins update` when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @codex.
- WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.
- Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.
- Gateway/chat: keep duplicate attachment-backed `chat.send` retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.
- Plugins: share package entrypoint resolution between install and discovery, reject mismatched `runtimeExtensions`, and cache bundled runtime-dependency manifest reads during scans. Thanks @codex.
- WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.
- Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.
- Cron/context engine: run isolated cron jobs under run-scoped context-engine session keys so prior runs of the same job are not inherited unless the job is explicitly session-bound. (#72292) Thanks @jalehman.
- Control UI: localize command palette labels, categories, skill shortcuts, footer hints, and connect-command copy labels while preserving localized command palette search matching. (#61130, #61119) Thanks @rubensfox20.
## 2026.4.26
### Fixes
- Plugins/CLI: let flag-driven `openclaw channels add` install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation. Thanks @codex.
- Onboarding/setup: keep first-run config reads, plugin compatibility notices, and post-model sanity checks on cold metadata paths unless the user chooses to browse all models, avoiding full plugin/runtime catalog work between prompts. Thanks @shakkernerd.
- Onboarding/auth: run manifest-owned provider auth choices through scoped setup providers so selecting OpenAI Codex browser/device auth no longer loads every provider runtime before OAuth starts. Thanks @shakkernerd.
- Onboarding/auth: keep the post-auth default-model policy lookup on manifest/setup metadata so the next prompt appears without loading broad provider runtime. Thanks @shakkernerd.
- Onboarding/models: keep skip-auth and provider-scoped model picker prompts off the full global model catalog path, and cache provider catalog hook resolution so setup no longer stalls after auth on large plugin registries. Thanks @shakkernerd.
- Gateway/Bonjour: suppress known @homebridge/ciao cancellation and network assertion failures through scoped process handlers so malformed mDNS packets or restricted VPS networking disable/restart Bonjour instead of crashing the gateway. Fixes #67578. Thanks @zenassist26-create.
- Discord: keep late clicks on already-resolved exec approval buttons quiet when elevated mode auto-resolved the request, while still surfacing real approval submission failures. Fixes #66906. Thanks @rlerikse.
- Telegram: send a fresh final message for long-lived preview-streamed replies so the visible Telegram timestamp reflects completion time instead of the preview creation time. Thanks @rubencu.
## 2026.4.25
### Highlights
- Voice replies get a full TTS upgrade: `/tts latest`, chat-scoped auto-TTS controls, personas, per-agent/per-account overrides, and new Azure Speech, Xiaomi, Local CLI, Inworld, Volcengine, and ElevenLabs v3 provider coverage. Thanks @leonchui, @zoujiejun, @solar2ain, @cshape, @xuruiray, @itsuzef, and @barronlroth.
- Plugin startup and install paths move to the cold persisted registry, cutting broad manifest scans while making plugin update, repair, provider discovery, and install metadata more deterministic. Thanks @vincentkoc and @shakkernerd.
- OpenTelemetry coverage expands across model calls, token usage, tool loops, harness runs, exec processes, outbound delivery, context assembly, and memory pressure with bounded low-cardinality attributes. Thanks @vincentkoc, @jlapenna, @Lidang-Jiang, and @oc-factus.
- Browser automation gets safer tab URLs, iframe-aware role snapshots, CDP readiness tuning, headless one-shot launch, and deeper browser doctor probes for slow hosts. Thanks @beat843796 and @BenediktSchackenberg.
- Control UI and setup flows add PWA/Web Push support, Crestodian first-run repair, TUI setup, context mode selection, and a shorter startup greeting. Thanks @eduardocruz, @SebTardif, and @kevinlin-openai.
- Install/update hardening covers Windows, macOS, Linux, Docker, bundled plugin runtime deps, Node service restarts, LaunchAgent token rotation, and mixed-version gateway verification. Thanks @Kobevictor, @igormf, @abhinas90, @jsompis, @Solvely-Colin, and @gucasbrg.
### Changes
- Codex/agent: translate `--thinking minimal` to `low` for modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) at request build time so the first turn is accepted instead of paying a wasted call + retry-with-low fallback. Older Codex models still receive `minimal` directly. Fixes #71946. Thanks @hclsys.
- TTS/WhatsApp: add `/tts latest` read-aloud support with duplicate suppression and `/tts chat on|off|default` session-scoped auto-TTS overrides, completing the on-demand voice-note UX for current-chat replies. Fixes #66032.
- TTS/channels: resolve channel and account TTS overrides generically, enabling Feishu and QQBot accounts to deep-merge `channels.<channel>.accounts.<id>.tts` over global and per-agent TTS config. Thanks @sahilsatralkar.
- TTS/agents: allow `agents.list[].tts` to override global `messages.tts` for per-agent voices, and make `/tts audio`, `/tts status`, and the `tts` agent tool honor the active voice/provider override while keeping shared provider credentials and preferences in the existing TTS config surface.
@@ -16,7 +72,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: add calendar-backed attendance export workflows, export manifests, dry-run previews, and tool parity for meeting records.
- Control UI: add PWA install support and Web Push notifications for Gateway chat. (#44590) Thanks @eduardocruz.
- Browser automation: add safe tab URLs in agent responses plus a CDP-native role snapshot fallback with iframe-aware refs, cursor-clickable detection, target attach preparation, and `openclaw browser doctor --deep` live snapshot probing.
- CLI/image generation: expose generic `--background` on `openclaw infer image generate` and `openclaw infer image edit`, keep `--openai-background` as an OpenAI alias, and let fal image generation honor `--output-format png|jpeg`. Thanks @steipete.
- CLI/image generation: expose generic `--background` on `openclaw infer image generate` and `openclaw infer image edit`, keep `--openai-background` as an OpenAI alias, and let fal image generation honor `--output-format png|jpeg`.
- Browser/config: allow local managed Chrome launch discovery and post-launch CDP readiness timeouts to be raised for slower hosts such as Raspberry Pi. Fixes #66803. Thanks @beat843796.
- Discord: allow `channels.discord.voice.model` to override the LLM used for voice channel responses while keeping STT and TTS on their existing media settings. (#64368) Thanks @mrdavey.
- Browser/CLI: add `openclaw browser start --headless` as a one-shot local managed browser launch override without rewriting persisted browser config. Thanks @BenediktSchackenberg.
@@ -30,6 +86,7 @@ Docs: https://docs.openclaw.ai
- Diagnostics/OTEL: emit bounded telemetry exporter health diagnostics for startup and log-export failures without exporting raw error text. Thanks @vincentkoc.
- Diagnostics/OTEL: export agent harness lifecycle telemetry as bounded `openclaw.harness.run` spans and `openclaw.harness.duration_ms` metrics so QA-lab, Codex, and future harnesses share one trace shape. Thanks @vincentkoc.
- Diagnostics/trace: propagate W3C `traceparent` headers from trusted model-call trace context to provider transports while replacing caller-supplied traceparent values. Thanks @vincentkoc.
- Diagnostics/Prometheus: add a bundled `diagnostics-prometheus` plugin with a protected gateway scrape route for low-cardinality diagnostics metrics. Thanks @vincentkoc.
- Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc.
- Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc.
- Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc.
@@ -42,9 +99,13 @@ Docs: https://docs.openclaw.ai
- Providers/plugins: keep onboarding and auth-choice setup lists on cold manifest/install metadata and add Provider Index install metadata for not-yet-installed provider plugins. Thanks @vincentkoc.
- Providers/plugins: keep provider setup guidance and configure auth imports on cold manifest metadata, with a regression guard against static provider-runtime imports on setup/configure list paths. Thanks @vincentkoc.
- CLI/capabilities: keep capability command registration from importing the models auth runtime until `model auth login` actually runs. Thanks @vincentkoc.
- CLI/configure: keep web-search configure prompts on cold plugin registry metadata until the user chooses managed search setup. Thanks @vincentkoc.
- Plugins/chat commands: refresh the persisted plugin registry after `/plugins enable` and `/plugins disable`, matching the CLI mutation path. Thanks @vincentkoc.
- Plugins/compat: mark `OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY` as a deprecated break-glass switch and point operators at registry repair instead. Thanks @vincentkoc.
- Plugins/compat: expand the central compatibility registry with dated owners, replacements, and maximum three-month removal targets for legacy SDK, manifest, setup, registry-migration, and agent-runtime surfaces. Thanks @vincentkoc.
- Plugins/registry: ignore stale persisted registry reads when plugin policy no longer matches current config, and stamp generated registry files with a do-not-edit warning. Thanks @vincentkoc.
- Config/plugins: keep plugin command-alias validation on cold manifest metadata instead of importing the runtime alias resolver. Thanks @vincentkoc.
- Security/plugins: keep web-search credential presence checks on cold config, env, and manifest metadata instead of importing web-search provider runtime. Thanks @vincentkoc.
- Diagnostics/OTEL: surface provider request identifiers as bounded hashes on model-call diagnostics and span events, without exporting raw request IDs or metric labels. Thanks @Lidang-Jiang and @vincentkoc.
- Plugins/diagnostics: add metadata-only `model_call_started` and `model_call_ended` hooks for provider/model call telemetry without exposing prompts, responses, headers, request bodies, or raw provider request IDs. Thanks @vincentkoc.
- Diagnostics/OTEL: emit bounded context assembly diagnostics and export `openclaw.context.assembled` spans with prompt/history sizes but no prompt, history, response, or session-key content. Thanks @vincentkoc.
@@ -80,20 +141,28 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/subagents: deliver completed yielded-subagent results back to no-thread requester routes via direct fallback when the dormant parent announce turn produces no visible reply, and add QA-lab coverage for the regression. Thanks @vincentkoc.
- Gateway/Tailscale: let Tailscale-authenticated Control UI operator sessions with browser device identity skip the device-pairing round trip while still rejecting device-less and node-role connections. Refs #71986. Thanks @jokedul.
- Doctor: honor `OPENCLAW_SERVICE_REPAIR_POLICY=external` by reporting gateway service health while skipping service install/start/restart/bootstrap, supervisor rewrites, and legacy service cleanup for externally managed environments. Thanks @shakkernerd.
- CLI/update: run package post-update doctor with `--fix` so package updates repair config migrations before restart. Thanks @shakkernerd.
- CLI/update: retry failed npm global updates with `--omit=optional` and ignore the superseded first failure when the fallback succeeds. Thanks @shakkernerd.
- Plugins/uninstall: migrate and reset `plugins.slots.contextEngine` alongside memory slots when plugin ids change or selected plugins are removed. Thanks @shakkernerd.
- Agents/Discord: keep raw `Agent failed before reply` runner failures out of Discord group/channel chats and show detailed runner errors in direct chats only when `/verbose` is enabled. Thanks @codex.
- UI/Windows: quote resolved pnpm `.cmd` launcher paths before spawning UI install/build/test commands so Node installs under `C:\Program Files` no longer fail as `C:\Program`. Fixes #45275. Thanks @Kobevictor, @stoppieboy, and @iubns.
- Codex/agent: translate `--thinking minimal` to `low` for modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.2) at request build time so the first turn is accepted instead of paying a wasted call + retry-with-low fallback. Older Codex models still receive `minimal` directly. Fixes #71946. Thanks @hclsys.
- Plugins/uninstall: remove tracked plugin files from their recorded managed extensions root even when the current state directory points somewhere else, so `openclaw plugins uninstall --force` does not leave the plugin discoverable. Thanks @shakkernerd.
- Agents/runtime: add `agentRuntime.id` as the canonical config key, migrate
legacy runtime-policy configs with `openclaw doctor --fix`, and route
canonical Anthropic models through `claude-cli` without passing CLI backend
aliases to embedded harness selection. Fixes #71957. Thanks @WolvenRA.
- Agents/runtime: add `agentRuntime.id` as the canonical config key, migrate legacy runtime-policy configs with `openclaw doctor --fix`, route canonical Anthropic models through `claude-cli` without passing CLI backend aliases to embedded harness selection, and load CLI backend owner plugins before channel startup. Fixes #71957. Thanks @WolvenRA.
- CLI/update: guard Windows scheduled-task stops by state and timeout so auto-update restart cannot hang indefinitely on `schtasks /End` before stale-listener cleanup. Fixes #69970. Thanks @yangswld and @sherlock-huang.
- Windows install/Lobster: execute `pnpm.exe` directly when `npm_execpath` points at the native pnpm binary, add an installed-package fallback for the Lobster embedded runtime, and include the Lobster runner regression test in Windows CI. Fixes #69456. Thanks @igormf.
- Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex.
- Update: ignore bundled plugin `.openclaw-install-stage` directories during global install verification and packaged dist pruning so leftover runtime-dep staging files do not turn successful updates into `unexpected packaged dist file` failures. Fixes #71752. Thanks @waynegault.
- CLI/update: fail package updates when post-update plugin sync fails and refresh legacy npm plugin install records before trusting unchanged artifacts, preventing successful updates from restarting with stale or failed plugin state. Thanks @vincentkoc and @shakkernerd.
- Release/update: reject pre-populated bundled plugin `.openclaw-install-stage` directories, including mixed-case path variants, before package inventory generation so release tarballs cannot ship poisoned runtime-dependency staging debris. Fixes #71752. Thanks @hclsys.
- Node runtime: keep node-host retry timers alive across Gateway restarts and exit on terminal credential pauses so supervised nodes do not become silent zombies. Fixes #69800. Thanks @meroli28.
- Gateway/plugins: stop persisted WhatsApp auth state from activating bundled channel runtime-dependency repair during startup when `channels.whatsapp` is absent, avoiding npm/git stalls on packaged Linux installs. Fixes #71994. Thanks @xiao398008.
- Gateway/device tokens: enforce caller-scope containment inside token rotation and revocation so pairing-only sessions cannot mutate higher-scope operator tokens. Fixes #71990. Thanks @coygeek.
- Plugins/channels: keep security checks, thread-binding placement, provider summaries, health formatting, and message action labels on read-only or already-loaded channel metadata instead of importing full channel runtime. Thanks @shakkernerd.
- Plugins/status: keep config-only channel labels and status security summaries from importing plugin runtime modules just to render metadata. Thanks @shakkernerd.
- Sessions/channels: stop group-session metadata from loading bundled channel runtime just to classify `#channel` subjects, using only already-loaded channel capabilities on that path. Thanks @shakkernerd.
- Plugins/channels: keep native command and native skill `auto` defaults on static channel metadata so config, audit, and command-list checks do not load channel runtime just to read those defaults. Thanks @shakkernerd.
- CLI/channels: keep channel remove selection and all-channel capabilities summaries on read-only plugin metadata, loading channel runtime only for the selected mutation path. Thanks @shakkernerd.
@@ -106,13 +175,15 @@ Docs: https://docs.openclaw.ai
- Installer: warn when multiple npm global roots contain OpenClaw installs, showing active Node/npm/openclaw plus each install path and version so stale version-manager installs are visible. Fixes #40839. Thanks @zhixianio.
- Cron/tasks: recover completed cron task ledger records from durable run logs and job state before marking them `lost`, reducing false `backing session missing` audit errors for isolated cron runs and keeping offline CLI audit from treating its empty local cron active-job set as authoritative. Fixes #71963.
- Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg.
- Package: include patched dependency files in the published npm package so downstream installs can resolve `patchedDependencies`. (#69224) Thanks @gucasbrg and @vincentkoc.
- Plugins/channels: treat malformed bundled channel plugin loaders that return `undefined` as unavailable instead of crashing config and help paths. Fixes #69044. Thanks @frankhli843 and @vincentkoc.
- Scripts/watch: show corrupted dependency package-config recovery guidance when `gateway:watch` fails during watcher startup, without double-logging unrelated import failures. (#58780) Thanks @roytong9 and @vincentkoc.
- Signal: read signal-cli RPC, health checks, and SSE events through Node's HTTP client so Node 24/25 fetch regressions do not break Signal sends or inbound events. Fixes #51716 and #53040. Thanks @Barukimang, @minupla, and @vincentkoc.
- Skills/Docker: run npm-backed skill dependency installs with an OpenClaw-managed user prefix so non-root Docker images do not write to `/usr/local`. Fixes #59601. Thanks @chanjarster and @vincentkoc.
- Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker.
- Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai.
- Node/Linux: make `openclaw node install` enable and restart the `openclaw-node` systemd unit instead of the gateway unit on node-only VMs. Fixes #68287. Thanks @dlebee-agent.
- Browser/CDP: retry transient raw-CDP WebSocket handshake failures before any
browser command is sent, and reconnect stale persistent Playwright CDP
sessions for safe tab-list reads without replaying mutating browser actions.
Fixes #67728.
- Browser/CDP: retry transient raw-CDP WebSocket handshake failures before any browser command is sent, and reconnect stale persistent Playwright CDP sessions for safe tab-list reads without replaying mutating browser actions. Fixes #67728.
- Gateway/Linux: retry `systemctl --user enable` after a second daemon reload when the freshly written gateway unit is not visible yet on migrated systemd installs. Fixes #65184. Thanks @liushuaiiu.
- Telegram: preserve exact selected quote text when sending native quote replies, and retry with legacy replies if Telegram rejects quote parameters. (#71952) Thanks @rubencu.
- Plugins/CLI: preserve manifest name, description, format, and source metadata in cold `openclaw plugins list` output without importing plugin runtime. Thanks @shakkernerd.
@@ -120,27 +191,13 @@ Docs: https://docs.openclaw.ai
- Plugins/chat: keep `/plugins list`, `/plugins enable`, and `/plugins disable` on the persisted plugin index path so chat plugin management does not load diagnostic/runtime plugin registries before execution. Thanks @shakkernerd.
- Plugins/doctor: read workspace plugin status and legacy web-search ownership through installed-index manifest metadata instead of broad manifest registry scans. Thanks @shakkernerd.
- CLI/agents: read channel provider status from read-only plugin index metadata for text `agents list` output instead of the loaded channel registry. Thanks @shakkernerd.
- Logging: redact configured secret patterns at console and file-log sink exits
so credentials that reach the logger are masked before terminal display or
JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan.
- Gateway/services: refuse process and service mutations from an older OpenClaw
binary when the config was last written by a newer version, preventing
split-brain installs from stopping or rewriting newer gateway services. Fixes
#57079.
- Logging: redact configured secret patterns at console and file-log sink exits so credentials that reach the logger are masked before terminal display or JSONL persistence. Fixes #67953. Thanks @Ziy1-Tan.
- Gateway/services: refuse process and service mutations from an older OpenClaw binary when the config was last written by a newer version, preventing split-brain installs from stopping or rewriting newer gateway services. Fixes #57079.
- Gateway: reserve `/healthz` and `/readyz` ahead of plugin, canvas, and Control UI HTTP stages so liveness/readiness probes still answer when a later route handler stalls. Fixes #69674. Thanks @Xike-Creek.
- Logging: load `logging.file` and redaction settings directly from the active
OpenClaw config path in bundled runtimes, so packaged gateways stop falling
back to `/tmp/openclaw`. Fixes #59370, #67168, and #61295. Thanks @KeaneYan,
@Pan9hu, and @zsjlovelike.
- Logging: rotate file logs at `logging.maxFileBytes`, keep bounded numbered
archives, and make long-lived rolling loggers follow the current-day file
instead of suppressing diagnostics or writing stale dated files. Fixes #58583
and #62381. Thanks @jpeghead and @zhaoleink.
- Logging: load `logging.file` and redaction settings directly from the active OpenClaw config path in bundled runtimes, so packaged gateways stop falling back to `/tmp/openclaw`. Fixes #59370, #67168, and #61295. Thanks @KeaneYan, @Pan9hu, and @zsjlovelike.
- Logging: rotate file logs at `logging.maxFileBytes`, keep bounded numbered archives, and make long-lived rolling loggers follow the current-day file instead of suppressing diagnostics or writing stale dated files. Fixes #58583 and #62381. Thanks @jpeghead and @zhaoleink.
- Agents/groups: treat clean empty assistant stops as silent `NO_REPLY` only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`,
start browser-capable CLI node services through the restored
`openclaw node start` command, and show an actionable browser-control error
when the local control service is missing. Fixes #66637.
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`, start browser-capable CLI node services through the restored `openclaw node start` command, and show an actionable browser-control error when the local control service is missing. Fixes #66637.
- Gateway/update: fail package updates when the restarted managed gateway reports the wrong version, including fallback restarts and JSON mode, avoiding false-success mixed-version restarts after macOS LaunchAgent updates. Fixes #71835. Thanks @abhinas90 and @jsompis.
- Gateway/update: warn before package updates and bundled plugin runtime-dependency repairs when the target volume appears low on disk space, without blocking installs on best-effort filesystem checks. Fixes #71835. Thanks @abhinas90 and @jsompis.
- Plugins/runtime deps: surface activated plugin load failures in health and fail package-update restart verification or doctor repair when bundled runtime deps still cannot load, avoiding false-success repairs. (#71883) Thanks @Solvely-Colin.
@@ -154,52 +211,25 @@ Docs: https://docs.openclaw.ai
- Plugins: scope setup and web-provider metadata manifest reads to explicit plugin ids when callers already know the owning plugin set. Thanks @vincentkoc.
- Plugins/onboarding: defer onboarding install-record index writes until the guarded config commit so setup failures cannot leave the plugin index ahead of `openclaw.json`. Thanks @shakkernerd.
- Plugins/registry: resolve web provider ownership from the installed plugin index instead of broad manifest scans on secret, tool, and pricing paths. Thanks @shakkernerd.
- Config/providers: accept `video` and `audio` in configured model `input` values and
preserve them in provider catalog entries. Fixes #20721. Thanks @alvinttang.
- Config/providers: accept `video` and `audio` in configured model `input` values and preserve them in provider catalog entries. Fixes #20721. Thanks @alvinttang.
- Models/auth: honor the parent `--agent` flag for auth write commands (`add`, `login`, `setup-token`, `paste-token`, and the GitHub Copilot shortcut) so OAuth/API-key/token results are written to the requested agent store instead of the default agent. Fixes #71864. (#71933) Thanks @balric-seo.
- TTS: strip model-emitted TTS directives from streamed block text before channel
delivery, including directives split across adjacent blocks, while preserving
the accumulated raw reply for final-mode synthesis. Fixes #38937.
- TTS: keep explicit `provider=...` directive keys scoped to that provider and
warn on unsupported keys instead of letting another speech provider consume
overlapping keys. Fixes #60131.
- TTS/Feishu: normalize final-mode streamed TTS-only audio before delivery so
generated voice-note files use the same safe media path and native voice
routing as normal final replies. Fixes #71920.
- Feishu: transcribe inbound voice-note audio with the shared media audio path
before agent dispatch and keep raw Feishu `file_key` payloads out of message
text. Fixes #67120 and #61876.
- TTS: strip model-emitted TTS directives from streamed block text before channel delivery, including directives split across adjacent blocks, while preserving the accumulated raw reply for final-mode synthesis. Fixes #38937.
- TTS: keep explicit `provider=...` directive keys scoped to that provider and warn on unsupported keys instead of letting another speech provider consume overlapping keys. Fixes #60131.
- TTS/Feishu: normalize final-mode streamed TTS-only audio before delivery so generated voice-note files use the same safe media path and native voice routing as normal final replies. Fixes #71920.
- Feishu: transcribe inbound voice-note audio with the shared media audio path before agent dispatch and keep raw Feishu `file_key` payloads out of message text. Fixes #67120 and #61876.
- Tasks: terminalize async Gateway agent task records from the Gateway run result while preserving aborted, failed, and cancelled outcomes instead of leaving completed runs stuck as active or lost. (#71905) Thanks @likewen-tech.
- WhatsApp: let authorized group voice-note transcripts satisfy mention gating
before reply dispatch, while keeping unmentioned transcripts in pending group
history. Fixes #44908.
- Media understanding: carry channel voice-note preflight state into attachment
selection so WhatsApp, Feishu, Telegram, and Discord do not transcribe the
same inbound audio twice. Fixes #70580.
- TTS/BlueBubbles: deliver compatible auto-TTS audio as iMessage voice memo
bubbles instead of plain MP3/CAF file attachments. Fixes #16848.
- TTS: resolve voice-note and voice-memo routing from channel plugin
capabilities instead of speech-core-owned channel id lists.
- ACP: send subagent and async-task completion wakes to external ACP harnesses as
plain prompts instead of OpenClaw internal runtime-context envelopes, while
keeping those envelopes out of ACP transcripts.
- WhatsApp: let authorized group voice-note transcripts satisfy mention gating before reply dispatch, while keeping unmentioned transcripts in pending group history. Fixes #44908.
- Media understanding: carry channel voice-note preflight state into attachment selection so WhatsApp, Feishu, Telegram, and Discord do not transcribe the same inbound audio twice. Fixes #70580.
- TTS/BlueBubbles: deliver compatible auto-TTS audio as iMessage voice memo bubbles instead of plain MP3/CAF file attachments. Fixes #16848.
- TTS: resolve voice-note and voice-memo routing from channel plugin capabilities instead of speech-core-owned channel id lists.
- ACP: send subagent and async-task completion wakes to external ACP harnesses as plain prompts instead of OpenClaw internal runtime-context envelopes, while keeping those envelopes out of ACP transcripts.
- TTS/status: show configured TTS model, voice, and sanitized custom endpoint in `/status`, preserve OpenAI-compatible TTS instructions on custom endpoints, and retry empty Microsoft/Edge TTS output once. Addresses #46602, #47232, and #43936. Thanks @leekuangtao, @Huntterxx, and @rex993.
- Agents/Gateway: steer agent-driven config edits and restarts through the owner-only `gateway` tool, document `config.schema.lookup` as the field-doc source, and warn against using `gateway stop && gateway start` as a restart substitute on macOS. Fixes #71929. Thanks @ygc3817922006-sketch.
- Media understanding/audio: inject a deterministic transcript placeholder for too-small voice notes so agents do not hallucinate transcription or provider failures. Fixes #48944. Thanks @eulicesl.
- Providers/vLLM: send Nemotron 3 chat-template kwargs when thinking is off
and honor configured `params.chat_template_kwargs` for OpenAI-compatible
completions, so vLLM/Nemotron replies stay visible instead of becoming
thinking-only. Fixes #71891. Thanks @jmystaki-create and @dennis-lynch.
- Channels/replies: strip copied inbound metadata blocks from user-facing
assistant replies and model replay history, so Discord/vLLM sessions do not
leak `Conversation info` / `UNTRUSTED ... message body` envelopes after a
model echoes them. Fixes #71847. Thanks @jmystaki-create.
- Subagents/memory: keep inter-session completion wakes out of memory and
dreaming session exports, and strip internal runtime-context blocks from
realtime Control UI chat events.
- Agents/Claude: treat zero-token empty `stop` turns as failed provider output,
retry once, repair replay, and allow configured model fallback instead of
preserving them as successful silent replies. Fixes #71880. Thanks @MagnaAI.
- Providers/vLLM: send Nemotron 3 chat-template kwargs when thinking is off and honor configured `params.chat_template_kwargs` for OpenAI-compatible completions, so vLLM/Nemotron replies stay visible instead of becoming thinking-only. Fixes #71891. Thanks @jmystaki-create and @dennis-lynch.
- Channels/replies: strip copied inbound metadata blocks from user-facing assistant replies and model replay history, so Discord/vLLM sessions do not leak `Conversation info` / `UNTRUSTED ... message body` envelopes after a model echoes them. Fixes #71847. Thanks @jmystaki-create.
- Subagents/memory: keep inter-session completion wakes out of memory and dreaming session exports, and strip internal runtime-context blocks from realtime Control UI chat events.
- Agents/Claude: treat zero-token empty `stop` turns as failed provider output, retry once, repair replay, and allow configured model fallback instead of preserving them as successful silent replies. Fixes #71880. Thanks @MagnaAI.
- Tasks: normalize task lifecycle timestamps at create, update, and restore time, and report retained lost tasks as audit warnings until their cleanup window expires. (#71871) Thanks @likewen-tech.
- Diagnostics/OTEL: treat normal early model stream cleanup as a completed model call instead of exporting a misleading `StreamAbandoned` error span. Thanks @vincentkoc.
- Gateway/pairing: stop corrupt or unreadable device/node pairing stores from being treated as empty state, preserving `paired.json` for repair instead of overwriting approved pairings. Fixes #71873. Thanks @iret77.
@@ -211,13 +241,8 @@ Docs: https://docs.openclaw.ai
- Control UI: hide the chat loading skeleton during background history reloads when existing messages or active stream content are already visible, avoiding reload flashes on high-latency local gateways. Fixes #71844. Thanks @WolvenRA.
- Control UI: keep locally optimistic chat messages visible when a history reload temporarily returns empty, avoiding lost first-turn messages on high-latency gateways. Fixes #71878. Thanks @WolvenRA.
- Control UI: keep chat history limits based on visible messages after filtering heartbeat and control-only transcript rows, so recent hidden entries no longer make older visible replies disappear. Thanks @WolvenRA.
- Agents/images: scrub old `[media attached: ...]`, `[Image: source: ...]`,
and `media://inbound/...` markers from pruned model replay context so stale
media refs are not rehydrated as fresh prompt images. Fixes #71868. Thanks
@jmeadlock.
- Docker/Bonjour: disable Bonjour/mDNS advertising by default for bundled
Compose gateways on bridge networking, while keeping host/macvlan opt-in with
`OPENCLAW_DISABLE_BONJOUR=0`. Fixes #71879. Thanks @gbballpack.
- Agents/images: scrub old `[media attached: ...]`, `[Image: source: ...]`, and `media://inbound/...` markers from pruned model replay context so stale media refs are not rehydrated as fresh prompt images. Fixes #71868. Thanks @jmeadlock.
- Docker/Bonjour: disable Bonjour/mDNS advertising by default for bundled Compose gateways on bridge networking, while keeping host/macvlan opt-in with `OPENCLAW_DISABLE_BONJOUR=0`. Fixes #71879. Thanks @gbballpack.
- CLI/status: label the OpenClaw Serve/Funnel setting as `Tailscale exposure` and show daemon state separately when available, so `gateway.tailscale.mode: "off"` no longer reads like the Tailscale daemon is stopped. Fixes #71790. Thanks @pesvobodak.
- Plugins/Bonjour: stop ciao mDNS watchdog failures from looping forever when the advertiser stays stuck in `probing` or `announcing`; Bonjour now disables itself for the current Gateway process after repeated failed restarts while the Gateway keeps running. Fixes #69011. Thanks @siddharthaagarwalofficial-ux, @FiredMosquito831, and @spikefcz.
- Gateway/Fly.io: seed Control UI allowed origins from the actual runtime bind and port so CLI-driven non-loopback starts do not crash before config exists. Fixes #71823.
@@ -236,9 +261,7 @@ Docs: https://docs.openclaw.ai
- Agents/Codex: keep ACP prompt/skill routing hidden unless an ACP runtime backend is available, and warn in doctor when enabled Codex plugin configs still route `openai-codex/*` models through PI. Thanks @vincentkoc.
- Media delivery: avoid sending generated image attachments twice when the assistant reply already includes explicit `MEDIA:` lines for the same turn, and reject unsafe remote `MEDIA:` URLs before delivery. Thanks @pashpashpash.
- Codex harness: ignore retryable app-server error notifications after Codex recovers, and preserve the real nested error message for terminal app-server failures instead of replacing it with a generic failure. Thanks @pashpashpash.
- Agents/Codex: prepare native Codex sub-agent session metadata without a
nested Gateway session patch and add a focused Docker smoke for the app-server
sub-agent path. Thanks @vincentkoc.
- Agents/Codex: prepare native Codex sub-agent session metadata without a nested Gateway session patch and add a focused Docker smoke for the app-server sub-agent path. Thanks @vincentkoc.
- Agents/subagents: keep queued subagent announces session-only when the requester has no external channel target, avoiding ambiguous multi-channel delivery failures. Fixes #59201. Thanks @larrylhollan.
- Image understanding: preserve configured provider-prefixed vision model metadata when callers request the model without the provider prefix, so custom image models keep their `input: ["text", "image"]` capability. Fixes #33185. Thanks @Kobe9312 and @vincentkoc.
- Plugins/install: restore the previous plugin index records if a concurrent config write conflict interrupts install, update, or uninstall metadata commits. Thanks @shakkernerd.
@@ -252,8 +275,8 @@ Docs: https://docs.openclaw.ai
- Sessions: keep embedded runtime context out of the visible user prompt by sending it as a hidden next-turn custom message, and teach doctor to repair affected 2026.4.24 transcripts with duplicated prompt-rewrite branches. Fixes #71761.
- Gateway/subagents: keep direct-loopback backend RPCs authenticated with the shared gateway token/password off stale CLI paired-device scope baselines, so internal calls no longer hit `scope-upgrade` pairing prompts while remote, browser, node, device-token, and explicit-device paths still require normal pairing approval. Fixes #63548.
- Providers/Azure OpenAI: give deployment-scoped image generation requests a longer 600s default timeout so slow `gpt-image-2` generations can complete without a per-call `timeoutMs`. Fixes #71705. Thanks @voytas75.
- Gateway/plugins: link source-checkout bundled runtime dependency caches instead of recursively copying `node_modules` on the gateway main thread, preventing local status, node, and skill probes from timing out during startup cache restores. Thanks @steipete.
- Skills/remote nodes: only expose remote macOS skill bins for connected nodes, clear stale bin matches when node probes fail, and include probe command, timeout, bin count, and connection state in timeout logs. Thanks @steipete.
- Gateway/plugins: link source-checkout bundled runtime dependency caches instead of recursively copying `node_modules` on the gateway main thread, preventing local status, node, and skill probes from timing out during startup cache restores.
- Skills/remote nodes: only expose remote macOS skill bins for connected nodes, clear stale bin matches when node probes fail, and include probe command, timeout, bin count, and connection state in timeout logs.
- Skills/remote nodes: recognize `system.which` object-map responses when probing connected macOS nodes, so Linux gateways can expose macOS-only skills such as Apple Notes when the required binaries are installed remotely. Fixes #71877. Thanks @miguelarios.
- CLI/gateway: keep diagnostic probes from creating first-time read-only device pairings, while still reusing cached device tokens for detailed read probes. Fixes #71766. Thanks @SunboZ.
- CLI/plugins: keep `message` startup, `channels logs`, `agents delete`, and `agents set-identity` off broad plugin preloading; message delivery still loads plugins when the action actually runs.
@@ -263,12 +286,12 @@ Docs: https://docs.openclaw.ai
- Sessions: clear queued system-event notices during `/new`, `/reset`, gateway `sessions.reset`, and daily/idle rollover so stale background updates cannot leak into the first prompt of the fresh session. Fixes #66864. Thanks @opeyio, @Magicray1217, and @cedillarack.
- CLI/agents: keep `agents bind`, `agents unbind`, and `agents bindings` on setup-safe channel metadata paths so they do not preload bundled plugin runtimes or stage runtime dependencies. Fixes #71743.
- Plugins/registry: preserve explicit disabled plugin records during registry migration without persisting every unused bundled plugin discovered on disk. Thanks @shakkernerd.
- Windows/native: keep CLI startup and bundled provider plugin loading off Windows ESM raw-path failure paths, fixing native onboarding/install smoke on Node 24. Thanks @steipete.
- Windows/native: keep CLI startup and bundled provider plugin loading off Windows ESM raw-path failure paths, fixing native onboarding/install smoke on Node 24.
- Plugins/doctor: read bundled channel doctor capabilities through the same packaged plugin directory resolver used by plugin loading, so published installs keep Matrix DM allowlist repairs on `channels.matrix.dm.*` instead of writing invalid top-level `dmPolicy` keys. Fixes #71757.
- Plugins/Windows: keep bundled plugin Jiti loaders off the native import path on Windows so channel plugins such as Telegram no longer crash with `ERR_UNSUPPORTED_ESM_URL_SCHEME` on `C:\...` paths. Fixes #71749. Thanks @smeyer9.
- Providers/Ollama: use Ollama's current `/api/web_search` endpoint and honor `https://ollama.com` model-provider base URLs for Ollama Web Search. Fixes #71741. Thanks @madhvidua.
- Memory/Ollama: serialize Ollama memory embedding batches and add an inline batch timeout override, with longer defaults for local/self-hosted embedding providers. Thanks @steipete.
- Sessions/usage: exclude compaction checkpoint transcript snapshots from usage totals and session discovery, while keeping old checkpoint files removable. Thanks @steipete.
- Memory/Ollama: serialize Ollama memory embedding batches and add an inline batch timeout override, with longer defaults for local/self-hosted embedding providers.
- Sessions/usage: exclude compaction checkpoint transcript snapshots from usage totals and session discovery, while keeping old checkpoint files removable.
- CLI/agents: keep `openclaw agents list --json` on the config-only path by default, avoiding bundled plugin loading unless callers request `--bindings`. Fixes #71739. Thanks @kaloster.
- Plugins/install: force plugin dependency installs to stay project-local even when inherited npm config requests global installs, so successful installs still materialize the plugin's staged `node_modules`.
- Providers/Google: transcode Gemini TTS PCM to Opus for voice-note targets so WhatsApp and other native voice-note replies can play as voice messages.
@@ -282,16 +305,16 @@ Docs: https://docs.openclaw.ai
- ACP/oneshot: reconcile runtime session identity before closing completed oneshot ACP runs, so finished `sessions.json` entries do not stay stuck with `acp.identity.state="pending"`.
- ACPX: bundle `acpx@0.6.1` so unsupported generic model overrides fail clearly instead of silently falling back to the target adapter default.
- ACP/models: document that non-Codex ACP model overrides require adapter support for ACP `models` plus `session/set_model`, so unsupported harnesses fail clearly instead of silently falling back to their defaults.
- Plugins/Voice Call: treat missing provider credentials as setup-incomplete during Gateway startup and log the missing keys as a warning instead of a runtime startup error, while keeping explicit command/tool errors when used. Thanks @steipete.
- Plugins/Voice Call: treat missing provider credentials as setup-incomplete during Gateway startup and log the missing keys as a warning instead of a runtime startup error, while keeping explicit command/tool errors when used.
- Android/Talk Mode: prevent duplicate TTS playback when fast or repeated final chat events arrive while Talk Mode is waiting for its own response. Fixes #46546.
- Tooling/check:changed: pass parent heavy-check lock markers to lint lanes so `pnpm check:changed` no longer waits on its own `lint:extensions` child. Thanks @steipete.
- Tooling/check:changed: pass parent heavy-check lock markers to lint lanes so `pnpm check:changed` no longer waits on its own `lint:extensions` child.
- CLI/completion: dedupe provider auth flags before registering `openclaw onboard` options, so completion-cache refresh during update no longer fails when stale core fallback flags overlap plugin manifest flags. Fixes #71667.
- Diagnostics/trace: report live context usage from the current prompt snapshot instead of provider turn totals, avoiding false near-full context spikes on cached or tool-heavy runs.
- Providers/Google: honor `models.providers.google.request.allowPrivateNetwork` for Gemini TTS and telephony TTS, matching Google image generation and media understanding. (#71723) Thanks @ro-hansolo.
- Providers/MiniMax: register `minimax-portal` for music and video generation, preserving OAuth auth and regional MiniMax base URLs across the shared `music_generate` and `video_generate` tools. (#63241) Thanks @tars90percent.
- Providers/onboarding: keep Runway and Alibaba Model Studio out of the text-inference setup picker by scoping their video-generation auth choices to the media setup flow. (#65856) Thanks @Jah-yee.
- Plugins/Bonjour: stop the gateway from crash-looping on `CIAO PROBING CANCELLED` when the mDNS watchdog cancels a stuck probe. Restores the rejection-handler wiring dropped during the bonjour plugin migration and shares unhandled-rejection state across module instances so plugin-staged copies of `openclaw/plugin-sdk/runtime` register into the same handler set the host consults. Especially affects Docker on macOS, where mDNS probing reliably hits the watchdog. Thanks @troyhitch.
- Google Meet: report pinned Chrome nodes as offline or missing capabilities in setup/join diagnostics, keep inaccessible nodes out of auto-selection, and preflight local BlackHole/SoX requirements before agents try local Chrome. Thanks @steipete.
- Google Meet: report pinned Chrome nodes as offline or missing capabilities in setup/join diagnostics, keep inaccessible nodes out of auto-selection, and preflight local BlackHole/SoX requirements before agents try local Chrome.
- Providers/MiniMax: route `image-01` requests to the dedicated image generation endpoint while preserving CN endpoint selection. Fixes #61149. Thanks @mushuiyu886.
- Plugins/startup: remove ownerless bundled runtime-dependency install locks after a short grace window and include lock owner details when startup times out waiting for a plugin runtime-deps lock.
- Plugins/install: anchor bundled runtime-dependency npm installs with an OpenClaw-owned package manifest so Linux updates cannot accidentally write to a parent `$HOME/node_modules` tree. Fixes #71730.
@@ -814,16 +837,9 @@ Docs: https://docs.openclaw.ai
### Fixes
- Dependencies: refresh workspace package pins and lockfile entries for AWS SDK,
Anthropic SDK, ACP SDK, Matrix crypto, TypeBox, Vite, tsdown, Slack Bolt,
CopilotKit AIMock, and related bundled plugin packages. Thanks @steipete.
- Gateway/env: import each missing expected login-shell env var independently,
so an existing gateway token no longer prevents `env.shellEnv` from loading
plugin credentials such as `TWILIO_*` from `.profile`. Thanks @steipete.
- macOS/Gateway pairing: silently accept same-host native app
`metadata-upgrade` reconnects, so macOS patch-version changes update paired
metadata instead of spamming security audit warnings and `pairing required`
disconnects. Thanks @steipete.
- Dependencies: refresh workspace package pins and lockfile entries for AWS SDK, Anthropic SDK, ACP SDK, Matrix crypto, TypeBox, Vite, tsdown, Slack Bolt, CopilotKit AIMock, and related bundled plugin packages. Thanks @steipete.
- Gateway/env: import each missing expected login-shell env var independently, so an existing gateway token no longer prevents `env.shellEnv` from loading plugin credentials such as `TWILIO_*` from `.profile`. Thanks @steipete.
- macOS/Gateway pairing: silently accept same-host native app `metadata-upgrade` reconnects, so macOS patch-version changes update paired metadata instead of spamming security audit warnings and `pairing required` disconnects. Thanks @steipete.
- CLI/Gateway: wait for one-shot gateway RPC clients to finish WebSocket teardown before the CLI process exits, reducing hangs where commands like `openclaw status` or `openclaw version` could finish their work but stay alive until an external timeout killed them (#70691). Thanks @Takhoffman.
- Thinking defaults/status: raise the implicit default thinking level for reasoning-capable models from legacy `off`/`low` fallback behavior to a safe provider-supported `medium` equivalent when no explicit config default is set, preserve configured-model reasoning metadata when runtime catalog loading is empty, and make `/status` report the same resolved default as runtime (#70601). Thanks @Takhoffman.
- Gateway/model pricing: extend OpenRouter and LiteLLM catalog fetch timeouts to 60 seconds, reducing noisy timeout warnings during slow upstream responses. Thanks @steipete.
@@ -862,9 +878,7 @@ Docs: https://docs.openclaw.ai
- Config/includes: write through single-file top-level includes for isolated OpenClaw-owned mutations, so `plugins install` and `plugins update` update an included `plugins.json5` file instead of flattening modular `$include` configs. Fixes #41050 and #66048.
- Config/reload: plan gateway reloads from source-authored config instead of runtime-materialized snapshots, so plugin update writes no longer trigger false restarts from derived provider/plugin config paths. Fixes #68732.
- Plugins/update: skip npm plugin reinstall/config rewrites when the installed version and recorded artifact identity already match the registry target, let bare npm package names resolve back to tracked install records, and point already-installed `plugins install` attempts at `plugins update` / `--force` instead of a hook-pack fallback. Fixes #46955, #67957, and #68073.
- Agents/MCP: keep `mcp.servers` and bundle MCP tools available in Pi embedded runs.
`coding` and `messaging` sessions while preserving `minimal` profile and
`tools.deny: ["bundle-mcp"]` opt-out behavior. Fixes #68875 and #68818.
- Agents/MCP: keep `mcp.servers` and bundle MCP tools available in Pi embedded runs. `coding` and `messaging` sessions while preserving `minimal` profile and `tools.deny: ["bundle-mcp"]` opt-out behavior. Fixes #68875 and #68818.
- Plugins/startup: tolerate transient bundled-channel catalog/metadata drift while auto-enabling configured plugins, so CLI and gateway startup no longer crash when a channel id is known but its display metadata is unavailable.
- CLI/Claude: report CLI-backed reply runs as streaming while Claude/Codex CLI turns are still in flight, so WebChat keeps visible response state until the backend finishes. Fixes #70125.
- Slack/streaming: fall back to normal Slack replies for Slack Connect streams rejected before the SDK flushes its local buffer, so short replies no longer disappear or report success before Slack acknowledges delivery. Fixes #70295. (#70370) Thanks @mvanhorn.

View File

@@ -9,22 +9,19 @@
# bundled plugin workspace tree, so the main build layer is not invalidated by
# unrelated plugin source changes.
#
# Two runtime variants:
# Default (bookworm): docker build .
# Slim (bookworm-slim): docker build --build-arg OPENCLAW_VARIANT=slim .
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_VARIANT=default
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_DOCKER_APT_UPGRADE=1
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_DIGEST="sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
# Base images are pinned to SHA256 digests for reproducible builds.
# Trade-off: digests must be updated manually when upstream tags move.
# To update, run: docker buildx imagetools inspect node:24-bookworm (or podman)
# and replace the digest below with the current multi-arch manifest list entry.
# Dependabot refreshes these blessed digests; release builds consume the
# reviewed base snapshot instead of mutating distro state on every build.
# To update, run: docker buildx imagetools inspect node:24-bookworm and
# node:24-bookworm-slim (or podman) and replace the digests below with the
# current multi-arch manifest list entries.
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS ext-deps
ARG OPENCLAW_EXTENSIONS
@@ -125,22 +122,15 @@ RUN printf 'packages:\n - .\n - ui\n' > /tmp/pnpm-workspace.runtime.yaml && \
node scripts/postinstall-bundled-plugins.mjs && \
find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete
# ── Runtime base images ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS base-default
ARG OPENCLAW_NODE_BOOKWORM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_DIGEST}"
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-slim
# ── Runtime base image ─────────────────────────────────────────
FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST
LABEL org.opencontainers.image.base.name="docker.io/library/node:24-bookworm-slim" \
org.opencontainers.image.base.digest="${OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST}"
# ── Stage 3: Runtime ────────────────────────────────────────────
FROM base-${OPENCLAW_VARIANT}
ARG OPENCLAW_VARIANT
FROM base-runtime
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
ARG OPENCLAW_DOCKER_APT_UPGRADE
# OCI base-image metadata for downstream image consumers.
# If you change these annotations, also update:
@@ -155,16 +145,10 @@ LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
WORKDIR /app
# Install system utilities present in bookworm but missing in bookworm-slim.
# On the full bookworm image these are already installed (apt-get is a no-op).
# Smoke workflows can opt out of distro upgrades to cut repeated CI time while
# keeping the default runtime image behavior unchanged.
# Install runtime system utilities missing from bookworm-slim.
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update && \
if [ "${OPENCLAW_DOCKER_APT_UPGRADE}" != "0" ]; then \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends; \
fi && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
procps hostname curl git lsof openssl

View File

@@ -7,7 +7,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \

View File

@@ -7,7 +7,6 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN --mount=type=cache,id=openclaw-sandbox-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \

View File

@@ -24,7 +24,6 @@ ENV PATH=${BUN_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/bin:${BREW_INSTALL_DIR}/sbin
RUN --mount=type=cache,id=openclaw-sandbox-common-apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=openclaw-sandbox-common-apt-lists,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends ${PACKAGES}
RUN if [ "${INSTALL_PNPM}" = "1" ]; then npm install -g pnpm; fi

View File

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

View File

@@ -1,5 +1,9 @@
# OpenClaw iOS Changelog
## 2026.4.26 - 2026-04-26
Maintenance update for the current OpenClaw development release.
## 2026.4.25 - 2026-04-25
Maintenance update for the current OpenClaw development release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.4.25
OPENCLAW_MARKETING_VERSION = 2026.4.25
OPENCLAW_IOS_VERSION = 2026.4.26
OPENCLAW_MARKETING_VERSION = 2026.4.26
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -1,3 +1,3 @@
{
"version": "2026.4.25"
"version": "2026.4.26"
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.4.25</string>
<string>2026.4.26</string>
<key>CFBundleVersion</key>
<string>2026042500</string>
<string>2026042600</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -6,9 +6,19 @@ services:
TERM: xterm-256color
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: ${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}
# Docker bridge networks usually do not carry mDNS multicast reliably.
# Set OPENCLAW_DISABLE_BONJOUR=0 only on host/macvlan/mDNS-capable networks.
OPENCLAW_DISABLE_BONJOUR: ${OPENCLAW_DISABLE_BONJOUR:-1}
# Empty means auto: Bonjour disables itself in detected containers.
# Set 0 only on host/macvlan/mDNS-capable networks; set 1 to force off.
OPENCLAW_DISABLE_BONJOUR: ${OPENCLAW_DISABLE_BONJOUR:-}
# OpenTelemetry export is outbound OTLP/HTTP from the Gateway. Prometheus
# uses the existing authenticated Gateway route; it does not need a port.
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-}
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: ${OTEL_EXPORTER_OTLP_LOGS_ENDPOINT:-}
OTEL_EXPORTER_OTLP_PROTOCOL: ${OTEL_EXPORTER_OTLP_PROTOCOL:-http/protobuf}
OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-}
OTEL_SEMCONV_STABILITY_OPT_IN: ${OTEL_SEMCONV_STABILITY_OPT_IN:-}
OPENCLAW_OTEL_PRELOADED: ${OPENCLAW_OTEL_PRELOADED:-}
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-}
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}

View File

@@ -1,4 +1,4 @@
d8e7866e0c3f633222f75a35defed3c3a03d849f4aa4f70871e3436e80074e76 config-baseline.json
5f5fb87fd46f9cbb84d8af17e00ae3c4b74062e8ad517bc2260ba83da2e9014f config-baseline.core.json
3e6dd8292d9350b0ccc243f81f7b6e95494fc769c01c084d8d6d6e9e1f668a14 config-baseline.json
e040e5818afe66d71fc8a7ae1653f1e8c252cc5b51480ef3b4ae1269682b9ade config-baseline.core.json
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
a5479c182ec987bb21e814b8a4e7b3bda7190ae5c2b35fd5ca403dfa48afa115 config-baseline.plugin.json
74b74cb18ac37c0acaa765f398f1f9edbcee4c43567f02d45c89598a1e13afb4 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
947221d62a0eb0b66250fba2b011ca28a11cb1058bc542b9c155d55479f15935 plugin-sdk-api-baseline.json
0d750f785adbe4d90f209842ed9297476669dd62f7be81fa41e06b6736cc2aaf plugin-sdk-api-baseline.jsonl
21914ef8c5840e0defc36d571834dc28a92d6d5ca2d42a088c33b4de681e836a plugin-sdk-api-baseline.json
3f22e6af0dad3433d25d996802d7436a3cc0e68bc86ecaf813a22e2b4e5333eb plugin-sdk-api-baseline.jsonl

View File

@@ -173,7 +173,7 @@ openclaw hooks enable <hook-name>
### session-memory details
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md`. Requires `workspace.dir` to be configured.
Extracts the last 15 user/assistant messages, generates a descriptive filename slug via LLM, and saves to `<workspace>/memory/YYYY-MM-DD-slug.md` using the host local date. Requires `workspace.dir` to be configured.
<a id="bootstrap-extra-files"></a>

View File

@@ -5,10 +5,12 @@ read_when:
- Debugging multi-agent replies in WhatsApp
status: experimental
title: "Broadcast groups"
sidebarTitle: "Broadcast groups"
---
**Status:** Experimental
**Version:** Added in 2026.1.9
<Note>
**Status:** Experimental. Added in 2026.1.9.
</Note>
## Overview
@@ -18,55 +20,55 @@ Current scope: **WhatsApp only** (web channel).
Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings).
## Use Cases
## Use cases
### 1. Specialized Agent Teams
<AccordionGroup>
<Accordion title="1. Specialized agent teams">
Deploy multiple agents with atomic, focused responsibilities:
Deploy multiple agents with atomic, focused responsibilities:
```
Group: "Development Team"
Agents:
- CodeReviewer (reviews code snippets)
- DocumentationBot (generates docs)
- SecurityAuditor (checks for vulnerabilities)
- TestGenerator (suggests test cases)
```
```
Group: "Development Team"
Agents:
- CodeReviewer (reviews code snippets)
- DocumentationBot (generates docs)
- SecurityAuditor (checks for vulnerabilities)
- TestGenerator (suggests test cases)
```
Each agent processes the same message and provides its specialized perspective.
Each agent processes the same message and provides its specialized perspective.
### 2. Multi-Language Support
```
Group: "International Support"
Agents:
- Agent_EN (responds in English)
- Agent_DE (responds in German)
- Agent_ES (responds in Spanish)
```
### 3. Quality Assurance Workflows
```
Group: "Customer Support"
Agents:
- SupportAgent (provides answer)
- QAAgent (reviews quality, only responds if issues found)
```
### 4. Task Automation
```
Group: "Project Management"
Agents:
- TaskTracker (updates task database)
- TimeLogger (logs time spent)
- ReportGenerator (creates summaries)
```
</Accordion>
<Accordion title="2. Multi-language support">
```
Group: "International Support"
Agents:
- Agent_EN (responds in English)
- Agent_DE (responds in German)
- Agent_ES (responds in Spanish)
```
</Accordion>
<Accordion title="3. Quality assurance workflows">
```
Group: "Customer Support"
Agents:
- SupportAgent (provides answer)
- QAAgent (reviews quality, only responds if issues found)
```
</Accordion>
<Accordion title="4. Task automation">
```
Group: "Project Management"
Agents:
- TaskTracker (updates task database)
- TimeLogger (logs time spent)
- ReportGenerator (creates summaries)
```
</Accordion>
</AccordionGroup>
## Configuration
### Basic Setup
### Basic setup
Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids:
@@ -83,37 +85,40 @@ Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer
**Result:** When OpenClaw would reply in this chat, it will run all three agents.
### Processing Strategy
### Processing strategy
Control how agents process messages:
#### Parallel (Default)
<Tabs>
<Tab title="parallel (default)">
All agents process simultaneously:
All agents process simultaneously:
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
</Tab>
<Tab title="sequential">
Agents process in order (one waits for previous to finish):
#### Sequential
```json
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
Agents process in order (one waits for previous to finish):
</Tab>
</Tabs>
```json
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
### Complete Example
### Complete example
```json
{
@@ -148,22 +153,32 @@ Agents process in order (one waits for previous to finish):
}
```
## How It Works
## How it works
### Message Flow
### Message flow
1. **Incoming message** arrives in a WhatsApp group
2. **Broadcast check**: System checks if peer ID is in `broadcast`
3. **If in broadcast list**:
- All listed agents process the message
- Each agent has its own session key and isolated context
- Agents process in parallel (default) or sequentially
4. **If not in broadcast list**:
- Normal routing applies (first matching binding)
<Steps>
<Step title="Incoming message arrives">
A WhatsApp group or DM message arrives.
</Step>
<Step title="Broadcast check">
System checks if peer ID is in `broadcast`.
</Step>
<Step title="If in broadcast list">
- All listed agents process the message.
- Each agent has its own session key and isolated context.
- Agents process in parallel (default) or sequentially.
</Step>
<Step title="If not in broadcast list">
Normal routing applies (first matching binding).
</Step>
</Steps>
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change _which agents run_ when a message is eligible for processing.
<Note>
Broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change _which agents run_ when a message is eligible for processing.
</Note>
### Session Isolation
### Session isolation
Each agent in a broadcast group maintains completely separate:
@@ -181,92 +196,95 @@ This allows each agent to have:
- Different models (e.g., opus vs. sonnet)
- Different skills installed
### Example: Isolated Sessions
### Example: isolated sessions
In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
**Alfred's context:**
<Tabs>
<Tab title="Alfred's context">
```
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: /Users/user/openclaw-alfred/
Tools: read, write, exec
```
</Tab>
<Tab title="Bärbel's context">
```
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: /Users/user/openclaw-baerbel/
Tools: read only
```
</Tab>
</Tabs>
```
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: /Users/user/openclaw-alfred/
Tools: read, write, exec
```
## Best practices
**Bärbel's context:**
<AccordionGroup>
<Accordion title="1. Keep agents focused">
Design each agent with a single, clear responsibility:
```
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: /Users/user/openclaw-baerbel/
Tools: read only
```
## Best Practices
### 1. Keep Agents Focused
Design each agent with a single, clear responsibility:
```json
{
"broadcast": {
"DEV_GROUP": ["formatter", "linter", "tester"]
}
}
```
**Good:** Each agent has one job
**Bad:** One generic "dev-helper" agent
### 2. Use Descriptive Names
Make it clear what each agent does:
```json
{
"agents": {
"security-scanner": { "name": "Security Scanner" },
"code-formatter": { "name": "Code Formatter" },
"test-generator": { "name": "Test Generator" }
}
}
```
### 3. Configure Different Tool Access
Give agents only the tools they need:
```json
{
"agents": {
"reviewer": {
"tools": { "allow": ["read", "exec"] } // Read-only
},
"fixer": {
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
```json
{
"broadcast": {
"DEV_GROUP": ["formatter", "linter", "tester"]
}
}
}
}
```
```
### 4. Monitor Performance
✅ **Good:** Each agent has one job. ❌ **Bad:** One generic "dev-helper" agent.
With many agents, consider:
</Accordion>
<Accordion title="2. Use descriptive names">
Make it clear what each agent does:
- Using `"strategy": "parallel"` (default) for speed
- Limiting broadcast groups to 5-10 agents
- Using faster models for simpler agents
```json
{
"agents": {
"security-scanner": { "name": "Security Scanner" },
"code-formatter": { "name": "Code Formatter" },
"test-generator": { "name": "Test Generator" }
}
}
```
### 5. Handle Failures Gracefully
</Accordion>
<Accordion title="3. Configure different tool access">
Give agents only the tools they need:
Agents fail independently. One agent's error doesn't block others:
```json
{
"agents": {
"reviewer": {
"tools": { "allow": ["read", "exec"] } // Read-only
},
"fixer": {
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
}
}
}
```
```
Message → [Agent A ✓, Agent B ✗ error, Agent C ✓]
Result: Agent A and C respond, Agent B logs error
```
</Accordion>
<Accordion title="4. Monitor performance">
With many agents, consider:
- Using `"strategy": "parallel"` (default) for speed
- Limiting broadcast groups to 5-10 agents
- Using faster models for simpler agents
</Accordion>
<Accordion title="5. Handle failures gracefully">
Agents fail independently. One agent's error doesn't block others:
```
Message → [Agent A ✓, Agent B ✗ error, Agent C ✓]
Result: Agent A and C respond, Agent B logs error
```
</Accordion>
</AccordionGroup>
## Compatibility
@@ -297,108 +315,116 @@ Broadcast groups work alongside existing routing:
}
```
- `GROUP_A`: Only alfred responds (normal routing)
- `GROUP_B`: agent1 AND agent2 respond (broadcast)
- `GROUP_A`: Only alfred responds (normal routing).
- `GROUP_B`: agent1 AND agent2 respond (broadcast).
<Note>
**Precedence:** `broadcast` takes priority over `bindings`.
</Note>
## Troubleshooting
### Agents Not Responding
<AccordionGroup>
<Accordion title="Agents not responding">
**Check:**
**Check:**
1. Agent IDs exist in `agents.list`.
2. Peer ID format is correct (e.g., `120363403215116621@g.us`).
3. Agents are not in deny lists.
1. Agent IDs exist in `agents.list`
2. Peer ID format is correct (e.g., `120363403215116621@g.us`)
3. Agents are not in deny lists
**Debug:**
**Debug:**
```bash
tail -f ~/.openclaw/logs/gateway.log | grep broadcast
```
```bash
tail -f ~/.openclaw/logs/gateway.log | grep broadcast
```
</Accordion>
<Accordion title="Only one agent responding">
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
### Only One Agent Responding
**Fix:** Add to broadcast config or remove from bindings.
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
</Accordion>
<Accordion title="Performance issues">
If slow with many agents:
**Fix:** Add to broadcast config or remove from bindings.
- Reduce number of agents per group.
- Use lighter models (sonnet instead of opus).
- Check sandbox startup time.
### Performance Issues
**If slow with many agents:**
- Reduce number of agents per group
- Use lighter models (sonnet instead of opus)
- Check sandbox startup time
</Accordion>
</AccordionGroup>
## Examples
### Example 1: Code Review Team
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": [
"code-formatter",
"security-scanner",
"test-coverage",
"docs-checker"
]
},
"agents": {
"list": [
{
"id": "code-formatter",
"workspace": "~/agents/formatter",
"tools": { "allow": ["read", "write"] }
<AccordionGroup>
<Accordion title="Example 1: Code review team">
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": [
"code-formatter",
"security-scanner",
"test-coverage",
"docs-checker"
]
},
{
"id": "security-scanner",
"workspace": "~/agents/security",
"tools": { "allow": ["read", "exec"] }
"agents": {
"list": [
{
"id": "code-formatter",
"workspace": "~/agents/formatter",
"tools": { "allow": ["read", "write"] }
},
{
"id": "security-scanner",
"workspace": "~/agents/security",
"tools": { "allow": ["read", "exec"] }
},
{
"id": "test-coverage",
"workspace": "~/agents/testing",
"tools": { "allow": ["read", "exec"] }
},
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
]
}
}
```
**User sends:** Code snippet.
**Responses:**
- code-formatter: "Fixed indentation and added type hints"
- security-scanner: "⚠️ SQL injection vulnerability in line 12"
- test-coverage: "Coverage is 45%, missing tests for error cases"
- docs-checker: "Missing docstring for function `process_data`"
</Accordion>
<Accordion title="Example 2: Multi-language support">
```json
{
"broadcast": {
"strategy": "sequential",
"+15555550123": ["detect-language", "translator-en", "translator-de"]
},
{
"id": "test-coverage",
"workspace": "~/agents/testing",
"tools": { "allow": ["read", "exec"] }
},
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
]
}
}
```
"agents": {
"list": [
{ "id": "detect-language", "workspace": "~/agents/lang-detect" },
{ "id": "translator-en", "workspace": "~/agents/translate-en" },
{ "id": "translator-de", "workspace": "~/agents/translate-de" }
]
}
}
```
</Accordion>
</AccordionGroup>
**User sends:** Code snippet
**Responses:**
## API reference
- code-formatter: "Fixed indentation and added type hints"
- security-scanner: "⚠️ SQL injection vulnerability in line 12"
- test-coverage: "Coverage is 45%, missing tests for error cases"
- docs-checker: "Missing docstring for function `process_data`"
### Example 2: Multi-Language Support
```json
{
"broadcast": {
"strategy": "sequential",
"+15555550123": ["detect-language", "translator-en", "translator-de"]
},
"agents": {
"list": [
{ "id": "detect-language", "workspace": "~/agents/lang-detect" },
{ "id": "translator-en", "workspace": "~/agents/translate-en" },
{ "id": "translator-de", "workspace": "~/agents/translate-de" }
]
}
}
```
## API Reference
### Config Schema
### Config schema
```typescript
interface OpenClawConfig {
@@ -411,20 +437,21 @@ interface OpenClawConfig {
### Fields
- `strategy` (optional): How to process agents
- `"parallel"` (default): All agents process simultaneously
- `"sequential"`: Agents process in array order
- `[peerId]`: WhatsApp group JID, E.164 number, or other peer ID
- Value: Array of agent IDs that should process messages
<ParamField path="strategy" type='"parallel" | "sequential"' default='"parallel"'>
How to process agents. `parallel` runs all agents simultaneously; `sequential` runs them in array order.
</ParamField>
<ParamField path="[peerId]" type="string[]">
WhatsApp group JID, E.164 number, or other peer ID. Value is the array of agent IDs that should process messages.
</ParamField>
## Limitations
1. **Max agents:** No hard limit, but 10+ agents may be slow
2. **Shared context:** Agents don't see each other's responses (by design)
3. **Message ordering:** Parallel responses may arrive in any order
4. **Rate limits:** All agents count toward WhatsApp rate limits
1. **Max agents:** No hard limit, but 10+ agents may be slow.
2. **Shared context:** Agents don't see each other's responses (by design).
3. **Message ordering:** Parallel responses may arrive in any order.
4. **Rate limits:** All agents count toward WhatsApp rate limits.
## Future Enhancements
## Future enhancements
Planned features:
@@ -435,8 +462,8 @@ Planned features:
## Related
- [Groups](/channels/groups)
- [Channel routing](/channels/channel-routing)
- [Pairing](/channels/pairing)
- [Groups](/channels/groups)
- [Multi-agent sandbox tools](/tools/multi-agent-sandbox-tools)
- [Pairing](/channels/pairing)
- [Session management](/concepts/session)

View File

@@ -3,14 +3,14 @@ summary: "Group chat behavior across surfaces (Discord/iMessage/Matrix/Microsoft
read_when:
- Changing group chat behavior or mention gating
title: "Groups"
sidebarTitle: "Groups"
---
OpenClaw treats group chats consistently across surfaces: Discord, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo.
## Beginner intro (2 minutes)
OpenClaw lives on your own messaging accounts. There is no separate WhatsApp bot user.
If **you** are in a group, OpenClaw can see that group and respond there.
OpenClaw "lives" on your own messaging accounts. There is no separate WhatsApp bot user. If **you** are in a group, OpenClaw can see that group and respond there.
Default behavior:
@@ -19,11 +19,13 @@ Default behavior:
Translation: allowlisted senders can trigger OpenClaw by mentioning it.
> TL;DR
>
> - **DM access** is controlled by `*.allowFrom`.
> - **Group access** is controlled by `*.groupPolicy` + allowlists (`*.groups`, `*.groupAllowFrom`).
> - **Reply triggering** is controlled by mention gating (`requireMention`, `/activation`).
<Note>
**TL;DR**
- **DM access** is controlled by `*.allowFrom`.
- **Group access** is controlled by `*.groupPolicy` + allowlists (`*.groups`, `*.groupAllowFrom`).
- **Reply triggering** is controlled by mention gating (`requireMention`, `/activation`).
</Note>
Quick flow (what happens to a group message):
@@ -43,18 +45,20 @@ Two different controls are involved in group safety:
By default, OpenClaw prioritizes normal chat behavior and keeps context mostly as received. This means allowlists primarily decide who can trigger actions, not a universal redaction boundary for every quoted or historical snippet.
Current behavior is channel-specific:
<AccordionGroup>
<Accordion title="Current behavior is channel-specific">
- Some channels already apply sender-based filtering for supplemental context in specific paths (for example Slack thread seeding, Matrix reply/thread lookups).
- Other channels still pass quote/reply/forward context through as received.
</Accordion>
<Accordion title="Hardening direction (planned)">
- `contextVisibility: "all"` (default) keeps current as-received behavior.
- `contextVisibility: "allowlist"` filters supplemental context to allowlisted senders.
- `contextVisibility: "allowlist_quote"` is `allowlist` plus one explicit quote/reply exception.
- Some channels already apply sender-based filtering for supplemental context in specific paths (for example Slack thread seeding, Matrix reply/thread lookups).
- Other channels still pass quote/reply/forward context through as received.
Until this hardening model is implemented consistently across channels, expect differences by surface.
Hardening direction (planned):
- `contextVisibility: "all"` (default) keeps current as-received behavior.
- `contextVisibility: "allowlist"` filters supplemental context to allowlisted senders.
- `contextVisibility: "allowlist_quote"` is `allowlist` plus one explicit quote/reply exception.
Until this hardening model is implemented consistently across channels, expect differences by surface.
</Accordion>
</AccordionGroup>
![Group message flow](/images/groups-flow.svg)
@@ -78,63 +82,69 @@ If you want...
## Pattern: personal DMs + public groups (single agent)
Yes — this works well if your personal traffic is **DMs** and your public traffic is **groups**.
Yes — this works well if your "personal" traffic is **DMs** and your "public" traffic is **groups**.
Why: in single-agent mode, DMs typically land in the **main** session key (`agent:main:main`), while groups always use **non-main** session keys (`agent:main:<channel>:group:<id>`). If you enable sandboxing with `mode: "non-main"`, those group sessions run in the configured sandbox backend while your main DM session stays on-host. Docker is the default backend if you do not choose one.
This gives you one agent brain (shared workspace + memory), but two execution postures:
This gives you one agent "brain" (shared workspace + memory), but two execution postures:
- **DMs**: full tools (host)
- **Groups**: sandbox + restricted tools
> If you need truly separate workspaces/personas (“personal” and “public” must never mix), use a second agent + bindings. See [Multi-Agent Routing](/concepts/multi-agent).
<Note>
If you need truly separate workspaces/personas ("personal" and "public" must never mix), use a second agent + bindings. See [Multi-Agent Routing](/concepts/multi-agent).
</Note>
Example (DMs on host, groups sandboxed + messaging-only tools):
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // groups/channels are non-main -> sandboxed
scope: "session", // strongest isolation (one container per group/channel)
workspaceAccess: "none",
},
},
},
tools: {
sandbox: {
tools: {
// If allow is non-empty, everything else is blocked (deny still wins).
allow: ["group:messaging", "group:sessions"],
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
},
},
},
}
```
Want “groups can only see folder X” instead of “no host access”? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox:
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main",
scope: "session",
workspaceAccess: "none",
docker: {
binds: [
// hostPath:containerPath:mode
"/home/user/FriendsShared:/data:ro",
],
<Tabs>
<Tab title="DMs on host, groups sandboxed">
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // groups/channels are non-main -> sandboxed
scope: "session", // strongest isolation (one container per group/channel)
workspaceAccess: "none",
},
},
},
},
},
}
```
tools: {
sandbox: {
tools: {
// If allow is non-empty, everything else is blocked (deny still wins).
allow: ["group:messaging", "group:sessions"],
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
},
},
},
}
```
</Tab>
<Tab title="Groups see only an allowlisted folder">
Want "groups can only see folder X" instead of "no host access"? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox:
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main",
scope: "session",
workspaceAccess: "none",
docker: {
binds: [
// hostPath:containerPath:mode
"/home/user/FriendsShared:/data:ro",
],
},
},
},
},
}
```
</Tab>
</Tabs>
Related:
@@ -202,33 +212,40 @@ Control how group/room messages are handled per channel:
| `"disabled"` | Block all group messages entirely. |
| `"allowlist"` | Only allow groups/rooms that match the configured allowlist. |
Notes:
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists.
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
- Slack: allowlist uses `channels.slack.channels`.
- Matrix: allowlist uses `channels.matrix.groups`. Prefer room IDs or aliases; joined-room name lookup is best-effort, and unresolved names are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`.
<AccordionGroup>
<Accordion title="Per-channel notes">
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists.
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
- Slack: allowlist uses `channels.slack.channels`.
- Matrix: allowlist uses `channels.matrix.groups`. Prefer room IDs or aliases; joined-room name lookup is best-effort, and unresolved names are ignored at runtime. Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported.
- Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`).
- Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`.
</Accordion>
</AccordionGroup>
Quick mental model (evaluation order for group messages):
1. `groupPolicy` (open/disabled/allowlist)
2. group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist)
3. mention gating (`requireMention`, `/activation`)
<Steps>
<Step title="groupPolicy">
`groupPolicy` (open/disabled/allowlist).
</Step>
<Step title="Group allowlists">
Group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist).
</Step>
<Step title="Mention gating">
Mention gating (`requireMention`, `/activation`).
</Step>
</Steps>
## Mention gating (default)
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
Replying to a bot message counts as an implicit mention when the channel
supports reply metadata. Quoting a bot message can also count as an implicit
mention on channels that expose quote metadata. Current built-in cases include
Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
Replying to a bot message counts as an implicit mention when the channel supports reply metadata. Quoting a bot message can also count as an implicit mention on channels that expose quote metadata. Current built-in cases include Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
```json5
{
@@ -266,32 +283,41 @@ Telegram, WhatsApp, Slack, Discord, Microsoft Teams, and ZaloUser.
}
```
Notes:
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored.
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Groups where silent replies are allowed treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats still treat empty replies as a failed agent turn.
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
<AccordionGroup>
<Accordion title="Mention gating notes">
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored.
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Groups where silent replies are allowed treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats still treat empty replies as a failed agent turn.
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
</Accordion>
</AccordionGroup>
## Group/channel tool restrictions (optional)
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
- `tools`: allow/deny tools for the whole group.
- `toolsBySender`: per-sender overrides within the group.
Use explicit key prefixes:
`id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard.
Legacy unprefixed keys are still accepted and matched as `id:` only.
- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard. Legacy unprefixed keys are still accepted and matched as `id:` only.
Resolution order (most specific wins):
1. group/channel `toolsBySender` match
2. group/channel `tools`
3. default (`"*"`) `toolsBySender` match
4. default (`"*"`) `tools`
<Steps>
<Step title="Group toolsBySender">
Group/channel `toolsBySender` match.
</Step>
<Step title="Group tools">
Group/channel `tools`.
</Step>
<Step title="Default toolsBySender">
Default (`"*"`) `toolsBySender` match.
</Step>
<Step title="Default tools">
Default (`"*"`) `tools`.
</Step>
</Steps>
Example (Telegram):
@@ -313,68 +339,67 @@ Example (Telegram):
}
```
Notes:
- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins).
- Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, Microsoft Teams `teams.*.channels.*`).
<Note>
Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins). Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, Microsoft Teams `teams.*.channels.*`).
</Note>
## Group allowlists
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
Common confusion: DM pairing approval is not the same as group authorization.
For channels that support DM pairing, the pairing store unlocks DMs only. Group commands still require explicit group sender authorization from config allowlists such as `groupAllowFrom` or the documented config fallback for that channel.
<Warning>
Common confusion: DM pairing approval is not the same as group authorization. For channels that support DM pairing, the pairing store unlocks DMs only. Group commands still require explicit group sender authorization from config allowlists such as `groupAllowFrom` or the documented config fallback for that channel.
</Warning>
Common intents (copy/paste):
1. Disable all group replies
```json5
{
channels: { whatsapp: { groupPolicy: "disabled" } },
}
```
2. Allow only specific groups (WhatsApp)
```json5
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false },
<Tabs>
<Tab title="Disable all group replies">
```json5
{
channels: { whatsapp: { groupPolicy: "disabled" } },
}
```
</Tab>
<Tab title="Allow only specific groups (WhatsApp)">
```json5
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false },
},
},
},
},
},
}
```
3. Allow all groups but require mention (explicit)
```json5
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } },
},
},
}
```
4. Only the owner can trigger in groups (WhatsApp)
```json5
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } },
},
},
}
```
}
```
</Tab>
<Tab title="Allow all groups but require mention">
```json5
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } },
},
},
}
```
</Tab>
<Tab title="Owner-only triggers (WhatsApp)">
```json5
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } },
},
},
}
```
</Tab>
</Tabs>
## Activation (owner-only)
@@ -383,7 +408,7 @@ Group owners can toggle per-group activation:
- `/activation mention`
- `/activation always`
Owner is determined by `channels.whatsapp.allowFrom` (or the bots self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
Owner is determined by `channels.whatsapp.allowFrom` (or the bot's self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
## Context fields
@@ -395,7 +420,7 @@ Group inbound payloads set:
- `WasMentioned` (mention gating result)
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
Channel specific notes:
Channel-specific notes:
- BlueBubbles can optionally enrich unnamed macOS group participants from the local Contacts database before populating `GroupMembers`. This is off by default and only runs after normal group gating passes.
@@ -417,7 +442,7 @@ See [Group messages](/channels/group-messages) for WhatsApp-only behavior (histo
## Related
- [Group messages](/channels/group-messages)
- [Broadcast groups](/channels/broadcast-groups)
- [Channel routing](/channels/channel-routing)
- [Group messages](/channels/group-messages)
- [Pairing](/channels/pairing)

View File

@@ -4,62 +4,68 @@ read_when:
- Setting up Mattermost
- Debugging Mattermost routing
title: "Mattermost"
sidebarTitle: "Mattermost"
---
Status: bundled plugin (bot token + WebSocket events). Channels, groups, and DMs are supported.
Mattermost is a self-hostable team messaging platform; see the official site at
[mattermost.com](https://mattermost.com) for product details and downloads.
Status: bundled plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads.
## Bundled plugin
Mattermost ships as a bundled plugin in current OpenClaw releases, so normal
packaged builds do not need a separate install.
<Note>
Mattermost ships as a bundled plugin in current OpenClaw releases, so normal packaged builds do not need a separate install.
</Note>
If you are on an older build or a custom install that excludes Mattermost,
install it manually:
If you are on an older build or a custom install that excludes Mattermost, install it manually:
Install via CLI (npm registry):
```bash
openclaw plugins install @openclaw/mattermost
```
Local checkout (when running from a git repo):
```bash
openclaw plugins install ./path/to/local/mattermost-plugin
```
<Tabs>
<Tab title="npm registry">
```bash
openclaw plugins install @openclaw/mattermost
```
</Tab>
<Tab title="Local checkout">
```bash
openclaw plugins install ./path/to/local/mattermost-plugin
```
</Tab>
</Tabs>
Details: [Plugins](/tools/plugin)
## Quick setup
1. Ensure the Mattermost plugin is available.
- Current packaged OpenClaw releases already bundle it.
- Older/custom installs can add it manually with the commands above.
2. Create a Mattermost bot account and copy the **bot token**.
3. Copy the Mattermost **base URL** (e.g., `https://chat.example.com`).
4. Configure OpenClaw and start the gateway.
<Steps>
<Step title="Ensure plugin is available">
Current packaged OpenClaw releases already bundle it. Older/custom installs can add it manually with the commands above.
</Step>
<Step title="Create a Mattermost bot">
Create a Mattermost bot account and copy the **bot token**.
</Step>
<Step title="Copy the base URL">
Copy the Mattermost **base URL** (e.g., `https://chat.example.com`).
</Step>
<Step title="Configure OpenClaw and start the gateway">
Minimal config:
Minimal config:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}
```
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}
```
</Step>
</Steps>
## Native slash commands
Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via
the Mattermost API and receives callback POSTs on the gateway HTTP server.
Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via the Mattermost API and receives callback POSTs on the gateway HTTP server.
```json5
{
@@ -77,27 +83,33 @@ the Mattermost API and receives callback POSTs on the gateway HTTP server.
}
```
Notes:
<AccordionGroup>
<Accordion title="Behavior notes">
- `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable.
- If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`.
- For multi-account setups, `commands` can be set at the top level or under `channels.mattermost.accounts.<id>.commands` (account values override top-level fields).
- Command callbacks are validated with the per-command tokens returned by Mattermost when OpenClaw registers `oc_*` commands.
- Slash callbacks fail closed when registration failed, startup was partial, or the callback token does not match one of the registered commands.
</Accordion>
<Accordion title="Reachability requirement">
The callback endpoint must be reachable from the Mattermost server.
- Do not set `callbackUrl` to `localhost` unless Mattermost runs on the same host/network namespace as OpenClaw.
- Do not set `callbackUrl` to your Mattermost base URL unless that URL reverse-proxies `/api/channels/mattermost/command` to OpenClaw.
- A quick check is `curl https://<gateway-host>/api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`.
</Accordion>
<Accordion title="Mattermost egress allowlist">
If your callback targets private/tailnet/internal addresses, set Mattermost `ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
Use host/domain entries, not full URLs.
- `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable.
- If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`.
- For multi-account setups, `commands` can be set at the top level or under
`channels.mattermost.accounts.<id>.commands` (account values override top-level fields).
- Command callbacks are validated with the per-command tokens returned by
Mattermost when OpenClaw registers `oc_*` commands.
- Slash callbacks fail closed when registration failed, startup was partial, or
the callback token does not match one of the registered commands.
- Reachability requirement: the callback endpoint must be reachable from the Mattermost server.
- Do not set `callbackUrl` to `localhost` unless Mattermost runs on the same host/network namespace as OpenClaw.
- Do not set `callbackUrl` to your Mattermost base URL unless that URL reverse-proxies `/api/channels/mattermost/command` to OpenClaw.
- A quick check is `curl https://<gateway-host>/api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`.
- Mattermost egress allowlist requirement:
- If your callback targets private/tailnet/internal addresses, set Mattermost
`ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain.
- Use host/domain entries, not full URLs.
- Good: `gateway.tailnet-name.ts.net`
- Bad: `https://gateway.tailnet-name.ts.net`
</Accordion>
</AccordionGroup>
## Environment variables (default account)
Set these on the gateway host if you prefer env vars:
@@ -105,17 +117,27 @@ Set these on the gateway host if you prefer env vars:
- `MATTERMOST_BOT_TOKEN=...`
- `MATTERMOST_URL=https://chat.example.com`
<Note>
Env vars apply only to the **default** account (`default`). Other accounts must use config values.
`MATTERMOST_URL` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security).
</Note>
## Chat modes
Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`:
- `oncall` (default): respond only when @mentioned in channels.
- `onmessage`: respond to every channel message.
- `onchar`: respond when a message starts with a trigger prefix.
<Tabs>
<Tab title="oncall (default)">
Respond only when @mentioned in channels.
</Tab>
<Tab title="onmessage">
Respond to every channel message.
</Tab>
<Tab title="onchar">
Respond when a message starts with a trigger prefix.
</Tab>
</Tabs>
Config example:
@@ -137,12 +159,10 @@ Notes:
## Threading and sessions
Use `channels.mattermost.replyToMode` to control whether channel and group replies stay in the
main channel or start a thread under the triggering post.
Use `channels.mattermost.replyToMode` to control whether channel and group replies stay in the main channel or start a thread under the triggering post.
- `off` (default): only reply in a thread when the inbound post is already in one.
- `first`: for top-level channel/group posts, start a thread under that post and route the
conversation to a thread-scoped session.
- `first`: for top-level channel/group posts, start a thread under that post and route the conversation to a thread-scoped session.
- `all`: same behavior as `first` for Mattermost today.
- Direct messages ignore this setting and stay non-threaded.
@@ -161,8 +181,7 @@ Config example:
Notes:
- Thread-scoped sessions use the triggering post id as the thread root.
- `first` and `all` are currently equivalent because once Mattermost has a thread root,
follow-up chunks and media continue in that same thread.
- `first` and `all` are currently equivalent because once Mattermost has a thread root, follow-up chunks and media continue in that same thread.
## Access control (DMs)
@@ -176,8 +195,7 @@ Notes:
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs recommended).
- Per-channel mention overrides live under `channels.mattermost.groups.<channelId>.requireMention`
or `channels.mattermost.groups["*"].requireMention` for a default.
- Per-channel mention overrides live under `channels.mattermost.groups.<channelId>.requireMention` or `channels.mattermost.groups["*"].requireMention` for a default.
- `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`.
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
- Runtime note: if `channels.mattermost` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set).
@@ -206,6 +224,7 @@ Use these target formats with `openclaw message send` or cron/webhooks:
- `user:<id>` for a DM
- `@username` for a DM (resolved via the Mattermost API)
<Warning>
Bare opaque IDs (like `64ifufp...`) are **ambiguous** in Mattermost (user ID vs channel ID).
OpenClaw resolves them **user-first**:
@@ -214,14 +233,13 @@ OpenClaw resolves them **user-first**:
- Otherwise the ID is treated as a **channel ID**.
If you need deterministic behavior, always use the explicit prefixes (`user:<id>` / `channel:<id>`).
</Warning>
## DM channel retry
When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it
retries transient direct-channel creation failures by default.
When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it retries transient direct-channel creation failures by default.
Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin,
or `channels.mattermost.accounts.<id>.dmChannelRetry` for one account.
Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin, or `channels.mattermost.accounts.<id>.dmChannelRetry` for one account.
```json5
{
@@ -260,15 +278,19 @@ Enable via `channels.mattermost.streaming`:
}
```
Notes:
- `partial` is the usual choice: one preview post that is edited as the reply grows, then finalized with the complete answer.
- `block` uses append-style draft chunks inside the preview post.
- `progress` shows a status preview while generating and only posts the final answer at completion.
- `off` disables preview streaming.
- If the stream cannot be finalized in place (for example the post was deleted mid-stream), OpenClaw falls back to sending a fresh final post so the reply is never lost.
- Reasoning-only payloads are suppressed from channel posts, including text that arrives as a `> Reasoning:` blockquote. Set `/reasoning on` to see thinking in other surfaces; the Mattermost final post keeps the answer only.
- See [Streaming](/concepts/streaming#preview-streaming-modes) for the channel-mapping matrix.
<AccordionGroup>
<Accordion title="Streaming modes">
- `partial` is the usual choice: one preview post that is edited as the reply grows, then finalized with the complete answer.
- `block` uses append-style draft chunks inside the preview post.
- `progress` shows a status preview while generating and only posts the final answer at completion.
- `off` disables preview streaming.
</Accordion>
<Accordion title="Streaming behavior notes">
- If the stream cannot be finalized in place (for example the post was deleted mid-stream), OpenClaw falls back to sending a fresh final post so the reply is never lost.
- Reasoning-only payloads are suppressed from channel posts, including text that arrives as a `> Reasoning:` blockquote. Set `/reasoning on` to see thinking in other surfaces; the Mattermost final post keeps the answer only.
- See [Streaming](/concepts/streaming#preview-streaming-modes) for the channel-mapping matrix.
</Accordion>
</AccordionGroup>
## Reactions (message tool)
@@ -292,8 +314,7 @@ Config:
## Interactive buttons (message tool)
Send messages with clickable buttons. When a user clicks a button, the agent receives the
selection and can respond.
Send messages with clickable buttons. When a user clicks a button, the agent receives the selection and can respond.
Enable buttons by adding `inlineButtons` to the channel capabilities:
@@ -315,44 +336,46 @@ message action=send channel=mattermost target=channel:<channelId> buttons=[[{"te
Button fields:
- `text` (required): display label.
- `callback_data` (required): value sent back on click (used as the action ID).
- `style` (optional): `"default"`, `"primary"`, or `"danger"`.
<ParamField path="text" type="string" required>
Display label.
</ParamField>
<ParamField path="callback_data" type="string" required>
Value sent back on click (used as the action ID).
</ParamField>
<ParamField path="style" type='"default" | "primary" | "danger"'>
Button style.
</ParamField>
When a user clicks a button:
1. All buttons are replaced with a confirmation line (e.g., "✓ **Yes** selected by @user").
2. The agent receives the selection as an inbound message and responds.
<Steps>
<Step title="Buttons replaced with confirmation">
All buttons are replaced with a confirmation line (e.g., "✓ **Yes** selected by @user").
</Step>
<Step title="Agent receives the selection">
The agent receives the selection as an inbound message and responds.
</Step>
</Steps>
Notes:
- Button callbacks use HMAC-SHA256 verification (automatic, no config needed).
- Mattermost strips callback data from its API responses (security feature), so all buttons
are removed on click — partial removal is not possible.
- Action IDs containing hyphens or underscores are sanitized automatically
(Mattermost routing limitation).
Config:
- `channels.mattermost.capabilities`: array of capability strings. Add `"inlineButtons"` to
enable the buttons tool description in the agent system prompt.
- `channels.mattermost.interactions.callbackBaseUrl`: optional external base URL for button
callbacks (for example `https://gateway.example.com`). Use this when Mattermost cannot
reach the gateway at its bind host directly.
- In multi-account setups, you can also set the same field under
`channels.mattermost.accounts.<id>.interactions.callbackBaseUrl`.
- If `interactions.callbackBaseUrl` is omitted, OpenClaw derives the callback URL from
`gateway.customBindHost` + `gateway.port`, then falls back to `http://localhost:<port>`.
- Reachability rule: the button callback URL must be reachable from the Mattermost server.
`localhost` only works when Mattermost and OpenClaw run on the same host/network namespace.
- If your callback target is private/tailnet/internal, add its host/domain to Mattermost
`ServiceSettings.AllowedUntrustedInternalConnections`.
<AccordionGroup>
<Accordion title="Implementation notes">
- Button callbacks use HMAC-SHA256 verification (automatic, no config needed).
- Mattermost strips callback data from its API responses (security feature), so all buttons are removed on click — partial removal is not possible.
- Action IDs containing hyphens or underscores are sanitized automatically (Mattermost routing limitation).
</Accordion>
<Accordion title="Config and reachability">
- `channels.mattermost.capabilities`: array of capability strings. Add `"inlineButtons"` to enable the buttons tool description in the agent system prompt.
- `channels.mattermost.interactions.callbackBaseUrl`: optional external base URL for button callbacks (for example `https://gateway.example.com`). Use this when Mattermost cannot reach the gateway at its bind host directly.
- In multi-account setups, you can also set the same field under `channels.mattermost.accounts.<id>.interactions.callbackBaseUrl`.
- If `interactions.callbackBaseUrl` is omitted, OpenClaw derives the callback URL from `gateway.customBindHost` + `gateway.port`, then falls back to `http://localhost:<port>`.
- Reachability rule: the button callback URL must be reachable from the Mattermost server. `localhost` only works when Mattermost and OpenClaw run on the same host/network namespace.
- If your callback target is private/tailnet/internal, add its host/domain to Mattermost `ServiceSettings.AllowedUntrustedInternalConnections`.
</Accordion>
</AccordionGroup>
### Direct API integration (external scripts)
External scripts and webhooks can post buttons directly via the Mattermost REST API
instead of going through the agent's `message` tool. Use `buildButtonAttachments()` from
the plugin when possible; if posting raw JSON, follow these rules:
External scripts and webhooks can post buttons directly via the Mattermost REST API instead of going through the agent's `message` tool. Use `buildButtonAttachments()` from the plugin when possible; if posting raw JSON, follow these rules:
**Payload structure:**
@@ -386,29 +409,38 @@ the plugin when possible; if posting raw JSON, follow these rules:
}
```
**Critical rules:**
<Warning>
**Critical rules**
1. Attachments go in `props.attachments`, not top-level `attachments` (silently ignored).
2. Every action needs `type: "button"` — without it, clicks are swallowed silently.
3. Every action needs an `id` field — Mattermost ignores actions without IDs.
4. Action `id` must be **alphanumeric only** (`[a-zA-Z0-9]`). Hyphens and underscores break
Mattermost's server-side action routing (returns 404). Strip them before use.
5. `context.action_id` must match the button's `id` so the confirmation message shows the
button name (e.g., "Approve") instead of a raw ID.
4. Action `id` must be **alphanumeric only** (`[a-zA-Z0-9]`). Hyphens and underscores break Mattermost's server-side action routing (returns 404). Strip them before use.
5. `context.action_id` must match the button's `id` so the confirmation message shows the button name (e.g., "Approve") instead of a raw ID.
6. `context.action_id` is required — the interaction handler returns 400 without it.
</Warning>
**HMAC token generation:**
**HMAC token generation**
The gateway verifies button clicks with HMAC-SHA256. External scripts must generate tokens
that match the gateway's verification logic:
The gateway verifies button clicks with HMAC-SHA256. External scripts must generate tokens that match the gateway's verification logic:
1. Derive the secret from the bot token:
`HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken)`
2. Build the context object with all fields **except** `_token`.
3. Serialize with **sorted keys** and **no spaces** (the gateway uses `JSON.stringify`
with sorted keys, which produces compact output).
4. Sign: `HMAC-SHA256(key=secret, data=serializedContext)`
5. Add the resulting hex digest as `_token` in the context.
<Steps>
<Step title="Derive the secret from the bot token">
`HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken)`
</Step>
<Step title="Build the context object">
Build the context object with all fields **except** `_token`.
</Step>
<Step title="Serialize with sorted keys">
Serialize with **sorted keys** and **no spaces** (the gateway uses `JSON.stringify` with sorted keys, which produces compact output).
</Step>
<Step title="Sign the payload">
`HMAC-SHA256(key=secret, data=serializedContext)`
</Step>
<Step title="Add the token">
Add the resulting hex digest as `_token` in the context.
</Step>
</Steps>
Python example:
@@ -427,22 +459,18 @@ token = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
context = {**ctx, "_token": token}
```
Common HMAC pitfalls:
- Python's `json.dumps` adds spaces by default (`{"key": "val"}`). Use
`separators=(",", ":")` to match JavaScript's compact output (`{"key":"val"}`).
- Always sign **all** context fields (minus `_token`). The gateway strips `_token` then
signs everything remaining. Signing a subset causes silent verification failure.
- Use `sort_keys=True` — the gateway sorts keys before signing, and Mattermost may
reorder context fields when storing the payload.
- Derive the secret from the bot token (deterministic), not random bytes. The secret
must be the same across the process that creates buttons and the gateway that verifies.
<AccordionGroup>
<Accordion title="Common HMAC pitfalls">
- Python's `json.dumps` adds spaces by default (`{"key": "val"}`). Use `separators=(",", ":")` to match JavaScript's compact output (`{"key":"val"}`).
- Always sign **all** context fields (minus `_token`). The gateway strips `_token` then signs everything remaining. Signing a subset causes silent verification failure.
- Use `sort_keys=True` — the gateway sorts keys before signing, and Mattermost may reorder context fields when storing the payload.
- Derive the secret from the bot token (deterministic), not random bytes. The secret must be the same across the process that creates buttons and the gateway that verifies.
</Accordion>
</AccordionGroup>
## Directory adapter
The Mattermost plugin includes a directory adapter that resolves channel and user names
via the Mattermost API. This enables `#channel-name` and `@username` targets in
`openclaw message send` and cron/webhook deliveries.
The Mattermost plugin includes a directory adapter that resolves channel and user names via the Mattermost API. This enables `#channel-name` and `@username` targets in `openclaw message send` and cron/webhook deliveries.
No configuration is needed — the adapter uses the bot token from the account config.
@@ -465,34 +493,38 @@ Mattermost supports multiple accounts under `channels.mattermost.accounts`:
## Troubleshooting
- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
- Auth errors: check the bot token, base URL, and whether the account is enabled.
- Multi-account issues: env vars only apply to the `default` account.
- Native slash commands return `Unauthorized: invalid command token.`: OpenClaw
did not accept the callback token. Typical causes:
- slash command registration failed or only partially completed at startup
- the callback is hitting the wrong gateway/account
- Mattermost still has old commands pointing at a previous callback target
- the gateway restarted without reactivating slash commands
- If native slash commands stop working, check logs for
`mattermost: failed to register slash commands` or
`mattermost: native slash commands enabled but no commands could be registered`.
- If `callbackUrl` is omitted and logs warn that the callback resolved to
`http://127.0.0.1:18789/...`, that URL is probably only reachable when
Mattermost runs on the same host/network namespace as OpenClaw. Set an
explicit externally reachable `commands.callbackUrl` instead.
- Buttons appear as white boxes: the agent may be sending malformed button data. Check that each button has both `text` and `callback_data` fields.
- Buttons render but clicks do nothing: verify `AllowedUntrustedInternalConnections` in Mattermost server config includes `127.0.0.1 localhost`, and that `EnablePostActionIntegration` is `true` in ServiceSettings.
- Buttons return 404 on click: the button `id` likely contains hyphens or underscores. Mattermost's action router breaks on non-alphanumeric IDs. Use `[a-zA-Z0-9]` only.
- Gateway logs `invalid _token`: HMAC mismatch. Check that you sign all context fields (not a subset), use sorted keys, and use compact JSON (no spaces). See the HMAC section above.
- Gateway logs `missing _token in context`: the `_token` field is not in the button's context. Ensure it is included when building the integration payload.
- Confirmation shows raw ID instead of button name: `context.action_id` does not match the button's `id`. Set both to the same sanitized value.
- Agent doesn't know about buttons: add `capabilities: ["inlineButtons"]` to the Mattermost channel config.
<AccordionGroup>
<Accordion title="No replies in channels">
Ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
</Accordion>
<Accordion title="Auth or multi-account errors">
- Check the bot token, base URL, and whether the account is enabled.
- Multi-account issues: env vars only apply to the `default` account.
</Accordion>
<Accordion title="Native slash commands fail">
- `Unauthorized: invalid command token.`: OpenClaw did not accept the callback token. Typical causes:
- slash command registration failed or only partially completed at startup
- the callback is hitting the wrong gateway/account
- Mattermost still has old commands pointing at a previous callback target
- the gateway restarted without reactivating slash commands
- If native slash commands stop working, check logs for `mattermost: failed to register slash commands` or `mattermost: native slash commands enabled but no commands could be registered`.
- If `callbackUrl` is omitted and logs warn that the callback resolved to `http://127.0.0.1:18789/...`, that URL is probably only reachable when Mattermost runs on the same host/network namespace as OpenClaw. Set an explicit externally reachable `commands.callbackUrl` instead.
</Accordion>
<Accordion title="Buttons issues">
- Buttons appear as white boxes: the agent may be sending malformed button data. Check that each button has both `text` and `callback_data` fields.
- Buttons render but clicks do nothing: verify `AllowedUntrustedInternalConnections` in Mattermost server config includes `127.0.0.1 localhost`, and that `EnablePostActionIntegration` is `true` in ServiceSettings.
- Buttons return 404 on click: the button `id` likely contains hyphens or underscores. Mattermost's action router breaks on non-alphanumeric IDs. Use `[a-zA-Z0-9]` only.
- Gateway logs `invalid _token`: HMAC mismatch. Check that you sign all context fields (not a subset), use sorted keys, and use compact JSON (no spaces). See the HMAC section above.
- Gateway logs `missing _token in context`: the `_token` field is not in the button's context. Ensure it is included when building the integration payload.
- Confirmation shows raw ID instead of button name: `context.action_id` does not match the button's `id`. Set both to the same sanitized value.
- Agent doesn't know about buttons: add `capabilities: ["inlineButtons"]` to the Mattermost channel config.
</Accordion>
</AccordionGroup>
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Channels Overview](/channels) — all supported channels
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Security](/gateway/security) — access model and hardening

View File

@@ -298,8 +298,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
For text-only replies:
- DM: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
- group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
- short DM/group/topic previews: OpenClaw keeps the same preview message and performs a final edit in place
- previews older than about one minute: OpenClaw sends the completed reply as a fresh final message and then cleans up the preview, so Telegram's visible timestamp reflects completion time instead of the preview creation time
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.

View File

@@ -3,50 +3,69 @@ summary: "Twitch chat bot configuration and setup"
read_when:
- Setting up Twitch chat integration for OpenClaw
title: "Twitch"
sidebarTitle: "Twitch"
---
Twitch chat support via IRC connection. OpenClaw connects as a Twitch user (bot account) to receive and send messages in channels.
## Bundled plugin
Twitch ships as a bundled plugin in current OpenClaw releases, so normal
packaged builds do not need a separate install.
<Note>
Twitch ships as a bundled plugin in current OpenClaw releases, so normal packaged builds do not need a separate install.
</Note>
If you are on an older build or a custom install that excludes Twitch, install
it manually:
If you are on an older build or a custom install that excludes Twitch, install it manually:
Install via CLI (npm registry):
```bash
openclaw plugins install @openclaw/twitch
```
Local checkout (when running from a git repo):
```bash
openclaw plugins install ./path/to/local/twitch-plugin
```
<Tabs>
<Tab title="npm registry">
```bash
openclaw plugins install @openclaw/twitch
```
</Tab>
<Tab title="Local checkout">
```bash
openclaw plugins install ./path/to/local/twitch-plugin
```
</Tab>
</Tabs>
Details: [Plugins](/tools/plugin)
## Quick setup (beginner)
1. Ensure the Twitch plugin is available.
- Current packaged OpenClaw releases already bundle it.
- Older/custom installs can add it manually with the commands above.
2. Create a dedicated Twitch account for the bot (or use an existing account).
3. Generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/)
- Select **Bot Token**
- Verify scopes `chat:read` and `chat:write` are selected
- Copy the **Client ID** and **Access Token**
4. Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/)
5. Configure the token:
- Env: `OPENCLAW_TWITCH_ACCESS_TOKEN=...` (default account only)
- Or config: `channels.twitch.accessToken`
- If both are set, config takes precedence (env fallback is default-account only).
6. Start the gateway.
<Steps>
<Step title="Ensure plugin is available">
Current packaged OpenClaw releases already bundle it. Older/custom installs can add it manually with the commands above.
</Step>
<Step title="Create a Twitch bot account">
Create a dedicated Twitch account for the bot (or use an existing account).
</Step>
<Step title="Generate credentials">
Use [Twitch Token Generator](https://twitchtokengenerator.com/):
**⚠️ Important:** Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`.
- Select **Bot Token**
- Verify scopes `chat:read` and `chat:write` are selected
- Copy the **Client ID** and **Access Token**
</Step>
<Step title="Find your Twitch user ID">
Use [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) to convert a username to a Twitch user ID.
</Step>
<Step title="Configure the token">
- Env: `OPENCLAW_TWITCH_ACCESS_TOKEN=...` (default account only)
- Or config: `channels.twitch.accessToken`
If both are set, config takes precedence (env fallback is default-account only).
</Step>
<Step title="Start the gateway">
Start the gateway with the configured channel.
</Step>
</Steps>
<Warning>
Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`.
</Warning>
Minimal config:
@@ -82,31 +101,34 @@ Use [Twitch Token Generator](https://twitchtokengenerator.com/):
- Verify scopes `chat:read` and `chat:write` are selected
- Copy the **Client ID** and **Access Token**
<Note>
No manual app registration needed. Tokens expire after several hours.
</Note>
### Configure the bot
**Env var (default account only):**
```bash
OPENCLAW_TWITCH_ACCESS_TOKEN=oauth:abc123...
```
**Or config:**
```json5
{
channels: {
twitch: {
enabled: true,
username: "openclaw",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "vevisk",
},
},
}
```
<Tabs>
<Tab title="Env var (default account only)">
```bash
OPENCLAW_TWITCH_ACCESS_TOKEN=oauth:abc123...
```
</Tab>
<Tab title="Config">
```json5
{
channels: {
twitch: {
enabled: true,
username: "openclaw",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "vevisk",
},
},
}
```
</Tab>
</Tabs>
If both env and config are set, config takes precedence.
@@ -126,9 +148,11 @@ Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want
**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`.
<Note>
**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) (Convert your Twitch username to ID)
</Note>
## Token refresh (optional)
@@ -151,7 +175,7 @@ The bot automatically refreshes tokens before expiration and logs refresh events
## Multi-account support
Use `channels.twitch.accounts` with per-account tokens. See [`gateway/configuration`](/gateway/configuration) for the shared pattern.
Use `channels.twitch.accounts` with per-account tokens. See [Configuration](/gateway/configuration) for the shared pattern.
Example (one bot account in two channels):
@@ -178,78 +202,65 @@ Example (one bot account in two channels):
}
```
**Note:** Each account needs its own token (one token per channel).
<Note>
Each account needs its own token (one token per channel).
</Note>
## Access control
### Role-based restrictions
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowedRoles: ["moderator", "vip"],
<Tabs>
<Tab title="User ID allowlist (most secure)">
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowFrom: ["123456789", "987654321"],
},
},
},
},
},
},
}
```
### Allowlist by User ID (most secure)
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowFrom: ["123456789", "987654321"],
}
```
</Tab>
<Tab title="Role-based">
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowedRoles: ["moderator", "vip"],
},
},
},
},
},
},
}
```
}
```
### Role-based access (alternative)
`allowFrom` is a hard allowlist. When set, only those user IDs are allowed. If you want role-based access, leave `allowFrom` unset and configure `allowedRoles` instead.
`allowFrom` is a hard allowlist. When set, only those user IDs are allowed.
If you want role-based access, leave `allowFrom` unset and configure `allowedRoles` instead:
</Tab>
<Tab title="Disable @mention requirement">
By default, `requireMention` is `true`. To disable and respond to all messages:
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowedRoles: ["moderator"],
```json5
{
channels: {
twitch: {
accounts: {
default: {
requireMention: false,
},
},
},
},
},
},
}
```
}
```
### Disable @mention requirement
By default, `requireMention` is `true`. To disable and respond to all messages:
```json5
{
channels: {
twitch: {
accounts: {
default: {
requireMention: false,
},
},
},
},
}
```
</Tab>
</Tabs>
## Troubleshooting
@@ -260,53 +271,77 @@ openclaw doctor
openclaw channels status --probe
```
### Bot does not respond to messages
<AccordionGroup>
<Accordion title="Bot does not respond to messages">
- **Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove `allowFrom` and set `allowedRoles: ["all"]` to test.
- **Check the bot is in the channel:** The bot must join the channel specified in `channel`.
</Accordion>
<Accordion title="Token issues">
"Failed to connect" or authentication errors:
**Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove
`allowFrom` and set `allowedRoles: ["all"]` to test.
- Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix)
- Check token has `chat:read` and `chat:write` scopes
- If using token refresh, verify `clientSecret` and `refreshToken` are set
**Check the bot is in the channel:** The bot must join the channel specified in `channel`.
</Accordion>
<Accordion title="Token refresh not working">
Check logs for refresh events:
### Token issues
```
Using env token source for mybot
Access token refreshed for user 123456 (expires in 14400s)
```
**"Failed to connect" or authentication errors:**
If you see "token refresh disabled (no refresh token)":
- Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix)
- Check token has `chat:read` and `chat:write` scopes
- If using token refresh, verify `clientSecret` and `refreshToken` are set
- Ensure `clientSecret` is provided
- Ensure `refreshToken` is provided
### Token refresh not working
**Check logs for refresh events:**
```
Using env token source for mybot
Access token refreshed for user 123456 (expires in 14400s)
```
If you see "token refresh disabled (no refresh token)":
- Ensure `clientSecret` is provided
- Ensure `refreshToken` is provided
</Accordion>
</AccordionGroup>
## Config
**Account config:**
### Account config
- `username` - Bot username
- `accessToken` - OAuth access token with `chat:read` and `chat:write`
- `clientId` - Twitch Client ID (from Token Generator or your app)
- `channel` - Channel to join (required)
- `enabled` - Enable this account (default: `true`)
- `clientSecret` - Optional: For automatic token refresh
- `refreshToken` - Optional: For automatic token refresh
- `expiresIn` - Token expiry in seconds
- `obtainmentTimestamp` - Token obtained timestamp
- `allowFrom` - User ID allowlist
- `allowedRoles` - Role-based access control (`"moderator" | "owner" | "vip" | "subscriber" | "all"`)
- `requireMention` - Require @mention (default: `true`)
<ParamField path="username" type="string">
Bot username.
</ParamField>
<ParamField path="accessToken" type="string">
OAuth access token with `chat:read` and `chat:write`.
</ParamField>
<ParamField path="clientId" type="string">
Twitch Client ID (from Token Generator or your app).
</ParamField>
<ParamField path="channel" type="string" required>
Channel to join.
</ParamField>
<ParamField path="enabled" type="boolean" default="true">
Enable this account.
</ParamField>
<ParamField path="clientSecret" type="string">
Optional: for automatic token refresh.
</ParamField>
<ParamField path="refreshToken" type="string">
Optional: for automatic token refresh.
</ParamField>
<ParamField path="expiresIn" type="number">
Token expiry in seconds.
</ParamField>
<ParamField path="obtainmentTimestamp" type="number">
Token obtained timestamp.
</ParamField>
<ParamField path="allowFrom" type="string[]">
User ID allowlist.
</ParamField>
<ParamField path="allowedRoles" type='Array<"moderator" | "owner" | "vip" | "subscriber" | "all">'>
Role-based access control.
</ParamField>
<ParamField path="requireMention" type="boolean" default="true">
Require @mention.
</ParamField>
**Provider options:**
### Provider options
- `channels.twitch.enabled` - Enable/disable channel startup
- `channels.twitch.username` - Bot username (simplified single-account config)
@@ -368,25 +403,25 @@ Example:
}
```
## Safety & ops
## Safety and ops
- **Treat tokens like passwords** - Never commit tokens to git
- **Use automatic token refresh** for long-running bots
- **Use user ID allowlists** instead of usernames for access control
- **Monitor logs** for token refresh events and connection status
- **Scope tokens minimally** - Only request `chat:read` and `chat:write`
- **If stuck**: Restart the gateway after confirming no other process owns the session
- **Treat tokens like passwords** Never commit tokens to git.
- **Use automatic token refresh** for long-running bots.
- **Use user ID allowlists** instead of usernames for access control.
- **Monitor logs** for token refresh events and connection status.
- **Scope tokens minimally** Only request `chat:read` and `chat:write`.
- **If stuck**: Restart the gateway after confirming no other process owns the session.
## Limits
- **500 characters** per message (auto-chunked at word boundaries)
- Markdown is stripped before chunking
- No rate limiting (uses Twitch's built-in rate limits)
- **500 characters** per message (auto-chunked at word boundaries).
- Markdown is stripped before chunking.
- No rate limiting (uses Twitch's built-in rate limits).
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Channels Overview](/channels) — all supported channels
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Security](/gateway/security) — access model and hardening

View File

@@ -146,6 +146,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
## Runtime model
- Gateway owns the WhatsApp socket and reconnect loop.
- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window.
- Outbound sends require an active WhatsApp listener for the target account.
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
@@ -510,6 +511,10 @@ Behavior notes:
<Accordion title="Linked but disconnected / reconnect loop">
Symptom: linked account with repeated disconnects or reconnect attempts.
Quiet accounts can stay connected past the normal message timeout; the watchdog
restarts when WhatsApp Web transport activity stops, the socket closes, or
application-level activity stays silent beyond the longer safety window.
Fix:
```bash

File diff suppressed because one or more lines are too long

View File

@@ -59,6 +59,8 @@ Common non-interactive add surfaces include:
- Tlon fields: `--ship`, `--url`, `--code`, `--group-channels`, `--dm-allowlist`, `--auto-discover-channels`
- `--use-env` for default-account env-backed auth where supported
If a channel plugin needs to be installed during a flag-driven add command, OpenClaw uses the channel's default install source without opening the interactive plugin install prompt.
When you run `openclaw channels add` without flags, the interactive wizard can prompt:
- account ids per selected channel

View File

@@ -3,29 +3,18 @@ summary: "CLI reference for `openclaw config` (get/set/unset/file/schema/validat
read_when:
- You want to read or edit config non-interactively
title: "Config"
sidebarTitle: "Config"
---
# `openclaw config`
Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/file/schema/validate values by path and print the active config file. Run without a subcommand to open the configure wizard (same as `openclaw configure`).
Config helpers for non-interactive edits in `openclaw.json`: get/set/unset/file/schema/validate
values by path and print the active config file. Run without a subcommand to
open the configure wizard (same as `openclaw configure`).
## Root options
Root options:
<ParamField path="--section <section>" type="string">
Repeatable guided-setup section filter when you run `openclaw config` without a subcommand.
</ParamField>
- `--section <section>`: repeatable guided-setup section filter when you run `openclaw config` without a subcommand
Supported guided sections:
- `workspace`
- `model`
- `web`
- `gateway`
- `daemon`
- `channels`
- `plugins`
- `skills`
- `health`
Supported guided sections: `workspace`, `model`, `web`, `gateway`, `daemon`, `channels`, `plugins`, `skills`, `health`.
## Examples
@@ -52,21 +41,19 @@ openclaw config validate --json
Print the generated JSON schema for `openclaw.json` to stdout as JSON.
What it includes:
- The current root config schema, plus a root `$schema` string field for editor tooling
- Field `title` and `description` docs metadata used by the Control UI
- Nested object, wildcard (`*`), and array-item (`[]`) nodes inherit the same `title` / `description` metadata when matching field documentation exists
- `anyOf` / `oneOf` / `allOf` branches inherit the same docs metadata too when matching field documentation exists
- Best-effort live plugin + channel schema metadata when runtime manifests can be loaded
- A clean fallback schema even when the current config is invalid
Related runtime RPC:
- `config.schema.lookup` returns one normalized config path with a shallow
schema node (`title`, `description`, `type`, `enum`, `const`, common bounds),
matched UI hint metadata, and immediate child summaries. Use it for
path-scoped drill-down in Control UI or custom clients.
<AccordionGroup>
<Accordion title="What it includes">
- The current root config schema, plus a root `$schema` string field for editor tooling.
- Field `title` and `description` docs metadata used by the Control UI.
- Nested object, wildcard (`*`), and array-item (`[]`) nodes inherit the same `title` / `description` metadata when matching field documentation exists.
- `anyOf` / `oneOf` / `allOf` branches inherit the same docs metadata too when matching field documentation exists.
- Best-effort live plugin + channel schema metadata when runtime manifests can be loaded.
- A clean fallback schema even when the current config is invalid.
</Accordion>
<Accordion title="Related runtime RPC">
`config.schema.lookup` returns one normalized config path with a shallow schema node (`title`, `description`, `type`, `enum`, `const`, common bounds), matched UI hint metadata, and immediate child summaries. Use it for path-scoped drill-down in Control UI or custom clients.
</Accordion>
</AccordionGroup>
```bash
openclaw config schema
@@ -96,8 +83,7 @@ openclaw config set agents.list[1].tools.exec.node "node-id-or-name"
## Values
Values are parsed as JSON5 when possible; otherwise they are treated as strings.
Use `--strict-json` to require JSON5 parsing. `--json` remains supported as a legacy alias.
Values are parsed as JSON5 when possible; otherwise they are treated as strings. Use `--strict-json` to require JSON5 parsing. `--json` remains supported as a legacy alias.
```bash
openclaw config set agents.defaults.heartbeat.every "0m"
@@ -107,11 +93,9 @@ openclaw config set channels.whatsapp.groups '["*"]' --strict-json
`config get <path> --json` prints the raw value as JSON instead of terminal-formatted text.
Object assignment replaces the target path by default. Protected map/list paths
that commonly hold user-added entries, such as `agents.defaults.models`,
`models.providers`, `models.providers.<id>.models`, `plugins.entries`, and
`auth.profiles`, refuse replacements that would remove existing entries unless
you pass `--replace`.
<Note>
Object assignment replaces the target path by default. Protected map/list paths that commonly hold user-added entries, such as `agents.defaults.models`, `models.providers`, `models.providers.<id>.models`, `plugins.entries`, and `auth.profiles`, refuse replacements that would remove existing entries unless you pass `--replace`.
</Note>
Use `--merge` when adding entries to those maps:
@@ -120,59 +104,65 @@ openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json
openclaw config set models.providers.ollama.models '[{"id":"llama3.2","name":"Llama 3.2"}]' --strict-json --merge
```
Use `--replace` only when you intentionally want the provided value to become
the complete target value.
Use `--replace` only when you intentionally want the provided value to become the complete target value.
## `config set` modes
`openclaw config set` supports four assignment styles:
1. Value mode: `openclaw config set <path> <value>`
2. SecretRef builder mode:
<Tabs>
<Tab title="Value mode">
```bash
openclaw config set <path> <value>
```
</Tab>
<Tab title="SecretRef builder mode">
```bash
openclaw config set channels.discord.token \
--ref-provider default \
--ref-source env \
--ref-id DISCORD_BOT_TOKEN
```
</Tab>
<Tab title="Provider builder mode">
Provider builder mode targets `secrets.providers.<alias>` paths only:
```bash
openclaw config set channels.discord.token \
--ref-provider default \
--ref-source env \
--ref-id DISCORD_BOT_TOKEN
```
```bash
openclaw config set secrets.providers.vault \
--provider-source exec \
--provider-command /usr/local/bin/openclaw-vault \
--provider-arg read \
--provider-arg openai/api-key \
--provider-timeout-ms 5000
```
3. Provider builder mode (`secrets.providers.<alias>` path only):
</Tab>
<Tab title="Batch mode">
```bash
openclaw config set --batch-json '[
{
"path": "secrets.providers.default",
"provider": { "source": "env" }
},
{
"path": "channels.discord.token",
"ref": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" }
}
]'
```
```bash
openclaw config set secrets.providers.vault \
--provider-source exec \
--provider-command /usr/local/bin/openclaw-vault \
--provider-arg read \
--provider-arg openai/api-key \
--provider-timeout-ms 5000
```
```bash
openclaw config set --batch-file ./config-set.batch.json --dry-run
```
4. Batch mode (`--batch-json` or `--batch-file`):
</Tab>
</Tabs>
```bash
openclaw config set --batch-json '[
{
"path": "secrets.providers.default",
"provider": { "source": "env" }
},
{
"path": "channels.discord.token",
"ref": { "source": "env", "provider": "default", "id": "DISCORD_BOT_TOKEN" }
}
]'
```
<Warning>
SecretRef assignments are rejected on unsupported runtime-mutable surfaces (for example `hooks.token`, `commands.ownerDisplaySecret`, Discord thread-binding webhook tokens, and WhatsApp creds JSON). See [SecretRef Credential Surface](/reference/secretref-credential-surface).
</Warning>
```bash
openclaw config set --batch-file ./config-set.batch.json --dry-run
```
Policy note:
- SecretRef assignments are rejected on unsupported runtime-mutable surfaces (for example `hooks.token`, `commands.ownerDisplaySecret`, Discord thread-binding webhook tokens, and WhatsApp creds JSON). See [SecretRef Credential Surface](/reference/secretref-credential-surface).
Batch parsing always uses the batch payload (`--batch-json`/`--batch-file`) as the source of truth.
`--strict-json` / `--json` do not change batch parsing behavior.
Batch parsing always uses the batch payload (`--batch-json`/`--batch-file`) as the source of truth. `--strict-json` / `--json` do not change batch parsing behavior.
JSON path/value mode remains supported for both SecretRefs and providers:
@@ -190,34 +180,33 @@ openclaw config set secrets.providers.vaultfile \
Provider builder targets must use `secrets.providers.<alias>` as the path.
Common flags:
- `--provider-source <env|file|exec>`
- `--provider-timeout-ms <ms>` (`file`, `exec`)
Env provider (`--provider-source env`):
- `--provider-allowlist <ENV_VAR>` (repeatable)
File provider (`--provider-source file`):
- `--provider-path <path>` (required)
- `--provider-mode <singleValue|json>`
- `--provider-max-bytes <bytes>`
- `--provider-allow-insecure-path`
Exec provider (`--provider-source exec`):
- `--provider-command <path>` (required)
- `--provider-arg <arg>` (repeatable)
- `--provider-no-output-timeout-ms <ms>`
- `--provider-max-output-bytes <bytes>`
- `--provider-json-only`
- `--provider-env <KEY=VALUE>` (repeatable)
- `--provider-pass-env <ENV_VAR>` (repeatable)
- `--provider-trusted-dir <path>` (repeatable)
- `--provider-allow-insecure-path`
- `--provider-allow-symlink-command`
<AccordionGroup>
<Accordion title="Common flags">
- `--provider-source <env|file|exec>`
- `--provider-timeout-ms <ms>` (`file`, `exec`)
</Accordion>
<Accordion title="Env provider (--provider-source env)">
- `--provider-allowlist <ENV_VAR>` (repeatable)
</Accordion>
<Accordion title="File provider (--provider-source file)">
- `--provider-path <path>` (required)
- `--provider-mode <singleValue|json>`
- `--provider-max-bytes <bytes>`
- `--provider-allow-insecure-path`
</Accordion>
<Accordion title="Exec provider (--provider-source exec)">
- `--provider-command <path>` (required)
- `--provider-arg <arg>` (repeatable)
- `--provider-no-output-timeout-ms <ms>`
- `--provider-max-output-bytes <bytes>`
- `--provider-json-only`
- `--provider-env <KEY=VALUE>` (repeatable)
- `--provider-pass-env <ENV_VAR>` (repeatable)
- `--provider-trusted-dir <path>` (repeatable)
- `--provider-allow-insecure-path`
- `--provider-allow-symlink-command`
</Accordion>
</AccordionGroup>
Hardened exec provider example:
@@ -259,25 +248,29 @@ openclaw config set channels.discord.token \
--allow-exec
```
Dry-run behavior:
<AccordionGroup>
<Accordion title="Dry-run behavior">
- Builder mode: runs SecretRef resolvability checks for changed refs/providers.
- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks.
- Policy validation also runs for known unsupported SecretRef target surfaces.
- Policy checks evaluate the full post-change config, so parent-object writes (for example setting `hooks` as an object) cannot bypass unsupported-surface validation.
- Exec SecretRef checks are skipped by default during dry-run to avoid command side effects.
- Use `--allow-exec` with `--dry-run` to opt in to exec SecretRef checks (this may execute provider commands).
- `--allow-exec` is dry-run only and errors if used without `--dry-run`.
</Accordion>
<Accordion title="--dry-run --json fields">
`--dry-run --json` prints a machine-readable report:
- Builder mode: runs SecretRef resolvability checks for changed refs/providers.
- JSON mode (`--strict-json`, `--json`, or batch mode): runs schema validation plus SecretRef resolvability checks.
- Policy validation also runs for known unsupported SecretRef target surfaces.
- Policy checks evaluate the full post-change config, so parent-object writes (for example setting `hooks` as an object) cannot bypass unsupported-surface validation.
- Exec SecretRef checks are skipped by default during dry-run to avoid command side effects.
- Use `--allow-exec` with `--dry-run` to opt in to exec SecretRef checks (this may execute provider commands).
- `--allow-exec` is dry-run only and errors if used without `--dry-run`.
- `ok`: whether dry-run passed
- `operations`: number of assignments evaluated
- `checks`: whether schema/resolvability checks ran
- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped)
- `refsChecked`: number of refs actually resolved during dry-run
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
- `errors`: structured schema/resolvability failures when `ok=false`
`--dry-run --json` prints a machine-readable report:
- `ok`: whether dry-run passed
- `operations`: number of assignments evaluated
- `checks`: whether schema/resolvability checks ran
- `checks.resolvabilityComplete`: whether resolvability checks ran to completion (false when exec refs are skipped)
- `refsChecked`: number of refs actually resolved during dry-run
- `skippedExecRefs`: number of exec refs skipped because `--allow-exec` was not set
- `errors`: structured schema/resolvability failures when `ok=false`
</Accordion>
</AccordionGroup>
### JSON output shape
@@ -304,66 +297,67 @@ Dry-run behavior:
}
```
Success example:
```json
{
"ok": true,
"operations": 1,
"configPath": "~/.openclaw/openclaw.json",
"inputModes": ["builder"],
"checks": {
"schema": false,
"resolvability": true,
"resolvabilityComplete": true
},
"refsChecked": 1,
"skippedExecRefs": 0
}
```
Failure example:
```json
{
"ok": false,
"operations": 1,
"configPath": "~/.openclaw/openclaw.json",
"inputModes": ["builder"],
"checks": {
"schema": false,
"resolvability": true,
"resolvabilityComplete": true
},
"refsChecked": 1,
"skippedExecRefs": 0,
"errors": [
<Tabs>
<Tab title="Success example">
```json
{
"kind": "resolvability",
"message": "Error: Environment variable \"MISSING_TEST_SECRET\" is not set.",
"ref": "env:default:MISSING_TEST_SECRET"
"ok": true,
"operations": 1,
"configPath": "~/.openclaw/openclaw.json",
"inputModes": ["builder"],
"checks": {
"schema": false,
"resolvability": true,
"resolvabilityComplete": true
},
"refsChecked": 1,
"skippedExecRefs": 0
}
]
}
```
```
</Tab>
<Tab title="Failure example">
```json
{
"ok": false,
"operations": 1,
"configPath": "~/.openclaw/openclaw.json",
"inputModes": ["builder"],
"checks": {
"schema": false,
"resolvability": true,
"resolvabilityComplete": true
},
"refsChecked": 1,
"skippedExecRefs": 0,
"errors": [
{
"kind": "resolvability",
"message": "Error: Environment variable \"MISSING_TEST_SECRET\" is not set.",
"ref": "env:default:MISSING_TEST_SECRET"
}
]
}
```
</Tab>
</Tabs>
If dry-run fails:
- `config schema validation failed`: your post-change config shape is invalid; fix path/value or provider/ref object shape.
- `Config policy validation failed: unsupported SecretRef usage`: move that credential back to plaintext/string input and keep SecretRefs on supported surfaces only.
- `SecretRef assignment(s) could not be resolved`: referenced provider/ref currently cannot resolve (missing env var, invalid file pointer, exec provider failure, or provider/source mismatch).
- `Dry run note: skipped <n> exec SecretRef resolvability check(s)`: dry-run skipped exec refs; rerun with `--allow-exec` if you need exec resolvability validation.
- For batch mode, fix failing entries and rerun `--dry-run` before writing.
<AccordionGroup>
<Accordion title="If dry-run fails">
- `config schema validation failed`: your post-change config shape is invalid; fix path/value or provider/ref object shape.
- `Config policy validation failed: unsupported SecretRef usage`: move that credential back to plaintext/string input and keep SecretRefs on supported surfaces only.
- `SecretRef assignment(s) could not be resolved`: referenced provider/ref currently cannot resolve (missing env var, invalid file pointer, exec provider failure, or provider/source mismatch).
- `Dry run note: skipped <n> exec SecretRef resolvability check(s)`: dry-run skipped exec refs; rerun with `--allow-exec` if you need exec resolvability validation.
- For batch mode, fix failing entries and rerun `--dry-run` before writing.
</Accordion>
</AccordionGroup>
## Write safety
`openclaw config set` and other OpenClaw-owned config writers validate the full
post-change config before committing it to disk. If the new payload fails schema
validation or looks like a destructive clobber, the active config is left alone
and the rejected payload is saved beside it as `openclaw.json.rejected.*`.
The active config path must be a regular file. Symlinked `openclaw.json`
layouts are unsupported for writes; use `OPENCLAW_CONFIG_PATH` to point directly
at the real file instead.
`openclaw config set` and other OpenClaw-owned config writers validate the full post-change config before committing it to disk. If the new payload fails schema validation or looks like a destructive clobber, the active config is left alone and the rejected payload is saved beside it as `openclaw.json.rejected.*`.
<Warning>
The active config path must be a regular file. Symlinked `openclaw.json` layouts are unsupported for writes; use `OPENCLAW_CONFIG_PATH` to point directly at the real file instead.
</Warning>
Prefer CLI writes for small edits:
@@ -381,19 +375,9 @@ ls -lt "$CONFIG".rejected.* 2>/dev/null | head
openclaw config validate
```
Direct editor writes are still allowed, but the running Gateway treats them as
untrusted until they validate. Invalid direct edits can be restored from the
last-known-good backup during startup or hot reload. See
[Gateway troubleshooting](/gateway/troubleshooting#gateway-restored-last-known-good-config).
Direct editor writes are still allowed, but the running Gateway treats them as untrusted until they validate. Invalid direct edits can be restored from the last-known-good backup during startup or hot reload. See [Gateway troubleshooting](/gateway/troubleshooting#gateway-restored-last-known-good-config).
Whole-file recovery is reserved for globally broken config, such as parse
errors, root-level schema failures, legacy migration failures, or mixed plugin
and root failures. If validation fails only under `plugins.entries.<id>...`,
OpenClaw keeps the active `openclaw.json` in place and reports the plugin-local
issue instead of restoring `.last-good`. This prevents plugin schema changes or
`minHostVersion` skew from rolling back unrelated user settings such as models,
providers, auth profiles, channels, gateway exposure, tools, memory, browser, or
cron config.
Whole-file recovery is reserved for globally broken config, such as parse errors, root-level schema failures, legacy migration failures, or mixed plugin and root failures. If validation fails only under `plugins.entries.<id>...`, OpenClaw keeps the active `openclaw.json` in place and reports the plugin-local issue instead of restoring `.last-good`. This prevents plugin schema changes or `minHostVersion` skew from rolling back unrelated user settings such as models, providers, auth profiles, channels, gateway exposure, tools, memory, browser, or cron config.
## Subcommands
@@ -403,21 +387,18 @@ Restart the gateway after edits.
## Validate
Validate the current config against the active schema without starting the
gateway.
Validate the current config against the active schema without starting the gateway.
```bash
openclaw config validate
openclaw config validate --json
```
After `openclaw config validate` is passing, you can use the local TUI to have
an embedded agent compare the active config against the docs while you validate
each change from the same terminal:
After `openclaw config validate` is passing, you can use the local TUI to have an embedded agent compare the active config against the docs while you validate each change from the same terminal:
If validation is already failing, start with `openclaw configure` or
`openclaw doctor --fix`. `openclaw chat` does not bypass the invalid-config
guard.
<Note>
If validation is already failing, start with `openclaw configure` or `openclaw doctor --fix`. `openclaw chat` does not bypass the invalid-config guard.
</Note>
```bash
openclaw chat
@@ -434,10 +415,20 @@ Then inside the TUI:
Typical repair loop:
- Ask the agent to compare your current config with the relevant docs page and suggest the smallest fix.
- Apply targeted edits with `openclaw config set` or `openclaw configure`.
- Rerun `openclaw config validate` after each change.
- If validation passes but the runtime is still unhealthy, run `openclaw doctor` or `openclaw doctor --fix` for migration and repair help.
<Steps>
<Step title="Compare with docs">
Ask the agent to compare your current config with the relevant docs page and suggest the smallest fix.
</Step>
<Step title="Apply targeted edits">
Apply targeted edits with `openclaw config set` or `openclaw configure`.
</Step>
<Step title="Re-validate">
Rerun `openclaw config validate` after each change.
</Step>
<Step title="Doctor for runtime issues">
If validation passes but the runtime is still unhealthy, run `openclaw doctor` or `openclaw doctor --fix` for migration and repair help.
</Step>
</Steps>
## Related

View File

@@ -44,6 +44,7 @@ Notes:
- State integrity checks now detect orphan transcript files in the sessions directory and can archive them as `.deleted.<timestamp>` to reclaim space safely.
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
- Doctor repairs missing bundled plugin runtime dependencies without writing into packaged global installs. For root-owned npm installs or hardened systemd units, set `OPENCLAW_PLUGIN_STAGE_DIR` to a writable directory such as `/var/lib/openclaw/plugin-runtime-deps`.
- Set `OPENCLAW_SERVICE_REPAIR_POLICY=external` when another supervisor owns the gateway lifecycle. Doctor still reports gateway/service health and applies non-service repairs, but skips service install/start/restart/bootstrap and legacy service cleanup.
- Doctor auto-migrates legacy flat Talk config (`talk.voiceId`, `talk.modelId`, and friends) into `talk.provider` + `talk.providers.<provider>`.
- Repeat `doctor --fix` runs no longer report/apply Talk normalization when the only difference is object key order.
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.

View File

@@ -5,19 +5,22 @@ read_when:
- Debugging Gateway auth, bind modes, and connectivity
- Discovering gateways via Bonjour (local + wide-area DNS-SD)
title: "Gateway"
sidebarTitle: "Gateway"
---
# Gateway CLI
The Gateway is OpenClaw's WebSocket server (channels, nodes, sessions, hooks). Subcommands in this page live under `openclaw gateway …`.
The Gateway is OpenClaws WebSocket server (channels, nodes, sessions, hooks).
Subcommands in this page live under `openclaw gateway …`.
Related docs:
- [/gateway/bonjour](/gateway/bonjour)
- [/gateway/discovery](/gateway/discovery)
- [/gateway/configuration](/gateway/configuration)
<CardGroup cols={3}>
<Card title="Bonjour discovery" href="/gateway/bonjour">
Local mDNS + wide-area DNS-SD setup.
</Card>
<Card title="Discovery overview" href="/gateway/discovery">
How OpenClaw advertises and finds gateways.
</Card>
<Card title="Configuration" href="/gateway/configuration">
Top-level gateway config keys.
</Card>
</CardGroup>
## Run the Gateway
@@ -33,37 +36,79 @@ Foreground alias:
openclaw gateway run
```
Notes:
- By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.openclaw/openclaw.json`. Use `--allow-unconfigured` for ad-hoc/dev runs.
- `openclaw onboard --mode local` and `openclaw setup` are expected to write `gateway.mode=local`. If the file exists but `gateway.mode` is missing, treat that as a broken or clobbered config and repair it instead of assuming local mode implicitly.
- If the file exists and `gateway.mode` is missing, the Gateway treats that as suspicious config damage and refuses to guess local for you.
- Binding beyond loopback without auth is blocked (safety guardrail).
- `SIGUSR1` triggers an in-process restart when authorized (`commands.restart` is enabled by default; set `commands.restart: false` to block manual restart, while gateway tool/config apply/update remain allowed).
- `SIGINT`/`SIGTERM` handlers stop the gateway process, but they dont restore any custom terminal state. If you wrap the CLI with a TUI or raw-mode input, restore the terminal before exit.
<AccordionGroup>
<Accordion title="Startup behavior">
- By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.openclaw/openclaw.json`. Use `--allow-unconfigured` for ad-hoc/dev runs.
- `openclaw onboard --mode local` and `openclaw setup` are expected to write `gateway.mode=local`. If the file exists but `gateway.mode` is missing, treat that as a broken or clobbered config and repair it instead of assuming local mode implicitly.
- If the file exists and `gateway.mode` is missing, the Gateway treats that as suspicious config damage and refuses to "guess local" for you.
- Binding beyond loopback without auth is blocked (safety guardrail).
- `SIGUSR1` triggers an in-process restart when authorized (`commands.restart` is enabled by default; set `commands.restart: false` to block manual restart, while gateway tool/config apply/update remain allowed).
- `SIGINT`/`SIGTERM` handlers stop the gateway process, but they don't restore any custom terminal state. If you wrap the CLI with a TUI or raw-mode input, restore the terminal before exit.
</Accordion>
</AccordionGroup>
### Options
- `--port <port>`: WebSocket port (default comes from config/env; usually `18789`).
- `--bind <loopback|lan|tailnet|auto|custom>`: listener bind mode.
- `--auth <token|password>`: auth mode override.
- `--token <token>`: token override (also sets `OPENCLAW_GATEWAY_TOKEN` for the process).
- `--password <password>`: password override. Warning: inline passwords can be exposed in local process listings.
- `--password-file <path>`: read the gateway password from a file.
- `--tailscale <off|serve|funnel>`: expose the Gateway via Tailscale.
- `--tailscale-reset-on-exit`: reset Tailscale serve/funnel config on shutdown.
- `--allow-unconfigured`: allow gateway start without `gateway.mode=local` in config. This bypasses the startup guard for ad-hoc/dev bootstrap only; it does not write or repair the config file.
- `--dev`: create a dev config + workspace if missing (skips BOOTSTRAP.md).
- `--reset`: reset dev config + credentials + sessions + workspace (requires `--dev`).
- `--force`: kill any existing listener on the selected port before starting.
- `--verbose`: verbose logs.
- `--cli-backend-logs`: only show CLI backend logs in the console (and enable stdout/stderr).
- `--ws-log <auto|full|compact>`: websocket log style (default `auto`).
- `--compact`: alias for `--ws-log compact`.
- `--raw-stream`: log raw model stream events to jsonl.
- `--raw-stream-path <path>`: raw stream jsonl path.
<ParamField path="--port <port>" type="number">
WebSocket port (default comes from config/env; usually `18789`).
</ParamField>
<ParamField path="--bind <loopback|lan|tailnet|auto|custom>" type="string">
Listener bind mode.
</ParamField>
<ParamField path="--auth <token|password>" type="string">
Auth mode override.
</ParamField>
<ParamField path="--token <token>" type="string">
Token override (also sets `OPENCLAW_GATEWAY_TOKEN` for the process).
</ParamField>
<ParamField path="--password <password>" type="string">
Password override.
</ParamField>
<ParamField path="--password-file <path>" type="string">
Read the gateway password from a file.
</ParamField>
<ParamField path="--tailscale <off|serve|funnel>" type="string">
Expose the Gateway via Tailscale.
</ParamField>
<ParamField path="--tailscale-reset-on-exit" type="boolean">
Reset Tailscale serve/funnel config on shutdown.
</ParamField>
<ParamField path="--allow-unconfigured" type="boolean">
Allow gateway start without `gateway.mode=local` in config. Bypasses the startup guard for ad-hoc/dev bootstrap only; does not write or repair the config file.
</ParamField>
<ParamField path="--dev" type="boolean">
Create a dev config + workspace if missing (skips BOOTSTRAP.md).
</ParamField>
<ParamField path="--reset" type="boolean">
Reset dev config + credentials + sessions + workspace (requires `--dev`).
</ParamField>
<ParamField path="--force" type="boolean">
Kill any existing listener on the selected port before starting.
</ParamField>
<ParamField path="--verbose" type="boolean">
Verbose logs.
</ParamField>
<ParamField path="--cli-backend-logs" type="boolean">
Only show CLI backend logs in the console (and enable stdout/stderr).
</ParamField>
<ParamField path="--ws-log <auto|full|compact>" type="string" default="auto">
Websocket log style.
</ParamField>
<ParamField path="--compact" type="boolean">
Alias for `--ws-log compact`.
</ParamField>
<ParamField path="--raw-stream" type="boolean">
Log raw model stream events to jsonl.
</ParamField>
<ParamField path="--raw-stream-path <path>" type="string">
Raw stream jsonl path.
</ParamField>
Startup profiling:
<Warning>
Inline `--password` can be exposed in local process listings. Prefer `--password-file`, env, or a SecretRef-backed `gateway.auth.password`.
</Warning>
### Startup profiling
- Set `OPENCLAW_GATEWAY_STARTUP_TRACE=1` to log phase timings during Gateway startup.
- Run `pnpm test:startup:gateway -- --runs 5 --warmup 1` to benchmark Gateway startup. The benchmark records first process output, `/healthz`, `/readyz`, and startup trace timings.
@@ -72,22 +117,24 @@ Startup profiling:
All query commands use WebSocket RPC.
Output modes:
<Tabs>
<Tab title="Output modes">
- Default: human-readable (colored in TTY).
- `--json`: machine-readable JSON (no styling/spinner).
- `--no-color` (or `NO_COLOR=1`): disable ANSI while keeping human layout.
</Tab>
<Tab title="Shared options">
- `--url <url>`: Gateway WebSocket URL.
- `--token <token>`: Gateway token.
- `--password <password>`: Gateway password.
- `--timeout <ms>`: timeout/budget (varies per command).
- `--expect-final`: wait for a "final" response (agent calls).
</Tab>
</Tabs>
- Default: human-readable (colored in TTY).
- `--json`: machine-readable JSON (no styling/spinner).
- `--no-color` (or `NO_COLOR=1`): disable ANSI while keeping human layout.
Shared options (where supported):
- `--url <url>`: Gateway WebSocket URL.
- `--token <token>`: Gateway token.
- `--password <password>`: Gateway password.
- `--timeout <ms>`: timeout/budget (varies per command).
- `--expect-final`: wait for a “final” response (agent calls).
Note: when you set `--url`, the CLI does not fall back to config or environment credentials.
Pass `--token` or `--password` explicitly. Missing explicit credentials is an error.
<Note>
When you set `--url`, the CLI does not fall back to config or environment credentials. Pass `--token` or `--password` explicitly. Missing explicit credentials is an error.
</Note>
### `gateway health`
@@ -107,9 +154,9 @@ openclaw gateway usage-cost --days 7
openclaw gateway usage-cost --json
```
Options:
- `--days <days>`: number of days to include (default `30`).
<ParamField path="--days <days>" type="number" default="30">
Number of days to include.
</ParamField>
### `gateway stability`
@@ -123,24 +170,35 @@ openclaw gateway stability --bundle latest --export
openclaw gateway stability --json
```
Options:
<ParamField path="--limit <limit>" type="number" default="25">
Maximum number of recent events to include (max `1000`).
</ParamField>
<ParamField path="--type <type>" type="string">
Filter by diagnostic event type, such as `payload.large` or `diagnostic.memory.pressure`.
</ParamField>
<ParamField path="--since-seq <seq>" type="number">
Include only events after a diagnostic sequence number.
</ParamField>
<ParamField path="--bundle [path]" type="string">
Read a persisted stability bundle instead of calling the running Gateway. Use `--bundle latest` (or just `--bundle`) for the newest bundle under the state directory, or pass a bundle JSON path directly.
</ParamField>
<ParamField path="--export" type="boolean">
Write a shareable support diagnostics zip instead of printing stability details.
</ParamField>
<ParamField path="--output <path>" type="string">
Output path for `--export`.
</ParamField>
- `--limit <limit>`: maximum number of recent events to include (default `25`, max `1000`).
- `--type <type>`: filter by diagnostic event type, such as `payload.large` or `diagnostic.memory.pressure`.
- `--since-seq <seq>`: include only events after a diagnostic sequence number.
- `--bundle [path]`: read a persisted stability bundle instead of calling the running Gateway. Use `--bundle latest` (or just `--bundle`) for the newest bundle under the state directory, or pass a bundle JSON path directly.
- `--export`: write a shareable support diagnostics zip instead of printing stability details.
- `--output <path>`: output path for `--export`.
Notes:
- Records keep operational metadata: event names, counts, byte sizes, memory readings, queue/session state, channel/plugin names, and redacted session summaries. They do not keep chat text, webhook bodies, tool outputs, raw request or response bodies, tokens, cookies, secret values, hostnames, or raw session ids. Set `diagnostics.enabled: false` to disable the recorder entirely.
- On fatal Gateway exits, shutdown timeouts, and restart startup failures, OpenClaw writes the same diagnostic snapshot to `~/.openclaw/logs/stability/openclaw-stability-*.json` when the recorder has events. Inspect the newest bundle with `openclaw gateway stability --bundle latest`; `--limit`, `--type`, and `--since-seq` also apply to bundle output.
<AccordionGroup>
<Accordion title="Privacy and bundle behavior">
- Records keep operational metadata: event names, counts, byte sizes, memory readings, queue/session state, channel/plugin names, and redacted session summaries. They do not keep chat text, webhook bodies, tool outputs, raw request or response bodies, tokens, cookies, secret values, hostnames, or raw session ids. Set `diagnostics.enabled: false` to disable the recorder entirely.
- On fatal Gateway exits, shutdown timeouts, and restart startup failures, OpenClaw writes the same diagnostic snapshot to `~/.openclaw/logs/stability/openclaw-stability-*.json` when the recorder has events. Inspect the newest bundle with `openclaw gateway stability --bundle latest`; `--limit`, `--type`, and `--since-seq` also apply to bundle output.
</Accordion>
</AccordionGroup>
### `gateway diagnostics export`
Write a local diagnostics zip that is designed to attach to bug reports.
For the privacy model and bundle contents, see [Diagnostics Export](/gateway/diagnostics).
Write a local diagnostics zip that is designed to attach to bug reports. For the privacy model and bundle contents, see [Diagnostics Export](/gateway/diagnostics).
```bash
openclaw gateway diagnostics export
@@ -148,17 +206,33 @@ openclaw gateway diagnostics export --output openclaw-diagnostics.zip
openclaw gateway diagnostics export --json
```
Options:
- `--output <path>`: output zip path. Defaults to a support export under the state directory.
- `--log-lines <count>`: maximum sanitized log lines to include (default `5000`).
- `--log-bytes <bytes>`: maximum log bytes to inspect (default `1000000`).
- `--url <url>`: Gateway WebSocket URL for the health snapshot.
- `--token <token>`: Gateway token for the health snapshot.
- `--password <password>`: Gateway password for the health snapshot.
- `--timeout <ms>`: status/health snapshot timeout (default `3000`).
- `--no-stability-bundle`: skip persisted stability bundle lookup.
- `--json`: print the written path, size, and manifest as JSON.
<ParamField path="--output <path>" type="string">
Output zip path. Defaults to a support export under the state directory.
</ParamField>
<ParamField path="--log-lines <count>" type="number" default="5000">
Maximum sanitized log lines to include.
</ParamField>
<ParamField path="--log-bytes <bytes>" type="number" default="1000000">
Maximum log bytes to inspect.
</ParamField>
<ParamField path="--url <url>" type="string">
Gateway WebSocket URL for the health snapshot.
</ParamField>
<ParamField path="--token <token>" type="string">
Gateway token for the health snapshot.
</ParamField>
<ParamField path="--password <password>" type="string">
Gateway password for the health snapshot.
</ParamField>
<ParamField path="--timeout <ms>" type="number" default="3000">
Status/health snapshot timeout.
</ParamField>
<ParamField path="--no-stability-bundle" type="boolean">
Skip persisted stability bundle lookup.
</ParamField>
<ParamField path="--json" type="boolean">
Print the written path, size, and manifest as JSON.
</ParamField>
The export contains a manifest, a Markdown summary, config shape, sanitized config details, sanitized log summaries, sanitized Gateway status/health snapshots, and the newest stability bundle when one exists.
@@ -174,93 +248,113 @@ openclaw gateway status --json
openclaw gateway status --require-rpc
```
Options:
<ParamField path="--url <url>" type="string">
Add an explicit probe target. Configured remote + localhost are still probed.
</ParamField>
<ParamField path="--token <token>" type="string">
Token auth for the probe.
</ParamField>
<ParamField path="--password <password>" type="string">
Password auth for the probe.
</ParamField>
<ParamField path="--timeout <ms>" type="number" default="10000">
Probe timeout.
</ParamField>
<ParamField path="--no-probe" type="boolean">
Skip the connectivity probe (service-only view).
</ParamField>
<ParamField path="--deep" type="boolean">
Scan system-level services too.
</ParamField>
<ParamField path="--require-rpc" type="boolean">
Upgrade the default connectivity probe to a read probe and exit non-zero when that read probe fails. Cannot be combined with `--no-probe`.
</ParamField>
- `--url <url>`: add an explicit probe target. Configured remote + localhost are still probed.
- `--token <token>`: token auth for the probe.
- `--password <password>`: password auth for the probe.
- `--timeout <ms>`: probe timeout (default `10000`).
- `--no-probe`: skip the connectivity probe (service-only view).
- `--deep`: scan system-level services too.
- `--require-rpc`: upgrade the default connectivity probe to a read probe and exit non-zero when that read probe fails. Cannot be combined with `--no-probe`.
Notes:
- `gateway status` stays available for diagnostics even when the local CLI config is missing or invalid.
- Default `gateway status` proves service state, WebSocket connect, and the auth capability visible at handshake time. It does not prove read/write/admin operations.
- Diagnostic probes are non-mutating for first-time device auth: they reuse an
existing cached device token when one exists, but they do not create a new CLI
device identity or read-only device pairing record just to check status.
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need read-scope RPC calls to be healthy too.
- `--deep` adds a best-effort scan for extra launchd/systemd/schtasks installs. When multiple gateway-like services are detected, human output prints cleanup hints and warns that most setups should run one gateway per machine.
- Human output includes the resolved file log path plus the CLI-vs-service config paths/validity snapshot to help diagnose profile or state-dir drift.
- On Linux systemd installs, service auth drift checks read both `Environment=` and `EnvironmentFile=` values from the unit (including `%h`, quoted paths, multiple files, and optional `-` files).
- Drift checks resolve `gateway.auth.token` SecretRefs using merged runtime env (service command env first, then process env fallback).
- If token auth is not effectively active (explicit `gateway.auth.mode` of `password`/`none`/`trusted-proxy`, or mode unset where password can win and no token candidate can win), token-drift checks skip config token resolution.
<AccordionGroup>
<Accordion title="Status semantics">
- `gateway status` stays available for diagnostics even when the local CLI config is missing or invalid.
- Default `gateway status` proves service state, WebSocket connect, and the auth capability visible at handshake time. It does not prove read/write/admin operations.
- Diagnostic probes are non-mutating for first-time device auth: they reuse an existing cached device token when one exists, but they do not create a new CLI device identity or read-only device pairing record just to check status.
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need read-scope RPC calls to be healthy too.
- `--deep` adds a best-effort scan for extra launchd/systemd/schtasks installs. When multiple gateway-like services are detected, human output prints cleanup hints and warns that most setups should run one gateway per machine.
- Human output includes the resolved file log path plus the CLI-vs-service config paths/validity snapshot to help diagnose profile or state-dir drift.
</Accordion>
<Accordion title="Linux systemd auth-drift checks">
- On Linux systemd installs, service auth drift checks read both `Environment=` and `EnvironmentFile=` values from the unit (including `%h`, quoted paths, multiple files, and optional `-` files).
- Drift checks resolve `gateway.auth.token` SecretRefs using merged runtime env (service command env first, then process env fallback).
- If token auth is not effectively active (explicit `gateway.auth.mode` of `password`/`none`/`trusted-proxy`, or mode unset where password can win and no token candidate can win), token-drift checks skip config token resolution.
</Accordion>
</AccordionGroup>
### `gateway probe`
`gateway probe` is the debug everything command. It always probes:
`gateway probe` is the "debug everything" command. It always probes:
- your configured remote gateway (if set), and
- localhost (loopback) **even if remote is configured**.
If you pass `--url`, that explicit target is added ahead of both. Human output labels the
targets as:
If you pass `--url`, that explicit target is added ahead of both. Human output labels the targets as:
- `URL (explicit)`
- `Remote (configured)` or `Remote (configured, inactive)`
- `Local loopback`
<Note>
If multiple gateways are reachable, it prints all of them. Multiple gateways are supported when you use isolated profiles/ports (e.g., a rescue bot), but most installs still run a single gateway.
</Note>
```bash
openclaw gateway probe
openclaw gateway probe --json
```
Interpretation:
<AccordionGroup>
<Accordion title="Interpretation">
- `Reachable: yes` means at least one target accepted a WebSocket connect.
- `Capability: read-only|write-capable|admin-capable|pairing-pending|connect-only` reports what the probe could prove about auth. It is separate from reachability.
- `Read probe: ok` means read-scope detail RPC calls (`health`/`status`/`system-presence`/`config.get`) also succeeded.
- `Read probe: limited - missing scope: operator.read` means connect succeeded but read-scope RPC is limited. This is reported as **degraded** reachability, not full failure.
- Like `gateway status`, probe reuses existing cached device auth but does not create first-time device identity or pairing state.
- Exit code is non-zero only when no probed target is reachable.
</Accordion>
<Accordion title="JSON output">
Top level:
- `Reachable: yes` means at least one target accepted a WebSocket connect.
- `Capability: read-only|write-capable|admin-capable|pairing-pending|connect-only` reports what the probe could prove about auth. It is separate from reachability.
- `Read probe: ok` means read-scope detail RPC calls (`health`/`status`/`system-presence`/`config.get`) also succeeded.
- `Read probe: limited - missing scope: operator.read` means connect succeeded but read-scope RPC is limited. This is reported as **degraded** reachability, not full failure.
- Like `gateway status`, probe reuses existing cached device auth but does not
create first-time device identity or pairing state.
- Exit code is non-zero only when no probed target is reachable.
- `ok`: at least one target is reachable.
- `degraded`: at least one target had scope-limited detail RPC.
- `capability`: best capability seen across reachable targets (`read_only`, `write_capable`, `admin_capable`, `pairing_pending`, `connected_no_operator_scope`, or `unknown`).
- `primaryTargetId`: best target to treat as the active winner in this order: explicit URL, SSH tunnel, configured remote, then local loopback.
- `warnings[]`: best-effort warning records with `code`, `message`, and optional `targetIds`.
- `network`: local loopback/tailnet URL hints derived from current config and host networking.
- `discovery.timeoutMs` and `discovery.count`: the actual discovery budget/result count used for this probe pass.
JSON notes (`--json`):
Per target (`targets[].connect`):
- Top level:
- `ok`: at least one target is reachable.
- `degraded`: at least one target had scope-limited detail RPC.
- `capability`: best capability seen across reachable targets (`read_only`, `write_capable`, `admin_capable`, `pairing_pending`, `connected_no_operator_scope`, or `unknown`).
- `primaryTargetId`: best target to treat as the active winner in this order: explicit URL, SSH tunnel, configured remote, then local loopback.
- `warnings[]`: best-effort warning records with `code`, `message`, and optional `targetIds`.
- `network`: local loopback/tailnet URL hints derived from current config and host networking.
- `discovery.timeoutMs` and `discovery.count`: the actual discovery budget/result count used for this probe pass.
- Per target (`targets[].connect`):
- `ok`: reachability after connect + degraded classification.
- `rpcOk`: full detail RPC success.
- `scopeLimited`: detail RPC failed due to missing operator scope.
- Per target (`targets[].auth`):
- `role`: auth role reported in `hello-ok` when available.
- `scopes`: granted scopes reported in `hello-ok` when available.
- `capability`: the surfaced auth capability classification for that target.
- `ok`: reachability after connect + degraded classification.
- `rpcOk`: full detail RPC success.
- `scopeLimited`: detail RPC failed due to missing operator scope.
Common warning codes:
Per target (`targets[].auth`):
- `ssh_tunnel_failed`: SSH tunnel setup failed; the command fell back to direct probes.
- `multiple_gateways`: more than one target was reachable; this is unusual unless you intentionally run isolated profiles, such as a rescue bot.
- `auth_secretref_unresolved`: a configured auth SecretRef could not be resolved for a failed target.
- `probe_scope_limited`: WebSocket connect succeeded, but the read probe was limited by missing `operator.read`.
- `role`: auth role reported in `hello-ok` when available.
- `scopes`: granted scopes reported in `hello-ok` when available.
- `capability`: the surfaced auth capability classification for that target.
</Accordion>
<Accordion title="Common warning codes">
- `ssh_tunnel_failed`: SSH tunnel setup failed; the command fell back to direct probes.
- `multiple_gateways`: more than one target was reachable; this is unusual unless you intentionally run isolated profiles, such as a rescue bot.
- `auth_secretref_unresolved`: a configured auth SecretRef could not be resolved for a failed target.
- `probe_scope_limited`: WebSocket connect succeeded, but the read probe was limited by missing `operator.read`.
</Accordion>
</AccordionGroup>
#### Remote over SSH (Mac app parity)
The macOS app Remote over SSH mode uses a local port-forward so the remote gateway (which may be bound to loopback only) becomes reachable at `ws://127.0.0.1:<port>`.
The macOS app "Remote over SSH" mode uses a local port-forward so the remote gateway (which may be bound to loopback only) becomes reachable at `ws://127.0.0.1:<port>`.
CLI equivalent:
@@ -268,13 +362,15 @@ CLI equivalent:
openclaw gateway probe --ssh user@gateway-host
```
Options:
- `--ssh <target>`: `user@host` or `user@host:port` (port defaults to `22`).
- `--ssh-identity <path>`: identity file.
- `--ssh-auto`: pick the first discovered gateway host as SSH target from the resolved
discovery endpoint (`local.` plus the configured wide-area domain, if any). TXT-only
hints are ignored.
<ParamField path="--ssh <target>" type="string">
`user@host` or `user@host:port` (port defaults to `22`).
</ParamField>
<ParamField path="--ssh-identity <path>" type="string">
Identity file.
</ParamField>
<ParamField path="--ssh-auto" type="boolean">
Pick the first discovered gateway host as SSH target from the resolved discovery endpoint (`local.` plus the configured wide-area domain, if any). TXT-only hints are ignored.
</ParamField>
Config (optional, used as defaults):
@@ -290,20 +386,31 @@ openclaw gateway call status
openclaw gateway call logs.tail --params '{"sinceMs": 60000}'
```
Options:
<ParamField path="--params <json>" type="string" default="{}">
JSON object string for params.
</ParamField>
<ParamField path="--url <url>" type="string">
Gateway WebSocket URL.
</ParamField>
<ParamField path="--token <token>" type="string">
Gateway token.
</ParamField>
<ParamField path="--password <password>" type="string">
Gateway password.
</ParamField>
<ParamField path="--timeout <ms>" type="number">
Timeout budget.
</ParamField>
<ParamField path="--expect-final" type="boolean">
Mainly for agent-style RPCs that stream intermediate events before a final payload.
</ParamField>
<ParamField path="--json" type="boolean">
Machine-readable JSON output.
</ParamField>
- `--params <json>`: JSON object string for params (default `{}`)
- `--url <url>`
- `--token <token>`
- `--password <password>`
- `--timeout <ms>`
- `--expect-final`
- `--json`
Notes:
- `--params` must be valid JSON.
- `--expect-final` is mainly for agent-style RPCs that stream intermediate events before a final payload.
<Note>
`--params` must be valid JSON.
</Note>
## Manage the Gateway service
@@ -315,29 +422,30 @@ openclaw gateway restart
openclaw gateway uninstall
```
Command options:
- `gateway status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
- `gateway install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
- `gateway uninstall|start|stop|restart`: `--json`
Notes:
- `gateway install` supports `--port`, `--runtime`, `--token`, `--force`, `--json`.
- Use `gateway restart` to restart a managed service. Do not chain `gateway stop` and `gateway start` as a restart substitute; on macOS, `gateway stop` intentionally disables the LaunchAgent before stopping it.
- When token auth requires a token and `gateway.auth.token` is SecretRef-managed, `gateway install` validates that the SecretRef is resolvable but does not persist the resolved token into service environment metadata.
- If token auth requires a token and the configured token SecretRef is unresolved, install fails closed instead of persisting fallback plaintext.
- For password auth on `gateway run`, prefer `OPENCLAW_GATEWAY_PASSWORD`, `--password-file`, or a SecretRef-backed `gateway.auth.password` over inline `--password`.
- In inferred auth mode, shell-only `OPENCLAW_GATEWAY_PASSWORD` does not relax install token requirements; use durable config (`gateway.auth.password` or config `env`) when installing a managed service.
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, install is blocked until mode is set explicitly.
- Lifecycle commands accept `--json` for scripting.
<AccordionGroup>
<Accordion title="Command options">
- `gateway status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
- `gateway install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
- `gateway uninstall|start|stop|restart`: `--json`
</Accordion>
<Accordion title="Service install and lifecycle notes">
- `gateway install` supports `--port`, `--runtime`, `--token`, `--force`, `--json`.
- Use `gateway restart` to restart a managed service. Do not chain `gateway stop` and `gateway start` as a restart substitute; on macOS, `gateway stop` intentionally disables the LaunchAgent before stopping it.
- When token auth requires a token and `gateway.auth.token` is SecretRef-managed, `gateway install` validates that the SecretRef is resolvable but does not persist the resolved token into service environment metadata.
- If token auth requires a token and the configured token SecretRef is unresolved, install fails closed instead of persisting fallback plaintext.
- For password auth on `gateway run`, prefer `OPENCLAW_GATEWAY_PASSWORD`, `--password-file`, or a SecretRef-backed `gateway.auth.password` over inline `--password`.
- In inferred auth mode, shell-only `OPENCLAW_GATEWAY_PASSWORD` does not relax install token requirements; use durable config (`gateway.auth.password` or config `env`) when installing a managed service.
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, install is blocked until mode is set explicitly.
- Lifecycle commands accept `--json` for scripting.
</Accordion>
</AccordionGroup>
## Discover gateways (Bonjour)
`gateway discover` scans for Gateway beacons (`_openclaw-gw._tcp`).
- Multicast DNS-SD: `local.`
- Unicast DNS-SD (Wide-Area Bonjour): choose a domain (example: `openclaw.internal.`) and set up split DNS + a DNS server; see [/gateway/bonjour](/gateway/bonjour)
- Unicast DNS-SD (Wide-Area Bonjour): choose a domain (example: `openclaw.internal.`) and set up split DNS + a DNS server; see [Bonjour](/gateway/bonjour).
Only gateways with Bonjour discovery enabled (default) advertise the beacon.
@@ -357,10 +465,12 @@ Wide-Area discovery records include (TXT):
openclaw gateway discover
```
Options:
- `--timeout <ms>`: per-command timeout (browse/resolve); default `2000`.
- `--json`: machine-readable output (also disables styling/spinner).
<ParamField path="--timeout <ms>" type="number" default="2000">
Per-command timeout (browse/resolve).
</ParamField>
<ParamField path="--json" type="boolean">
Machine-readable output (also disables styling/spinner).
</ParamField>
Examples:
@@ -369,14 +479,11 @@ openclaw gateway discover --timeout 4000
openclaw gateway discover --json | jq '.beacons[].wsUrl'
```
Notes:
<Note>
- The CLI scans `local.` plus the configured wide-area domain when one is enabled.
- `wsUrl` in JSON output is derived from the resolved service endpoint, not from TXT-only
hints such as `lanHost` or `tailnetDns`.
- On `local.` mDNS, `sshPort` and `cliPath` are only broadcast when
`discovery.mdns.mode` is `full`. Wide-area DNS-SD still writes `cliPath`; `sshPort`
stays optional there too.
- `wsUrl` in JSON output is derived from the resolved service endpoint, not from TXT-only hints such as `lanHost` or `tailnetDns`.
- On `local.` mDNS, `sshPort` and `cliPath` are only broadcast when `discovery.mdns.mode` is `full`. Wide-area DNS-SD still writes `cliPath`; `sshPort` stays optional there too.
</Note>
## Related

View File

@@ -4,18 +4,25 @@ read_when:
- You want to install or manage Gateway plugins or compatible bundles
- You want to debug plugin load failures
title: "Plugins"
sidebarTitle: "Plugins"
---
# `openclaw plugins`
Manage Gateway plugins, hook packs, and compatible bundles.
Related:
- Plugin system: [Plugins](/tools/plugin)
- Bundle compatibility: [Plugin bundles](/plugins/bundles)
- Plugin manifest + schema: [Plugin manifest](/plugins/manifest)
- Security hardening: [Security](/gateway/security)
<CardGroup cols={2}>
<Card title="Plugin system" href="/tools/plugin">
End-user guide for installing, enabling, and troubleshooting plugins.
</Card>
<Card title="Plugin bundles" href="/plugins/bundles">
Bundle compatibility model.
</Card>
<Card title="Plugin manifest" href="/plugins/manifest">
Manifest fields and config schema.
</Card>
<Card title="Security" href="/gateway/security">
Security hardening for plugin installs.
</Card>
</CardGroup>
## Commands
@@ -41,17 +48,13 @@ openclaw plugins marketplace list <marketplace>
openclaw plugins marketplace list <marketplace> --json
```
Bundled plugins ship with OpenClaw. Some are enabled by default (for example
bundled model providers, bundled speech providers, and the bundled browser
plugin); others require `plugins enable`.
<Note>
Bundled plugins ship with OpenClaw. Some are enabled by default (for example bundled model providers, bundled speech providers, and the bundled browser plugin); others require `plugins enable`.
Native OpenClaw plugins must ship `openclaw.plugin.json` with an inline JSON
Schema (`configSchema`, even if empty). Compatible bundles use their own bundle
manifests instead.
Native OpenClaw plugins must ship `openclaw.plugin.json` with an inline JSON Schema (`configSchema`, even if empty). Compatible bundles use their own bundle manifests instead.
`plugins list` shows `Format: openclaw` or `Format: bundle`. Verbose list/info
output also shows the bundle subtype (`codex`, `claude`, or `cursor`) plus detected bundle
capabilities.
`plugins list` shows `Format: openclaw` or `Format: bundle`. Verbose list/info output also shows the bundle subtype (`codex`, `claude`, or `cursor`) plus detected bundle capabilities.
</Note>
### Install
@@ -67,66 +70,49 @@ openclaw plugins install <plugin> --marketplace <name> # marketplace (explicit)
openclaw plugins install <plugin> --marketplace https://github.com/<owner>/<repo>
```
Bare package names are checked against ClawHub first, then npm. Security note:
treat plugin installs like running code. Prefer pinned versions.
<Warning>
Bare package names are checked against ClawHub first, then npm. Treat plugin installs like running code. Prefer pinned versions.
</Warning>
If your `plugins` section is backed by a single-file `$include`, `plugins install/update/enable/disable/uninstall` write through to that included file and leave `openclaw.json` untouched. Root includes, include arrays, and includes with sibling overrides fail closed instead of flattening. See [Config includes](/gateway/configuration) for the supported shapes.
<AccordionGroup>
<Accordion title="Config includes and invalid-config recovery">
If your `plugins` section is backed by a single-file `$include`, `plugins install/update/enable/disable/uninstall` write through to that included file and leave `openclaw.json` untouched. Root includes, include arrays, and includes with sibling overrides fail closed instead of flattening. See [Config includes](/gateway/configuration) for the supported shapes.
If config is invalid, `plugins install` normally fails closed and tells you to
run `openclaw doctor --fix` first. The only documented exception is a narrow
bundled-plugin recovery path for plugins that explicitly opt into
`openclaw.install.allowInvalidConfigRecovery`.
If config is invalid, `plugins install` normally fails closed and tells you to run `openclaw doctor --fix` first. The only documented exception is a narrow bundled-plugin recovery path for plugins that explicitly opt into `openclaw.install.allowInvalidConfigRecovery`.
`--force` reuses the existing install target and overwrites an already-installed
plugin or hook pack in place. Use it when you are intentionally reinstalling
the same id from a new local path, archive, ClawHub package, or npm artifact.
For routine upgrades of an already tracked npm plugin, prefer
`openclaw plugins update <id-or-npm-spec>`.
</Accordion>
<Accordion title="--force and reinstall vs update">
`--force` reuses the existing install target and overwrites an already-installed plugin or hook pack in place. Use it when you are intentionally reinstalling the same id from a new local path, archive, ClawHub package, or npm artifact. For routine upgrades of an already tracked npm plugin, prefer `openclaw plugins update <id-or-npm-spec>`.
If you run `plugins install` for a plugin id that is already installed, OpenClaw
stops and points you at `plugins update <id-or-npm-spec>` for a normal upgrade,
or at `plugins install <package> --force` when you genuinely want to overwrite
the current install from a different source.
If you run `plugins install` for a plugin id that is already installed, OpenClaw stops and points you at `plugins update <id-or-npm-spec>` for a normal upgrade, or at `plugins install <package> --force` when you genuinely want to overwrite the current install from a different source.
`--pin` applies to npm installs only. It is not supported with `--marketplace`,
because marketplace installs persist marketplace source metadata instead of an
npm spec.
</Accordion>
<Accordion title="--pin scope">
`--pin` applies to npm installs only. It is not supported with `--marketplace`, because marketplace installs persist marketplace source metadata instead of an npm spec.
</Accordion>
<Accordion title="--dangerously-force-unsafe-install">
`--dangerously-force-unsafe-install` is a break-glass option for false positives in the built-in dangerous-code scanner. It allows the install to continue even when the built-in scanner reports `critical` findings, but it does **not** bypass plugin `before_install` hook policy blocks and does **not** bypass scan failures.
`--dangerously-force-unsafe-install` is a break-glass option for false positives
in the built-in dangerous-code scanner. It allows the install to continue even
when the built-in scanner reports `critical` findings, but it does **not**
bypass plugin `before_install` hook policy blocks and does **not** bypass scan
failures.
This CLI flag applies to plugin install/update flows. Gateway-backed skill dependency installs use the matching `dangerouslyForceUnsafeInstall` request override, while `openclaw skills install` remains a separate ClawHub skill download/install flow.
This CLI flag applies to plugin install/update flows. Gateway-backed skill
dependency installs use the matching `dangerouslyForceUnsafeInstall` request
override, while `openclaw skills install` remains a separate ClawHub skill
download/install flow.
</Accordion>
<Accordion title="Hook packs and npm specs">
`plugins install` is also the install surface for hook packs that expose `openclaw.hooks` in `package.json`. Use `openclaw hooks` for filtered hook visibility and per-hook enablement, not package installation.
`plugins install` is also the install surface for hook packs that expose
`openclaw.hooks` in `package.json`. Use `openclaw hooks` for filtered hook
visibility and per-hook enablement, not package installation.
Npm specs are **registry-only** (package name + optional **exact version** or **dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts` for safety, even when your shell has global npm install settings.
Npm specs are **registry-only** (package name + optional **exact version** or
**dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency
installs run project-local with `--ignore-scripts` for safety, even when your
shell has global npm install settings.
Bare specs and `@latest` stay on the stable track. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
Bare specs and `@latest` stay on the stable track. If npm resolves either of
those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a
prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as
`@1.2.3-beta.4`.
If a bare install spec matches a bundled plugin id (for example `diffs`), OpenClaw installs the bundled plugin directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`).
If a bare install spec matches a bundled plugin id (for example `diffs`), OpenClaw
installs the bundled plugin directly. To install an npm package with the same
name, use an explicit scoped spec (for example `@scope/diffs`).
</Accordion>
<Accordion title="Archives">
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`. Native OpenClaw plugin archives must contain a valid `openclaw.plugin.json` at the extracted plugin root; archives that only contain `package.json` are rejected before OpenClaw writes install records.
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`.
Native OpenClaw plugin archives must contain a valid `openclaw.plugin.json` at
the extracted plugin root; archives that only contain `package.json` are
rejected before OpenClaw writes install records.
Claude marketplace installs are also supported.
Claude marketplace installs are also supported.
</Accordion>
</AccordionGroup>
ClawHub installs use an explicit `clawhub:<package>` locator:
@@ -135,20 +121,17 @@ openclaw plugins install clawhub:openclaw-codex-app-server
openclaw plugins install clawhub:openclaw-codex-app-server@1.2.3
```
OpenClaw now also prefers ClawHub for bare npm-safe plugin specs. It only falls
back to npm if ClawHub does not have that package or version:
OpenClaw now also prefers ClawHub for bare npm-safe plugin specs. It only falls back to npm if ClawHub does not have that package or version:
```bash
openclaw plugins install openclaw-codex-app-server
```
OpenClaw downloads the package archive from ClawHub, checks the advertised
plugin API / minimum gateway compatibility, then installs it through the normal
archive path. Recorded installs keep their ClawHub source metadata for later
updates.
OpenClaw downloads the package archive from ClawHub, checks the advertised plugin API / minimum gateway compatibility, then installs it through the normal archive path. Recorded installs keep their ClawHub source metadata for later updates.
Use `plugin@marketplace` shorthand when the marketplace name exists in Claude's
local registry cache at `~/.claude/plugins/known_marketplaces.json`:
#### Marketplace shorthand
Use `plugin@marketplace` shorthand when the marketplace name exists in Claude's local registry cache at `~/.claude/plugins/known_marketplaces.json`:
```bash
openclaw plugins marketplace list <marketplace-name>
@@ -164,33 +147,29 @@ openclaw plugins install <plugin-name> --marketplace https://github.com/<owner>/
openclaw plugins install <plugin-name> --marketplace ./my-marketplace
```
Marketplace sources can be:
- a Claude known-marketplace name from `~/.claude/plugins/known_marketplaces.json`
- a local marketplace root or `marketplace.json` path
- a GitHub repo shorthand such as `owner/repo`
- a GitHub repo URL such as `https://github.com/owner/repo`
- a git URL
For remote marketplaces loaded from GitHub or git, plugin entries must stay
inside the cloned marketplace repo. OpenClaw accepts relative path sources from
that repo and rejects HTTP(S), absolute-path, git, GitHub, and other non-path
plugin sources from remote manifests.
<Tabs>
<Tab title="Marketplace sources">
- a Claude known-marketplace name from `~/.claude/plugins/known_marketplaces.json`
- a local marketplace root or `marketplace.json` path
- a GitHub repo shorthand such as `owner/repo`
- a GitHub repo URL such as `https://github.com/owner/repo`
- a git URL
</Tab>
<Tab title="Remote marketplace rules">
For remote marketplaces loaded from GitHub or git, plugin entries must stay inside the cloned marketplace repo. OpenClaw accepts relative path sources from that repo and rejects HTTP(S), absolute-path, git, GitHub, and other non-path plugin sources from remote manifests.
</Tab>
</Tabs>
For local paths and archives, OpenClaw auto-detects:
- native OpenClaw plugins (`openclaw.plugin.json`)
- Codex-compatible bundles (`.codex-plugin/plugin.json`)
- Claude-compatible bundles (`.claude-plugin/plugin.json` or the default Claude
component layout)
- Claude-compatible bundles (`.claude-plugin/plugin.json` or the default Claude component layout)
- Cursor-compatible bundles (`.cursor-plugin/plugin.json`)
Compatible bundles install into the normal plugin root and participate in
the same list/info/enable/disable flow. Today, bundle skills, Claude
command-skills, Claude `settings.json` defaults, Claude `.lsp.json` /
manifest-declared `lspServers` defaults, Cursor command-skills, and compatible
Codex hook directories are supported; other detected bundle capabilities are
shown in diagnostics/info but are not yet wired into runtime execution.
<Note>
Compatible bundles install into the normal plugin root and participate in the same list/info/enable/disable flow. Today, bundle skills, Claude command-skills, Claude `settings.json` defaults, Claude `.lsp.json` / manifest-declared `lspServers` defaults, Cursor command-skills, and compatible Codex hook directories are supported; other detected bundle capabilities are shown in diagnostics/info but are not yet wired into runtime execution.
</Note>
### List
@@ -201,30 +180,31 @@ openclaw plugins list --verbose
openclaw plugins list --json
```
Use `--enabled` to show only enabled plugins. Use `--verbose` to switch from the
table view to per-plugin detail lines with source/origin/version/activation
metadata. Use `--json` for machine-readable inventory plus registry
diagnostics.
<ParamField path="--enabled" type="boolean">
Show only enabled plugins.
</ParamField>
<ParamField path="--verbose" type="boolean">
Switch from the table view to per-plugin detail lines with source/origin/version/activation metadata.
</ParamField>
<ParamField path="--json" type="boolean">
Machine-readable inventory plus registry diagnostics.
</ParamField>
`plugins list` reads the persisted local plugin registry first, with a
manifest-only derived fallback when the registry is missing or invalid. It is
useful for checking whether a plugin is installed, enabled, and visible to cold
startup planning, but it is not a live runtime probe of an already-running
Gateway process. After changing plugin code, enablement, hook policy, or
`plugins.load.paths`, restart the Gateway that serves the channel before
expecting new `register(api)` code or hooks to run. For remote/container
deployments, verify you are restarting the actual `openclaw gateway run` child,
not only a wrapper process.
<Note>
`plugins list` reads the persisted local plugin registry first, with a manifest-only derived fallback when the registry is missing or invalid. It is useful for checking whether a plugin is installed, enabled, and visible to cold startup planning, but it is not a live runtime probe of an already-running Gateway process. After changing plugin code, enablement, hook policy, or `plugins.load.paths`, restart the Gateway that serves the channel before expecting new `register(api)` code or hooks to run. For remote/container deployments, verify you are restarting the actual `openclaw gateway run` child, not only a wrapper process.
</Note>
For bundled plugin work inside a packaged Docker image, bind-mount the plugin
source directory over the matching packaged source path, such as
`/app/extensions/synology-chat`. OpenClaw will discover that mounted source
overlay before `/app/dist/extensions/synology-chat`; a plain copied source
directory remains inert so normal packaged installs still use compiled dist.
For runtime hook debugging:
- `openclaw plugins inspect <id> --json` shows registered hooks and diagnostics
from a module-loaded inspection pass.
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway,
service/process hints, config path, and RPC health.
- Non-bundled conversation hooks (`llm_input`, `llm_output`,
`before_agent_finalize`, `agent_end`) require
`plugins.entries.<id>.hooks.allowConversationAccess=true`.
- `openclaw plugins inspect <id> --json` shows registered hooks and diagnostics from a module-loaded inspection pass.
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway, service/process hints, config path, and RPC health.
- Non-bundled conversation hooks (`llm_input`, `llm_output`, `before_agent_finalize`, `agent_end`) require `plugins.entries.<id>.hooks.allowConversationAccess=true`.
Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
@@ -232,24 +212,17 @@ Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
openclaw plugins install -l ./my-plugin
```
`--force` is not supported with `--link` because linked installs reuse the
source path instead of copying over a managed install target.
<Note>
`--force` is not supported with `--link` because linked installs reuse the source path instead of copying over a managed install target.
Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in
the managed plugin index while keeping the default behavior unpinned.
Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in the managed plugin index while keeping the default behavior unpinned.
</Note>
### Plugin Index
### Plugin index
Plugin install metadata is machine-managed state, not user config. Installs
and updates write it to `plugins/installs.json` under the active OpenClaw state
directory. Its top-level `installRecords` map is the durable source of install
metadata, including records for broken or missing plugin manifests. The
`plugins` array is the manifest-derived cold registry cache. The file includes a
do-not-edit warning and is used by `openclaw plugins update`, uninstall,
diagnostics, and the cold plugin registry.
When OpenClaw sees shipped legacy `plugins.installs` records in config, it moves
them into the plugin index and removes the config key; if either write fails,
the config records are kept so the install metadata is not lost.
Plugin install metadata is machine-managed state, not user config. Installs and updates write it to `plugins/installs.json` under the active OpenClaw state directory. Its top-level `installRecords` map is the durable source of install metadata, including records for broken or missing plugin manifests. The `plugins` array is the manifest-derived cold registry cache. The file includes a do-not-edit warning and is used by `openclaw plugins update`, uninstall, diagnostics, and the cold plugin registry.
When OpenClaw sees shipped legacy `plugins.installs` records in config, it moves them into the plugin index and removes the config key; if either write fails, the config records are kept so the install metadata is not lost.
### Uninstall
@@ -259,13 +232,11 @@ openclaw plugins uninstall <id> --dry-run
openclaw plugins uninstall <id> --keep-files
```
`uninstall` removes plugin records from `plugins.entries`, the persisted plugin
index, the plugin allowlist, and linked `plugins.load.paths` entries when
applicable. Unless `--keep-files` is set, uninstall also removes the tracked
managed install directory when it is inside OpenClaw's plugin extensions root.
For active memory plugins, the memory slot resets to `memory-core`.
`uninstall` removes plugin records from `plugins.entries`, the persisted plugin index, plugin allow/deny list entries, and linked `plugins.load.paths` entries when applicable. Unless `--keep-files` is set, uninstall also removes the tracked managed install directory when it is inside OpenClaw's plugin extensions root. For active memory plugins, the memory slot resets to `memory-core`.
<Note>
`--keep-config` is supported as a deprecated alias for `--keep-files`.
</Note>
### Update
@@ -277,38 +248,27 @@ openclaw plugins update @openclaw/voice-call@beta
openclaw plugins update openclaw-codex-app-server --dangerously-force-unsafe-install
```
Updates apply to tracked plugin installs in the managed plugin index and
tracked hook-pack installs in `hooks.internal.installs`.
Updates apply to tracked plugin installs in the managed plugin index and tracked hook-pack installs in `hooks.internal.installs`.
When you pass a plugin id, OpenClaw reuses the recorded install spec for that
plugin. That means previously stored dist-tags such as `@beta` and exact pinned
versions continue to be used on later `update <id>` runs.
<AccordionGroup>
<Accordion title="Resolving plugin id vs npm spec">
When you pass a plugin id, OpenClaw reuses the recorded install spec for that plugin. That means previously stored dist-tags such as `@beta` and exact pinned versions continue to be used on later `update <id>` runs.
For npm installs, you can also pass an explicit npm package spec with a dist-tag
or exact version. OpenClaw resolves that package name back to the tracked plugin
record, updates that installed plugin, and records the new npm spec for future
id-based updates.
For npm installs, you can also pass an explicit npm package spec with a dist-tag or exact version. OpenClaw resolves that package name back to the tracked plugin record, updates that installed plugin, and records the new npm spec for future id-based updates.
Passing the npm package name without a version or tag also resolves back to the
tracked plugin record. Use this when a plugin was pinned to an exact version and
you want to move it back to the registry's default release line.
Passing the npm package name without a version or tag also resolves back to the tracked plugin record. Use this when a plugin was pinned to an exact version and you want to move it back to the registry's default release line.
Before a live npm update, OpenClaw checks the installed package version against
the npm registry metadata. If the installed version and recorded artifact
identity already match the resolved target, the update is skipped without
downloading, reinstalling, or rewriting `openclaw.json`.
</Accordion>
<Accordion title="Version checks and integrity drift">
Before a live npm update, OpenClaw checks the installed package version against the npm registry metadata. If the installed version and recorded artifact identity already match the resolved target, the update is skipped without downloading, reinstalling, or rewriting `openclaw.json`.
When a stored integrity hash exists and the fetched artifact hash changes,
OpenClaw treats that as npm artifact drift. The interactive
`openclaw plugins update` command prints the expected and actual hashes and asks
for confirmation before proceeding. Non-interactive update helpers fail closed
unless the caller supplies an explicit continuation policy.
When a stored integrity hash exists and the fetched artifact hash changes, OpenClaw treats that as npm artifact drift. The interactive `openclaw plugins update` command prints the expected and actual hashes and asks for confirmation before proceeding. Non-interactive update helpers fail closed unless the caller supplies an explicit continuation policy.
`--dangerously-force-unsafe-install` is also available on `plugins update` as a
break-glass override for built-in dangerous-code scan false positives during
plugin updates. It still does not bypass plugin `before_install` policy blocks
or scan-failure blocking, and it only applies to plugin updates, not hook-pack
updates.
</Accordion>
<Accordion title="--dangerously-force-unsafe-install on update">
`--dangerously-force-unsafe-install` is also available on `plugins update` as a break-glass override for built-in dangerous-code scan false positives during plugin updates. It still does not bypass plugin `before_install` policy blocks or scan-failure blocking, and it only applies to plugin updates, not hook-pack updates.
</Accordion>
</AccordionGroup>
### Inspect
@@ -317,10 +277,7 @@ openclaw plugins inspect <id>
openclaw plugins inspect <id> --json
```
Deep introspection for a single plugin. Shows identity, load status, source,
registered capabilities, hooks, tools, commands, services, gateway methods,
HTTP routes, policy flags, diagnostics, install metadata, bundle capabilities,
and any detected MCP or LSP server support.
Deep introspection for a single plugin. Shows identity, load status, source, registered capabilities, hooks, tools, commands, services, gateway methods, HTTP routes, policy flags, diagnostics, install metadata, bundle capabilities, and any detected MCP or LSP server support.
Each plugin is classified by what it actually registers at runtime:
@@ -331,13 +288,9 @@ Each plugin is classified by what it actually registers at runtime:
See [Plugin shapes](/plugins/architecture#plugin-shapes) for more on the capability model.
The `--json` flag outputs a machine-readable report suitable for scripting and
auditing.
`inspect --all` renders a fleet-wide table with shape, capability kinds,
compatibility notices, bundle capabilities, and hook summary columns.
`info` is an alias for `inspect`.
<Note>
The `--json` flag outputs a machine-readable report suitable for scripting and auditing. `inspect --all` renders a fleet-wide table with shape, capability kinds, compatibility notices, bundle capabilities, and hook summary columns. `info` is an alias for `inspect`.
</Note>
### Doctor
@@ -345,13 +298,9 @@ compatibility notices, bundle capabilities, and hook summary columns.
openclaw plugins doctor
```
`doctor` reports plugin load errors, manifest/discovery diagnostics, and
compatibility notices. When everything is clean it prints `No plugin issues
detected.`
`doctor` reports plugin load errors, manifest/discovery diagnostics, and compatibility notices. When everything is clean it prints `No plugin issues detected.`
For module-shape failures such as missing `register`/`activate` exports, rerun
with `OPENCLAW_PLUGIN_LOAD_DEBUG=1` to include a compact export-shape summary in
the diagnostic output.
For module-shape failures such as missing `register`/`activate` exports, rerun with `OPENCLAW_PLUGIN_LOAD_DEBUG=1` to include a compact export-shape summary in the diagnostic output.
### Registry
@@ -361,20 +310,13 @@ openclaw plugins registry --refresh
openclaw plugins registry --json
```
The local plugin registry is OpenClaw's persisted cold read model for installed
plugin identity, enablement, source metadata, and contribution ownership.
Normal startup, provider owner lookup, channel setup classification, and plugin
inventory can read it without importing plugin runtime modules.
The local plugin registry is OpenClaw's persisted cold read model for installed plugin identity, enablement, source metadata, and contribution ownership. Normal startup, provider owner lookup, channel setup classification, and plugin inventory can read it without importing plugin runtime modules.
Use `plugins registry` to inspect whether the persisted registry is present,
current, or stale. Use `--refresh` to rebuild it from the persisted plugin
index, config policy, and manifest/package metadata. This is a repair path, not
a runtime activation path.
Use `plugins registry` to inspect whether the persisted registry is present, current, or stale. Use `--refresh` to rebuild it from the persisted plugin index, config policy, and manifest/package metadata. This is a repair path, not a runtime activation path.
`OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY=1` is a deprecated break-glass
compatibility switch for registry read failures. Prefer `plugins registry
--refresh` or `openclaw doctor --fix`; the env fallback is only for emergency
startup recovery while the migration rolls out.
<Warning>
`OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY=1` is a deprecated break-glass compatibility switch for registry read failures. Prefer `plugins registry --refresh` or `openclaw doctor --fix`; the env fallback is only for emergency startup recovery while the migration rolls out.
</Warning>
### Marketplace
@@ -383,13 +325,10 @@ openclaw plugins marketplace list <source>
openclaw plugins marketplace list <source> --json
```
Marketplace list accepts a local marketplace path, a `marketplace.json` path, a
GitHub shorthand like `owner/repo`, a GitHub repo URL, or a git URL. `--json`
prints the resolved source label plus the parsed marketplace manifest and
plugin entries.
Marketplace list accepts a local marketplace path, a `marketplace.json` path, a GitHub shorthand like `owner/repo`, a GitHub repo URL, or a git URL. `--json` prints the resolved source label plus the parsed marketplace manifest and plugin entries.
## Related
- [CLI reference](/cli)
- [Building plugins](/plugins/building-plugins)
- [CLI reference](/cli)
- [Community plugins](/plugins/community)

View File

@@ -39,7 +39,7 @@ openclaw --update
- `--json`: print machine-readable `UpdateRunResult` JSON, including
`postUpdate.plugins.integrityDrifts` when npm plugin artifact drift is
detected during post-update plugin sync.
- `--timeout <seconds>`: per-step timeout (default is 1200s).
- `--timeout <seconds>`: per-step timeout (default is 1800s).
- `--yes`: skip confirmation prompts (for example downgrade confirmation)
Note: downgrades require confirmation because older versions can break configuration.
@@ -67,7 +67,7 @@ offers to create one.
Options:
- `--timeout <seconds>`: timeout for each update step (default `1200`)
- `--timeout <seconds>`: timeout for each update step (default `1800`)
## What it does
@@ -83,10 +83,11 @@ install method aligned:
The Gateway core auto-updater (when enabled via config) reuses this same update path.
For package-manager installs, `openclaw update` resolves the target package
version before invoking the package manager. If the installed version exactly
matches the target and no update-channel change needs to be persisted, the
command exits as skipped before package install, plugin sync, completion refresh,
or gateway restart work.
version before invoking the package manager. Even when the installed version
already matches the target, the command refreshes the global package install,
then runs plugin sync, completion refresh, and restart work. This keeps packaged
sidecars and channel-owned plugin records aligned with the installed OpenClaw
build.
## Git checkout flow
@@ -114,6 +115,10 @@ differs from the stored install record, `openclaw update` aborts that plugin
artifact update instead of installing it. Reinstall or update the plugin
explicitly only after verifying that you trust the new artifact.
Post-update plugin sync failures fail the update result and stop restart
follow-up work. Fix the plugin install/update error, then rerun
`openclaw update`.
If pnpm bootstrap still fails, the updater now stops early with a package-manager-specific error instead of trying `npm run build` inside the checkout.
## `--update` shorthand

View File

@@ -4,26 +4,23 @@ read_when:
- You need to explain the agent workspace or its file layout
- You want to back up or migrate an agent workspace
title: "Agent workspace"
sidebarTitle: "Agent workspace"
---
The workspace is the agent's home. It is the only working directory used for
file tools and for workspace context. Keep it private and treat it as memory.
The workspace is the agent's home. It is the only working directory used for file tools and for workspace context. Keep it private and treat it as memory.
This is separate from `~/.openclaw/`, which stores config, credentials, and
sessions.
This is separate from `~/.openclaw/`, which stores config, credentials, and sessions.
**Important:** the workspace is the **default cwd**, not a hard sandbox. Tools
resolve relative paths against the workspace, but absolute paths can still reach
elsewhere on the host unless sandboxing is enabled. If you need isolation, use
[`agents.defaults.sandbox`](/gateway/sandboxing) (and/or peragent sandbox config).
When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate
inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspace.
<Warning>
The workspace is the **default cwd**, not a hard sandbox. Tools resolve relative paths against the workspace, but absolute paths can still reach elsewhere on the host unless sandboxing is enabled. If you need isolation, use [`agents.defaults.sandbox`](/gateway/sandboxing) (and/or per-agent sandbox config).
When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspace.
</Warning>
## Default location
- Default: `~/.openclaw/workspace`
- If `OPENCLAW_PROFILE` is set and not `"default"`, the default becomes
`~/.openclaw/workspace-<profile>`.
- If `OPENCLAW_PROFILE` is set and not `"default"`, the default becomes `~/.openclaw/workspace-<profile>`.
- Override in `~/.openclaw/openclaw.json`:
```json5
@@ -36,13 +33,13 @@ inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspac
}
```
`openclaw onboard`, `openclaw configure`, or `openclaw setup` will create the
workspace and seed the bootstrap files if they are missing.
Sandbox seed copies only accept regular in-workspace files; symlink/hardlink
aliases that resolve outside the source workspace are ignored.
`openclaw onboard`, `openclaw configure`, or `openclaw setup` will create the workspace and seed the bootstrap files if they are missing.
If you already manage the workspace files yourself, you can disable bootstrap
file creation:
<Note>
Sandbox seed copies only accept regular in-workspace files; symlink/hardlink aliases that resolve outside the source workspace are ignored.
</Note>
If you already manage the workspace files yourself, you can disable bootstrap file creation:
```json5
{ agents: { defaults: { skipBootstrap: true } } }
@@ -50,80 +47,60 @@ file creation:
## Extra workspace folders
Older installs may have created `~/openclaw`. Keeping multiple workspace
directories around can cause confusing auth or state drift, because only one
workspace is active at a time.
Older installs may have created `~/openclaw`. Keeping multiple workspace directories around can cause confusing auth or state drift, because only one workspace is active at a time.
**Recommendation:** keep a single active workspace. If you no longer use the
extra folders, archive or move them to Trash (for example `trash ~/openclaw`).
If you intentionally keep multiple workspaces, make sure
`agents.defaults.workspace` points to the active one.
<Note>
**Recommendation:** keep a single active workspace. If you no longer use the extra folders, archive or move them to Trash (for example `trash ~/openclaw`). If you intentionally keep multiple workspaces, make sure `agents.defaults.workspace` points to the active one.
`openclaw doctor` warns when it detects extra workspace directories.
</Note>
## Workspace file map (what each file means)
## Workspace file map
These are the standard files OpenClaw expects inside the workspace:
- `AGENTS.md`
- Operating instructions for the agent and how it should use memory.
- Loaded at the start of every session.
- Good place for rules, priorities, and "how to behave" details.
<AccordionGroup>
<Accordion title="AGENTS.md — operating instructions">
Operating instructions for the agent and how it should use memory. Loaded at the start of every session. Good place for rules, priorities, and "how to behave" details.
</Accordion>
<Accordion title="SOUL.md — persona and tone">
Persona, tone, and boundaries. Loaded every session. Guide: [SOUL.md personality guide](/concepts/soul).
</Accordion>
<Accordion title="USER.md — who the user is">
Who the user is and how to address them. Loaded every session.
</Accordion>
<Accordion title="IDENTITY.md — name, vibe, emoji">
The agent's name, vibe, and emoji. Created/updated during the bootstrap ritual.
</Accordion>
<Accordion title="TOOLS.md — local tool conventions">
Notes about your local tools and conventions. Does not control tool availability; it is only guidance.
</Accordion>
<Accordion title="HEARTBEAT.md — heartbeat checklist">
Optional tiny checklist for heartbeat runs. Keep it short to avoid token burn.
</Accordion>
<Accordion title="BOOT.md — startup checklist">
Optional startup checklist run automatically on gateway restart (when [internal hooks](/automation/hooks) are enabled). Keep it short; use the message tool for outbound sends.
</Accordion>
<Accordion title="BOOTSTRAP.md — first-run ritual">
One-time first-run ritual. Only created for a brand-new workspace. Delete it after the ritual is complete.
</Accordion>
<Accordion title="memory/YYYY-MM-DD.md — daily memory log">
Daily memory log (one file per day). Recommended to read today + yesterday on session start.
</Accordion>
<Accordion title="MEMORY.md — curated long-term memory (optional)">
Curated long-term memory. Only load in the main, private session (not shared/group contexts). See [Memory](/concepts/memory) for the workflow and automatic memory flush.
</Accordion>
<Accordion title="skills/ — workspace skills (optional)">
Workspace-specific skills. Highest-precedence skill location for that workspace. Overrides project agent skills, personal agent skills, managed skills, bundled skills, and `skills.load.extraDirs` when names collide.
</Accordion>
<Accordion title="canvas/ — Canvas UI files (optional)">
Canvas UI files for node displays (for example `canvas/index.html`).
</Accordion>
</AccordionGroup>
- `SOUL.md`
- Persona, tone, and boundaries.
- Loaded every session.
- Guide: [SOUL.md Personality Guide](/concepts/soul)
- `USER.md`
- Who the user is and how to address them.
- Loaded every session.
- `IDENTITY.md`
- The agent's name, vibe, and emoji.
- Created/updated during the bootstrap ritual.
- `TOOLS.md`
- Notes about your local tools and conventions.
- Does not control tool availability; it is only guidance.
- `HEARTBEAT.md`
- Optional tiny checklist for heartbeat runs.
- Keep it short to avoid token burn.
- `BOOT.md`
- Optional startup checklist run automatically on gateway restart (when [internal hooks](/automation/hooks) are enabled).
- Keep it short; use the message tool for outbound sends.
- `BOOTSTRAP.md`
- One-time first-run ritual.
- Only created for a brand-new workspace.
- Delete it after the ritual is complete.
- `memory/YYYY-MM-DD.md`
- Daily memory log (one file per day).
- Recommended to read today + yesterday on session start.
- `MEMORY.md` (optional)
- Curated long-term memory.
- Only load in the main, private session (not shared/group contexts).
See [Memory](/concepts/memory) for the workflow and automatic memory flush.
- `skills/` (optional)
- Workspace-specific skills.
- Highest-precedence skill location for that workspace.
- Overrides project agent skills, personal agent skills, managed skills, bundled skills, and `skills.load.extraDirs` when names collide.
- `canvas/` (optional)
- Canvas UI files for node displays (for example `canvas/index.html`).
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into
the session and continues. Large bootstrap files are truncated when injected;
adjust limits with `agents.defaults.bootstrapMaxChars` (default: 12000) and
`agents.defaults.bootstrapTotalMaxChars` (default: 60000).
`openclaw setup` can recreate missing defaults without overwriting existing
files.
<Note>
If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; adjust limits with `agents.defaults.bootstrapMaxChars` (default: 12000) and `agents.defaults.bootstrapTotalMaxChars` (default: 60000). `openclaw setup` can recreate missing defaults without overwriting existing files.
</Note>
## What is NOT in the workspace
@@ -135,83 +112,82 @@ These live under `~/.openclaw/` and should NOT be committed to the workspace rep
- `~/.openclaw/agents/<agentId>/sessions/` (session transcripts + metadata)
- `~/.openclaw/skills/` (managed skills)
If you need to migrate sessions or config, copy them separately and keep them
out of version control.
If you need to migrate sessions or config, copy them separately and keep them out of version control.
## Git backup (recommended, private)
Treat the workspace as private memory. Put it in a **private** git repo so it is
backed up and recoverable.
Treat the workspace as private memory. Put it in a **private** git repo so it is backed up and recoverable.
Run these steps on the machine where the Gateway runs (that is where the
workspace lives).
Run these steps on the machine where the Gateway runs (that is where the workspace lives).
### 1) Initialize the repo
<Steps>
<Step title="Initialize the repo">
If git is installed, brand-new workspaces are initialized automatically. If this workspace is not already a repo, run:
If git is installed, brand-new workspaces are initialized automatically. If this
workspace is not already a repo, run:
```bash
cd ~/.openclaw/workspace
git init
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/
git commit -m "Add agent workspace"
```
```bash
cd ~/.openclaw/workspace
git init
git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/
git commit -m "Add agent workspace"
```
</Step>
<Step title="Add a private remote">
<Tabs>
<Tab title="GitHub web UI">
1. Create a new **private** repository on GitHub.
2. Do not initialize with a README (avoids merge conflicts).
3. Copy the HTTPS remote URL.
4. Add the remote and push:
### 2) Add a private remote (beginner-friendly options)
```bash
git branch -M main
git remote add origin <https-url>
git push -u origin main
```
</Tab>
<Tab title="GitHub CLI (gh)">
```bash
gh auth login
gh repo create openclaw-workspace --private --source . --remote origin --push
```
</Tab>
<Tab title="GitLab web UI">
1. Create a new **private** repository on GitLab.
2. Do not initialize with a README (avoids merge conflicts).
3. Copy the HTTPS remote URL.
4. Add the remote and push:
Option A: GitHub web UI
```bash
git branch -M main
git remote add origin <https-url>
git push -u origin main
```
</Tab>
</Tabs>
1. Create a new **private** repository on GitHub.
2. Do not initialize with a README (avoids merge conflicts).
3. Copy the HTTPS remote URL.
4. Add the remote and push:
```bash
git branch -M main
git remote add origin <https-url>
git push -u origin main
```
Option B: GitHub CLI (`gh`)
```bash
gh auth login
gh repo create openclaw-workspace --private --source . --remote origin --push
```
Option C: GitLab web UI
1. Create a new **private** repository on GitLab.
2. Do not initialize with a README (avoids merge conflicts).
3. Copy the HTTPS remote URL.
4. Add the remote and push:
```bash
git branch -M main
git remote add origin <https-url>
git push -u origin main
```
### 3) Ongoing updates
```bash
git status
git add .
git commit -m "Update memory"
git push
```
</Step>
<Step title="Ongoing updates">
```bash
git status
git add .
git commit -m "Update memory"
git push
```
</Step>
</Steps>
## Do not commit secrets
<Warning>
Even in a private repo, avoid storing secrets in the workspace:
- API keys, OAuth tokens, passwords, or private credentials.
- Anything under `~/.openclaw/`.
- Raw dumps of chats or sensitive attachments.
If you must store sensitive references, use placeholders and keep the real
secret elsewhere (password manager, environment variables, or `~/.openclaw/`).
If you must store sensitive references, use placeholders and keep the real secret elsewhere (password manager, environment variables, or `~/.openclaw/`).
</Warning>
Suggested `.gitignore` starter:
@@ -225,22 +201,29 @@ Suggested `.gitignore` starter:
## Moving the workspace to a new machine
1. Clone the repo to the desired path (default `~/.openclaw/workspace`).
2. Set `agents.defaults.workspace` to that path in `~/.openclaw/openclaw.json`.
3. Run `openclaw setup --workspace <path>` to seed any missing files.
4. If you need sessions, copy `~/.openclaw/agents/<agentId>/sessions/` from the
old machine separately.
<Steps>
<Step title="Clone the repo">
Clone the repo to the desired path (default `~/.openclaw/workspace`).
</Step>
<Step title="Update config">
Set `agents.defaults.workspace` to that path in `~/.openclaw/openclaw.json`.
</Step>
<Step title="Seed missing files">
Run `openclaw setup --workspace <path>` to seed any missing files.
</Step>
<Step title="Copy sessions (optional)">
If you need sessions, copy `~/.openclaw/agents/<agentId>/sessions/` from the old machine separately.
</Step>
</Steps>
## Advanced notes
- Multi-agent routing can use different workspaces per agent. See
[Channel routing](/channels/channel-routing) for routing configuration.
- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox
workspaces under `agents.defaults.sandbox.workspaceRoot`.
- Multi-agent routing can use different workspaces per agent. See [Channel routing](/channels/channel-routing) for routing configuration.
- If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox workspaces under `agents.defaults.sandbox.workspaceRoot`.
## Related
- [Standing Orders](/automation/standing-orders) — persistent instructions in workspace files
- [Heartbeat](/gateway/heartbeat) — HEARTBEAT.md workspace file
- [Session](/concepts/session) — session storage paths
- [Sandboxing](/gateway/sandboxing) — workspace access in sandboxed environments
- [Session](/concepts/session) — session storage paths
- [Standing orders](/automation/standing-orders) — persistent instructions in workspace files

View File

@@ -21,8 +21,12 @@ calls paired with their matching `toolResult` entries. If a split point lands
inside a tool block, OpenClaw moves the boundary so the pair stays together and
the current unsummarized tail is preserved.
The full conversation history stays on disk. Compaction only changes what the
model sees on the next turn.
By default, OpenClaw also rewrites the session transcript after compaction and
removes the message entries that were summarized. The persisted summary and
recent unsummarized tail remain on disk. Set
`agents.defaults.compaction.truncateAfterCompaction` to `false` if you need the
older behavior where compaction only changed what the model saw on the next
turn and left the full transcript intact.
## Auto-compaction

View File

@@ -253,6 +253,10 @@ A no-op `compact()` is unsafe for an active non-owning engine because it disable
The slot is exclusive at run time — only one registered context engine is resolved for a given run or compaction operation. Other enabled `kind: "context-engine"` plugins can still load and run their registration code; `plugins.slots.contextEngine` only selects which registered engine id OpenClaw resolves when it needs a context engine.
</Note>
<Note>
**Plugin uninstall:** when you uninstall the plugin currently selected as `plugins.slots.contextEngine`, OpenClaw resets the slot back to the default (`legacy`). The same reset behavior applies to `plugins.slots.memory`. No manual config edit is required.
</Note>
## Relationship to compaction and memory
<AccordionGroup>

View File

@@ -1,17 +1,18 @@
---
summary: "Background memory consolidation with light, deep, and REM phases plus a Dream Diary"
title: "Dreaming"
sidebarTitle: "Dreaming"
read_when:
- You want memory promotion to run automatically
- You want to understand what each dreaming phase does
- You want to tune consolidation without polluting MEMORY.md
---
Dreaming is the background memory consolidation system in `memory-core`.
It helps OpenClaw move strong short-term signals into durable memory while
keeping the process explainable and reviewable.
Dreaming is the background memory consolidation system in `memory-core`. It helps OpenClaw move strong short-term signals into durable memory while keeping the process explainable and reviewable.
<Note>
Dreaming is **opt-in** and disabled by default.
</Note>
## What dreaming writes
@@ -32,69 +33,63 @@ Dreaming uses three cooperative phases:
| Deep | Score and promote durable candidates | Yes (`MEMORY.md`) |
| REM | Reflect on themes and recurring ideas | No |
These phases are internal implementation details, not separate user-configured
"modes."
These phases are internal implementation details, not separate user-configured "modes."
### Light phase
<AccordionGroup>
<Accordion title="Light phase">
Light phase ingests recent daily memory signals and recall traces, dedupes them, and stages candidate lines.
Light phase ingests recent daily memory signals and recall traces, dedupes them,
and stages candidate lines.
- Reads from short-term recall state, recent daily memory files, and redacted session transcripts when available.
- Writes a managed `## Light Sleep` block when storage includes inline output.
- Records reinforcement signals for later deep ranking.
- Never writes to `MEMORY.md`.
- Reads from short-term recall state, recent daily memory files, and redacted session transcripts when available.
- Writes a managed `## Light Sleep` block when storage includes inline output.
- Records reinforcement signals for later deep ranking.
- Never writes to `MEMORY.md`.
</Accordion>
<Accordion title="Deep phase">
Deep phase decides what becomes long-term memory.
### Deep phase
- Ranks candidates using weighted scoring and threshold gates.
- Requires `minScore`, `minRecallCount`, and `minUniqueQueries` to pass.
- Rehydrates snippets from live daily files before writing, so stale/deleted snippets are skipped.
- Appends promoted entries to `MEMORY.md`.
- Writes a `## Deep Sleep` summary into `DREAMS.md` and optionally writes `memory/dreaming/deep/YYYY-MM-DD.md`.
Deep phase decides what becomes long-term memory.
</Accordion>
<Accordion title="REM phase">
REM phase extracts patterns and reflective signals.
- Ranks candidates using weighted scoring and threshold gates.
- Requires `minScore`, `minRecallCount`, and `minUniqueQueries` to pass.
- Rehydrates snippets from live daily files before writing, so stale/deleted snippets are skipped.
- Appends promoted entries to `MEMORY.md`.
- Writes a `## Deep Sleep` summary into `DREAMS.md` and optionally writes `memory/dreaming/deep/YYYY-MM-DD.md`.
- Builds theme and reflection summaries from recent short-term traces.
- Writes a managed `## REM Sleep` block when storage includes inline output.
- Records REM reinforcement signals used by deep ranking.
- Never writes to `MEMORY.md`.
### REM phase
REM phase extracts patterns and reflective signals.
- Builds theme and reflection summaries from recent short-term traces.
- Writes a managed `## REM Sleep` block when storage includes inline output.
- Records REM reinforcement signals used by deep ranking.
- Never writes to `MEMORY.md`.
</Accordion>
</AccordionGroup>
## Session transcript ingestion
Dreaming can ingest redacted session transcripts into the dreaming corpus. When
transcripts are available, they are fed into the light phase alongside daily
memory signals and recall traces. Personal and sensitive content is redacted
before ingestion.
Dreaming can ingest redacted session transcripts into the dreaming corpus. When transcripts are available, they are fed into the light phase alongside daily memory signals and recall traces. Personal and sensitive content is redacted before ingestion.
## Dream Diary
Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`.
After each phase has enough material, `memory-core` runs a best-effort background
subagent turn (using the default runtime model) and appends a short diary entry.
Dreaming also keeps a narrative **Dream Diary** in `DREAMS.md`. After each phase has enough material, `memory-core` runs a best-effort background subagent turn (using the default runtime model) and appends a short diary entry.
This diary is for human reading in the Dreams UI, not a promotion source.
Dreaming-generated diary/report artifacts are excluded from short-term
promotion. Only grounded memory snippets are eligible to promote into
`MEMORY.md`.
<Note>
This diary is for human reading in the Dreams UI, not a promotion source. Dreaming-generated diary/report artifacts are excluded from short-term promotion. Only grounded memory snippets are eligible to promote into `MEMORY.md`.
</Note>
There is also a grounded historical backfill lane for review and recovery work:
- `memory rem-harness --path ... --grounded` previews grounded diary output from historical `YYYY-MM-DD.md` notes.
- `memory rem-backfill --path ...` writes reversible grounded diary entries into `DREAMS.md`.
- `memory rem-backfill --path ... --stage-short-term` stages grounded durable candidates into the same short-term evidence store the normal deep phase already uses.
- `memory rem-backfill --rollback` and `--rollback-short-term` remove those staged backfill artifacts without touching ordinary diary entries or live short-term recall.
<AccordionGroup>
<Accordion title="Backfill commands">
- `memory rem-harness --path ... --grounded` previews grounded diary output from historical `YYYY-MM-DD.md` notes.
- `memory rem-backfill --path ...` writes reversible grounded diary entries into `DREAMS.md`.
- `memory rem-backfill --path ... --stage-short-term` stages grounded durable candidates into the same short-term evidence store the normal deep phase already uses.
- `memory rem-backfill --rollback` and `--rollback-short-term` remove those staged backfill artifacts without touching ordinary diary entries or live short-term recall.
</Accordion>
</AccordionGroup>
The Control UI exposes the same diary backfill/reset flow so you can inspect
results in the Dreams scene before deciding whether the grounded candidates
deserve promotion. The Scene also shows a distinct grounded lane so you can see
which staged short-term entries came from historical replay, which promoted
items were grounded-led, and clear only grounded-only staged entries without
touching ordinary live short-term state.
The Control UI exposes the same diary backfill/reset flow so you can inspect results in the Dreams scene before deciding whether the grounded candidates deserve promotion. The Scene also shows a distinct grounded lane so you can see which staged short-term entries came from historical replay, which promoted items were grounded-led, and clear only grounded-only staged entries without touching ordinary live short-term state.
## Deep ranking signals
@@ -109,13 +104,11 @@ Deep ranking uses six weighted base signals plus phase reinforcement:
| Consolidation | 0.10 | Multi-day recurrence strength |
| Conceptual richness | 0.06 | Concept-tag density from snippet/path |
Light and REM phase hits add a small recency-decayed boost from
`memory/.dreams/phase-signals.json`.
Light and REM phase hits add a small recency-decayed boost from `memory/.dreams/phase-signals.json`.
## Scheduling
When enabled, `memory-core` auto-manages one cron job for a full dreaming
sweep. Each sweep runs phases in order: light -> REM -> deep.
When enabled, `memory-core` auto-manages one cron job for a full dreaming sweep. Each sweep runs phases in order: light → REM → deep.
Default cadence behavior:
@@ -125,43 +118,44 @@ Default cadence behavior:
## Quick start
Enable dreaming:
```json
{
"plugins": {
"entries": {
"memory-core": {
"config": {
"dreaming": {
"enabled": true
<Tabs>
<Tab title="Enable dreaming">
```json
{
"plugins": {
"entries": {
"memory-core": {
"config": {
"dreaming": {
"enabled": true
}
}
}
}
}
}
}
}
```
Enable dreaming with a custom sweep cadence:
```json
{
"plugins": {
"entries": {
"memory-core": {
"config": {
"dreaming": {
"enabled": true,
"timezone": "America/Los_Angeles",
"frequency": "0 */6 * * *"
```
</Tab>
<Tab title="Custom sweep cadence">
```json
{
"plugins": {
"entries": {
"memory-core": {
"config": {
"dreaming": {
"enabled": true,
"timezone": "America/Los_Angeles",
"frequency": "0 */6 * * *"
}
}
}
}
}
}
}
}
```
```
</Tab>
</Tabs>
## Slash command
@@ -174,47 +168,52 @@ Enable dreaming with a custom sweep cadence:
## CLI workflow
Use CLI promotion for preview or manual apply:
<Tabs>
<Tab title="Promotion preview / apply">
```bash
openclaw memory promote
openclaw memory promote --apply
openclaw memory promote --limit 5
openclaw memory status --deep
```
```bash
openclaw memory promote
openclaw memory promote --apply
openclaw memory promote --limit 5
openclaw memory status --deep
```
Manual `memory promote` uses deep-phase thresholds by default unless overridden with CLI flags.
Manual `memory promote` uses deep-phase thresholds by default unless overridden
with CLI flags.
</Tab>
<Tab title="Explain promotion">
Explain why a specific candidate would or would not promote:
Explain why a specific candidate would or would not promote:
```bash
openclaw memory promote-explain "router vlan"
openclaw memory promote-explain "router vlan" --json
```
```bash
openclaw memory promote-explain "router vlan"
openclaw memory promote-explain "router vlan" --json
```
</Tab>
<Tab title="REM harness preview">
Preview REM reflections, candidate truths, and deep promotion output without writing anything:
Preview REM reflections, candidate truths, and deep promotion output without
writing anything:
```bash
openclaw memory rem-harness
openclaw memory rem-harness --json
```
```bash
openclaw memory rem-harness
openclaw memory rem-harness --json
```
</Tab>
</Tabs>
## Key defaults
All settings live under `plugins.entries.memory-core.config.dreaming`.
| Key | Default |
| ----------- | ----------- |
| `enabled` | `false` |
| `frequency` | `0 3 * * *` |
<ParamField path="enabled" type="boolean" default="false">
Enable or disable the dreaming sweep.
</ParamField>
<ParamField path="frequency" type="string" default="0 3 * * *">
Cron cadence for the full dreaming sweep.
</ParamField>
Phase policy, thresholds, and storage behavior are internal implementation
details (not user-facing config).
See [Memory configuration reference](/reference/memory-config#dreaming)
for the full key list.
<Note>
Phase policy, thresholds, and storage behavior are internal implementation details (not user-facing config). See [Memory configuration reference](/reference/memory-config#dreaming) for the full key list.
</Note>
## Dreams UI
@@ -230,6 +229,6 @@ When enabled, the Gateway **Dreams** tab shows:
## Related
- [Memory](/concepts/memory)
- [Memory Search](/concepts/memory-search)
- [memory CLI](/cli/memory)
- [Memory CLI](/cli/memory)
- [Memory configuration reference](/reference/memory-config)
- [Memory search](/concepts/memory-search)

View File

@@ -176,6 +176,11 @@ OpenClaw resolves that behavior by conversation type:
- Groups/channels allow silence by default.
- Internal orchestration allows silence by default.
OpenClaw also uses silent replies for internal runner failures that happen
before any assistant reply in non-direct chats, so groups/channels do not see
gateway error boilerplate. Direct chats show compact failure copy by default;
raw runner details are shown only when `/verbose` is `on` or `full`.
Defaults live under `agents.defaults.silentReply` and
`agents.defaults.silentReplyRewrite`; `surfaces.<id>.silentReply` and
`surfaces.<id>.silentReplyRewrite` can override them per surface.

View File

@@ -5,6 +5,7 @@ read_when:
- Updating failover rules for auth profiles or models
- Understanding how session model overrides interact with fallback retries
title: "Model failover"
sidebarTitle: "Model failover"
---
OpenClaw handles failures in two stages:
@@ -18,29 +19,31 @@ This doc explains the runtime rules and the data that backs them.
For a normal text run, OpenClaw evaluates candidates in this order:
1. The currently selected session model.
2. Configured `agents.defaults.model.fallbacks` in order.
3. The configured primary model at the end when the run started from an override.
<Steps>
<Step title="Resolve session state">
Resolve the active session model and auth-profile preference.
</Step>
<Step title="Build candidate chain">
Build the model candidate chain from the currently selected session model, then `agents.defaults.model.fallbacks` in order, ending with the configured primary when the run started from an override.
</Step>
<Step title="Try the current provider">
Try the current provider with auth-profile rotation/cooldown rules.
</Step>
<Step title="Advance on failover-worthy errors">
If that provider is exhausted with a failover-worthy error, move to the next model candidate.
</Step>
<Step title="Persist fallback override">
Persist the selected fallback override before the retry starts so other session readers see the same provider/model the runner is about to use.
</Step>
<Step title="Roll back narrowly on failure">
If the fallback candidate fails, roll back only the fallback-owned session override fields when they still match that failed candidate.
</Step>
<Step title="Throw FallbackSummaryError if exhausted">
If every candidate fails, throw a `FallbackSummaryError` with per-attempt detail and the soonest cooldown expiry when one is known.
</Step>
</Steps>
Inside each candidate, OpenClaw tries auth-profile failover before advancing to
the next model candidate.
High-level sequence:
1. Resolve the active session model and auth-profile preference.
2. Build the model candidate chain.
3. Try the current provider with auth-profile rotation/cooldown rules.
4. If that provider is exhausted with a failover-worthy error, move to the next
model candidate.
5. Persist the selected fallback override before the retry starts so other
session readers see the same provider/model the runner is about to use.
6. If the fallback candidate fails, roll back only the fallback-owned session
override fields when they still match that failed candidate.
7. If every candidate fails, throw a `FallbackSummaryError` with per-attempt
detail and the soonest cooldown expiry when one is known.
This is intentionally narrower than "save and restore the whole session". The
reply runner only persists the model-selection fields it owns for fallback:
This is intentionally narrower than "save and restore the whole session". The reply runner only persists the model-selection fields it owns for fallback:
- `providerOverride`
- `modelOverride`
@@ -48,9 +51,7 @@ reply runner only persists the model-selection fields it owns for fallback:
- `authProfileOverrideSource`
- `authProfileOverrideCompactionCount`
That prevents a failed fallback retry from overwriting newer unrelated session
mutations such as manual `/model` changes or session rotation updates that
happened while the attempt was running.
That prevents a failed fallback retry from overwriting newer unrelated session mutations such as manual `/model` changes or session rotation updates that happened while the attempt was running.
## Auth storage (keys + OAuth)
@@ -61,7 +62,7 @@ OpenClaw uses **auth profiles** for both API keys and OAuth tokens.
- Config `auth.profiles` / `auth.order` are **metadata + routing only** (no secrets).
- Legacy import-only OAuth file: `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use).
More detail: [/concepts/oauth](/concepts/oauth)
More detail: [OAuth](/concepts/oauth)
Credential types:
@@ -81,9 +82,17 @@ Profiles live in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` under `
When a provider has multiple profiles, OpenClaw chooses an order like this:
1. **Explicit config**: `auth.order[provider]` (if set).
2. **Configured profiles**: `auth.profiles` filtered by provider.
3. **Stored profiles**: entries in `auth-profiles.json` for the provider.
<Steps>
<Step title="Explicit config">
`auth.order[provider]` (if set).
</Step>
<Step title="Configured profiles">
`auth.profiles` filtered by provider.
</Step>
<Step title="Stored profiles">
Entries in `auth-profiles.json` for the provider.
</Step>
</Steps>
If no explicit order is configured, OpenClaw uses a roundrobin order:
@@ -93,20 +102,17 @@ If no explicit order is configured, OpenClaw uses a roundrobin order:
### Session stickiness (cache-friendly)
OpenClaw **pins the chosen auth profile per session** to keep provider caches warm.
It does **not** rotate on every request. The pinned profile is reused until:
OpenClaw **pins the chosen auth profile per session** to keep provider caches warm. It does **not** rotate on every request. The pinned profile is reused until:
- the session is reset (`/new` / `/reset`)
- a compaction completes (compaction count increments)
- the profile is in cooldown/disabled
Manual selection via `/model …@<profileId>` sets a **user override** for that session
and is not autorotated until a new session starts.
Manual selection via `/model …@<profileId>` sets a **user override** for that session and is not auto-rotated until a new session starts.
Autopinned profiles (selected by the session router) are treated as a **preference**:
they are tried first, but OpenClaw may rotate to another profile on rate limits/timeouts.
Userpinned profiles stay locked to that profile; if it fails and model fallbacks
are configured, OpenClaw moves to the next model instead of switching profiles.
<Note>
Auto-pinned profiles (selected by the session router) are treated as a **preference**: they are tried first, but OpenClaw may rotate to another profile on rate limits/timeouts. User-pinned profiles stay locked to that profile; if it fails and model fallbacks are configured, OpenClaw moves to the next model instead of switching profiles.
</Note>
### Why OAuth can "look lost"
@@ -117,45 +123,31 @@ If you have both an OAuth profile and an API key profile for the same provider,
## Cooldowns
When a profile fails due to auth/ratelimit errors (or a timeout that looks
like rate limiting), OpenClaw marks it in cooldown and moves to the next profile.
That rate-limit bucket is broader than plain `429`: it also includes provider
messages such as `Too many concurrent requests`, `ThrottlingException`,
`concurrency limit reached`, `workers_ai ... quota limit exceeded`,
`throttled`, `resource exhausted`, and periodic usage-window limits such as
`weekly/monthly limit reached`.
Format/invalidrequest errors (for example Cloud Code Assist tool call ID
validation failures) are treated as failoverworthy and use the same cooldowns.
OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`,
`stop reason: error`, and `reason: error` are classified as timeout/failover
signals.
Generic server text can also land in that timeout bucket when the source matches
a known transient pattern. For example, the bare pi-ai stream-wrapper message
`An unknown error occurred` is treated as failover-worthy for every provider
because pi-ai emits it when provider streams end with `stopReason: "aborted"` or
`stopReason: "error"` without specific details. JSON `api_error` payloads with
transient server text such as `internal server error`, `unknown error, 520`,
`upstream error`, or `backend error` are also treated as failover-worthy
timeouts.
OpenRouter-specific generic upstream text such as bare `Provider returned error`
is treated as timeout only when the provider context is actually OpenRouter.
Generic internal fallback text such as `LLM request failed with an unknown
error.` stays conservative and does not trigger failover by itself.
When a profile fails due to auth/rate-limit errors (or a timeout that looks like rate limiting), OpenClaw marks it in cooldown and moves to the next profile.
Some provider SDKs may otherwise sleep for a long `Retry-After` window before
returning control to OpenClaw. For Stainless-based SDKs such as Anthropic and
OpenAI, OpenClaw caps SDK-internal `retry-after-ms` / `retry-after` waits at 60
seconds by default and surfaces longer retryable responses immediately so this
failover path can run. Tune or disable the cap with
`OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS`; see [/concepts/retry](/concepts/retry).
<AccordionGroup>
<Accordion title="What lands in the rate-limit / timeout bucket">
That rate-limit bucket is broader than plain `429`: it also includes provider messages such as `Too many concurrent requests`, `ThrottlingException`, `concurrency limit reached`, `workers_ai ... quota limit exceeded`, `throttled`, `resource exhausted`, and periodic usage-window limits such as `weekly/monthly limit reached`.
Rate-limit cooldowns can also be model-scoped:
Format/invalid-request errors (for example Cloud Code Assist tool call ID validation failures) are treated as failover-worthy and use the same cooldowns. OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`, `stop reason: error`, and `reason: error` are classified as timeout/failover signals.
- OpenClaw records `cooldownModel` for rate-limit failures when the failing
model id is known.
- A sibling model on the same provider can still be tried when the cooldown is
scoped to a different model.
- Billing/disabled windows still block the whole profile across models.
Generic server text can also land in that timeout bucket when the source matches a known transient pattern. For example, the bare pi-ai stream-wrapper message `An unknown error occurred` is treated as failover-worthy for every provider because pi-ai emits it when provider streams end with `stopReason: "aborted"` or `stopReason: "error"` without specific details. JSON `api_error` payloads with transient server text such as `internal server error`, `unknown error, 520`, `upstream error`, or `backend error` are also treated as failover-worthy timeouts.
OpenRouter-specific generic upstream text such as bare `Provider returned error` is treated as timeout only when the provider context is actually OpenRouter. Generic internal fallback text such as `LLM request failed with an unknown error.` stays conservative and does not trigger failover by itself.
</Accordion>
<Accordion title="SDK retry-after caps">
Some provider SDKs may otherwise sleep for a long `Retry-After` window before returning control to OpenClaw. For Stainless-based SDKs such as Anthropic and OpenAI, OpenClaw caps SDK-internal `retry-after-ms` / `retry-after` waits at 60 seconds by default and surfaces longer retryable responses immediately so this failover path can run. Tune or disable the cap with `OPENCLAW_SDK_RETRY_MAX_WAIT_SECONDS`; see [Retry behavior](/concepts/retry).
</Accordion>
<Accordion title="Model-scoped cooldowns">
Rate-limit cooldowns can also be model-scoped:
- OpenClaw records `cooldownModel` for rate-limit failures when the failing model id is known.
- A sibling model on the same provider can still be tried when the cooldown is scoped to a different model.
- Billing/disabled windows still block the whole profile across models.
</Accordion>
</AccordionGroup>
Cooldowns use exponential backoff:
@@ -180,18 +172,13 @@ State is stored in `auth-state.json` under `usageStats`:
## Billing disables
Billing/credit failures (for example insufficient credits / credit balance too low) are treated as failoverworthy, but theyre usually not transient. Instead of a short cooldown, OpenClaw marks the profile as **disabled** (with a longer backoff) and rotates to the next profile/provider.
Billing/credit failures (for example "insufficient credits" / "credit balance too low") are treated as failover-worthy, but they're usually not transient. Instead of a short cooldown, OpenClaw marks the profile as **disabled** (with a longer backoff) and rotates to the next profile/provider.
Not every billing-shaped response is `402`, and not every HTTP `402` lands
here. OpenClaw keeps explicit billing text in the billing lane even when a
provider returns `401` or `403` instead, but provider-specific matchers stay
scoped to the provider that owns them (for example OpenRouter `403 Key limit
exceeded`). Meanwhile temporary `402` usage-window and
organization/workspace spend-limit errors are classified as `rate_limit` when
the message looks retryable (for example `weekly usage limit exhausted`, `daily
limit reached, resets tomorrow`, or `organization spending limit exceeded`).
Those stay on the short cooldown/failover path instead of the long
billing-disable path.
<Note>
Not every billing-shaped response is `402`, and not every HTTP `402` lands here. OpenClaw keeps explicit billing text in the billing lane even when a provider returns `401` or `403` instead, but provider-specific matchers stay scoped to the provider that owns them (for example OpenRouter `403 Key limit exceeded`).
Meanwhile temporary `402` usage-window and organization/workspace spend-limit errors are classified as `rate_limit` when the message looks retryable (for example `weekly usage limit exhausted`, `daily limit reached, resets tomorrow`, or `organization spending limit exceeded`). Those stay on the short cooldown/failover path instead of the long billing-disable path.
</Note>
State is stored in `auth-state.json`:
@@ -209,139 +196,115 @@ State is stored in `auth-state.json`:
Defaults:
- Billing backoff starts at **5 hours**, doubles per billing failure, and caps at **24 hours**.
- Backoff counters reset if the profile hasnt failed for **24 hours** (configurable).
- Backoff counters reset if the profile hasn't failed for **24 hours** (configurable).
- Overloaded retries allow **1 same-provider profile rotation** before model fallback.
- Overloaded retries use **0 ms backoff** by default.
## Model fallback
If all profiles for a provider fail, OpenClaw moves to the next model in
`agents.defaults.model.fallbacks`. This applies to auth failures, rate limits, and
timeouts that exhausted profile rotation (other errors do not advance fallback).
If all profiles for a provider fail, OpenClaw moves to the next model in `agents.defaults.model.fallbacks`. This applies to auth failures, rate limits, and timeouts that exhausted profile rotation (other errors do not advance fallback).
Overloaded and rate-limit errors are handled more aggressively than billing
cooldowns. By default, OpenClaw allows one same-provider auth-profile retry,
then switches to the next configured model fallback without waiting.
Provider-busy signals such as `ModelNotReadyException` land in that overloaded
bucket. Tune this with `auth.cooldowns.overloadedProfileRotations`,
`auth.cooldowns.overloadedBackoffMs`, and
`auth.cooldowns.rateLimitedProfileRotations`.
Overloaded and rate-limit errors are handled more aggressively than billing cooldowns. By default, OpenClaw allows one same-provider auth-profile retry, then switches to the next configured model fallback without waiting. Provider-busy signals such as `ModelNotReadyException` land in that overloaded bucket. Tune this with `auth.cooldowns.overloadedProfileRotations`, `auth.cooldowns.overloadedBackoffMs`, and `auth.cooldowns.rateLimitedProfileRotations`.
When a run starts with a model override (hooks or CLI), fallbacks still end at
`agents.defaults.model.primary` after trying any configured fallbacks.
When a run starts with a model override (hooks or CLI), fallbacks still end at `agents.defaults.model.primary` after trying any configured fallbacks.
### Candidate chain rules
OpenClaw builds the candidate list from the currently requested `provider/model`
plus configured fallbacks.
OpenClaw builds the candidate list from the currently requested `provider/model` plus configured fallbacks.
Rules:
- The requested model is always first.
- Explicit configured fallbacks are deduplicated but not filtered by the model
allowlist. They are treated as explicit operator intent.
- If the current run is already on a configured fallback in the same provider
family, OpenClaw keeps using the full configured chain.
- If the current run is on a different provider than config and that current
model is not already part of the configured fallback chain, OpenClaw does not
append unrelated configured fallbacks from another provider.
- When the run started from an override, the configured primary is appended at
the end so the chain can settle back onto the normal default once earlier
candidates are exhausted.
<AccordionGroup>
<Accordion title="Rules">
- The requested model is always first.
- Explicit configured fallbacks are deduplicated but not filtered by the model allowlist. They are treated as explicit operator intent.
- If the current run is already on a configured fallback in the same provider family, OpenClaw keeps using the full configured chain.
- If the current run is on a different provider than config and that current model is not already part of the configured fallback chain, OpenClaw does not append unrelated configured fallbacks from another provider.
- When the run started from an override, the configured primary is appended at the end so the chain can settle back onto the normal default once earlier candidates are exhausted.
</Accordion>
</AccordionGroup>
### Which errors advance fallback
Model fallback continues on:
- auth failures
- rate limits and cooldown exhaustion
- overloaded/provider-busy errors
- timeout-shaped failover errors
- billing disables
- `LiveSessionModelSwitchError`, which is normalized into a failover path so a
stale persisted model does not create an outer retry loop
- other unrecognized errors when there are still remaining candidates
Model fallback does not continue on:
- explicit aborts that are not timeout/failover-shaped
- context overflow errors that should stay inside compaction/retry logic
(for example `request_too_large`, `INVALID_ARGUMENT: input exceeds the maximum
number of tokens`, `input token count exceeds the maximum number of input
tokens`, `The input is too long for the model`, or `ollama error: context
length exceeded`)
- a final unknown error when there are no candidates left
<Tabs>
<Tab title="Continues on">
- auth failures
- rate limits and cooldown exhaustion
- overloaded/provider-busy errors
- timeout-shaped failover errors
- billing disables
- `LiveSessionModelSwitchError`, which is normalized into a failover path so a stale persisted model does not create an outer retry loop
- other unrecognized errors when there are still remaining candidates
</Tab>
<Tab title="Does not continue on">
- explicit aborts that are not timeout/failover-shaped
- context overflow errors that should stay inside compaction/retry logic (for example `request_too_large`, `INVALID_ARGUMENT: input exceeds the maximum number of tokens`, `input token count exceeds the maximum number of input tokens`, `The input is too long for the model`, or `ollama error: context length exceeded`)
- a final unknown error when there are no candidates left
</Tab>
</Tabs>
### Cooldown skip vs probe behavior
When every auth profile for a provider is already in cooldown, OpenClaw does
not automatically skip that provider forever. It makes a per-candidate decision:
When every auth profile for a provider is already in cooldown, OpenClaw does not automatically skip that provider forever. It makes a per-candidate decision:
- Persistent auth failures skip the whole provider immediately.
- Billing disables usually skip, but the primary candidate can still be probed
on a throttle so recovery is possible without restarting.
- The primary candidate may be probed near cooldown expiry, with a per-provider
throttle.
- Same-provider fallback siblings can be attempted despite cooldown when the
failure looks transient (`rate_limit`, `overloaded`, or unknown). This is
especially relevant when a rate limit is model-scoped and a sibling model may
still recover immediately.
- Transient cooldown probes are limited to one per provider per fallback run so
a single provider does not stall cross-provider fallback.
<AccordionGroup>
<Accordion title="Per-candidate decisions">
- Persistent auth failures skip the whole provider immediately.
- Billing disables usually skip, but the primary candidate can still be probed on a throttle so recovery is possible without restarting.
- The primary candidate may be probed near cooldown expiry, with a per-provider throttle.
- Same-provider fallback siblings can be attempted despite cooldown when the failure looks transient (`rate_limit`, `overloaded`, or unknown). This is especially relevant when a rate limit is model-scoped and a sibling model may still recover immediately.
- Transient cooldown probes are limited to one per provider per fallback run so a single provider does not stall cross-provider fallback.
</Accordion>
</AccordionGroup>
## Session overrides and live model switching
Session model changes are shared state. The active runner, `/model` command,
compaction/session updates, and live-session reconciliation all read or write
parts of the same session entry.
Session model changes are shared state. The active runner, `/model` command, compaction/session updates, and live-session reconciliation all read or write parts of the same session entry.
That means fallback retries have to coordinate with live model switching:
- Only explicit user-driven model changes mark a pending live switch. That
includes `/model`, `session_status(model=...)`, and `sessions.patch`.
- System-driven model changes such as fallback rotation, heartbeat overrides,
or compaction never mark a pending live switch on their own.
- Before a fallback retry starts, the reply runner persists the selected
fallback override fields to the session entry.
- Live-session reconciliation prefers persisted session overrides over stale
runtime model fields.
- If the fallback attempt fails, the runner rolls back only the override fields
it wrote, and only if they still match that failed candidate.
- Only explicit user-driven model changes mark a pending live switch. That includes `/model`, `session_status(model=...)`, and `sessions.patch`.
- System-driven model changes such as fallback rotation, heartbeat overrides, or compaction never mark a pending live switch on their own.
- Before a fallback retry starts, the reply runner persists the selected fallback override fields to the session entry.
- Live-session reconciliation prefers persisted session overrides over stale runtime model fields.
- If a live-switch error points at a later candidate in the active fallback chain, OpenClaw jumps directly to that selected model instead of walking unrelated candidates first.
- If the fallback attempt fails, the runner rolls back only the override fields it wrote, and only if they still match that failed candidate.
This prevents the classic race:
1. Primary fails.
2. Fallback candidate is chosen in memory.
3. Session store still says the old primary.
4. Live-session reconciliation reads the stale session state.
5. The retry gets snapped back to the old model before the fallback attempt
starts.
<Steps>
<Step title="Primary fails">
The selected primary model fails.
</Step>
<Step title="Fallback chosen in memory">
Fallback candidate is chosen in memory.
</Step>
<Step title="Session store still says old primary">
Session store still reflects the old primary.
</Step>
<Step title="Live reconciliation reads stale state">
Live-session reconciliation reads the stale session state.
</Step>
<Step title="Retry snapped back">
The retry gets snapped back to the old model before the fallback attempt starts.
</Step>
</Steps>
The persisted fallback override closes that window, and the narrow rollback
keeps newer manual or runtime session changes intact.
The persisted fallback override closes that window, and the narrow rollback keeps newer manual or runtime session changes intact.
## Observability and failure summaries
`runWithModelFallback(...)` records per-attempt details that feed logs and
user-facing cooldown messaging:
`runWithModelFallback(...)` records per-attempt details that feed logs and user-facing cooldown messaging:
- provider/model attempted
- reason (`rate_limit`, `overloaded`, `billing`, `auth`, `model_not_found`, and
similar failover reasons)
- reason (`rate_limit`, `overloaded`, `billing`, `auth`, `model_not_found`, and similar failover reasons)
- optional status/code
- human-readable error summary
When every candidate fails, OpenClaw throws `FallbackSummaryError`. The outer
reply runner can use that to build a more specific message such as "all models
are temporarily rate-limited" and include the soonest cooldown expiry when one
is known.
When every candidate fails, OpenClaw throws `FallbackSummaryError`. The outer reply runner can use that to build a more specific message such as "all models are temporarily rate-limited" and include the soonest cooldown expiry when one is known.
That cooldown summary is model-aware:
- unrelated model-scoped rate limits are ignored for the attempted
provider/model chain
- if the remaining block is a matching model-scoped rate limit, OpenClaw
reports the last matching expiry that still blocks that model
- unrelated model-scoped rate limits are ignored for the attempted provider/model chain
- if the remaining block is a matching model-scoped rate limit, OpenClaw reports the last matching expiry that still blocks that model
## Related config

View File

@@ -5,37 +5,53 @@ read_when:
- Changing model fallback behavior or selection UX
- Updating model scan probes (tools/images)
title: "Models CLI"
sidebarTitle: "Models CLI"
---
See [/concepts/model-failover](/concepts/model-failover) for auth profile
rotation, cooldowns, and how that interacts with fallbacks.
Quick provider overview + examples: [/concepts/model-providers](/concepts/model-providers).
Model refs choose a provider and model. They do not usually choose the
low-level agent runtime. For example, `openai/gpt-5.5` can run through the
normal OpenAI provider path or through the Codex app-server runtime, depending
on `agents.defaults.agentRuntime.id`. See
[/concepts/agent-runtimes](/concepts/agent-runtimes).
<CardGroup cols={2}>
<Card title="Model failover" href="/concepts/model-failover">
Auth profile rotation, cooldowns, and how that interacts with fallbacks.
</Card>
<Card title="Model providers" href="/concepts/model-providers">
Quick provider overview and examples.
</Card>
<Card title="Agent runtimes" href="/concepts/agent-runtimes">
PI, Codex, and other agent loop runtimes.
</Card>
<Card title="Configuration reference" href="/gateway/config-agents#agent-defaults">
Model config keys.
</Card>
</CardGroup>
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. For example, `openai/gpt-5.5` can run through the normal OpenAI provider path or through the Codex app-server runtime, depending on `agents.defaults.agentRuntime.id`. See [Agent runtimes](/concepts/agent-runtimes).
## How model selection works
OpenClaw selects models in this order:
1. **Primary** model (`agents.defaults.model.primary` or `agents.defaults.model`).
2. **Fallbacks** in `agents.defaults.model.fallbacks` (in order).
3. **Provider auth failover** happens inside a provider before moving to the
next model.
<Steps>
<Step title="Primary model">
`agents.defaults.model.primary` (or `agents.defaults.model`).
</Step>
<Step title="Fallbacks">
`agents.defaults.model.fallbacks` (in order).
</Step>
<Step title="Provider auth failover">
Auth failover happens inside a provider before moving to the next model.
</Step>
</Steps>
Related:
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
- `agents.defaults.imageModel` is used **only when** the primary model cant accept images.
- `agents.defaults.pdfModel` is used by the `pdf` tool. If omitted, the tool
falls back to `agents.defaults.imageModel`, then the resolved session/default
model.
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered image-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
- `agents.defaults.musicGenerationModel` is used by the shared music-generation capability. If omitted, `music_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered music-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
- `agents.defaults.videoGenerationModel` is used by the shared video-generation capability. If omitted, `video_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered video-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
<AccordionGroup>
<Accordion title="Related model surfaces">
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
- `agents.defaults.imageModel` is used **only when** the primary model can't accept images.
- `agents.defaults.pdfModel` is used by the `pdf` tool. If omitted, the tool falls back to `agents.defaults.imageModel`, then the resolved session/default model.
- `agents.defaults.imageGenerationModel` is used by the shared image-generation capability. If omitted, `image_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered image-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
- `agents.defaults.musicGenerationModel` is used by the shared music-generation capability. If omitted, `music_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered music-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
- `agents.defaults.videoGenerationModel` is used by the shared video-generation capability. If omitted, `video_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered video-generation providers in provider-id order. If you set a specific provider/model, also configure that provider's auth/API key.
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [Multi-agent routing](/concepts/multi-agent)).
</Accordion>
</AccordionGroup>
## Quick model policy
@@ -45,14 +61,13 @@ Related:
## Onboarding (recommended)
If you dont want to hand-edit config, run onboarding:
If you don't want to hand-edit config, run onboarding:
```bash
openclaw onboard
```
It can set up model + auth for common providers, including **OpenAI Code (Codex)
subscription** (OAuth) and **Anthropic** (API key or Claude CLI).
It can set up model + auth for common providers, including **OpenAI Code (Codex) subscription** (OAuth) and **Anthropic** (API key or Claude CLI).
## Config keys (overview)
@@ -64,11 +79,11 @@ subscription** (OAuth) and **Anthropic** (API key or Claude CLI).
- `agents.defaults.models` (allowlist + aliases + provider params)
- `models.providers` (custom providers written into `models.json`)
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize
to `zai/*`.
<Note>
Model refs are normalized to lowercase. Provider aliases like `z.ai/*` normalize to `zai/*`.
Provider configuration examples (including OpenCode) live in
[/providers/opencode](/providers/opencode).
Provider configuration examples (including OpenCode) live in [OpenCode](/providers/opencode).
</Note>
### Safe allowlist edits
@@ -78,36 +93,30 @@ Use additive writes when updating `agents.defaults.models` by hand:
openclaw config set agents.defaults.models '{"openai/gpt-5.4":{}}' --strict-json --merge
```
`openclaw config set` protects model/provider maps from accidental clobbers. A
plain object assignment to `agents.defaults.models`, `models.providers`, or
`models.providers.<id>.models` is rejected when it would remove existing
entries. Use `--merge` for additive changes; use `--replace` only when the
provided value should become the complete target value.
<AccordionGroup>
<Accordion title="Clobber protection rules">
`openclaw config set` protects model/provider maps from accidental clobbers. A plain object assignment to `agents.defaults.models`, `models.providers`, or `models.providers.<id>.models` is rejected when it would remove existing entries. Use `--merge` for additive changes; use `--replace` only when the provided value should become the complete target value.
Interactive provider setup and `openclaw configure --section model` also merge
provider-scoped selections into the existing allowlist, so adding Codex,
Ollama, or another provider does not drop unrelated model entries.
Configure preserves an existing `agents.defaults.model.primary` when provider
auth is re-applied. Explicit default-setting commands such as
`openclaw models auth login --provider <id> --set-default` and
`openclaw models set <model>` still replace `agents.defaults.model.primary`.
Interactive provider setup and `openclaw configure --section model` also merge provider-scoped selections into the existing allowlist, so adding Codex, Ollama, or another provider does not drop unrelated model entries. Configure preserves an existing `agents.defaults.model.primary` when provider auth is re-applied. Explicit default-setting commands such as `openclaw models auth login --provider <id> --set-default` and `openclaw models set <model>` still replace `agents.defaults.model.primary`.
</Accordion>
</AccordionGroup>
## "Model is not allowed" (and why replies stop)
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for
session overrides. When a user selects a model that isnt in that allowlist,
OpenClaw returns:
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and for session overrides. When a user selects a model that isn't in that allowlist, OpenClaw returns:
```
Model "provider/model" is not allowed. Use /model to list available models.
```
This happens **before** a normal reply is generated, so the message can feel
like it didnt respond. The fix is to either:
<Warning>
This happens **before** a normal reply is generated, so the message can feel like it "didn't respond." The fix is to either:
- Add the model to `agents.defaults.models`, or
- Clear the allowlist (remove `agents.defaults.models`), or
- Pick a model from `/model list`.
</Warning>
Example allowlist config:
@@ -135,26 +144,29 @@ You can switch models for the current session without restarting:
/model status
```
Notes:
- `/model` (and `/model list`) is a compact, numbered picker (model family + available providers).
- On Discord, `/model` and `/models` open an interactive picker with provider and model dropdowns plus a Submit step.
- `/models add` is deprecated and now returns a deprecation message instead of registering models from chat.
- `/model <#>` selects from that picker.
- `/model` persists the new session selection immediately.
- If the agent is idle, the next run uses the new model right away.
- If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point.
- If tool activity or reply output has already started, the pending switch can stay queued until a later retry opportunity or the next user turn.
- `/model status` is the detailed view (auth candidates and, when configured, provider endpoint `baseUrl` + `api` mode).
- Model refs are parsed by splitting on the **first** `/`. Use `provider/model` when typing `/model <ref>`.
- If the model ID itself contains `/` (OpenRouter-style), you must include the provider prefix (example: `/model openrouter/moonshotai/kimi-k2`).
- If you omit the provider, OpenClaw resolves the input in this order:
1. alias match
2. unique configured-provider match for that exact unprefixed model id
3. deprecated fallback to the configured default provider
If that provider no longer exposes the configured default model, OpenClaw
instead falls back to the first configured provider/model to avoid
surfacing a stale removed-provider default.
<AccordionGroup>
<Accordion title="Picker behavior">
- `/model` (and `/model list`) is a compact, numbered picker (model family + available providers).
- On Discord, `/model` and `/models` open an interactive picker with provider and model dropdowns plus a Submit step.
- `/models add` is deprecated and now returns a deprecation message instead of registering models from chat.
- `/model <#>` selects from that picker.
</Accordion>
<Accordion title="Persistence and live switching">
- `/model` persists the new session selection immediately.
- If the agent is idle, the next run uses the new model right away.
- If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point.
- If tool activity or reply output has already started, the pending switch can stay queued until a later retry opportunity or the next user turn.
- `/model status` is the detailed view (auth candidates and, when configured, provider endpoint `baseUrl` + `api` mode).
</Accordion>
<Accordion title="Ref parsing">
- Model refs are parsed by splitting on the **first** `/`. Use `provider/model` when typing `/model <ref>`.
- If the model ID itself contains `/` (OpenRouter-style), you must include the provider prefix (example: `/model openrouter/moonshotai/kimi-k2`).
- If you omit the provider, OpenClaw resolves the input in this order:
1. alias match
2. unique configured-provider match for that exact unprefixed model id
3. deprecated fallback to the configured default provider — if that provider no longer exposes the configured default model, OpenClaw instead falls back to the first configured provider/model to avoid surfacing a stale removed-provider default.
</Accordion>
</AccordionGroup>
Full command behavior/config: [Slash commands](/tools/slash-commands).
@@ -187,38 +199,39 @@ openclaw models image-fallbacks clear
Shows configured models by default. Useful flags:
- `--all`: full catalog
- `--local`: local providers only
- `--provider <id>`: filter by provider id, for example `moonshot`; display
labels from interactive pickers are not accepted
- `--plain`: one model per line
- `--json`: machinereadable output
`--all` includes bundled provider-owned static catalog rows before auth is
configured, so discovery-only views can show models that are unavailable until
you add matching provider credentials.
<ParamField path="--all" type="boolean">
Full catalog. Includes bundled provider-owned static catalog rows before auth is configured, so discovery-only views can show models that are unavailable until you add matching provider credentials.
</ParamField>
<ParamField path="--local" type="boolean">
Local providers only.
</ParamField>
<ParamField path="--provider <id>" type="string">
Filter by provider id, for example `moonshot`. Display labels from interactive pickers are not accepted.
</ParamField>
<ParamField path="--plain" type="boolean">
One model per line.
</ParamField>
<ParamField path="--json" type="boolean">
Machine-readable output.
</ParamField>
### `models status`
Shows the resolved primary model, fallbacks, image model, and an auth overview
of configured providers. It also surfaces OAuth expiry status for profiles found
in the auth store (warns within 24h by default). `--plain` prints only the
resolved primary model.
OAuth status is always shown (and included in `--json` output). If a configured
provider has no credentials, `models status` prints a **Missing auth** section.
JSON includes `auth.oauth` (warn window + profiles) and `auth.providers`
(effective auth per provider, including env-backed credentials). `auth.oauth`
is auth-store profile health only; env-only providers do not appear there.
Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
Use `--probe` for live auth checks; probe rows can come from auth profiles, env
credentials, or `models.json`.
If explicit `auth.order.<provider>` omits a stored profile, probe reports
`excluded_by_auth_order` instead of trying it. If auth exists but no probeable
model can be resolved for that provider, probe reports `status: no_model`.
Shows the resolved primary model, fallbacks, image model, and an auth overview of configured providers. It also surfaces OAuth expiry status for profiles found in the auth store (warns within 24h by default). `--plain` prints only the resolved primary model.
Auth choice is provider/account dependent. For always-on gateway hosts, API
keys are usually the most predictable; Claude CLI reuse and existing Anthropic
OAuth/token profiles are also supported.
<AccordionGroup>
<Accordion title="Auth and probe behavior">
- OAuth status is always shown (and included in `--json` output). If a configured provider has no credentials, `models status` prints a **Missing auth** section.
- JSON includes `auth.oauth` (warn window + profiles) and `auth.providers` (effective auth per provider, including env-backed credentials). `auth.oauth` is auth-store profile health only; env-only providers do not appear there.
- Use `--check` for automation (exit `1` when missing/expired, `2` when expiring).
- Use `--probe` for live auth checks; probe rows can come from auth profiles, env credentials, or `models.json`.
- If explicit `auth.order.<provider>` omits a stored profile, probe reports `excluded_by_auth_order` instead of trying it. If auth exists but no probeable model can be resolved for that provider, probe reports `status: no_model`.
</Accordion>
</AccordionGroup>
<Note>
Auth choice is provider/account dependent. For always-on gateway hosts, API keys are usually the most predictable; Claude CLI reuse and existing Anthropic OAuth/token profiles are also supported.
</Note>
Example (Claude CLI):
@@ -229,24 +242,33 @@ openclaw models status
## Scanning (OpenRouter free models)
`openclaw models scan` inspects OpenRouters **free model catalog** and can
optionally probe models for tool and image support.
`openclaw models scan` inspects OpenRouter's **free model catalog** and can optionally probe models for tool and image support.
Key flags:
<ParamField path="--no-probe" type="boolean">
Skip live probes (metadata only).
</ParamField>
<ParamField path="--min-params <b>" type="number">
Minimum parameter size (billions).
</ParamField>
<ParamField path="--max-age-days <days>" type="number">
Skip older models.
</ParamField>
<ParamField path="--provider <name>" type="string">
Provider prefix filter.
</ParamField>
<ParamField path="--max-candidates <n>" type="number">
Fallback list size.
</ParamField>
<ParamField path="--set-default" type="boolean">
Set `agents.defaults.model.primary` to the first selection.
</ParamField>
<ParamField path="--set-image" type="boolean">
Set `agents.defaults.imageModel.primary` to the first image selection.
</ParamField>
- `--no-probe`: skip live probes (metadata only)
- `--min-params <b>`: minimum parameter size (billions)
- `--max-age-days <days>`: skip older models
- `--provider <name>`: provider prefix filter
- `--max-candidates <n>`: fallback list size
- `--set-default`: set `agents.defaults.model.primary` to the first selection
- `--set-image`: set `agents.defaults.imageModel.primary` to the first image selection
The OpenRouter `/models` catalog is public, so metadata-only scans can list
free candidates without a key. Probing and inference still require an
OpenRouter API key (from auth profiles or `OPENROUTER_API_KEY`). If no key is
available, `openclaw models scan` falls back to metadata-only output and leaves
config unchanged. Use `--no-probe` to request metadata-only mode explicitly.
<Note>
The OpenRouter `/models` catalog is public, so metadata-only scans can list free candidates without a key. Probing and inference still require an OpenRouter API key (from auth profiles or `OPENROUTER_API_KEY`). If no key is available, `openclaw models scan` falls back to metadata-only output and leaves config unchanged. Use `--no-probe` to request metadata-only mode explicitly.
</Note>
Scan results are ranked by:
@@ -255,42 +277,43 @@ Scan results are ranked by:
3. Context size
4. Parameter count
Input
Input:
- OpenRouter `/models` list (filter `:free`)
- Live probes require OpenRouter API key from auth profiles or `OPENROUTER_API_KEY` (see [/environment](/help/environment))
- Live probes require OpenRouter API key from auth profiles or `OPENROUTER_API_KEY` (see [Environment variables](/help/environment))
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`
- Request/probe controls: `--timeout`, `--concurrency`
When live probes run in a TTY, you can select fallbacks interactively. In
noninteractive mode, pass `--yes` to accept defaults. Metadata-only results are
informational; `--set-default` and `--set-image` require live probes so
OpenClaw does not configure an unusable keyless OpenRouter model.
When live probes run in a TTY, you can select fallbacks interactively. In non-interactive mode, pass `--yes` to accept defaults. Metadata-only results are informational; `--set-default` and `--set-image` require live probes so OpenClaw does not configure an unusable keyless OpenRouter model.
## Models registry (`models.json`)
Custom providers in `models.providers` are written into `models.json` under the
agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). This file
is merged by default unless `models.mode` is set to `replace`.
Custom providers in `models.providers` are written into `models.json` under the agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). This file is merged by default unless `models.mode` is set to `replace`.
Merge mode precedence for matching provider IDs:
<AccordionGroup>
<Accordion title="Merge mode precedence">
Merge mode precedence for matching provider IDs:
- Non-empty `baseUrl` already present in the agent `models.json` wins.
- Non-empty `apiKey` in the agent `models.json` wins only when that provider is not SecretRef-managed in current config/auth-profile context.
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
- SecretRef-managed provider header values are refreshed from source markers (`secretref-env:ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs).
- Empty or missing agent `apiKey`/`baseUrl` fall back to config `models.providers`.
- Other provider fields are refreshed from config and normalized catalog data.
- Non-empty `baseUrl` already present in the agent `models.json` wins.
- Non-empty `apiKey` in the agent `models.json` wins only when that provider is not SecretRef-managed in current config/auth-profile context.
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
- SecretRef-managed provider header values are refreshed from source markers (`secretref-env:ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs).
- Empty or missing agent `apiKey`/`baseUrl` fall back to config `models.providers`.
- Other provider fields are refreshed from config and normalized catalog data.
Marker persistence is source-authoritative: OpenClaw writes markers from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
This applies whenever OpenClaw regenerates `models.json`, including command-driven paths like `openclaw agent`.
</Accordion>
</AccordionGroup>
<Note>
Marker persistence is source-authoritative: OpenClaw writes markers from the active source config snapshot (pre-resolution), not from resolved runtime secret values. This applies whenever OpenClaw regenerates `models.json`, including command-driven paths like `openclaw agent`.
</Note>
## Related
- [Model Providers](/concepts/model-providers) — provider routing and auth
- [Agent Runtimes](/concepts/agent-runtimes) — PI, Codex, and other agent loop runtimes
- [Model Failover](/concepts/model-failover) — fallback chains
- [Image Generation](/tools/image-generation) — image model configuration
- [Music Generation](/tools/music-generation) — music model configuration
- [Video Generation](/tools/video-generation) — video model configuration
- [Configuration Reference](/gateway/config-agents#agent-defaults) — model config keys
- [Agent runtimes](/concepts/agent-runtimes) — PI, Codex, and other agent loop runtimes
- [Configuration reference](/gateway/config-agents#agent-defaults) — model config keys
- [Image generation](/tools/image-generation) — image model configuration
- [Model failover](/concepts/model-failover) — fallback chains
- [Model providers](/concepts/model-providers) — provider routing and auth
- [Music generation](/tools/music-generation) — music model configuration
- [Video generation](/tools/video-generation) — video model configuration

View File

@@ -1,6 +1,7 @@
---
summary: "Multi-agent routing: isolated agents, channel accounts, and bindings"
title: Multi-agent routing
title: "Multi-agent routing"
sidebarTitle: "Multi-agent routing"
read_when: "You want multiple isolated agents (workspaces + auth) in one gateway process."
status: active
---
@@ -23,32 +24,21 @@ Auth profiles are **per-agent**. Each agent reads from its own:
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
```
`sessions_history` is the safer cross-session recall path here too: it returns
a bounded, sanitized view, not a raw transcript dump. Assistant recall strips
thinking tags, `<relevant-memories>` scaffolding, plain-text tool-call XML
payloads (including `<tool_call>...</tool_call>`,
`<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`,
`<function_calls>...</function_calls>`, and truncated tool-call blocks),
downgraded tool-call scaffolding, leaked ASCII/full-width model control
tokens, and malformed MiniMax tool-call XML before redaction/truncation.
<Note>
`sessions_history` is the safer cross-session recall path here too: it returns a bounded, sanitized view, not a raw transcript dump. Assistant recall strips thinking tags, `<relevant-memories>` scaffolding, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks), downgraded tool-call scaffolding, leaked ASCII/full-width model control tokens, and malformed MiniMax tool-call XML before redaction/truncation.
</Note>
Main agent credentials are **not** shared automatically. Never reuse `agentDir`
across agents (it causes auth/session collisions). If you want to share creds,
copy `auth-profiles.json` into the other agent's `agentDir`.
<Warning>
Main agent credentials are **not** shared automatically. Never reuse `agentDir` across agents (it causes auth/session collisions). If you want to share creds, copy `auth-profiles.json` into the other agent's `agentDir`.
</Warning>
Skills are loaded from each agent workspace plus shared roots such as
`~/.openclaw/skills`, then filtered by the effective agent skill allowlist when
configured. Use `agents.defaults.skills` for a shared baseline and
`agents.list[].skills` for per-agent replacement. See
[Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and
[Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists).
Skills are loaded from each agent workspace plus shared roots such as `~/.openclaw/skills`, then filtered by the effective agent skill allowlist when configured. Use `agents.defaults.skills` for a shared baseline and `agents.list[].skills` for per-agent replacement. See [Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills) and [Skills: agent skill allowlists](/tools/skills#agent-skill-allowlists).
The Gateway can host **one agent** (default) or **many agents** side-by-side.
**Workspace note:** each agents workspace is the **default cwd**, not a hard
sandbox. Relative paths resolve inside the workspace, but absolute paths can
reach other host locations unless sandboxing is enabled. See
[Sandboxing](/gateway/sandboxing).
<Note>
**Workspace note:** each agent's workspace is the **default cwd**, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See [Sandboxing](/gateway/sandboxing).
</Note>
## Paths (quick map)
@@ -87,48 +77,39 @@ openclaw agents list --bindings
<Steps>
<Step title="Create each agent workspace">
Use the wizard or create workspaces manually:
Use the wizard or create workspaces manually:
```bash
openclaw agents add coding
openclaw agents add social
```
```bash
openclaw agents add coding
openclaw agents add social
```
Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/<agentId>`.
Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/<agentId>`.
</Step>
<Step title="Create channel accounts">
Create one account per agent on your preferred channels:
Create one account per agent on your preferred channels:
- Discord: one bot per agent, enable Message Content Intent, copy each token.
- Telegram: one bot per agent via BotFather, copy each token.
- WhatsApp: link each phone number per account.
- Discord: one bot per agent, enable Message Content Intent, copy each token.
- Telegram: one bot per agent via BotFather, copy each token.
- WhatsApp: link each phone number per account.
```bash
openclaw channels login --channel whatsapp --account work
```
```bash
openclaw channels login --channel whatsapp --account work
```
See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp).
See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp).
</Step>
<Step title="Add agents, accounts, and bindings">
Add agents under `agents.list`, channel accounts under `channels.<channel>.accounts`, and connect them with `bindings` (examples below).
Add agents under `agents.list`, channel accounts under `channels.<channel>.accounts`, and connect them with `bindings` (examples below).
</Step>
<Step title="Restart and verify">
```bash
openclaw gateway restart
openclaw agents list --bindings
openclaw channels status --probe
```
```bash
openclaw gateway restart
openclaw agents list --bindings
openclaw channels status --probe
```
</Step>
</Steps>
@@ -140,14 +121,11 @@ With **multiple agents**, each `agentId` becomes a **fully isolated persona**:
- **Different personalities** (per-agent workspace files like `AGENTS.md` and `SOUL.md`).
- **Separate auth + sessions** (no cross-talk unless explicitly enabled).
This lets **multiple people** share one Gateway server while keeping their AI brains and data isolated.
This lets **multiple people** share one Gateway server while keeping their AI "brains" and data isolated.
## Cross-agent QMD memory search
If one agent should search another agent's QMD session transcripts, add
extra collections under `agents.list[].memorySearch.qmd.extraCollections`.
Use `agents.defaults.memorySearch.qmd.extraCollections` only when every agent
should inherit the same shared transcript collections.
If one agent should search another agent's QMD session transcripts, add extra collections under `agents.list[].memorySearch.qmd.extraCollections`. Use `agents.defaults.memorySearch.qmd.extraCollections` only when every agent should inherit the same shared transcript collections.
```json5
{
@@ -180,15 +158,15 @@ should inherit the same shared transcript collections.
}
```
The extra collection path can be shared across agents, but the collection name
stays explicit when the path is outside the agent workspace. Paths inside the
workspace remain agent-scoped so each agent keeps its own transcript search set.
The extra collection path can be shared across agents, but the collection name stays explicit when the path is outside the agent workspace. Paths inside the workspace remain agent-scoped so each agent keeps its own transcript search set.
## One WhatsApp number, multiple people (DM split)
You can route **different WhatsApp DMs** to different agents while staying on **one WhatsApp account**. Match on sender E.164 (like `+15551234567`) with `peer.kind: "direct"`. Replies still come from the same WhatsApp number (no peragent sender identity).
You can route **different WhatsApp DMs** to different agents while staying on **one WhatsApp account**. Match on sender E.164 (like `+15551234567`) with `peer.kind: "direct"`. Replies still come from the same WhatsApp number (no per-agent sender identity).
Important detail: direct chats collapse to the agents **main session key**, so true isolation requires **one agent per person**.
<Note>
Direct chats collapse to the agent's **main session key**, so true isolation requires **one agent per person**.
</Note>
Example:
@@ -228,33 +206,50 @@ Notes:
Bindings are **deterministic** and **most-specific wins**:
1. `peer` match (exact DM/group/channel id)
2. `parentPeer` match (thread inheritance)
3. `guildId + roles` (Discord role routing)
4. `guildId` (Discord)
5. `teamId` (Slack)
6. `accountId` match for a channel
7. channel-level match (`accountId: "*"`)
8. fallback to default agent (`agents.list[].default`, else first list entry, default: `main`)
<Steps>
<Step title="peer match">
Exact DM/group/channel id.
</Step>
<Step title="parentPeer match">
Thread inheritance.
</Step>
<Step title="guildId + roles">
Discord role routing.
</Step>
<Step title="guildId">
Discord.
</Step>
<Step title="teamId">
Slack.
</Step>
<Step title="accountId match for a channel">
Per-account fallback.
</Step>
<Step title="Channel-level match">
`accountId: "*"`.
</Step>
<Step title="Default agent">
Fallback to `agents.list[].default`, else first list entry, default: `main`.
</Step>
</Steps>
If multiple bindings match in the same tier, the first one in config order wins.
If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics).
Important account-scope detail:
- A binding that omits `accountId` matches the default account only.
- Use `accountId: "*"` for a channel-wide fallback across all accounts.
- If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it.
<AccordionGroup>
<Accordion title="Tie-breaking and AND semantics">
- If multiple bindings match in the same tier, the first one in config order wins.
- If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics).
</Accordion>
<Accordion title="Account-scope detail">
- A binding that omits `accountId` matches the default account only.
- Use `accountId: "*"` for a channel-wide fallback across all accounts.
- If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it.
</Accordion>
</AccordionGroup>
## Multiple accounts / phone numbers
Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify
each login. Each `accountId` can be routed to a different agent, so one server can host
multiple phone numbers without mixing sessions.
Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify each login. Each `accountId` can be routed to a different agent, so one server can host multiple phone numbers without mixing sessions.
If you want a channel-wide default account when `accountId` is omitted, set
`channels.<channel>.defaultAccount` (optional). When unset, OpenClaw falls back
to `default` if present, otherwise the first configured account id (sorted).
If you want a channel-wide default account when `accountId` is omitted, set `channels.<channel>.defaultAccount` (optional). When unset, OpenClaw falls back to `default` if present, otherwise the first configured account id (sorted).
Common channels supporting this pattern include:
@@ -264,297 +259,298 @@ Common channels supporting this pattern include:
## Concepts
- `agentId`: one brain (workspace, per-agent auth, per-agent session store).
- `agentId`: one "brain" (workspace, per-agent auth, per-agent session store).
- `accountId`: one channel account instance (e.g. WhatsApp account `"personal"` vs `"biz"`).
- `binding`: routes inbound messages to an `agentId` by `(channel, accountId, peer)` and optionally guild/team ids.
- Direct chats collapse to `agent:<agentId>:<mainKey>` (per-agent main; `session.mainKey`).
- Direct chats collapse to `agent:<agentId>:<mainKey>` (per-agent "main"; `session.mainKey`).
## Platform examples
### Discord bots per agent
<AccordionGroup>
<Accordion title="Discord bots per agent">
Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot.
Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot.
```json5
{
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace-main" },
{ id: "coding", workspace: "~/.openclaw/workspace-coding" },
],
},
bindings: [
{ agentId: "main", match: { channel: "discord", accountId: "default" } },
{ agentId: "coding", match: { channel: "discord", accountId: "coding" } },
],
channels: {
discord: {
groupPolicy: "allowlist",
accounts: {
default: {
token: "DISCORD_BOT_TOKEN_MAIN",
guilds: {
"123456789012345678": {
channels: {
"222222222222222222": { allow: true, requireMention: false },
```json5
{
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace-main" },
{ id: "coding", workspace: "~/.openclaw/workspace-coding" },
],
},
bindings: [
{ agentId: "main", match: { channel: "discord", accountId: "default" } },
{ agentId: "coding", match: { channel: "discord", accountId: "coding" } },
],
channels: {
discord: {
groupPolicy: "allowlist",
accounts: {
default: {
token: "DISCORD_BOT_TOKEN_MAIN",
guilds: {
"123456789012345678": {
channels: {
"222222222222222222": { allow: true, requireMention: false },
},
},
},
},
},
},
coding: {
token: "DISCORD_BOT_TOKEN_CODING",
guilds: {
"123456789012345678": {
channels: {
"333333333333333333": { allow: true, requireMention: false },
coding: {
token: "DISCORD_BOT_TOKEN_CODING",
guilds: {
"123456789012345678": {
channels: {
"333333333333333333": { allow: true, requireMention: false },
},
},
},
},
},
},
},
},
},
}
```
}
```
Notes:
- Invite each bot to the guild and enable Message Content Intent.
- Tokens live in `channels.discord.accounts.<id>.token` (default account can use `DISCORD_BOT_TOKEN`).
- Invite each bot to the guild and enable Message Content Intent.
- Tokens live in `channels.discord.accounts.<id>.token` (default account can use `DISCORD_BOT_TOKEN`).
### Telegram bots per agent
```json5
{
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace-main" },
{ id: "alerts", workspace: "~/.openclaw/workspace-alerts" },
],
},
bindings: [
{ agentId: "main", match: { channel: "telegram", accountId: "default" } },
{ agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } },
],
channels: {
telegram: {
accounts: {
default: {
botToken: "123456:ABC...",
dmPolicy: "pairing",
},
alerts: {
botToken: "987654:XYZ...",
dmPolicy: "allowlist",
allowFrom: ["tg:123456789"],
},
},
},
},
}
```
Notes:
- Create one bot per agent with BotFather and copy each token.
- Tokens live in `channels.telegram.accounts.<id>.botToken` (default account can use `TELEGRAM_BOT_TOKEN`).
### WhatsApp numbers per agent
Link each account before starting the gateway:
```bash
openclaw channels login --channel whatsapp --account personal
openclaw channels login --channel whatsapp --account biz
```
`~/.openclaw/openclaw.json` (JSON5):
```js
{
agents: {
list: [
{
id: "home",
default: true,
name: "Home",
workspace: "~/.openclaw/workspace-home",
agentDir: "~/.openclaw/agents/home/agent",
},
{
id: "work",
name: "Work",
workspace: "~/.openclaw/workspace-work",
agentDir: "~/.openclaw/agents/work/agent",
},
],
},
// Deterministic routing: first match wins (most-specific first).
bindings: [
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
// Optional per-peer override (example: send a specific group to work agent).
</Accordion>
<Accordion title="Telegram bots per agent">
```json5
{
agentId: "work",
match: {
channel: "whatsapp",
accountId: "personal",
peer: { kind: "group", id: "1203630...@g.us" },
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace-main" },
{ id: "alerts", workspace: "~/.openclaw/workspace-alerts" },
],
},
},
],
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
tools: {
agentToAgent: {
enabled: false,
allow: ["home", "work"],
},
},
channels: {
whatsapp: {
accounts: {
personal: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
// authDir: "~/.openclaw/credentials/whatsapp/personal",
},
biz: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
// authDir: "~/.openclaw/credentials/whatsapp/biz",
bindings: [
{ agentId: "main", match: { channel: "telegram", accountId: "default" } },
{ agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } },
],
channels: {
telegram: {
accounts: {
default: {
botToken: "123456:ABC...",
dmPolicy: "pairing",
},
alerts: {
botToken: "987654:XYZ...",
dmPolicy: "allowlist",
allowFrom: ["tg:123456789"],
},
},
},
},
},
},
}
```
}
```
## Example: WhatsApp daily chat + Telegram deep work
- Create one bot per agent with BotFather and copy each token.
- Tokens live in `channels.telegram.accounts.<id>.botToken` (default account can use `TELEGRAM_BOT_TOKEN`).
Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
</Accordion>
<Accordion title="WhatsApp numbers per agent">
Link each account before starting the gateway:
```json5
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
bindings: [
{ agentId: "chat", match: { channel: "whatsapp" } },
{ agentId: "opus", match: { channel: "telegram" } },
],
}
```
```bash
openclaw channels login --channel whatsapp --account personal
openclaw channels login --channel whatsapp --account biz
```
Notes:
`~/.openclaw/openclaw.json` (JSON5):
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
## Example: same channel, one peer to Opus
Keep WhatsApp on the fast agent, but route one DM to Opus:
```json5
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
bindings: [
```js
{
agentId: "opus",
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } },
},
{ agentId: "chat", match: { channel: "whatsapp" } },
],
}
```
agents: {
list: [
{
id: "home",
default: true,
name: "Home",
workspace: "~/.openclaw/workspace-home",
agentDir: "~/.openclaw/agents/home/agent",
},
{
id: "work",
name: "Work",
workspace: "~/.openclaw/workspace-work",
agentDir: "~/.openclaw/agents/work/agent",
},
],
},
Peer bindings always win, so keep them above the channel-wide rule.
// Deterministic routing: first match wins (most-specific first).
bindings: [
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
## Family agent bound to a WhatsApp group
Bind a dedicated family agent to a single WhatsApp group, with mention gating
and a tighter tool policy:
```json5
{
agents: {
list: [
{
id: "family",
name: "Family",
workspace: "~/.openclaw/workspace-family",
identity: { name: "Family Bot" },
groupChat: {
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
// Optional per-peer override (example: send a specific group to work agent).
{
agentId: "work",
match: {
channel: "whatsapp",
accountId: "personal",
peer: { kind: "group", id: "1203630...@g.us" },
},
},
sandbox: {
mode: "all",
scope: "agent",
},
tools: {
allow: [
"exec",
"read",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
],
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
tools: {
agentToAgent: {
enabled: false,
allow: ["home", "work"],
},
},
],
},
bindings: [
channels: {
whatsapp: {
accounts: {
personal: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
// authDir: "~/.openclaw/credentials/whatsapp/personal",
},
biz: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
// authDir: "~/.openclaw/credentials/whatsapp/biz",
},
},
},
},
}
```
</Accordion>
</AccordionGroup>
## Common patterns
<Tabs>
<Tab title="WhatsApp daily + Telegram deep work">
Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.
```json5
{
agentId: "family",
match: {
channel: "whatsapp",
peer: { kind: "group", id: "120363999999999999@g.us" },
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
},
],
}
```
bindings: [
{ agentId: "chat", match: { channel: "whatsapp" } },
{ agentId: "opus", match: { channel: "telegram" } },
],
}
```
Notes:
Notes:
- Tool allow/deny lists are **tools**, not skills. If a skill needs to run a
binary, ensure `exec` is allowed and the binary exists in the sandbox.
- For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep
group allowlists enabled for the channel.
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
## Per-Agent Sandbox and Tool Configuration
</Tab>
<Tab title="Same channel, one peer to Opus">
Keep WhatsApp on the fast agent, but route one DM to Opus:
```json5
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
bindings: [
{
agentId: "opus",
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } },
},
{ agentId: "chat", match: { channel: "whatsapp" } },
],
}
```
Peer bindings always win, so keep them above the channel-wide rule.
</Tab>
<Tab title="Family agent bound to a WhatsApp group">
Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy:
```json5
{
agents: {
list: [
{
id: "family",
name: "Family",
workspace: "~/.openclaw/workspace-family",
identity: { name: "Family Bot" },
groupChat: {
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
},
sandbox: {
mode: "all",
scope: "agent",
},
tools: {
allow: [
"exec",
"read",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
},
},
],
},
bindings: [
{
agentId: "family",
match: {
channel: "whatsapp",
peer: { kind: "group", id: "120363999999999999@g.us" },
},
},
],
}
```
Notes:
- Tool allow/deny lists are **tools**, not skills. If a skill needs to run a binary, ensure `exec` is allowed and the binary exists in the sandbox.
- For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep group allowlists enabled for the channel.
</Tab>
</Tabs>
## Per-agent sandbox and tool configuration
Each agent can have its own sandbox and tool restrictions:
@@ -591,25 +587,26 @@ Each agent can have its own sandbox and tool restrictions:
}
```
Note: `setupCommand` lives under `sandbox.docker` and runs once on container creation.
Per-agent `sandbox.docker.*` overrides are ignored when the resolved scope is `"shared"`.
<Note>
`setupCommand` lives under `sandbox.docker` and runs once on container creation. Per-agent `sandbox.docker.*` overrides are ignored when the resolved scope is `"shared"`.
</Note>
**Benefits:**
- **Security isolation**: Restrict tools for untrusted agents
- **Resource control**: Sandbox specific agents while keeping others on host
- **Flexible policies**: Different permissions per agent
- **Security isolation**: restrict tools for untrusted agents.
- **Resource control**: sandbox specific agents while keeping others on host.
- **Flexible policies**: different permissions per agent.
Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent.
If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`.
For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent.
<Note>
`tools.elevated` is **global** and sender-based; it is not configurable per agent. If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`. For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent.
</Note>
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for detailed examples.
See [Multi-agent sandbox and tools](/tools/multi-agent-sandbox-tools) for detailed examples.
## Related
- [Channel Routing](/channels/channel-routing) — how messages route to agents
- [Sub-Agents](/tools/subagents) — spawning background agent runs
- [ACP Agents](/tools/acp-agents) — running external coding harnesses
- [ACP agents](/tools/acp-agents) — running external coding harnesses
- [Channel routing](/channels/channel-routing) — how messages route to agents
- [Presence](/concepts/presence) — agent presence and availability
- [Session](/concepts/session) — session isolation and routing
- [Sub-agents](/tools/subagents) — spawning background agent runs

View File

@@ -65,6 +65,15 @@ model calls must not export `StreamAbandoned` on successful turns; raw diagnosti
`openclaw.content.*` attributes must stay out of the trace. It writes
`otel-smoke-summary.json` next to the QA suite artifacts.
The normal Docker aggregate and release-path core chunk also run an
observability lane. It reuses the shared package-installed functional Docker
image, mounts the QA harness files read-only, runs the OTEL trace smoke inside
the container, then runs the `docker-prometheus-smoke` QA scenario with the
`diagnostics-prometheus` plugin enabled. Set
`OPENCLAW_DOCKER_OBSERVABILITY_LOOPS=<count>` to repeat both checks inside one
Docker run while preserving per-loop artifacts under
`.artifacts/docker-observability/...`.
For a transport-real Matrix smoke lane, run:
```bash

View File

@@ -152,6 +152,7 @@ Legacy key migration:
Telegram:
- Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
- Sends a fresh final message instead of editing in place when a preview has been visible for about one minute, then cleans up the preview so Telegram's timestamp reflects reply completion.
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
- `/reasoning stream` can write reasoning to preview.

View File

@@ -1442,6 +1442,7 @@
"gateway/doctor",
"logging",
"gateway/opentelemetry",
"gateway/prometheus",
"gateway/logging",
"gateway/diagnostics",
"gateway/troubleshooting"

View File

@@ -179,11 +179,10 @@ openclaw plugins disable bonjour
## Docker gotchas
Bundled Docker Compose sets `OPENCLAW_DISABLE_BONJOUR=1` for the Gateway service
by default. Docker bridge networks usually do not forward mDNS multicast
(`224.0.0.251:5353`) between the container and the LAN, so leaving Bonjour on can
produce repeated ciao `probing` or `announcing` failures without making discovery
work.
The bundled Bonjour plugin auto-disables LAN multicast advertising in detected
containers when `OPENCLAW_DISABLE_BONJOUR` is unset. Docker bridge networks
usually do not forward mDNS multicast (`224.0.0.251:5353`) between the container
and the LAN, so advertising from the container rarely makes discovery work.
Important gotchas:
@@ -193,16 +192,16 @@ Important gotchas:
`OPENCLAW_GATEWAY_BIND=lan` so the published host port can work.
- Disabling Bonjour does not disable wide-area DNS-SD. Use wide-area discovery
or Tailnet when the Gateway and node are not on the same LAN.
- Reusing the same `OPENCLAW_CONFIG_DIR` outside Docker does not inherit the
Compose default unless the environment still sets `OPENCLAW_DISABLE_BONJOUR`.
- Reusing the same `OPENCLAW_CONFIG_DIR` outside Docker does not persist the
container auto-disable policy.
- Set `OPENCLAW_DISABLE_BONJOUR=0` only for host networking, macvlan, or another
network where mDNS multicast is known to pass.
network where mDNS multicast is known to pass; set it to `1` to force-disable.
## Troubleshooting disabled Bonjour
If a node no longer auto-discovers the Gateway after Docker setup:
1. Confirm whether the Gateway is intentionally suppressing LAN advertising:
1. Confirm whether the Gateway is running in auto, forced-on, or forced-off mode:
```bash
docker compose config | grep OPENCLAW_DISABLE_BONJOUR
@@ -239,9 +238,9 @@ If a node no longer auto-discovers the Gateway after Docker setup:
container bridges, WSL, or interface churn can leave the ciao advertiser in a
non-announced state. OpenClaw retries a few times and then disables Bonjour
for the current Gateway process instead of restarting the advertiser forever.
- **Docker bridge networking**: bundled Docker Compose disables Bonjour by
default with `OPENCLAW_DISABLE_BONJOUR=1`. Set it to `0` only for host,
macvlan, or another mDNS-capable network.
- **Docker bridge networking**: Bonjour auto-disables in detected containers.
Set `OPENCLAW_DISABLE_BONJOUR=0` only for host, macvlan, or another
mDNS-capable network.
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or
punctuation), then restart the Gateway. The service instance name derives from
@@ -260,7 +259,8 @@ sequences (e.g. spaces become `\032`).
- `openclaw plugins disable bonjour` disables LAN multicast advertising by disabling the bundled plugin.
- `openclaw plugins enable bonjour` restores the default LAN discovery plugin.
- `OPENCLAW_DISABLE_BONJOUR=1` disables LAN multicast advertising without changing plugin config; accepted truthy values are `1`, `true`, `yes`, and `on` (legacy: `OPENCLAW_DISABLE_BONJOUR`).
- Docker Compose sets `OPENCLAW_DISABLE_BONJOUR=1` by default for bridge networking; override with `OPENCLAW_DISABLE_BONJOUR=0` only when mDNS multicast is available.
- `OPENCLAW_DISABLE_BONJOUR=0` forces LAN multicast advertising on, including inside detected containers; accepted falsy values are `0`, `false`, `no`, and `off`.
- When `OPENCLAW_DISABLE_BONJOUR` is unset, Bonjour advertises on normal hosts and auto-disables inside detected containers.
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
- `OPENCLAW_SSH_PORT` overrides the SSH port when `sshPort` is advertised (legacy: `OPENCLAW_SSH_PORT`).
- `OPENCLAW_TAILNET_DNS` publishes a MagicDNS hint in TXT when mDNS full mode is enabled (legacy: `OPENCLAW_TAILNET_DNS`).

View File

@@ -859,6 +859,7 @@ Notes:
- Set `logging.file` for a stable path.
- `consoleLevel` bumps to `debug` when `--verbose`.
- `maxFileBytes`: maximum active log file size in bytes before rotation (positive integer; default: `104857600` = 100 MB). OpenClaw keeps up to five numbered archives beside the active file.
- `redactSensitive` / `redactPatterns`: best-effort masking for console output, file logs, OTLP log records, and persisted session transcript text.
---

View File

@@ -86,9 +86,9 @@ Security notes:
Disable/override:
- `OPENCLAW_DISABLE_BONJOUR=1` disables advertising.
- Docker Compose defaults `OPENCLAW_DISABLE_BONJOUR=1` because bridge networks
usually do not carry mDNS multicast reliably; use `0` only on host, macvlan,
or another mDNS-capable network.
- When `OPENCLAW_DISABLE_BONJOUR` is unset, Bonjour advertises on normal hosts
and auto-disables inside detected containers. Use `0` only on host, macvlan,
or another mDNS-capable network; use `1` to force-disable.
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
- `OPENCLAW_SSH_PORT` overrides the SSH port advertised when `sshPort` is emitted.
- `OPENCLAW_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS).

View File

@@ -430,6 +430,7 @@ That stages grounded durable candidates into the short-term dreaming store while
- `openclaw doctor --yes` accepts the default repair prompts.
- `openclaw doctor --repair` applies recommended fixes without prompts.
- `openclaw doctor --repair --force` overwrites custom supervisor configs.
- `OPENCLAW_SERVICE_REPAIR_POLICY=external` keeps doctor read-only for gateway service lifecycle. It still reports service health and runs non-service repairs, but skips service install/start/restart/bootstrap, supervisor config rewrites, and legacy service cleanup because an external supervisor owns that lifecycle.
- If token auth requires a token and `gateway.auth.token` is SecretRef-managed, doctor service install/repair validates the SecretRef but does not persist resolved plaintext token values into supervisor service environment metadata.
- If token auth requires a token and the configured token SecretRef is unresolved, doctor blocks the install/repair path with actionable guidance.
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, doctor blocks install/repair until mode is set explicitly.

View File

@@ -52,10 +52,12 @@ You can tune console verbosity independently via:
- `logging.consoleLevel` (default `info`)
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
## Tool summary redaction
## Redaction
Verbose tool summaries (e.g. `🛠️ Exec: ...`) can mask sensitive tokens before they hit the
console stream. This is **tools-only** and does not alter file logs.
OpenClaw can mask sensitive tokens before log or transcript output leaves the
process. The same redaction policy is applied at console, file-log, OTLP
log-record, and session transcript text sinks, so matching secret values are
masked before JSONL lines or messages are written to disk.
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
- `logging.redactPatterns`: array of regex strings (overrides defaults)

View File

@@ -147,9 +147,17 @@ When any subkey is enabled, model and tool spans get bounded, redacted
- **Traces:** `diagnostics.otel.sampleRate` (root-span only, `0.0` drops all,
`1.0` keeps all).
- **Metrics:** `diagnostics.otel.flushIntervalMs` (minimum `1000`).
- **Logs:** OTLP logs respect `logging.level` (file log level). Console
redaction does **not** apply to OTLP logs. High-volume installs should
prefer OTLP collector sampling/filtering over local sampling.
- **Logs:** OTLP logs respect `logging.level` (file log level). They use the
diagnostic log-record redaction path, not console formatting. High-volume
installs should prefer OTLP collector sampling/filtering over local sampling.
- **File-log correlation:** JSONL file logs include top-level `traceId`,
`spanId`, `parentSpanId`, and `traceFlags` when the log call carries a valid
diagnostic trace context, which lets log processors join local log lines with
exported spans.
- **Request correlation:** Gateway HTTP requests and WebSocket frames create an
internal request trace scope. Logs and diagnostic events inside that scope
inherit the request trace by default, while agent run and model-call spans are
created as children so provider `traceparent` headers stay on the same trace.
## Exported metrics
@@ -161,6 +169,10 @@ When any subkey is enabled, model and tool spans get bounded, redacted
- `openclaw.context.tokens` (histogram, attrs: `openclaw.context`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
- `gen_ai.client.token.usage` (histogram, GenAI semantic-conventions metric, attrs: `gen_ai.token.type` = `input`/`output`, `gen_ai.provider.name`, `gen_ai.operation.name`, `gen_ai.request.model`)
- `gen_ai.client.operation.duration` (histogram, seconds, GenAI semantic-conventions metric, attrs: `gen_ai.provider.name`, `gen_ai.operation.name`, `gen_ai.request.model`, optional `error.type`)
- `openclaw.model_call.duration_ms` (histogram, attrs: `openclaw.provider`, `openclaw.model`, `openclaw.api`, `openclaw.transport`)
- `openclaw.model_call.request_bytes` (histogram, UTF-8 byte size of the final model request payload; no raw payload content)
- `openclaw.model_call.response_bytes` (histogram, UTF-8 byte size of streamed model response events; no raw response content)
- `openclaw.model_call.time_to_first_byte_ms` (histogram, elapsed time before the first streamed response event)
### Message flow
@@ -212,6 +224,7 @@ When any subkey is enabled, model and tool spans get bounded, redacted
- `openclaw.model.call`
- `gen_ai.system` by default, or `gen_ai.provider.name` when the latest GenAI semantic conventions are opted in
- `gen_ai.request.model`, `gen_ai.operation.name`, `openclaw.provider`, `openclaw.model`, `openclaw.api`, `openclaw.transport`
- `openclaw.model_call.request_bytes`, `openclaw.model_call.response_bytes`, `openclaw.model_call.time_to_first_byte_ms`
- `openclaw.provider.request_id_hash` (bounded SHA-based hash of the upstream provider request id; raw ids are not exported)
- `openclaw.harness.run`
- `openclaw.harness.id`, `openclaw.harness.plugin`, `openclaw.outcome`, `openclaw.provider`, `openclaw.model`, `openclaw.channel`

209
docs/gateway/prometheus.md Normal file
View File

@@ -0,0 +1,209 @@
---
summary: "Expose OpenClaw diagnostics as Prometheus text metrics through the diagnostics-prometheus plugin"
title: "Prometheus metrics"
sidebarTitle: "Prometheus"
read_when:
- You want Prometheus, Grafana, VictoriaMetrics, or another scraper to collect OpenClaw Gateway metrics
- You need the Prometheus metric names and label policy for dashboards or alerts
- You want metrics without running an OpenTelemetry collector
---
OpenClaw can expose diagnostics metrics through the bundled `diagnostics-prometheus` plugin. It listens to trusted internal diagnostics and renders a Prometheus text endpoint at:
```text
GET /api/diagnostics/prometheus
```
Content type is `text/plain; version=0.0.4; charset=utf-8`, the standard Prometheus exposition format.
<Warning>
The route uses Gateway authentication (operator scope). Do not expose it as a public unauthenticated `/metrics` endpoint. Scrape it through the same auth path you use for other operator APIs.
</Warning>
For traces, logs, OTLP push, and OpenTelemetry GenAI semantic attributes, see [OpenTelemetry export](/gateway/opentelemetry).
## Quick start
<Steps>
<Step title="Enable the plugin">
<Tabs>
<Tab title="Config">
```json5
{
plugins: {
allow: ["diagnostics-prometheus"],
entries: {
"diagnostics-prometheus": { enabled: true },
},
},
diagnostics: {
enabled: true,
},
}
```
</Tab>
<Tab title="CLI">
```bash
openclaw plugins enable diagnostics-prometheus
```
</Tab>
</Tabs>
</Step>
<Step title="Restart the Gateway">
The HTTP route is registered at plugin startup, so reload after enabling.
</Step>
<Step title="Scrape the protected route">
Send the same gateway auth your operator clients use:
```bash
curl -H "Authorization: Bearer $OPENCLAW_GATEWAY_TOKEN" \
http://127.0.0.1:18789/api/diagnostics/prometheus
```
</Step>
<Step title="Wire Prometheus">
```yaml
# prometheus.yml
scrape_configs:
- job_name: openclaw
scrape_interval: 30s
metrics_path: /api/diagnostics/prometheus
authorization:
credentials_file: /etc/prometheus/openclaw-gateway-token
static_configs:
- targets: ["openclaw-gateway:18789"]
```
</Step>
</Steps>
<Note>
`diagnostics.enabled: true` is required. Without it, the plugin still registers the HTTP route but no diagnostic events flow into the exporter, so the response is empty.
</Note>
## Metrics exported
| Metric | Type | Labels |
| --------------------------------------------- | --------- | ----------------------------------------------------------------------------------------- |
| `openclaw_run_completed_total` | counter | `channel`, `model`, `outcome`, `provider`, `trigger` |
| `openclaw_run_duration_seconds` | histogram | `channel`, `model`, `outcome`, `provider`, `trigger` |
| `openclaw_model_call_total` | counter | `api`, `error_category`, `model`, `outcome`, `provider`, `transport` |
| `openclaw_model_call_duration_seconds` | histogram | `api`, `error_category`, `model`, `outcome`, `provider`, `transport` |
| `openclaw_model_tokens_total` | counter | `agent`, `channel`, `model`, `provider`, `token_type` |
| `openclaw_gen_ai_client_token_usage` | histogram | `model`, `provider`, `token_type` |
| `openclaw_model_cost_usd_total` | counter | `agent`, `channel`, `model`, `provider` |
| `openclaw_tool_execution_total` | counter | `error_category`, `outcome`, `params_kind`, `tool` |
| `openclaw_tool_execution_duration_seconds` | histogram | `error_category`, `outcome`, `params_kind`, `tool` |
| `openclaw_harness_run_total` | counter | `channel`, `error_category`, `harness`, `model`, `outcome`, `phase`, `plugin`, `provider` |
| `openclaw_harness_run_duration_seconds` | histogram | `channel`, `error_category`, `harness`, `model`, `outcome`, `phase`, `plugin`, `provider` |
| `openclaw_message_processed_total` | counter | `channel`, `outcome`, `reason` |
| `openclaw_message_processed_duration_seconds` | histogram | `channel`, `outcome`, `reason` |
| `openclaw_message_delivery_total` | counter | `channel`, `delivery_kind`, `error_category`, `outcome` |
| `openclaw_message_delivery_duration_seconds` | histogram | `channel`, `delivery_kind`, `error_category`, `outcome` |
| `openclaw_queue_lane_size` | gauge | `lane` |
| `openclaw_queue_lane_wait_seconds` | histogram | `lane` |
| `openclaw_session_state_total` | counter | `reason`, `state` |
| `openclaw_session_queue_depth` | gauge | `state` |
| `openclaw_memory_bytes` | gauge | `kind` |
| `openclaw_memory_rss_bytes` | histogram | none |
| `openclaw_memory_pressure_total` | counter | `level`, `reason` |
| `openclaw_telemetry_exporter_total` | counter | `exporter`, `reason`, `signal`, `status` |
| `openclaw_prometheus_series_dropped_total` | counter | none |
## Label policy
<AccordionGroup>
<Accordion title="Bounded, low-cardinality labels">
Prometheus labels stay bounded and low-cardinality. The exporter does not emit raw diagnostic identifiers such as `runId`, `sessionKey`, `sessionId`, `callId`, `toolCallId`, message IDs, chat IDs, or provider request IDs.
Label values are redacted and must match OpenClaw's low-cardinality character policy. Values that fail the policy are replaced with `unknown`, `other`, or `none`, depending on the metric.
</Accordion>
<Accordion title="Series cap and overflow accounting">
The exporter caps retained time series in memory at **2048** series across counters, gauges, and histograms combined. New series beyond that cap are dropped, and `openclaw_prometheus_series_dropped_total` increments by one each time.
Watch this counter as a hard signal that an attribute upstream is leaking high-cardinality values. The exporter never lifts the cap automatically; if it climbs, fix the source rather than disabling the cap.
</Accordion>
<Accordion title="What never appears in Prometheus output">
- prompt text, response text, tool inputs, tool outputs, system prompts
- raw provider request IDs (only bounded hashes, where applicable, on spans — never on metrics)
- session keys and session IDs
- hostnames, file paths, secret values
</Accordion>
</AccordionGroup>
## PromQL recipes
```promql
# Tokens per minute, split by provider
sum by (provider) (rate(openclaw_model_tokens_total[1m]))
# Spend (USD) over the last hour, by model
sum by (model) (increase(openclaw_model_cost_usd_total[1h]))
# 95th percentile model run duration
histogram_quantile(
0.95,
sum by (le, provider, model)
(rate(openclaw_run_duration_seconds_bucket[5m]))
)
# Queue wait time SLO (95p under 2s)
histogram_quantile(
0.95,
sum by (le, lane) (rate(openclaw_queue_lane_wait_seconds_bucket[5m]))
) < 2
# Dropped Prometheus series (cardinality alarm)
increase(openclaw_prometheus_series_dropped_total[15m]) > 0
```
<Tip>
Prefer `gen_ai_client_token_usage` for cross-provider dashboards: it follows the OpenTelemetry GenAI semantic conventions and is consistent with metrics from non-OpenClaw GenAI services.
</Tip>
## Choosing between Prometheus and OpenTelemetry export
OpenClaw supports both surfaces independently. You can run either, both, or neither.
<Tabs>
<Tab title="diagnostics-prometheus">
- **Pull** model: Prometheus scrapes `/api/diagnostics/prometheus`.
- No external collector required.
- Authenticated through normal Gateway auth.
- Surface is metrics only (no traces or logs).
- Best for stacks already standardized on Prometheus + Grafana.
</Tab>
<Tab title="diagnostics-otel">
- **Push** model: OpenClaw sends OTLP/HTTP to a collector or OTLP-compatible backend.
- Surface includes metrics, traces, and logs.
- Bridges to Prometheus through an OpenTelemetry Collector (`prometheus` or `prometheusremotewrite` exporter) when you need both.
- See [OpenTelemetry export](/gateway/opentelemetry) for the full catalog.
</Tab>
</Tabs>
## Troubleshooting
<AccordionGroup>
<Accordion title="Empty response body">
- Check `diagnostics.enabled: true` in config.
- Confirm the plugin is enabled and loaded with `openclaw plugins list --enabled`.
- Generate some traffic; counters and histograms only emit lines after at least one event.
</Accordion>
<Accordion title="401 / unauthorized">
The endpoint requires the Gateway operator scope (`auth: "gateway"` with `gatewayRuntimeScopeSurface: "trusted-operator"`). Use the same token or password Prometheus uses for any other Gateway operator route. There is no public unauthenticated mode.
</Accordion>
<Accordion title="`openclaw_prometheus_series_dropped_total` is climbing">
A new attribute is exceeding the **2048**-series cap. Inspect recent metrics for an unexpectedly high-cardinality label and fix it at the source. The exporter intentionally drops new series instead of silently rewriting labels.
</Accordion>
<Accordion title="Prometheus shows stale series after a restart">
The plugin keeps state in memory only. After a Gateway restart, counters reset to zero and gauges restart at their next reported value. Use PromQL `rate()` and `increase()` to handle resets cleanly.
</Accordion>
</AccordionGroup>
## Related
- [Diagnostics export](/gateway/diagnostics) — local diagnostics zip for support bundles
- [Health and readiness](/gateway/health) — `/healthz` and `/readyz` probes
- [Logging](/logging) — file-based logging
- [OpenTelemetry export](/gateway/opentelemetry) — OTLP push for traces, metrics, and logs

View File

@@ -1,31 +1,32 @@
---
summary: "How OpenClaw sandboxing works: modes, scopes, workspace access, and images"
title: Sandboxing
title: "Sandboxing"
sidebarTitle: "Sandboxing"
read_when: "You want a dedicated explanation of sandboxing or need to tune agents.defaults.sandbox."
status: active
---
OpenClaw can run **tools inside sandbox backends** to reduce blast radius.
This is **optional** and controlled by configuration (`agents.defaults.sandbox` or
`agents.list[].sandbox`). If sandboxing is off, tools run on the host.
The Gateway stays on the host; tool execution runs in an isolated sandbox
when enabled.
OpenClaw can run **tools inside sandbox backends** to reduce blast radius. This is **optional** and controlled by configuration (`agents.defaults.sandbox` or `agents.list[].sandbox`). If sandboxing is off, tools run on the host. The Gateway stays on the host; tool execution runs in an isolated sandbox when enabled.
This is not a perfect security boundary, but it materially limits filesystem
and process access when the model does something dumb.
<Note>
This is not a perfect security boundary, but it materially limits filesystem and process access when the model does something dumb.
</Note>
## What gets sandboxed
- Tool execution (`exec`, `read`, `write`, `edit`, `apply_patch`, `process`, etc.).
- Optional sandboxed browser (`agents.defaults.sandbox.browser`).
- By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it.
Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`.
- By default, sandbox browser containers use a dedicated Docker network (`openclaw-sandbox-browser`) instead of the global `bridge` network.
Configure with `agents.defaults.sandbox.browser.network`.
- Optional `agents.defaults.sandbox.browser.cdpSourceRange` restricts container-edge CDP ingress with a CIDR allowlist (for example `172.21.0.1/32`).
- noVNC observer access is password-protected by default; OpenClaw emits a short-lived token URL that serves a local bootstrap page and opens noVNC with password in URL fragment (not query/header logs).
- `agents.defaults.sandbox.browser.allowHostControl` lets sandboxed sessions target the host browser explicitly.
- Optional allowlists gate `target: "custom"`: `allowedControlUrls`, `allowedControlHosts`, `allowedControlPorts`.
<AccordionGroup>
<Accordion title="Sandboxed browser details">
- By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it. Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`.
- By default, sandbox browser containers use a dedicated Docker network (`openclaw-sandbox-browser`) instead of the global `bridge` network. Configure with `agents.defaults.sandbox.browser.network`.
- Optional `agents.defaults.sandbox.browser.cdpSourceRange` restricts container-edge CDP ingress with a CIDR allowlist (for example `172.21.0.1/32`).
- noVNC observer access is password-protected by default; OpenClaw emits a short-lived token URL that serves a local bootstrap page and opens noVNC with password in URL fragment (not query/header logs).
- `agents.defaults.sandbox.browser.allowHostControl` lets sandboxed sessions target the host browser explicitly.
- Optional allowlists gate `target: "custom"`: `allowedControlUrls`, `allowedControlHosts`, `allowedControlPorts`.
</Accordion>
</AccordionGroup>
Not sandboxed:
@@ -38,11 +39,20 @@ Not sandboxed:
`agents.defaults.sandbox.mode` controls **when** sandboxing is used:
- `"off"`: no sandboxing.
- `"non-main"`: sandbox only **non-main** sessions (default if you want normal chats on host).
- `"all"`: every session runs in a sandbox.
Note: `"non-main"` is based on `session.mainKey` (default `"main"`), not agent id.
Group/channel sessions use their own keys, so they count as non-main and will be sandboxed.
<Tabs>
<Tab title="off">
No sandboxing.
</Tab>
<Tab title="non-main">
Sandbox only **non-main** sessions (default if you want normal chats on host).
`"non-main"` is based on `session.mainKey` (default `"main"`), not agent id. Group/channel sessions use their own keys, so they count as non-main and will be sandboxed.
</Tab>
<Tab title="all">
Every session runs in a sandbox.
</Tab>
</Tabs>
## Scope
@@ -60,8 +70,7 @@ Not sandboxed:
- `"ssh"`: generic SSH-backed remote sandbox runtime.
- `"openshell"`: OpenShell-backed sandbox runtime.
SSH-specific config lives under `agents.defaults.sandbox.ssh`.
OpenShell-specific config lives under `plugins.entries.openshell.config`.
SSH-specific config lives under `agents.defaults.sandbox.ssh`. OpenShell-specific config lives under `plugins.entries.openshell.config`.
### Choosing a backend
@@ -77,23 +86,22 @@ OpenShell-specific config lives under `plugins.entries.openshell.config`.
### Docker backend
Sandboxing is off by default. If you enable sandboxing and do not choose a
backend, OpenClaw uses the Docker backend. It executes tools and sandbox browsers
locally via the Docker daemon socket (`/var/run/docker.sock`). Sandbox container
isolation is determined by Docker namespaces.
Sandboxing is off by default. If you enable sandboxing and do not choose a backend, OpenClaw uses the Docker backend. It executes tools and sandbox browsers locally via the Docker daemon socket (`/var/run/docker.sock`). Sandbox container isolation is determined by Docker namespaces.
<Warning>
**Docker-out-of-Docker (DooD) constraints**
**Docker-out-of-Docker (DooD) Constraints**:
If you deploy the OpenClaw Gateway itself as a Docker container, it orchestrates sibling sandbox containers using the host's Docker socket (DooD). This introduces a specific path mapping constraint:
- **Config Requires Host Paths**: The `openclaw.json` `workspace` configuration MUST contain the **Host's absolute path** (e.g. `/home/user/.openclaw/workspaces`), not the internal Gateway container path. When OpenClaw asks the Docker daemon to spawn a sandbox, the daemon evaluates paths relative to the Host OS namespace, not the Gateway namespace.
- **FS Bridge Parity (Identical Volume Map)**: The OpenClaw Gateway native process also writes heartbeat and bridge files to the `workspace` directory. Because the Gateway evaluates the exact same string (the host path) from within its own containerized environment, the Gateway deployment MUST include an identical volume map linking the host namespace natively (`-v /home/user/.openclaw:/home/user/.openclaw`).
- **Config requires host paths**: The `openclaw.json` `workspace` configuration MUST contain the **Host's absolute path** (e.g. `/home/user/.openclaw/workspaces`), not the internal Gateway container path. When OpenClaw asks the Docker daemon to spawn a sandbox, the daemon evaluates paths relative to the Host OS namespace, not the Gateway namespace.
- **FS bridge parity (identical volume map)**: The OpenClaw Gateway native process also writes heartbeat and bridge files to the `workspace` directory. Because the Gateway evaluates the exact same string (the host path) from within its own containerized environment, the Gateway deployment MUST include an identical volume map linking the host namespace natively (`-v /home/user/.openclaw:/home/user/.openclaw`).
If you map paths internally without absolute host parity, OpenClaw natively throws an `EACCES` permission error attempting to write its heartbeat inside the container environment because the fully qualified path string doesn't exist natively.
</Warning>
### SSH backend
Use `backend: "ssh"` when you want OpenClaw to sandbox `exec`, file tools, and media reads on
an arbitrary SSH-accessible machine.
Use `backend: "ssh"` when you want OpenClaw to sandbox `exec`, file tools, and media reads on an arbitrary SSH-accessible machine.
```json5
{
@@ -123,39 +131,34 @@ an arbitrary SSH-accessible machine.
}
```
How it works:
<AccordionGroup>
<Accordion title="How it works">
- OpenClaw creates a per-scope remote root under `sandbox.ssh.workspaceRoot`.
- On first use after create or recreate, OpenClaw seeds that remote workspace from the local workspace once.
- After that, `exec`, `read`, `write`, `edit`, `apply_patch`, prompt media reads, and inbound media staging run directly against the remote workspace over SSH.
- OpenClaw does not sync remote changes back to the local workspace automatically.
</Accordion>
<Accordion title="Authentication material">
- `identityFile`, `certificateFile`, `knownHostsFile`: use existing local files and pass them through OpenSSH config.
- `identityData`, `certificateData`, `knownHostsData`: use inline strings or SecretRefs. OpenClaw resolves them through the normal secrets runtime snapshot, writes them to temp files with `0600`, and deletes them when the SSH session ends.
- If both `*File` and `*Data` are set for the same item, `*Data` wins for that SSH session.
</Accordion>
<Accordion title="Remote-canonical consequences">
This is a **remote-canonical** model. The remote SSH workspace becomes the real sandbox state after the initial seed.
- OpenClaw creates a per-scope remote root under `sandbox.ssh.workspaceRoot`.
- On first use after create or recreate, OpenClaw seeds that remote workspace from the local workspace once.
- After that, `exec`, `read`, `write`, `edit`, `apply_patch`, prompt media reads, and inbound media staging run directly against the remote workspace over SSH.
- OpenClaw does not sync remote changes back to the local workspace automatically.
- Host-local edits made outside OpenClaw after the seed step are not visible remotely until you recreate the sandbox.
- `openclaw sandbox recreate` deletes the per-scope remote root and seeds again from local on next use.
- Browser sandboxing is not supported on the SSH backend.
- `sandbox.docker.*` settings do not apply to the SSH backend.
Authentication material:
- `identityFile`, `certificateFile`, `knownHostsFile`: use existing local files and pass them through OpenSSH config.
- `identityData`, `certificateData`, `knownHostsData`: use inline strings or SecretRefs. OpenClaw resolves them through the normal secrets runtime snapshot, writes them to temp files with `0600`, and deletes them when the SSH session ends.
- If both `*File` and `*Data` are set for the same item, `*Data` wins for that SSH session.
This is a **remote-canonical** model. The remote SSH workspace becomes the real sandbox state after the initial seed.
Important consequences:
- Host-local edits made outside OpenClaw after the seed step are not visible remotely until you recreate the sandbox.
- `openclaw sandbox recreate` deletes the per-scope remote root and seeds again from local on next use.
- Browser sandboxing is not supported on the SSH backend.
- `sandbox.docker.*` settings do not apply to the SSH backend.
</Accordion>
</AccordionGroup>
### OpenShell backend
Use `backend: "openshell"` when you want OpenClaw to sandbox tools in an
OpenShell-managed remote environment. For the full setup guide, configuration
reference, and workspace mode comparison, see the dedicated
[OpenShell page](/gateway/openshell).
Use `backend: "openshell"` when you want OpenClaw to sandbox tools in an OpenShell-managed remote environment. For the full setup guide, configuration reference, and workspace mode comparison, see the dedicated [OpenShell page](/gateway/openshell).
OpenShell reuses the same core SSH transport and remote filesystem bridge as the
generic SSH backend, and adds OpenShell-specific lifecycle
(`sandbox create/get/delete`, `sandbox ssh-config`) plus the optional `mirror`
workspace mode.
OpenShell reuses the same core SSH transport and remote filesystem bridge as the generic SSH backend, and adds OpenShell-specific lifecycle (`sandbox create/get/delete`, `sandbox ssh-config`) plus the optional `mirror` workspace mode.
```json5
{
@@ -190,68 +193,69 @@ OpenShell modes:
- `mirror` (default): local workspace stays canonical. OpenClaw syncs local files into OpenShell before exec and syncs the remote workspace back after exec.
- `remote`: OpenShell workspace is canonical after the sandbox is created. OpenClaw seeds the remote workspace once from the local workspace, then file tools and exec run directly against the remote sandbox without syncing changes back.
Remote transport details:
- OpenClaw asks OpenShell for sandbox-specific SSH config via `openshell sandbox ssh-config <name>`.
- Core writes that SSH config to a temp file, opens the SSH session, and reuses the same remote filesystem bridge used by `backend: "ssh"`.
- In `mirror` mode only the lifecycle differs: sync local to remote before exec, then sync back after exec.
Current OpenShell limitations:
- sandbox browser is not supported yet
- `sandbox.docker.binds` is not supported on the OpenShell backend
- Docker-specific runtime knobs under `sandbox.docker.*` still apply only to the Docker backend
<AccordionGroup>
<Accordion title="Remote transport details">
- OpenClaw asks OpenShell for sandbox-specific SSH config via `openshell sandbox ssh-config <name>`.
- Core writes that SSH config to a temp file, opens the SSH session, and reuses the same remote filesystem bridge used by `backend: "ssh"`.
- In `mirror` mode only the lifecycle differs: sync local to remote before exec, then sync back after exec.
</Accordion>
<Accordion title="Current OpenShell limitations">
- sandbox browser is not supported yet
- `sandbox.docker.binds` is not supported on the OpenShell backend
- Docker-specific runtime knobs under `sandbox.docker.*` still apply only to the Docker backend
</Accordion>
</AccordionGroup>
#### Workspace modes
OpenShell has two workspace models. This is the part that matters most in practice.
##### `mirror`
<Tabs>
<Tab title="mirror (local canonical)">
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local workspace to stay canonical**.
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local workspace to stay canonical**.
Behavior:
Behavior:
- Before `exec`, OpenClaw syncs the local workspace into the OpenShell sandbox.
- After `exec`, OpenClaw syncs the remote workspace back to the local workspace.
- File tools still operate through the sandbox bridge, but the local workspace remains the source of truth between turns.
- Before `exec`, OpenClaw syncs the local workspace into the OpenShell sandbox.
- After `exec`, OpenClaw syncs the remote workspace back to the local workspace.
- File tools still operate through the sandbox bridge, but the local workspace remains the source of truth between turns.
Use this when:
Use this when:
- you edit files locally outside OpenClaw and want those changes to show up in the sandbox automatically
- you want the OpenShell sandbox to behave as much like the Docker backend as possible
- you want the host workspace to reflect sandbox writes after each exec turn
- you edit files locally outside OpenClaw and want those changes to show up in the sandbox automatically
- you want the OpenShell sandbox to behave as much like the Docker backend as possible
- you want the host workspace to reflect sandbox writes after each exec turn
Tradeoff: extra sync cost before and after exec.
Tradeoff:
</Tab>
<Tab title="remote (OpenShell canonical)">
Use `plugins.entries.openshell.config.mode: "remote"` when you want the **OpenShell workspace to become canonical**.
- extra sync cost before and after exec
Behavior:
##### `remote`
- When the sandbox is first created, OpenClaw seeds the remote workspace from the local workspace once.
- After that, `exec`, `read`, `write`, `edit`, and `apply_patch` operate directly against the remote OpenShell workspace.
- OpenClaw does **not** sync remote changes back into the local workspace after exec.
- Prompt-time media reads still work because file and media tools read through the sandbox bridge instead of assuming a local host path.
- Transport is SSH into the OpenShell sandbox returned by `openshell sandbox ssh-config`.
Use `plugins.entries.openshell.config.mode: "remote"` when you want the **OpenShell workspace to become canonical**.
Important consequences:
Behavior:
- If you edit files on the host outside OpenClaw after the seed step, the remote sandbox will **not** see those changes automatically.
- If the sandbox is recreated, the remote workspace is seeded from the local workspace again.
- With `scope: "agent"` or `scope: "shared"`, that remote workspace is shared at that same scope.
- When the sandbox is first created, OpenClaw seeds the remote workspace from the local workspace once.
- After that, `exec`, `read`, `write`, `edit`, and `apply_patch` operate directly against the remote OpenShell workspace.
- OpenClaw does **not** sync remote changes back into the local workspace after exec.
- Prompt-time media reads still work because file and media tools read through the sandbox bridge instead of assuming a local host path.
- Transport is SSH into the OpenShell sandbox returned by `openshell sandbox ssh-config`.
Use this when:
Important consequences:
- the sandbox should live primarily on the remote OpenShell side
- you want lower per-turn sync overhead
- you do not want host-local edits to silently overwrite remote sandbox state
- If you edit files on the host outside OpenClaw after the seed step, the remote sandbox will **not** see those changes automatically.
- If the sandbox is recreated, the remote workspace is seeded from the local workspace again.
- With `scope: "agent"` or `scope: "shared"`, that remote workspace is shared at that same scope.
</Tab>
</Tabs>
Use this when:
- the sandbox should live primarily on the remote OpenShell side
- you want lower per-turn sync overhead
- you do not want host-local edits to silently overwrite remote sandbox state
Choose `mirror` if you think of the sandbox as a temporary execution environment.
Choose `remote` if you think of the sandbox as the real workspace.
Choose `mirror` if you think of the sandbox as a temporary execution environment. Choose `remote` if you think of the sandbox as the real workspace.
#### OpenShell lifecycle
@@ -266,16 +270,23 @@ For `remote` mode, recreate is especially important:
- recreate deletes the canonical remote workspace for that scope
- the next use seeds a fresh remote workspace from the local workspace
For `mirror` mode, recreate mainly resets the remote execution environment
because the local workspace remains canonical anyway.
For `mirror` mode, recreate mainly resets the remote execution environment because the local workspace remains canonical anyway.
## Workspace access
`agents.defaults.sandbox.workspaceAccess` controls **what the sandbox can see**:
- `"none"` (default): tools see a sandbox workspace under `~/.openclaw/sandboxes`.
- `"ro"`: mounts the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`).
- `"rw"`: mounts the agent workspace read/write at `/workspace`.
<Tabs>
<Tab title="none (default)">
Tools see a sandbox workspace under `~/.openclaw/sandboxes`.
</Tab>
<Tab title="ro">
Mounts the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`).
</Tab>
<Tab title="rw">
Mounts the agent workspace read/write at `/workspace`.
</Tab>
</Tabs>
With the OpenShell backend:
@@ -284,15 +295,14 @@ With the OpenShell backend:
- `workspaceAccess: "ro"` and `"none"` still restrict write behavior the same way
Inbound media is copied into the active sandbox workspace (`media/inbound/*`).
Skills note: the `read` tool is sandbox-rooted. With `workspaceAccess: "none"`,
OpenClaw mirrors eligible skills into the sandbox workspace (`.../skills`) so
they can be read. With `"rw"`, workspace skills are readable from
`/workspace/skills`.
<Note>
**Skills note:** the `read` tool is sandbox-rooted. With `workspaceAccess: "none"`, OpenClaw mirrors eligible skills into the sandbox workspace (`.../skills`) so they can be read. With `"rw"`, workspace skills are readable from `/workspace/skills`.
</Note>
## Custom bind mounts
`agents.defaults.sandbox.docker.binds` mounts additional host directories into the container.
Format: `host:container:mode` (e.g., `"/home/user/source:/source:rw"`).
`agents.defaults.sandbox.docker.binds` mounts additional host directories into the container. Format: `host:container:mode` (e.g., `"/home/user/source:/source:rw"`).
Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"`, per-agent binds are ignored.
@@ -327,7 +337,8 @@ Example (read-only source + an extra data directory):
}
```
Security notes:
<Warning>
**Bind security**
- Binds bypass the sandbox filesystem: they expose host paths with whatever mode you set (`:ro` or `:rw`).
- OpenClaw blocks dangerous bind sources (for example: `docker.sock`, `/etc`, `/proc`, `/sys`, `/dev`, and parent mounts that would expose them).
@@ -338,128 +349,115 @@ Security notes:
- Sensitive mounts (secrets, SSH keys, service credentials) should be `:ro` unless absolutely required.
- Combine with `workspaceAccess: "ro"` if you only need read access to the workspace; bind modes stay independent.
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for how binds interact with tool policy and elevated exec.
</Warning>
## Images + setup
## Images and setup
Default Docker image: `openclaw-sandbox:bookworm-slim`
Build it once:
<Steps>
<Step title="Build the default image">
```bash
scripts/sandbox-setup.sh
```
```bash
scripts/sandbox-setup.sh
```
The default image does **not** include Node. If a skill needs Node (or other runtimes), either bake a custom image or install via `sandbox.docker.setupCommand` (requires network egress + writable root + root user).
Note: the default image does **not** include Node. If a skill needs Node (or
other runtimes), either bake a custom image or install via
`sandbox.docker.setupCommand` (requires network egress + writable root +
root user).
</Step>
<Step title="Optional: build the common image">
For a more functional sandbox image with common tooling (for example `curl`, `jq`, `nodejs`, `python3`, `git`):
If you want a more functional sandbox image with common tooling (for example
`curl`, `jq`, `nodejs`, `python3`, `git`), build:
```bash
scripts/sandbox-common-setup.sh
```
```bash
scripts/sandbox-common-setup.sh
```
Then set `agents.defaults.sandbox.docker.image` to `openclaw-sandbox-common:bookworm-slim`.
Then set `agents.defaults.sandbox.docker.image` to
`openclaw-sandbox-common:bookworm-slim`.
</Step>
<Step title="Optional: build the sandbox browser image">
```bash
scripts/sandbox-browser-setup.sh
```
</Step>
</Steps>
Sandboxed browser image:
By default, Docker sandbox containers run with **no network**. Override with `agents.defaults.sandbox.docker.network`.
```bash
scripts/sandbox-browser-setup.sh
```
<AccordionGroup>
<Accordion title="Sandbox browser Chromium defaults">
The bundled sandbox browser image also applies conservative Chromium startup defaults for containerized workloads. Current container defaults include:
By default, Docker sandbox containers run with **no network**.
Override with `agents.defaults.sandbox.docker.network`.
- `--remote-debugging-address=127.0.0.1`
- `--remote-debugging-port=<derived from OPENCLAW_BROWSER_CDP_PORT>`
- `--user-data-dir=${HOME}/.chrome`
- `--no-first-run`
- `--no-default-browser-check`
- `--disable-3d-apis`
- `--disable-gpu`
- `--disable-dev-shm-usage`
- `--disable-background-networking`
- `--disable-extensions`
- `--disable-features=TranslateUI`
- `--disable-breakpad`
- `--disable-crash-reporter`
- `--disable-software-rasterizer`
- `--no-zygote`
- `--metrics-recording-only`
- `--renderer-process-limit=2`
- `--no-sandbox` when `noSandbox` is enabled.
- The three graphics hardening flags (`--disable-3d-apis`, `--disable-software-rasterizer`, `--disable-gpu`) are optional and are useful when containers lack GPU support. Set `OPENCLAW_BROWSER_DISABLE_GRAPHICS_FLAGS=0` if your workload requires WebGL or other 3D/browser features.
- `--disable-extensions` is enabled by default and can be disabled with `OPENCLAW_BROWSER_DISABLE_EXTENSIONS=0` for extension-reliant flows.
- `--renderer-process-limit=2` is controlled by `OPENCLAW_BROWSER_RENDERER_PROCESS_LIMIT=<N>`, where `0` keeps Chromium's default.
The bundled sandbox browser image also applies conservative Chromium startup defaults
for containerized workloads. Current container defaults include:
If you need a different runtime profile, use a custom browser image and provide your own entrypoint. For local (non-container) Chromium profiles, use `browser.extraArgs` to append additional startup flags.
- `--remote-debugging-address=127.0.0.1`
- `--remote-debugging-port=<derived from OPENCLAW_BROWSER_CDP_PORT>`
- `--user-data-dir=${HOME}/.chrome`
- `--no-first-run`
- `--no-default-browser-check`
- `--disable-3d-apis`
- `--disable-gpu`
- `--disable-dev-shm-usage`
- `--disable-background-networking`
- `--disable-extensions`
- `--disable-features=TranslateUI`
- `--disable-breakpad`
- `--disable-crash-reporter`
- `--disable-software-rasterizer`
- `--no-zygote`
- `--metrics-recording-only`
- `--renderer-process-limit=2`
- `--no-sandbox` when `noSandbox` is enabled.
- The three graphics hardening flags (`--disable-3d-apis`,
`--disable-software-rasterizer`, `--disable-gpu`) are optional and are useful
when containers lack GPU support. Set `OPENCLAW_BROWSER_DISABLE_GRAPHICS_FLAGS=0`
if your workload requires WebGL or other 3D/browser features.
- `--disable-extensions` is enabled by default and can be disabled with
`OPENCLAW_BROWSER_DISABLE_EXTENSIONS=0` for extension-reliant flows.
- `--renderer-process-limit=2` is controlled by
`OPENCLAW_BROWSER_RENDERER_PROCESS_LIMIT=<N>`, where `0` keeps Chromium's default.
</Accordion>
<Accordion title="Network security defaults">
- `network: "host"` is blocked.
- `network: "container:<id>"` is blocked by default (namespace join bypass risk).
- Break-glass override: `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true`.
</Accordion>
</AccordionGroup>
If you need a different runtime profile, use a custom browser image and provide
your own entrypoint. For local (non-container) Chromium profiles, use
`browser.extraArgs` to append additional startup flags.
Docker installs and the containerized gateway live here: [Docker](/install/docker)
Security defaults:
- `network: "host"` is blocked.
- `network: "container:<id>"` is blocked by default (namespace join bypass risk).
- Break-glass override: `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true`.
Docker installs and the containerized gateway live here:
[Docker](/install/docker)
For Docker gateway deployments, `scripts/docker/setup.sh` can bootstrap sandbox config.
Set `OPENCLAW_SANDBOX=1` (or `true`/`yes`/`on`) to enable that path. You can
override socket location with `OPENCLAW_DOCKER_SOCKET`. Full setup and env
reference: [Docker](/install/docker#agent-sandbox).
For Docker gateway deployments, `scripts/docker/setup.sh` can bootstrap sandbox config. Set `OPENCLAW_SANDBOX=1` (or `true`/`yes`/`on`) to enable that path. You can override socket location with `OPENCLAW_DOCKER_SOCKET`. Full setup and env reference: [Docker](/install/docker#agent-sandbox).
## setupCommand (one-time container setup)
`setupCommand` runs **once** after the sandbox container is created (not on every run).
It executes inside the container via `sh -lc`.
`setupCommand` runs **once** after the sandbox container is created (not on every run). It executes inside the container via `sh -lc`.
Paths:
- Global: `agents.defaults.sandbox.docker.setupCommand`
- Per-agent: `agents.list[].sandbox.docker.setupCommand`
Common pitfalls:
<AccordionGroup>
<Accordion title="Common pitfalls">
- Default `docker.network` is `"none"` (no egress), so package installs will fail.
- `docker.network: "container:<id>"` requires `dangerouslyAllowContainerNamespaceJoin: true` and is break-glass only.
- `readOnlyRoot: true` prevents writes; set `readOnlyRoot: false` or bake a custom image.
- `user` must be root for package installs (omit `user` or set `user: "0:0"`).
- Sandbox exec does **not** inherit host `process.env`. Use `agents.defaults.sandbox.docker.env` (or a custom image) for skill API keys.
</Accordion>
</AccordionGroup>
- Default `docker.network` is `"none"` (no egress), so package installs will fail.
- `docker.network: "container:<id>"` requires `dangerouslyAllowContainerNamespaceJoin: true` and is break-glass only.
- `readOnlyRoot: true` prevents writes; set `readOnlyRoot: false` or bake a custom image.
- `user` must be root for package installs (omit `user` or set `user: "0:0"`).
- Sandbox exec does **not** inherit host `process.env`. Use
`agents.defaults.sandbox.docker.env` (or a custom image) for skill API keys.
## Tool policy and escape hatches
## Tool policy + escape hatches
Tool allow/deny policies still apply before sandbox rules. If a tool is denied globally or per-agent, sandboxing doesn't bring it back.
Tool allow/deny policies still apply before sandbox rules. If a tool is denied
globally or per-agent, sandboxing doesnt bring it back.
`tools.elevated` is an explicit escape hatch that runs `exec` outside the sandbox (`gateway` by default, or `node` when the exec target is `node`).
`/exec` directives only apply for authorized senders and persist per session; to hard-disable
`exec`, use tool policy deny (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)).
`tools.elevated` is an explicit escape hatch that runs `exec` outside the sandbox (`gateway` by default, or `node` when the exec target is `node`). `/exec` directives only apply for authorized senders and persist per session; to hard-disable `exec`, use tool policy deny (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)).
Debugging:
- Use `openclaw sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for the why is this blocked? mental model.
Keep it locked down.
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for the "why is this blocked?" mental model.
Keep it locked down.
## Multi-agent overrides
Each agent can override sandbox + tools:
`agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools` for sandbox tool policy).
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for precedence.
Each agent can override sandbox + tools: `agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools` for sandbox tool policy). See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for precedence.
## Minimal enable example
@@ -477,10 +475,10 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
}
```
## Related docs
## Related
- [OpenShell](/gateway/openshell) -- managed sandbox backend setup, workspace modes, and config reference
- [Sandbox Configuration](/gateway/config-agents#agentsdefaultssandbox)
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) -- per-agent overrides and precedence
- [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) — per-agent overrides and precedence
- [OpenShell](/gateway/openshell) — managed sandbox backend setup, workspace modes, and config reference
- [Sandbox configuration](/gateway/config-agents#agentsdefaultssandbox)
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) — debugging "why is this blocked?"
- [Security](/gateway/security)

View File

@@ -999,7 +999,7 @@ Logs and transcripts can leak sensitive info even when access controls are corre
Recommendations:
- Keep tool summary redaction on (`logging.redactSensitive: "tools"`; default).
- Keep log and transcript redaction on (`logging.redactSensitive: "tools"`; default).
- Add custom patterns for your environment via `logging.redactPatterns` (tokens, hostnames, internal URLs).
- When sharing diagnostics, prefer `openclaw status --all` (pasteable, secrets redacted) over raw logs.
- Prune old session transcripts and log files if you dont need long retention.

View File

@@ -37,6 +37,11 @@ daemon (`tailscale whois`) and matching it to the header before accepting it.
OpenClaw only treats a request as Serve when it arrives from loopback with
Tailscales `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`
headers.
For Control UI operator sessions that include browser device identity, this
verified Serve path also skips the device-pairing round trip. It does not bypass
browser device identity: device-less clients are still rejected, and node-role
or non-Control UI WebSocket connections still follow the normal pairing and
auth checks.
HTTP API endpoints (for example `/v1/*`, `/tools/invoke`, and `/api/channels/*`)
do **not** use Tailscale identity-header auth. They still follow the gateway's
normal HTTP auth mode: shared-secret auth by default, or an intentionally

View File

@@ -1,6 +1,7 @@
---
summary: "Delegate gateway authentication to a trusted reverse proxy (Pomerium, Caddy, nginx + OAuth)"
title: "Trusted proxy auth"
sidebarTitle: "Trusted proxy auth"
read_when:
- Running OpenClaw behind an identity-aware proxy
- Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw
@@ -8,37 +9,49 @@ read_when:
- Deciding where to set HSTS and other HTTP hardening headers
---
> ⚠️ **Security-sensitive feature.** This mode delegates authentication entirely to your reverse proxy. Misconfiguration can expose your Gateway to unauthorized access. Read this page carefully before enabling.
<Warning>
**Security-sensitive feature.** This mode delegates authentication entirely to your reverse proxy. Misconfiguration can expose your Gateway to unauthorized access. Read this page carefully before enabling.
</Warning>
## When to Use
## When to use
Use `trusted-proxy` auth mode when:
- You run OpenClaw behind an **identity-aware proxy** (Pomerium, Caddy + OAuth, nginx + oauth2-proxy, Traefik + forward auth)
- Your proxy handles all authentication and passes user identity via headers
- You're in a Kubernetes or container environment where the proxy is the only path to the Gateway
- You're hitting WebSocket `1008 unauthorized` errors because browsers can't pass tokens in WS payloads
- You run OpenClaw behind an **identity-aware proxy** (Pomerium, Caddy + OAuth, nginx + oauth2-proxy, Traefik + forward auth).
- Your proxy handles all authentication and passes user identity via headers.
- You're in a Kubernetes or container environment where the proxy is the only path to the Gateway.
- You're hitting WebSocket `1008 unauthorized` errors because browsers can't pass tokens in WS payloads.
## When NOT to Use
## When NOT to use
- If your proxy doesn't authenticate users (just a TLS terminator or load balancer)
- If there's any path to the Gateway that bypasses the proxy (firewall holes, internal network access)
- If you're unsure whether your proxy correctly strips/overwrites forwarded headers
- If you only need personal single-user access (consider Tailscale Serve + loopback for simpler setup)
- If your proxy doesn't authenticate users (just a TLS terminator or load balancer).
- If there's any path to the Gateway that bypasses the proxy (firewall holes, internal network access).
- If you're unsure whether your proxy correctly strips/overwrites forwarded headers.
- If you only need personal single-user access (consider Tailscale Serve + loopback for simpler setup).
## How It Works
## How it works
1. Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.)
2. Proxy adds a header with the authenticated user identity (e.g., `x-forwarded-user: nick@example.com`)
3. OpenClaw checks that the request came from a **trusted proxy IP** (configured in `gateway.trustedProxies`)
4. OpenClaw extracts the user identity from the configured header
5. If everything checks out, the request is authorized
<Steps>
<Step title="Proxy authenticates the user">
Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.).
</Step>
<Step title="Proxy adds an identity header">
Proxy adds a header with the authenticated user identity (e.g., `x-forwarded-user: nick@example.com`).
</Step>
<Step title="Gateway verifies trusted source">
OpenClaw checks that the request came from a **trusted proxy IP** (configured in `gateway.trustedProxies`).
</Step>
<Step title="Gateway extracts identity">
OpenClaw extracts the user identity from the configured header.
</Step>
<Step title="Authorize">
If everything checks out, the request is authorized.
</Step>
</Steps>
## Control UI Pairing Behavior
## Control UI pairing behavior
When `gateway.auth.mode = "trusted-proxy"` is active and the request passes
trusted-proxy checks, Control UI WebSocket sessions can connect without device
pairing identity.
When `gateway.auth.mode = "trusted-proxy"` is active and the request passes trusted-proxy checks, Control UI WebSocket sessions can connect without device pairing identity.
Implications:
@@ -74,61 +87,73 @@ Implications:
}
```
Important runtime rule:
<Warning>
**Important runtime rules**
- Trusted-proxy auth rejects loopback-source requests (`127.0.0.1`, `::1`, loopback CIDRs).
- Same-host loopback reverse proxies do **not** satisfy trusted-proxy auth.
- For same-host loopback proxy setups, use token/password auth instead, or route through a non-loopback trusted proxy address that OpenClaw can verify.
- Non-loopback Control UI deployments still need explicit `gateway.controlUi.allowedOrigins`.
- **Forwarded-header evidence overrides loopback locality.** If a request arrives on loopback but carries `X-Forwarded-For` / `X-Forwarded-Host` / `X-Forwarded-Proto` headers pointing at a non-local origin, that evidence disqualifies the loopback locality claim. The request is treated as remote for pairing, trusted-proxy auth, and Control UI device-identity gating. This prevents a same-host loopback proxy from laundering forwarded-header identity into trusted-proxy auth.
</Warning>
### Configuration Reference
### Configuration reference
| Field | Required | Description |
| ------------------------------------------- | -------- | --------------------------------------------------------------------------- |
| `gateway.trustedProxies` | Yes | Array of proxy IP addresses to trust. Requests from other IPs are rejected. |
| `gateway.auth.mode` | Yes | Must be `"trusted-proxy"` |
| `gateway.auth.trustedProxy.userHeader` | Yes | Header name containing the authenticated user identity |
| `gateway.auth.trustedProxy.requiredHeaders` | No | Additional headers that must be present for the request to be trusted |
| `gateway.auth.trustedProxy.allowUsers` | No | Allowlist of user identities. Empty means allow all authenticated users. |
<ParamField path="gateway.trustedProxies" type="string[]" required>
Array of proxy IP addresses to trust. Requests from other IPs are rejected.
</ParamField>
<ParamField path="gateway.auth.mode" type="string" required>
Must be `"trusted-proxy"`.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.userHeader" type="string" required>
Header name containing the authenticated user identity.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.requiredHeaders" type="string[]">
Additional headers that must be present for the request to be trusted.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.allowUsers" type="string[]">
Allowlist of user identities. Empty means allow all authenticated users.
</ParamField>
## TLS termination and HSTS
Use one TLS termination point and apply HSTS there.
### Recommended pattern: proxy TLS termination
<Tabs>
<Tab title="Proxy TLS termination (recommended)">
When your reverse proxy handles HTTPS for `https://control.example.com`, set `Strict-Transport-Security` at the proxy for that domain.
When your reverse proxy handles HTTPS for `https://control.example.com`, set
`Strict-Transport-Security` at the proxy for that domain.
- Good fit for internet-facing deployments.
- Keeps certificate + HTTP hardening policy in one place.
- OpenClaw can stay on loopback HTTP behind the proxy.
- Good fit for internet-facing deployments.
- Keeps certificate + HTTP hardening policy in one place.
- OpenClaw can stay on loopback HTTP behind the proxy.
Example header value:
Example header value:
```text
Strict-Transport-Security: max-age=31536000; includeSubDomains
```
```text
Strict-Transport-Security: max-age=31536000; includeSubDomains
```
</Tab>
<Tab title="Gateway TLS termination">
If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
### Gateway TLS termination
If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
```json5
{
gateway: {
tls: { enabled: true },
http: {
securityHeaders: {
strictTransportSecurity: "max-age=31536000; includeSubDomains",
```json5
{
gateway: {
tls: { enabled: true },
http: {
securityHeaders: {
strictTransportSecurity: "max-age=31536000; includeSubDomains",
},
},
},
},
},
}
```
}
```
`strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.
`strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.
</Tab>
</Tabs>
### Rollout guidance
@@ -138,124 +163,126 @@ If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
- Use preload only if you intentionally meet preload requirements for your full domain set.
- Loopback-only local development does not benefit from HSTS.
## Proxy Setup Examples
## Proxy setup examples
### Pomerium
<AccordionGroup>
<Accordion title="Pomerium">
Pomerium passes identity in `x-pomerium-claim-email` (or other claim headers) and a JWT in `x-pomerium-jwt-assertion`.
Pomerium passes identity in `x-pomerium-claim-email` (or other claim headers) and a JWT in `x-pomerium-jwt-assertion`.
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // Pomerium's IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-pomerium-claim-email",
requiredHeaders: ["x-pomerium-jwt-assertion"],
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // Pomerium's IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-pomerium-claim-email",
requiredHeaders: ["x-pomerium-jwt-assertion"],
},
},
},
},
},
}
```
Pomerium config snippet:
```yaml
routes:
- from: https://openclaw.example.com
to: http://openclaw-gateway:18789
policy:
- allow:
or:
- email:
is: nick@example.com
pass_identity_headers: true
```
### Caddy with OAuth
Caddy with the `caddy-security` plugin can authenticate users and pass identity headers.
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // Caddy/sidecar proxy IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
```
Caddyfile snippet:
```
openclaw.example.com {
authenticate with oauth2_provider
authorize with policy1
reverse_proxy openclaw:18789 {
header_up X-Forwarded-User {http.auth.user.email}
}
}
```
```
### nginx + oauth2-proxy
Pomerium config snippet:
oauth2-proxy authenticates users and passes identity in `x-auth-request-email`.
```yaml
routes:
- from: https://openclaw.example.com
to: http://openclaw-gateway:18789
policy:
- allow:
or:
- email:
is: nick@example.com
pass_identity_headers: true
```
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // nginx/oauth2-proxy IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-auth-request-email",
</Accordion>
<Accordion title="Caddy with OAuth">
Caddy with the `caddy-security` plugin can authenticate users and pass identity headers.
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // Caddy/sidecar proxy IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
},
},
}
```
}
```
nginx config snippet:
Caddyfile snippet:
```nginx
location / {
auth_request /oauth2/auth;
auth_request_set $user $upstream_http_x_auth_request_email;
```
openclaw.example.com {
authenticate with oauth2_provider
authorize with policy1
proxy_pass http://openclaw:18789;
proxy_set_header X-Auth-Request-Email $user;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
reverse_proxy openclaw:18789 {
header_up X-Forwarded-User {http.auth.user.email}
}
}
```
### Traefik with Forward Auth
</Accordion>
<Accordion title="nginx + oauth2-proxy">
oauth2-proxy authenticates users and passes identity in `x-auth-request-email`.
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["172.17.0.1"], // Traefik container IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // nginx/oauth2-proxy IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-auth-request-email",
},
},
},
},
},
}
```
}
```
nginx config snippet:
```nginx
location / {
auth_request /oauth2/auth;
auth_request_set $user $upstream_http_x_auth_request_email;
proxy_pass http://openclaw:18789;
proxy_set_header X-Auth-Request-Email $user;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
</Accordion>
<Accordion title="Traefik with forward auth">
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["172.17.0.1"], // Traefik container IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
```
</Accordion>
</AccordionGroup>
## Mixed token configuration
@@ -270,8 +297,7 @@ Loopback trusted-proxy auth also fails closed: same-host callers must supply the
## Operator scopes header
Trusted-proxy auth is an **identity-bearing** HTTP mode, so callers may
optionally declare operator scopes with `x-openclaw-scopes`.
Trusted-proxy auth is an **identity-bearing** HTTP mode, so callers may optionally declare operator scopes with `x-openclaw-scopes`.
Examples:
@@ -287,26 +313,22 @@ Behavior:
- Gateway-auth **plugin HTTP routes** are narrower by default: when `x-openclaw-scopes` is absent, their runtime scope falls back to `operator.write`.
- Browser-origin HTTP requests still have to pass `gateway.controlUi.allowedOrigins` (or deliberate Host-header fallback mode) even after trusted-proxy auth succeeds.
Practical rule:
Practical rule: send `x-openclaw-scopes` explicitly when you want a trusted-proxy request to be narrower than the defaults, or when a gateway-auth plugin route needs something stronger than write scope.
- Send `x-openclaw-scopes` explicitly when you want a trusted-proxy request to
be narrower than the defaults, or when a gateway-auth plugin route needs
something stronger than write scope.
## Security Checklist
## Security checklist
Before enabling trusted-proxy auth, verify:
- [ ] **Proxy is the only path**: The Gateway port is firewalled from everything except your proxy
- [ ] **trustedProxies is minimal**: Only your actual proxy IPs, not entire subnets
- [ ] **No loopback proxy source**: trusted-proxy auth fails closed for loopback-source requests
- [ ] **Proxy strips headers**: Your proxy overwrites (not appends) `x-forwarded-*` headers from clients
- [ ] **TLS termination**: Your proxy handles TLS; users connect via HTTPS
- [ ] **allowedOrigins is explicit**: Non-loopback Control UI uses explicit `gateway.controlUi.allowedOrigins`
- [ ] **allowUsers is set** (recommended): Restrict to known users rather than allowing anyone authenticated
- [ ] **No mixed token config**: Do not set both `gateway.auth.token` and `gateway.auth.mode: "trusted-proxy"`
- [ ] **Proxy is the only path**: The Gateway port is firewalled from everything except your proxy.
- [ ] **trustedProxies is minimal**: Only your actual proxy IPs, not entire subnets.
- [ ] **No loopback proxy source**: trusted-proxy auth fails closed for loopback-source requests.
- [ ] **Proxy strips headers**: Your proxy overwrites (not appends) `x-forwarded-*` headers from clients.
- [ ] **TLS termination**: Your proxy handles TLS; users connect via HTTPS.
- [ ] **allowedOrigins is explicit**: Non-loopback Control UI uses explicit `gateway.controlUi.allowedOrigins`.
- [ ] **allowUsers is set** (recommended): Restrict to known users rather than allowing anyone authenticated.
- [ ] **No mixed token config**: Do not set both `gateway.auth.token` and `gateway.auth.mode: "trusted-proxy"`.
## Security Audit
## Security audit
`openclaw security audit` will flag trusted-proxy auth with a **critical** severity finding. This is intentional — it's a reminder that you're delegating security to your proxy setup.
@@ -320,79 +342,95 @@ The audit checks for:
## Troubleshooting
### "trusted_proxy_untrusted_source"
<AccordionGroup>
<Accordion title="trusted_proxy_untrusted_source">
The request didn't come from an IP in `gateway.trustedProxies`. Check:
The request didn't come from an IP in `gateway.trustedProxies`. Check:
- Is the proxy IP correct? (Docker container IPs can change.)
- Is there a load balancer in front of your proxy?
- Use `docker inspect` or `kubectl get pods -o wide` to find actual IPs.
- Is the proxy IP correct? (Docker container IPs can change)
- Is there a load balancer in front of your proxy?
- Use `docker inspect` or `kubectl get pods -o wide` to find actual IPs
</Accordion>
<Accordion title="trusted_proxy_loopback_source">
OpenClaw rejected a loopback-source trusted-proxy request.
### "trusted_proxy_loopback_source"
Check:
OpenClaw rejected a loopback-source trusted-proxy request.
- Is the proxy connecting from `127.0.0.1` / `::1`?
- Are you trying to use trusted-proxy auth with a same-host loopback reverse proxy?
Check:
Fix:
- Is the proxy connecting from `127.0.0.1` / `::1`?
- Are you trying to use trusted-proxy auth with a same-host loopback reverse proxy?
- Use token/password auth for same-host loopback proxy setups, or
- Route through a non-loopback trusted proxy address and keep that IP in `gateway.trustedProxies`.
Fix:
</Accordion>
<Accordion title="trusted_proxy_user_missing">
The user header was empty or missing. Check:
- Use token/password auth for same-host loopback proxy setups, or
- Route through a non-loopback trusted proxy address and keep that IP in `gateway.trustedProxies`.
- Is your proxy configured to pass identity headers?
- Is the header name correct? (case-insensitive, but spelling matters)
- Is the user actually authenticated at the proxy?
### "trusted_proxy_user_missing"
</Accordion>
<Accordion title="trusted_proxy_missing_header_*">
A required header wasn't present. Check:
The user header was empty or missing. Check:
- Your proxy configuration for those specific headers.
- Whether headers are being stripped somewhere in the chain.
- Is your proxy configured to pass identity headers?
- Is the header name correct? (case-insensitive, but spelling matters)
- Is the user actually authenticated at the proxy?
</Accordion>
<Accordion title="trusted_proxy_user_not_allowed">
The user is authenticated but not in `allowUsers`. Either add them or remove the allowlist.
</Accordion>
<Accordion title="trusted_proxy_origin_not_allowed">
Trusted-proxy auth succeeded, but the browser `Origin` header did not pass Control UI origin checks.
### "trusted*proxy_missing_header*\*"
Check:
A required header wasn't present. Check:
- `gateway.controlUi.allowedOrigins` includes the exact browser origin.
- You are not relying on wildcard origins unless you intentionally want allow-all behavior.
- If you intentionally use Host-header fallback mode, `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` is set deliberately.
- Your proxy configuration for those specific headers
- Whether headers are being stripped somewhere in the chain
</Accordion>
<Accordion title="WebSocket still failing">
Make sure your proxy:
### "trusted_proxy_user_not_allowed"
- Supports WebSocket upgrades (`Upgrade: websocket`, `Connection: upgrade`).
- Passes the identity headers on WebSocket upgrade requests (not just HTTP).
- Doesn't have a separate auth path for WebSocket connections.
The user is authenticated but not in `allowUsers`. Either add them or remove the allowlist.
</Accordion>
</AccordionGroup>
### "trusted_proxy_origin_not_allowed"
Trusted-proxy auth succeeded, but the browser `Origin` header did not pass Control UI origin checks.
Check:
- `gateway.controlUi.allowedOrigins` includes the exact browser origin
- You are not relying on wildcard origins unless you intentionally want allow-all behavior
- If you intentionally use Host-header fallback mode, `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` is set deliberately
### WebSocket Still Failing
Make sure your proxy:
- Supports WebSocket upgrades (`Upgrade: websocket`, `Connection: upgrade`)
- Passes the identity headers on WebSocket upgrade requests (not just HTTP)
- Doesn't have a separate auth path for WebSocket connections
## Migration from Token Auth
## Migration from token auth
If you're moving from token auth to trusted-proxy:
1. Configure your proxy to authenticate users and pass headers
2. Test the proxy setup independently (curl with headers)
3. Update OpenClaw config with trusted-proxy auth
4. Restart the Gateway
5. Test WebSocket connections from the Control UI
6. Run `openclaw security audit` and review findings
<Steps>
<Step title="Configure the proxy">
Configure your proxy to authenticate users and pass headers.
</Step>
<Step title="Test the proxy independently">
Test the proxy setup independently (curl with headers).
</Step>
<Step title="Update OpenClaw config">
Update OpenClaw config with trusted-proxy auth.
</Step>
<Step title="Restart the Gateway">
Restart the Gateway.
</Step>
<Step title="Test WebSocket">
Test WebSocket connections from the Control UI.
</Step>
<Step title="Audit">
Run `openclaw security audit` and review findings.
</Step>
</Steps>
## Related
- [Security](/gateway/security) — full security guide
- [Configuration](/gateway/configuration) — config reference
- [Remote Access](/gateway/remote) — other remote access patterns
- [Remote access](/gateway/remote) — other remote access patterns
- [Security](/gateway/security) — full security guide
- [Tailscale](/gateway/tailscale) — simpler alternative for tailnet-only access

View File

@@ -766,30 +766,32 @@ and troubleshooting see the main [FAQ](/help/faq).
</Accordion>
<Accordion title="Can I switch between npm and git installs later?">
Yes. Install the other flavor, then run Doctor so the gateway service points at the new entrypoint.
This **does not delete your data** - it only changes the OpenClaw code install. Your state
(`~/.openclaw`) and workspace (`~/.openclaw/workspace`) stay untouched.
Yes. Use `openclaw update --channel ...` when OpenClaw is already installed.
This **does not delete your data** - it only changes the OpenClaw code install.
Your state (`~/.openclaw`) and workspace (`~/.openclaw/workspace`) stay untouched.
From npm to git:
```bash
git clone https://github.com/openclaw/openclaw.git
cd openclaw
pnpm install
pnpm build
openclaw doctor
openclaw gateway restart
openclaw update --channel dev
```
From git to npm:
```bash
npm install -g openclaw@latest
openclaw doctor
openclaw gateway restart
openclaw update --channel stable
```
Doctor detects a gateway service entrypoint mismatch and offers to rewrite the service config to match the current install (use `--repair` in automation).
Add `--dry-run` to preview the planned mode switch first. The updater runs
Doctor follow-ups, refreshes plugin sources for the target channel, and
restarts the gateway unless you pass `--no-restart`.
The installer can force either mode too:
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm
```
Backup tips: see [Backup strategy](#where-things-live-on-disk).

View File

@@ -227,10 +227,12 @@ Notes:
- `OPENCLAW_LIVE_ACP_BIND_CODEX_MODEL=gpt-5.2`
- `OPENCLAW_LIVE_ACP_BIND_OPENCODE_MODEL=opencode/kimi-k2.6`
- `OPENCLAW_LIVE_ACP_BIND_REQUIRE_TRANSCRIPT=1`
- `OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON=1`
- `OPENCLAW_LIVE_ACP_BIND_PARENT_MODEL=openai/gpt-5.2`
- Notes:
- This lane uses the gateway `chat.send` surface with admin-only synthetic originating-route fields so tests can attach message-channel context without pretending to deliver externally.
- When `OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND` is unset, the test uses the embedded `acpx` plugin's built-in agent registry for the selected ACP harness agent.
- Bound-session cron MCP creation is best-effort by default because external ACP harnesses can cancel MCP calls after the bind/image proof has passed; set `OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON=1` to make that post-bind cron probe strict.
Example:

View File

@@ -172,6 +172,10 @@ runs the same lanes before release approval.
- Use `--platform macos`, `--platform windows`, or `--platform linux` while
iterating on one guest. Use `--json` for the summary artifact path and
per-lane status.
- The OpenAI lane uses `openai/gpt-5.5` for the live agent-turn proof by
default. Pass `--model <provider/model>` or set
`OPENCLAW_PARALLELS_OPENAI_MODEL` when deliberately validating another
OpenAI model.
- Wrap long local runs in a host timeout so Parallels transport stalls cannot
consume the rest of the testing window:
@@ -407,9 +411,9 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- Untargeted `pnpm test` runs twelve smaller shard configs (`core-unit-fast`, `core-unit-src`, `core-unit-security`, `core-unit-ui`, `core-unit-support`, `core-support-boundary`, `core-contracts`, `core-bundled`, `core-runtime`, `agentic`, `auto-reply`, `extensions`) instead of one giant native root-project process. This cuts peak RSS on loaded machines and avoids auto-reply/extension work starving unrelated suites.
- `pnpm test --watch` still uses the native root `vitest.config.ts` project graph, because a multi-shard watch loop is not practical.
- `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax.
- `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun.
- `pnpm check:changed` is the normal smart local gate for narrow work. It classifies the diff into core, core tests, extensions, extension tests, apps, docs, release metadata, live Docker tooling, and tooling, then runs the matching typecheck/lint/test lanes. Public Plugin SDK and plugin-contract changes include one extension validation pass because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks instead of the full suite, with a guard that rejects package changes outside the top-level version field.
- Live Docker ACP harness edits run a focused local gate: shell syntax for the live Docker auth scripts, live Docker scheduler dry-run, ACP bind unit tests, and the ACPX extension tests. `package.json` changes are included only when the diff is limited to `scripts["test:docker:live-*"]`; dependency, export, version, and other package-surface edits still use the broader guards.
- `pnpm test:changed` expands changed git paths into cheap scoped lanes by default: direct test edits, sibling `*.test.ts` files, explicit source mappings, and local import-graph dependents. Config/setup/package edits do not broad-run tests unless you explicitly use `OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed`.
- `pnpm check:changed` is the normal smart local check gate for narrow work. It classifies the diff into core, core tests, extensions, extension tests, apps, docs, release metadata, live Docker tooling, and tooling, then runs the matching typecheck, lint, and guard commands. It does not run Vitest tests; call `pnpm test:changed` or explicit `pnpm test <target>` for test proof. Release metadata-only version bumps run targeted version/config/root-dependency checks, with a guard that rejects package changes outside the top-level version field.
- Live Docker ACP harness edits run focused checks: shell syntax for the live Docker auth scripts and a live Docker scheduler dry-run. `package.json` changes are included only when the diff is limited to `scripts["test:docker:live-*"]`; dependency, export, version, and other package-surface edits still use the broader guards.
- Import-light unit tests from agents, commands, plugins, auto-reply helpers, `plugin-sdk`, and similar pure utility areas route through the `unit-fast` lane, which skips `test/setup-openclaw-runtime.ts`; stateful/runtime-heavy files stay on the existing lanes.
- Selected `plugin-sdk` and `commands` helper source files also map changed-mode runs to explicit sibling tests in those light lanes, so helper edits avoid rerunning the full heavy suite for that directory.
- `auto-reply` has dedicated buckets for top-level core helpers, top-level `reply.*` integration tests, and the `src/auto-reply/reply/**` subtree. CI further splits the reply subtree into agent-runner, dispatch, and commands/state-routing shards so one import-heavy bucket does not own the full Node tail.
@@ -454,10 +458,11 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
- The pre-commit hook is formatting-only. It restages formatted files and
does not run lint, typecheck, or tests.
- Run `pnpm check:changed` explicitly before handoff or push when you
need the smart local gate. Public Plugin SDK and plugin-contract
changes include one extension validation pass.
- `pnpm test:changed` routes through scoped lanes when the changed paths
map cleanly to a smaller suite.
need the smart local check gate.
- `pnpm test:changed` routes through cheap scoped lanes by default. Use
`OPENCLAW_TEST_CHANGED_BROAD=1 pnpm test:changed` only when the agent
decides a harness, config, package, or contract edit really needs broader
Vitest coverage.
- `pnpm test:max` and `pnpm test:changed:max` keep the same routing
behavior, just with a higher worker cap.
- Local worker auto-scaling is intentionally conservative and backs off
@@ -602,8 +607,8 @@ These Docker runners split into two buckets:
`OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=45000`, and
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
explicitly want the larger exhaustive scan.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the live Docker lanes. It also builds one shared `scripts/e2e/Dockerfile` image via `test:docker:e2e-build` and reuses it for the E2E container smoke runners that exercise the built app. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. 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. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
@@ -612,12 +617,14 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
- CLI backend smoke: `pnpm test:docker:live-cli-backend` (script: `scripts/test-live-cli-backend-docker.sh`)
- Codex app-server harness smoke: `pnpm test:docker:live-codex-harness` (script: `scripts/test-live-codex-harness-docker.sh`)
- Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`)
- Docker observability smoke: included in `pnpm test:docker:all`, `pnpm test:docker:local:all`, and the release-path `core` chunk (script: `scripts/e2e/docker-observability-smoke.sh`). It runs QA-lab OTEL and Prometheus diagnostics checks inside the shared package-installed functional Docker image, with only QA harness files mounted read-only. Set `OPENCLAW_DOCKER_OBSERVABILITY_LOOPS=<count>` to repeat both checks in one container run.
- Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`)
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`.
- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`.
- Update channel switch smoke: `pnpm test:docker:update-channel-switch` installs the packed OpenClaw tarball globally in Docker, switches from package `stable` to git `dev`, verifies the persisted channel and plugin post-update work, then switches back to package `stable` and checks update status.
- Session runtime context smoke: `pnpm test:docker:session-runtime-context` verifies hidden runtime context transcript persistence plus doctor repair of affected duplicated prompt-rewrite branches.
- Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`.
- Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns.
- Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Override with `OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE=2026.4.22` locally, or with the Install Smoke workflow's `update_baseline_version` input on GitHub. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns.
- Install Smoke CI skips the duplicate direct-npm global update with `OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL=1`; run the script locally without that env when direct `npm install -g` coverage is needed.
- Agents delete shared workspace CLI smoke: `pnpm test:docker:agents-delete-shared-workspace` (script: `scripts/e2e/agents-delete-shared-workspace-docker.sh`) builds the root Dockerfile image by default, seeds two agents with one workspace in an isolated container home, runs `agents delete --json`, and verifies valid JSON plus retained workspace behavior. Reuse the install-smoke image with `OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE=openclaw-dockerfile-smoke:local OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD=1`.
- Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`)
@@ -626,18 +633,19 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
- MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`)
- Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`)
- Cron/subagent MCP cleanup (real Gateway + stdio MCP child teardown after isolated cron and one-shot subagent runs): `pnpm test:docker:cron-mcp-cleanup` (script: `scripts/e2e/cron-mcp-cleanup-docker.sh`)
- Plugins (install smoke + `/plugin` alias + Claude-bundle restart semantics): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
- Plugins (install smoke, ClawHub install/uninstall, marketplace updates, and Claude-bundle enable/inspect): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the live ClawHub block, or override the default package with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`.
- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`)
- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`)
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate pre-packs this tarball once, then shards bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate pre-packs this tarball once, then shards bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example:
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 pnpm test:docker:bundled-channel-deps`.
To prebuild and reuse the shared built-app image manually:
To prebuild and reuse the shared functional image manually:
```bash
OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e:local pnpm test:docker:e2e-build
OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e:local OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels
OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e-functional:local pnpm test:docker:e2e-build
OPENCLAW_DOCKER_E2E_IMAGE=openclaw-docker-e2e-functional:local OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels
```
Suite-specific image overrides such as `OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE` still win when set. When `OPENCLAW_SKIP_DOCKER_BUILD=1` points at a remote shared image, the scripts pull it if it is not already local. The QR and installer Docker tests keep their own Dockerfiles because they validate package/install behavior rather than the shared built-app runtime.

View File

@@ -122,16 +122,65 @@ and setup-time config writes through `openclaw-gateway` with
The setup script accepts these optional environment variables:
| Variable | Purpose |
| ------------------------------ | --------------------------------------------------------------- |
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
| `OPENCLAW_DOCKER_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
| `OPENCLAW_EXTENSIONS` | Pre-install plugin deps at build time (space-separated names) |
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
| Variable | Purpose |
| ------------------------------------------ | --------------------------------------------------------------- |
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
| `OPENCLAW_DOCKER_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
| `OPENCLAW_EXTENSIONS` | Pre-install plugin deps at build time (space-separated names) |
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
| `OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS` | Disable bundled plugin source bind-mount overlays |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared OTLP/HTTP collector endpoint for OpenTelemetry export |
| `OTEL_EXPORTER_OTLP_*_ENDPOINT` | Signal-specific OTLP endpoints for traces, metrics, or logs |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol override. Only `http/protobuf` is supported today |
| `OTEL_SERVICE_NAME` | Service name used for OpenTelemetry resources |
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Opt in to latest experimental GenAI semantic attributes |
| `OPENCLAW_OTEL_PRELOADED` | Skip starting a second OpenTelemetry SDK when one is preloaded |
Maintainers can test bundled plugin source against a packaged image by mounting
one plugin source directory over its packaged source path, for example
`OPENCLAW_EXTRA_MOUNTS=/path/to/fork/extensions/synology-chat:/app/extensions/synology-chat:ro`.
That mounted source directory overrides the matching compiled
`/app/dist/extensions/synology-chat` bundle for the same plugin id.
### Observability
OpenTelemetry export is outbound from the Gateway container to your OTLP
collector. It does not require a published Docker port. If you build the image
locally and want the bundled OpenTelemetry exporter available inside the image,
include its runtime dependencies:
```bash
export OPENCLAW_EXTENSIONS="diagnostics-otel"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4318"
export OTEL_SERVICE_NAME="openclaw-gateway"
./scripts/docker/setup.sh
```
The official OpenClaw Docker release image includes the bundled
`diagnostics-otel` plugin source. Depending on the image and cache state, the
Gateway may still stage plugin-local OpenTelemetry runtime dependencies the
first time the plugin is enabled, so allow that first boot to reach the package
registry or prewarm the image in your release lane. To enable export, allow and
enable the `diagnostics-otel` plugin in config, then set
`diagnostics.otel.enabled=true` or use the config example in
[OpenTelemetry export](/gateway/opentelemetry). Collector auth headers are
configured through `diagnostics.otel.headers`, not through Docker environment
variables.
Prometheus metrics use the already-published Gateway port. Enable the
`diagnostics-prometheus` plugin, then scrape:
```text
http://<gateway-host>:18789/api/diagnostics/prometheus
```
The route is protected by Gateway authentication. Do not expose a separate
public `/metrics` port or unauthenticated reverse-proxy path. See
[Prometheus metrics](/gateway/prometheus).
### Health checks
@@ -308,9 +357,11 @@ See [ClawDock](/install/clawdock) for the full helper guide.
</Accordion>
<Accordion title="Base image metadata">
The main Docker image uses `node:24-bookworm` and publishes OCI base-image
annotations including `org.opencontainers.image.base.name`,
`org.opencontainers.image.source`, and others. See
The main Docker runtime image uses `node:24-bookworm-slim` and publishes OCI
base-image annotations including `org.opencontainers.image.base.name`,
`org.opencontainers.image.source`, and others. The Node base digest is
refreshed through Dependabot Docker base-image PRs; release builds do not run
a distro upgrade layer. See
[OCI image annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md).
</Accordion>
</AccordionGroup>

View File

@@ -61,6 +61,10 @@ curl -fsSL https://openclaw.ai/install-cli.sh | bash
It supports npm installs by default, plus git-checkout installs under the same
prefix flow. Full reference: [Installer internals](/install/installer#install-clish).
Already installed? Switch between package and git installs with
`openclaw update --channel dev` and `openclaw update --channel stable`. See
[Updating](/install/updating#switch-between-npm-and-git-installs).
### npm, pnpm, or bun
If you already manage Node yourself:

View File

@@ -20,6 +20,7 @@ To switch channels or target a specific version:
```bash
openclaw update --channel beta
openclaw update --channel dev
openclaw update --tag main
openclaw update --dry-run # preview without applying
```
@@ -30,13 +31,55 @@ if you want the raw npm beta dist-tag for a one-off package update.
See [Development channels](/install/development-channels) for channel semantics.
## Switch between npm and git installs
Use channels when you want to change the install type. The updater keeps your
state, config, credentials, and workspace in `~/.openclaw`; it only changes
which OpenClaw code install the CLI and gateway use.
```bash
# npm package install -> editable git checkout
openclaw update --channel dev
# git checkout -> npm package install
openclaw update --channel stable
```
Run with `--dry-run` first to preview the exact install-mode switch:
```bash
openclaw update --channel dev --dry-run
openclaw update --channel stable --dry-run
```
The `dev` channel ensures a git checkout, builds it, and installs the global CLI
from that checkout. The `stable` and `beta` channels use package installs. If the
gateway is already installed, `openclaw update` refreshes the service metadata
and restarts it unless you pass `--no-restart`.
## Alternative: re-run the installer
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```
Add `--no-onboard` to skip onboarding. For source installs, pass `--install-method git --no-onboard`.
Add `--no-onboard` to skip onboarding. To force a specific install type through
the installer, pass `--install-method git --no-onboard` or
`--install-method npm --no-onboard`.
If `openclaw update` fails after the npm package install phase, re-run the
installer. The installer does not call the old updater; it runs the global
package install directly and can recover a partially updated npm install.
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm
```
To pin the recovery to a specific version or dist-tag, add `--version`:
```bash
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm --version <version-or-dist-tag>
```
## Alternative: manual npm, pnpm, or bun
@@ -44,6 +87,12 @@ Add `--no-onboard` to skip onboarding. For source installs, pass `--install-meth
npm i -g openclaw@latest
```
When `openclaw update` manages a global npm install, it first runs the normal
global install command. If that command fails, OpenClaw retries once with
`--omit=optional`. That retry helps hosts where native optional dependencies
cannot compile, while keeping the original failure visible if the fallback also
fails.
```bash
pnpm add -g openclaw@latest
```

View File

@@ -103,6 +103,18 @@ openclaw channels logs --channel whatsapp
Each line in the log file is a JSON object. The CLI and Control UI parse these
entries to render structured output (time, level, subsystem, message).
File-log JSONL records also include machine-filterable top-level fields when
available:
- `hostname`: gateway host name.
- `message`: flattened log message text for full-text search.
- `agent_id`: active agent id when the log call carries agent context.
- `session_id`: active session id/key when the log call carries session context.
- `channel`: active channel when the log call carries channel context.
OpenClaw preserves the original structured log arguments alongside these fields
so existing parsers that read numbered tslog argument keys keep working.
### Console output
Console logs are **TTY-aware** and formatted for readability:
@@ -157,6 +169,33 @@ You can override both via the **`OPENCLAW_LOG_LEVEL`** environment variable (e.g
`--verbose` only affects console output and WS log verbosity; it does not change
file log levels.
### Trace correlation
File logs are JSONL. When a log call carries a valid diagnostic trace context,
OpenClaw writes the trace fields as top-level JSON keys (`traceId`, `spanId`,
`parentSpanId`, `traceFlags`) so external log processors can correlate the line
with OTEL spans and provider `traceparent` propagation.
Gateway HTTP requests and Gateway WebSocket frames establish an internal request
trace scope. Logs and diagnostic events emitted inside that async scope inherit
the request trace when they do not pass an explicit trace context. Agent run and
model-call traces become children of the active request trace, so local logs,
diagnostic snapshots, OTEL spans, and trusted provider `traceparent` headers can
be joined by `traceId` without logging raw request or model content.
### Model call size and timing
Model-call diagnostics record bounded request/response measurements without
capturing raw prompt or response content:
- `requestPayloadBytes`: UTF-8 byte size of the final model request payload
- `responseStreamBytes`: UTF-8 byte size of streamed model response events
- `timeToFirstByteMs`: elapsed time before the first streamed response event
- `durationMs`: total model-call duration
These fields are available to diagnostic snapshots, model-call plugin hooks, and
OTEL model-call spans/metrics when diagnostics export is enabled.
### Console styles
`logging.consoleStyle`:
@@ -167,14 +206,16 @@ file log levels.
### Redaction
Tool summaries can redact sensitive tokens before they hit the console:
OpenClaw can redact sensitive tokens before they hit console output, file logs,
OTLP log records, or persisted session transcript text:
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
- `logging.redactPatterns`: list of regex strings to override the default set
Redaction applies at the logging sinks for **console output**, **stderr-routed
console diagnostics**, and **file logs**. File logs stay JSONL, but matching
secret values are masked before the line is written to disk.
File logs and session transcripts stay JSONL, but matching secret values are
masked before the line or message is written to disk. Redaction is best-effort:
it applies to text-bearing message content and log strings, not every
identifier or binary payload field.
## Diagnostics and OpenTelemetry

View File

@@ -4,51 +4,65 @@ read_when:
- Designing or refactoring media understanding
- Tuning inbound audio/video/image preprocessing
title: "Media understanding"
sidebarTitle: "Media understanding"
---
# Media Understanding - Inbound (2026-01-17)
OpenClaw can **summarize inbound media** (image/audio/video) before the reply pipeline runs. It auto-detects when local tools or provider keys are available, and can be disabled or customized. If understanding is off, models still receive the original files/URLs as usual.
OpenClaw can **summarize inbound media** (image/audio/video) before the reply pipeline runs. It autodetects when local tools or provider keys are available, and can be disabled or customized. If understanding is off, models still receive the original files/URLs as usual.
Vendor-specific media behavior is registered by vendor plugins, while OpenClaw
core owns the shared `tools.media` config, fallback order, and reply-pipeline
integration.
Vendor-specific media behavior is registered by vendor plugins, while OpenClaw core owns the shared `tools.media` config, fallback order, and reply-pipeline integration.
## Goals
- Optional: predigest inbound media into short text for faster routing + better command parsing.
- Optional: pre-digest inbound media into short text for faster routing + better command parsing.
- Preserve original media delivery to the model (always).
- Support **provider APIs** and **CLI fallbacks**.
- Allow multiple models with ordered fallback (error/size/timeout).
## High-level behavior
1. Collect inbound attachments (`MediaPaths`, `MediaUrls`, `MediaTypes`).
2. For each enabled capability (image/audio/video), select attachments per policy (default: **first**).
3. Choose the first eligible model entry (size + capability + auth).
4. If a model fails or the media is too large, **fall back to the next entry**.
5. On success:
- `Body` becomes `[Image]`, `[Audio]`, or `[Video]` block.
- Audio sets `{{Transcript}}`; command parsing uses caption text when present,
otherwise the transcript.
- Captions are preserved as `User text:` inside the block.
<Steps>
<Step title="Collect attachments">
Collect inbound attachments (`MediaPaths`, `MediaUrls`, `MediaTypes`).
</Step>
<Step title="Select per-capability">
For each enabled capability (image/audio/video), select attachments per policy (default: **first**).
</Step>
<Step title="Choose model">
Choose the first eligible model entry (size + capability + auth).
</Step>
<Step title="Fallback on failure">
If a model fails or the media is too large, **fall back to the next entry**.
</Step>
<Step title="Apply success block">
On success:
- `Body` becomes `[Image]`, `[Audio]`, or `[Video]` block.
- Audio sets `{{Transcript}}`; command parsing uses caption text when present, otherwise the transcript.
- Captions are preserved as `User text:` inside the block.
</Step>
</Steps>
If understanding fails or is disabled, **the reply flow continues** with the original body + attachments.
## Config overview
`tools.media` supports **shared models** plus percapability overrides:
`tools.media` supports **shared models** plus per-capability overrides:
- `tools.media.models`: shared model list (use `capabilities` to gate).
- `tools.media.image` / `tools.media.audio` / `tools.media.video`:
- defaults (`prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`)
- provider overrides (`baseUrl`, `headers`, `providerOptions`)
- Deepgram audio options via `tools.media.audio.providerOptions.deepgram`
- audio transcript echo controls (`echoTranscript`, default `false`; `echoFormat`)
- optional **percapability `models` list** (preferred before shared models)
- `attachments` policy (`mode`, `maxAttachments`, `prefer`)
- `scope` (optional gating by channel/chatType/session key)
- `tools.media.concurrency`: max concurrent capability runs (default **2**).
<AccordionGroup>
<Accordion title="Top-level keys">
- `tools.media.models`: shared model list (use `capabilities` to gate).
- `tools.media.image` / `tools.media.audio` / `tools.media.video`:
- defaults (`prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`)
- provider overrides (`baseUrl`, `headers`, `providerOptions`)
- Deepgram audio options via `tools.media.audio.providerOptions.deepgram`
- audio transcript echo controls (`echoTranscript`, default `false`; `echoFormat`)
- optional **per-capability `models` list** (preferred before shared models)
- `attachments` policy (`mode`, `maxAttachments`, `prefer`)
- `scope` (optional gating by channel/chatType/session key)
- `tools.media.concurrency`: max concurrent capability runs (default **2**).
</Accordion>
</AccordionGroup>
```json5
{
@@ -77,99 +91,110 @@ If understanding fails or is disabled, **the reply flow continues** with the ori
Each `models[]` entry can be **provider** or **CLI**:
```json5
{
type: "provider", // default if omitted
provider: "openai",
model: "gpt-5.5",
prompt: "Describe the image in <= 500 chars.",
maxChars: 500,
maxBytes: 10485760,
timeoutSeconds: 60,
capabilities: ["image"], // optional, used for multimodal entries
profile: "vision-profile",
preferredProfile: "vision-fallback",
}
```
<Tabs>
<Tab title="Provider entry">
```json5
{
type: "provider", // default if omitted
provider: "openai",
model: "gpt-5.5",
prompt: "Describe the image in <= 500 chars.",
maxChars: 500,
maxBytes: 10485760,
timeoutSeconds: 60,
capabilities: ["image"], // optional, used for multi-modal entries
profile: "vision-profile",
preferredProfile: "vision-fallback",
}
```
</Tab>
<Tab title="CLI entry">
```json5
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
],
maxChars: 500,
maxBytes: 52428800,
timeoutSeconds: 120,
capabilities: ["video", "image"],
}
```
```json5
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
],
maxChars: 500,
maxBytes: 52428800,
timeoutSeconds: 120,
capabilities: ["video", "image"],
}
```
CLI templates can also use:
CLI templates can also use:
- `{{MediaDir}}` (directory containing the media file)
- `{{OutputDir}}` (scratch dir created for this run)
- `{{OutputBase}}` (scratch file base path, no extension)
- `{{MediaDir}}` (directory containing the media file)
- `{{OutputDir}}` (scratch dir created for this run)
- `{{OutputBase}}` (scratch file base path, no extension)
</Tab>
</Tabs>
## Defaults and limits
Recommended defaults:
- `maxChars`: **500** for image/video (short, commandfriendly)
- `maxChars`: **500** for image/video (short, command-friendly)
- `maxChars`: **unset** for audio (full transcript unless you set a limit)
- `maxBytes`:
- image: **10MB**
- audio: **20MB**
- video: **50MB**
Rules:
- If media exceeds `maxBytes`, that model is skipped and the **next model is tried**.
- Audio files smaller than **1024 bytes** are treated as empty/corrupt and skipped before provider/CLI transcription; inbound reply context receives a deterministic placeholder transcript so the agent knows the note was too small.
- If the model returns more than `maxChars`, output is trimmed.
- `prompt` defaults to simple Describe the {media}. plus the `maxChars` guidance (image/video only).
- If the active primary image model already supports vision natively, OpenClaw
skips the `[Image]` summary block and passes the original image into the
model instead.
- If a Gateway/WebChat primary model is text-only, image attachments are
preserved as offloaded `media://inbound/*` refs so the image/PDF tools or
configured image model can still inspect them instead of losing the attachment.
- Explicit `openclaw infer image describe --model <provider/model>` requests
are different: they run that image-capable provider/model directly, including
Ollama refs such as `ollama/qwen2.5vl:7b`.
- If `<capability>.enabled: true` but no models are configured, OpenClaw tries the
**active reply model** when its provider supports the capability.
<AccordionGroup>
<Accordion title="Rules">
- If media exceeds `maxBytes`, that model is skipped and the **next model is tried**.
- Audio files smaller than **1024 bytes** are treated as empty/corrupt and skipped before provider/CLI transcription; inbound reply context receives a deterministic placeholder transcript so the agent knows the note was too small.
- If the model returns more than `maxChars`, output is trimmed.
- `prompt` defaults to simple "Describe the {media}." plus the `maxChars` guidance (image/video only).
- If the active primary image model already supports vision natively, OpenClaw skips the `[Image]` summary block and passes the original image into the model instead.
- If a Gateway/WebChat primary model is text-only, image attachments are preserved as offloaded `media://inbound/*` refs so the image/PDF tools or configured image model can still inspect them instead of losing the attachment.
- Explicit `openclaw infer image describe --model <provider/model>` requests are different: they run that image-capable provider/model directly, including Ollama refs such as `ollama/qwen2.5vl:7b`.
- If `<capability>.enabled: true` but no models are configured, OpenClaw tries the **active reply model** when its provider supports the capability.
</Accordion>
</AccordionGroup>
### Auto-detect media understanding (default)
If `tools.media.<capability>.enabled` is **not** set to `false` and you havent
configured models, OpenClaw auto-detects in this order and **stops at the first
working option**:
If `tools.media.<capability>.enabled` is **not** set to `false` and you haven't configured models, OpenClaw auto-detects in this order and **stops at the first working option**:
1. **Active reply model** when its provider supports the capability.
2. **`agents.defaults.imageModel`** primary/fallback refs (image only).
3. **Local CLIs** (audio only; if installed)
- `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with encoder/decoder/joiner/tokens)
- `whisper-cli` (`whisper-cpp`; uses `WHISPER_CPP_MODEL` or the bundled tiny model)
- `whisper` (Python CLI; downloads models automatically)
4. **Gemini CLI** (`gemini`) using `read_many_files`
5. **Provider auth**
- Configured `models.providers.*` entries that support the capability are
tried before the bundled fallback order.
- Image-only config providers with an image-capable model auto-register for
media understanding even when they are not a bundled vendor plugin.
- Ollama image understanding is available when selected explicitly, for
example through `agents.defaults.imageModel` or
`openclaw infer image describe --model ollama/<vision-model>`.
- Bundled fallback order:
- Audio: OpenAI → Groq → xAI → Deepgram → Google → SenseAudio → ElevenLabs → Mistral
- Image: OpenAI → Anthropic → Google → MiniMax → MiniMax Portal → Z.AI
- Video: Google → Qwen → Moonshot
<Steps>
<Step title="Active reply model">
Active reply model when its provider supports the capability.
</Step>
<Step title="agents.defaults.imageModel">
`agents.defaults.imageModel` primary/fallback refs (image only).
</Step>
<Step title="Local CLIs (audio only)">
Local CLIs (if installed):
- `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with encoder/decoder/joiner/tokens)
- `whisper-cli` (`whisper-cpp`; uses `WHISPER_CPP_MODEL` or the bundled tiny model)
- `whisper` (Python CLI; downloads models automatically)
</Step>
<Step title="Gemini CLI">
`gemini` using `read_many_files`.
</Step>
<Step title="Provider auth">
- Configured `models.providers.*` entries that support the capability are tried before the bundled fallback order.
- Image-only config providers with an image-capable model auto-register for media understanding even when they are not a bundled vendor plugin.
- Ollama image understanding is available when selected explicitly, for example through `agents.defaults.imageModel` or `openclaw infer image describe --model ollama/<vision-model>`.
Bundled fallback order:
- Audio: OpenAI → Groq → xAI → Deepgram → Google → SenseAudio → ElevenLabs → Mistral
- Image: OpenAI → Anthropic → Google → MiniMax → MiniMax Portal → Z.AI
- Video: Google → Qwen → Moonshot
</Step>
</Steps>
To disable auto-detection, set:
@@ -185,26 +210,24 @@ To disable auto-detection, set:
}
```
Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on `PATH` (we expand `~`), or set an explicit CLI model with a full command path.
<Note>
Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on `PATH` (we expand `~`), or set an explicit CLI model with a full command path.
</Note>
### Proxy environment support (provider models)
When provider-based **audio** and **video** media understanding is enabled, OpenClaw
honors standard outbound proxy environment variables for provider HTTP calls:
When provider-based **audio** and **video** media understanding is enabled, OpenClaw honors standard outbound proxy environment variables for provider HTTP calls:
- `HTTPS_PROXY`
- `HTTP_PROXY`
- `https_proxy`
- `http_proxy`
If no proxy env vars are set, media understanding uses direct egress.
If the proxy value is malformed, OpenClaw logs a warning and falls back to direct
fetch.
If no proxy env vars are set, media understanding uses direct egress. If the proxy value is malformed, OpenClaw logs a warning and falls back to direct fetch.
## Capabilities (optional)
If you set `capabilities`, the entry only runs for those media types. For shared
lists, OpenClaw can infer defaults:
If you set `capabilities`, the entry only runs for those media types. For shared lists, OpenClaw can infer defaults:
- `openai`, `anthropic`, `minimax`: **image**
- `minimax-portal`: **image**
@@ -217,11 +240,9 @@ lists, OpenClaw can infer defaults:
- `groq`: **audio**
- `xai`: **audio**
- `deepgram`: **audio**
- Any `models.providers.<id>.models[]` catalog with an image-capable model:
**image**
- Any `models.providers.<id>.models[]` catalog with an image-capable model: **image**
For CLI entries, **set `capabilities` explicitly** to avoid surprising matches.
If you omit `capabilities`, the entry is eligible for the list it appears in.
For CLI entries, **set `capabilities` explicitly** to avoid surprising matches. If you omit `capabilities`, the entry is eligible for the list it appears in.
## Provider support matrix (OpenClaw integrations)
@@ -231,12 +252,12 @@ If you omit `capabilities`, the entry is eligible for the list it appears in.
| Audio | OpenAI, Groq, xAI, Deepgram, Google, SenseAudio, ElevenLabs, Mistral | Provider transcription (Whisper/Groq/xAI/Deepgram/Gemini/SenseAudio/Scribe/Voxtral). |
| Video | Google, Qwen, Moonshot | Provider video understanding via vendor plugins; Qwen video understanding uses the Standard DashScope endpoints. |
MiniMax note:
<Note>
**MiniMax note**
- `minimax` and `minimax-portal` image understanding comes from the plugin-owned
`MiniMax-VL-01` media provider.
- The bundled MiniMax text catalog still starts text-only; explicit
`models.providers.minimax` entries materialize image-capable M2.7 chat refs.
- `minimax` and `minimax-portal` image understanding comes from the plugin-owned `MiniMax-VL-01` media provider.
- The bundled MiniMax text catalog still starts text-only; explicit `models.providers.minimax` entries materialize image-capable M2.7 chat refs.
</Note>
## Model selection guidance
@@ -248,177 +269,176 @@ MiniMax note:
## Attachment policy
Percapability `attachments` controls which attachments are processed:
Per-capability `attachments` controls which attachments are processed:
- `mode`: `first` (default) or `all`
- `maxAttachments`: cap the number processed (default **1**)
- `prefer`: `first`, `last`, `path`, `url`
<ParamField path="mode" type='"first" | "all"' default="first">
Whether to process the first selected attachment or all of them.
</ParamField>
<ParamField path="maxAttachments" type="number" default="1">
Cap the number processed.
</ParamField>
<ParamField path="prefer" type='"first" | "last" | "path" | "url"'>
Selection preference among candidate attachments.
</ParamField>
When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
File-attachment extraction behavior:
- Extracted file text is wrapped as **untrusted external content** before it is
appended to the media prompt.
- The injected block uses explicit boundary markers like
`<<<EXTERNAL_UNTRUSTED_CONTENT id="...">>>` /
`<<<END_EXTERNAL_UNTRUSTED_CONTENT id="...">>>` and includes a
`Source: External` metadata line.
- This attachment-extraction path intentionally omits the long
`SECURITY NOTICE:` banner to avoid bloating the media prompt; the boundary
markers and metadata still remain.
- If a file has no extractable text, OpenClaw injects `[No extractable text]`.
- If a PDF falls back to rendered page images in this path, the media prompt keeps
the placeholder `[PDF content rendered to images; images not forwarded to model]`
because this attachment-extraction step forwards text blocks, not the rendered PDF images.
<AccordionGroup>
<Accordion title="File-attachment extraction behavior">
- Extracted file text is wrapped as **untrusted external content** before it is appended to the media prompt.
- The injected block uses explicit boundary markers like `<<<EXTERNAL_UNTRUSTED_CONTENT id="...">>>` / `<<<END_EXTERNAL_UNTRUSTED_CONTENT id="...">>>` and includes a `Source: External` metadata line.
- This attachment-extraction path intentionally omits the long `SECURITY NOTICE:` banner to avoid bloating the media prompt; the boundary markers and metadata still remain.
- If a file has no extractable text, OpenClaw injects `[No extractable text]`.
- If a PDF falls back to rendered page images in this path, the media prompt keeps the placeholder `[PDF content rendered to images; images not forwarded to model]` because this attachment-extraction step forwards text blocks, not the rendered PDF images.
</Accordion>
</AccordionGroup>
## Config examples
### 1) Shared models list + overrides
```json5
{
tools: {
media: {
models: [
{ provider: "openai", model: "gpt-5.5", capabilities: ["image"] },
{
provider: "google",
model: "gemini-3-flash-preview",
capabilities: ["image", "audio", "video"],
},
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
<Tabs>
<Tab title="Shared models + overrides">
```json5
{
tools: {
media: {
models: [
{ provider: "openai", model: "gpt-5.5", capabilities: ["image"] },
{
provider: "google",
model: "gemini-3-flash-preview",
capabilities: ["image", "audio", "video"],
},
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
],
capabilities: ["image", "video"],
},
],
capabilities: ["image", "video"],
audio: {
attachments: { mode: "all", maxAttachments: 2 },
},
video: {
maxChars: 500,
},
},
],
audio: {
attachments: { mode: "all", maxAttachments: 2 },
},
video: {
maxChars: 500,
},
},
},
}
```
### 2) Audio + Video only (image off)
```json5
{
tools: {
media: {
audio: {
enabled: true,
models: [
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{
type: "cli",
command: "whisper",
args: ["--model", "base", "{{MediaPath}}"],
},
],
},
video: {
enabled: true,
maxChars: 500,
models: [
{ provider: "google", model: "gemini-3-flash-preview" },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
}
```
</Tab>
<Tab title="Audio + video only">
```json5
{
tools: {
media: {
audio: {
enabled: true,
models: [
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{
type: "cli",
command: "whisper",
args: ["--model", "base", "{{MediaPath}}"],
},
],
},
],
},
},
},
}
```
### 3) Optional image understanding
```json5
{
tools: {
media: {
image: {
enabled: true,
maxBytes: 10485760,
maxChars: 500,
models: [
{ provider: "openai", model: "gpt-5.5" },
{ provider: "anthropic", model: "claude-opus-4-6" },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
video: {
enabled: true,
maxChars: 500,
models: [
{ provider: "google", model: "gemini-3-flash-preview" },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
],
},
],
},
],
},
},
},
},
}
```
### 4) Multi-modal single entry (explicit capabilities)
```json5
{
tools: {
media: {
image: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
}
```
</Tab>
<Tab title="Image-only">
```json5
{
tools: {
media: {
image: {
enabled: true,
maxBytes: 10485760,
maxChars: 500,
models: [
{ provider: "openai", model: "gpt-5.5" },
{ provider: "anthropic", model: "claude-opus-4-6" },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{MediaPath}} and describe it in <= {{MaxChars}} characters.",
],
},
],
},
],
},
},
audio: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
}
```
</Tab>
<Tab title="Multi-modal single entry">
```json5
{
tools: {
media: {
image: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
},
],
},
],
},
video: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
audio: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
},
],
},
],
video: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
},
],
},
},
},
},
},
}
```
}
```
</Tab>
</Tabs>
## Status output
@@ -428,15 +448,15 @@ When media understanding runs, `/status` includes a short summary line:
📎 Media: image ok (openai/gpt-5.4) · audio skipped (maxBytes)
```
This shows percapability outcomes and the chosen provider/model when applicable.
This shows per-capability outcomes and the chosen provider/model when applicable.
## Notes
- Understanding is **besteffort**. Errors do not block replies.
- Understanding is **best-effort**. Errors do not block replies.
- Attachments are still passed to models even when understanding is disabled.
- Use `scope` to limit where understanding runs (e.g. only DMs).
## Related docs
## Related
- [Configuration](/gateway/configuration)
- [Image & Media Support](/nodes/images)
- [Image & media support](/nodes/images)

View File

@@ -542,6 +542,72 @@ Environment overrides remain available for local testing:
preferred for repeatable deployments because it keeps the plugin behavior in the
same reviewed file as the rest of the Codex harness setup.
## Computer Use
Computer Use is a Codex-native MCP plugin. OpenClaw does not vendor the desktop
control app or execute desktop actions itself; it enables Codex app-server
plugins, installs the configured Codex marketplace plugin when requested, checks
that the `computer-use` MCP server is available, and then lets Codex handle the
native MCP tool calls during Codex-mode turns.
Set `plugins.entries.codex.config.computerUse` when you want Codex-mode turns to
require Computer Use:
```json5
{
plugins: {
entries: {
codex: {
enabled: true,
config: {
computerUse: {
autoInstall: true,
},
},
},
},
},
agents: {
defaults: {
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
},
},
},
}
```
With no marketplace fields, OpenClaw asks Codex app-server to use its discovered
marketplaces. On a fresh Codex home, app-server seeds the official curated
marketplace and OpenClaw follows the same loading shape as Codex: it polls
`plugin/list` during install before treating Computer Use as unavailable. The
default discovery wait is 60 seconds and can be tuned with
`marketplaceDiscoveryTimeoutMs`. If multiple known Codex marketplaces contain
Computer Use, OpenClaw uses the Codex marketplace preference order before
failing closed for unknown ambiguous matches.
Use `marketplaceSource` for a non-default Codex marketplace source that
app-server can add, or `marketplacePath` for a local marketplace file that
already exists on the machine. If the marketplace is already registered with
Codex app-server, use `marketplaceName` instead. The defaults are
`pluginName: "computer-use"` and `mcpServerName: "computer-use"`.
For safety, turn-start auto-install only uses marketplaces app-server has
already discovered. Use `/codex computer-use install` for explicit installs from
a configured `marketplaceSource` or `marketplacePath`.
The same setup can be checked or installed from the command surface:
- `/codex computer-use status`
- `/codex computer-use install`
- `/codex computer-use install --source <marketplace-source>`
- `/codex computer-use install --marketplace-path <path>`
Computer Use is macOS-specific and may require local OS permissions before the
Codex MCP server can control apps. If `computerUse.enabled` is true and the MCP
server is unavailable, Codex-mode turns fail before the thread starts instead of
silently running without the native Computer Use tools.
## Common recipes
Local Codex with default stdio transport:
@@ -644,6 +710,8 @@ Common forms:
- `/codex resume <thread-id>` attaches the current OpenClaw session to an existing Codex thread.
- `/codex compact` asks Codex app-server to compact the attached thread.
- `/codex review` starts Codex native review for the attached thread.
- `/codex computer-use status` checks the configured Computer Use plugin and MCP server.
- `/codex computer-use install` installs the configured Computer Use plugin and reloads MCP servers.
- `/codex account` shows account and rate-limit status.
- `/codex mcp` lists Codex app-server MCP server status.
- `/codex skills` lists Codex app-server skills.

View File

@@ -31,6 +31,18 @@ The registry is the source for maintainer planning and future plugin inspector
checks. If a plugin-facing behavior changes, add or update the compatibility
record in the same change that adds the adapter.
Doctor repair and migration compatibility is tracked separately at
`src/commands/doctor/shared/deprecation-compat.ts`. Those records cover old
config shapes, install-ledger layouts, and repair shims that may need to stay
available after the runtime compatibility path is removed.
Release sweeps should check both registries. Do not delete a doctor migration
just because the matching runtime or config compatibility record expired; first
verify there is no supported upgrade path that still needs the repair. Also
revalidate each replacement annotation during release planning because plugin
ownership and config footprint can change as providers and channels move out of
core.
## Plugin inspector package
The plugin inspector should live outside the core OpenClaw repo as a separate
@@ -71,7 +83,10 @@ The migration sequence is:
7. Remove only with explicit breaking-release approval.
Deprecated records must include a warning start date, replacement, docs link,
and target removal date when known.
and final removal date no more than three months after the warning starts. Do
not add a deprecated compatibility path with an open-ended removal window unless
maintainers explicitly decide it is permanent compatibility and mark it `active`
instead.
## Current compatibility areas
@@ -79,15 +94,40 @@ Current compatibility records include:
- legacy broad SDK imports such as `openclaw/plugin-sdk/compat`
- legacy hook-only plugin shapes and `before_agent_start`
- legacy `activate(api)` plugin entrypoints while plugins migrate to
`register(api)`
- legacy SDK aliases such as `openclaw/extension-api`,
`openclaw/plugin-sdk/channel-runtime`, `openclaw/plugin-sdk/command-auth`
status builders, `openclaw/plugin-sdk/test-utils`, and the `ClawdbotConfig` /
`OpenClawSchemaType` type aliases
- bundled plugin allowlist and enablement behavior
- legacy provider/channel env-var manifest metadata
- legacy provider plugin hooks and type aliases while providers move to
explicit catalog, auth, thinking, replay, and transport hooks
- legacy runtime aliases such as `api.runtime.taskFlow`,
`api.runtime.subagent.getSession`, and `api.runtime.stt`
- legacy memory-plugin split registration while memory plugins move to
`registerMemoryCapability`
- legacy channel SDK helpers for native message schemas, mention gating,
inbound envelope formatting, and approval capability nesting
- activation hints that are being replaced by manifest contribution ownership
- `setup-api` runtime fallback while setup descriptors move to cold
`setup.requiresRuntime: false` metadata
- provider `discovery` hooks while provider catalog hooks move to
`catalog.run(...)`
- channel `showConfigured` / `showInSetup` metadata while channel packages move
to `openclaw.channel.exposure`
- legacy runtime-policy config keys while doctor migrates operators to
`agentRuntime`
- generated bundled channel config metadata fallback while registry-first
`channelConfigs` metadata lands
- the persisted plugin registry disable env while repair flows migrate operators
to `openclaw plugins registry --refresh` and `openclaw doctor --fix`
- persisted plugin registry disable and install-migration env flags while
repair flows migrate operators to `openclaw plugins registry --refresh` and
`openclaw doctor --fix`
- legacy plugin-owned web search, web fetch, and x_search config paths while
doctor migrates them to `plugins.entries.<plugin>.config`
- legacy `plugins.installs` authored config and bundled plugin load-path
aliases while install metadata moves into the state-managed plugin ledger
New plugin code should prefer the replacement listed in the registry and in the
specific migration guide. Existing plugins can keep using a compatibility path

View File

@@ -1238,10 +1238,12 @@ openclaw googlemeet recover-tab https://meet.google.com/abc-defg-hij
```
The equivalent tool action is `recover_current_tab`. It focuses and inspects an
existing Meet tab on the configured Chrome node. It does not open a new tab or
create a new session; it reports the current blocker, such as login, admission,
permissions, or audio-choice state. The CLI command talks to the configured
Gateway, so the Gateway must be running and the Chrome node must be connected.
existing Meet tab for the selected transport. With `chrome`, it uses local
browser control through the Gateway; with `chrome-node`, it uses the configured
Chrome node. It does not open a new tab or create a new session; it reports the
current blocker, such as login, admission, permissions, or audio-choice state.
The CLI command talks to the configured Gateway, so the Gateway must be running;
`chrome-node` also requires the Chrome node to be connected.
### Twilio setup checks fail

View File

@@ -231,6 +231,9 @@ Prefer the narrowest metadata that already describes ownership. Use
`providers`, `channels`, `commandAliases`, setup descriptors, or `contracts`
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
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
not replace `register(...)`, `setupEntry`, or other runtime/plugin entrypoints.
@@ -250,18 +253,21 @@ change correctness while legacy manifest ownership fallbacks still exist.
}
```
| Field | Required | Type | What it means |
| ---------------- | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `onProviders` | No | `string[]` | Provider ids that should include this plugin in activation/load plans. |
| `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. |
| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. |
| `onRoutes` | No | `string[]` | Route kinds that should include this plugin in activation/load plans. |
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. Prefer narrower fields when possible. |
| Field | Required | Type | What it means |
| ------------------ | -------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onProviders` | No | `string[]` | Provider ids that should include this plugin in activation/load plans. |
| `onAgentHarnesses` | No | `string[]` | Embedded agent harness runtime ids that should include this plugin in activation/load plans. Use top-level `cliBackends` for CLI backend aliases. |
| `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. |
| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. |
| `onRoutes` | No | `string[]` | Route kinds that should include this plugin in activation/load plans. |
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. Prefer narrower fields when possible. |
Current live consumers:
- command-triggered CLI planning falls back to legacy
`commandAliases[].cliCommand` or `commandAliases[].name`
- agent-runtime startup planning uses `activation.onAgentHarnesses` for
embedded harnesses and top-level `cliBackends[]` for CLI runtime aliases
- channel-triggered setup/channel planning falls back to legacy `channels[]`
ownership when explicit channel activation metadata is missing
- provider-triggered setup/runtime planning falls back to legacy

View File

@@ -420,8 +420,9 @@ The same rule applies to other bundled-helper families such as:
`plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`,
`plugin-sdk/twitch`,
`plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`,
`plugin-sdk/diagnostics-otel`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`,
`plugin-sdk/thread-ownership`, and `plugin-sdk/voice-call`
`plugin-sdk/diagnostics-otel`, `plugin-sdk/diagnostics-prometheus`,
`plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/thread-ownership`,
and `plugin-sdk/voice-call`
`plugin-sdk/github-copilot-token` currently exposes the narrow token-helper
surface `DEFAULT_COPILOT_API_BASE_URL`,

View File

@@ -1,21 +1,23 @@
---
summary: "api.runtime -- the injected runtime helpers available to plugins"
title: "Plugin runtime helpers"
sidebarTitle: "Runtime Helpers"
sidebarTitle: "Runtime helpers"
read_when:
- You need to call core helpers from a plugin (TTS, STT, image gen, web search, subagent, nodes)
- You want to understand what api.runtime exposes
- You are accessing config, agent, or media helpers from plugin code
---
Reference for the `api.runtime` object injected into every plugin during
registration. Use these helpers instead of importing host internals directly.
Reference for the `api.runtime` object injected into every plugin during registration. Use these helpers instead of importing host internals directly.
<Tip>
**Looking for a walkthrough?** See [Channel Plugins](/plugins/sdk-channel-plugins)
or [Provider Plugins](/plugins/sdk-provider-plugins) for step-by-step guides
that show these helpers in context.
</Tip>
<CardGroup cols={2}>
<Card title="Channel plugins" href="/plugins/sdk-channel-plugins">
Step-by-step guide that uses these helpers in context for channel plugins.
</Card>
<Card title="Provider plugins" href="/plugins/sdk-provider-plugins">
Step-by-step guide that uses these helpers in context for provider plugins.
</Card>
</CardGroup>
```typescript
register(api) {
@@ -25,443 +27,449 @@ register(api) {
## Runtime namespaces
### `api.runtime.agent`
Agent identity, directories, and session management.
```typescript
// Resolve the agent's working directory
const agentDir = api.runtime.agent.resolveAgentDir(cfg);
// Resolve agent workspace
const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(cfg);
// Get agent identity
const identity = api.runtime.agent.resolveAgentIdentity(cfg);
// Get default thinking level
const thinking = api.runtime.agent.resolveThinkingDefault(cfg, provider, model);
// Get agent timeout
const timeoutMs = api.runtime.agent.resolveAgentTimeoutMs(cfg);
// Ensure workspace exists
await api.runtime.agent.ensureAgentWorkspace(cfg);
// Run an embedded agent turn
const agentDir = api.runtime.agent.resolveAgentDir(cfg);
const result = await api.runtime.agent.runEmbeddedAgent({
sessionId: "my-plugin:task-1",
runId: crypto.randomUUID(),
sessionFile: path.join(agentDir, "sessions", "my-plugin-task-1.jsonl"),
workspaceDir: api.runtime.agent.resolveAgentWorkspaceDir(cfg),
prompt: "Summarize the latest changes",
timeoutMs: api.runtime.agent.resolveAgentTimeoutMs(cfg),
});
```
`runEmbeddedAgent(...)` is the neutral helper for starting a normal OpenClaw
agent turn from plugin code. It uses the same provider/model resolution and
agent-harness selection as channel-triggered replies.
`runEmbeddedPiAgent(...)` remains as a compatibility alias.
**Session store helpers** are under `api.runtime.agent.session`:
```typescript
const storePath = api.runtime.agent.session.resolveStorePath(cfg);
const store = api.runtime.agent.session.loadSessionStore(cfg);
await api.runtime.agent.session.saveSessionStore(cfg, store);
const filePath = api.runtime.agent.session.resolveSessionFilePath(cfg, sessionId);
```
### `api.runtime.agent.defaults`
Default model and provider constants:
```typescript
const model = api.runtime.agent.defaults.model; // e.g. "anthropic/claude-sonnet-4-6"
const provider = api.runtime.agent.defaults.provider; // e.g. "anthropic"
```
### `api.runtime.subagent`
Launch and manage background subagent runs.
```typescript
// Start a subagent run
const { runId } = await api.runtime.subagent.run({
sessionKey: "agent:main:subagent:search-helper",
message: "Expand this query into focused follow-up searches.",
provider: "openai", // optional override
model: "gpt-4.1-mini", // optional override
deliver: false,
});
// Wait for completion
const result = await api.runtime.subagent.waitForRun({ runId, timeoutMs: 30000 });
// Read session messages
const { messages } = await api.runtime.subagent.getSessionMessages({
sessionKey: "agent:main:subagent:search-helper",
limit: 10,
});
// Delete a session
await api.runtime.subagent.deleteSession({
sessionKey: "agent:main:subagent:search-helper",
});
```
<Warning>
Model overrides (`provider`/`model`) require operator opt-in via
`plugins.entries.<id>.subagent.allowModelOverride: true` in config.
Untrusted plugins can still run subagents, but override requests are rejected.
</Warning>
### `api.runtime.nodes`
List connected nodes and invoke a node-host command from Gateway-loaded plugin
code or from plugin CLI commands. Use this when a plugin owns local work on a
paired device, for example a browser or audio bridge on another Mac.
```typescript
const { nodes } = await api.runtime.nodes.list({ connected: true });
const result = await api.runtime.nodes.invoke({
nodeId: "mac-studio",
command: "my-plugin.command",
params: { action: "start" },
timeoutMs: 30000,
});
```
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls
the configured Gateway over RPC, so commands such as `openclaw googlemeet
recover-tab` can inspect paired nodes from the terminal. Node commands still go
through normal Gateway node pairing, command allowlists, and node-local command
handling.
### `api.runtime.taskFlow`
Bind a Task Flow runtime to an existing OpenClaw session key or trusted tool
context, then create and manage Task Flows without passing an owner on every call.
```typescript
const taskFlow = api.runtime.taskFlow.fromToolContext(ctx);
const created = taskFlow.createManaged({
controllerId: "my-plugin/review-batch",
goal: "Review new pull requests",
});
const child = taskFlow.runTask({
flowId: created.flowId,
runtime: "acp",
childSessionKey: "agent:main:subagent:reviewer",
task: "Review PR #123",
status: "running",
startedAt: Date.now(),
});
const waiting = taskFlow.setWaiting({
flowId: created.flowId,
expectedRevision: created.revision,
currentStep: "await-human-reply",
waitJson: { kind: "reply", channel: "telegram" },
});
```
Use `bindSession({ sessionKey, requesterOrigin })` when you already have a
trusted OpenClaw session key from your own binding layer. Do not bind from raw
user input.
### `api.runtime.tts`
Text-to-speech synthesis.
```typescript
// Standard TTS
const clip = await api.runtime.tts.textToSpeech({
text: "Hello from OpenClaw",
cfg: api.config,
});
// Telephony-optimized TTS
const telephonyClip = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from OpenClaw",
cfg: api.config,
});
// List available voices
const voices = await api.runtime.tts.listVoices({
provider: "elevenlabs",
cfg: api.config,
});
```
Uses core `messages.tts` configuration and provider selection. Returns PCM audio
buffer + sample rate.
### `api.runtime.mediaUnderstanding`
Image, audio, and video analysis.
```typescript
// Describe an image
const image = await api.runtime.mediaUnderstanding.describeImageFile({
filePath: "/tmp/inbound-photo.jpg",
cfg: api.config,
agentDir: "/tmp/agent",
});
// Transcribe audio
const { text } = await api.runtime.mediaUnderstanding.transcribeAudioFile({
filePath: "/tmp/inbound-audio.ogg",
cfg: api.config,
mime: "audio/ogg", // optional, for when MIME cannot be inferred
});
// Describe a video
const video = await api.runtime.mediaUnderstanding.describeVideoFile({
filePath: "/tmp/inbound-video.mp4",
cfg: api.config,
});
// Generic file analysis
const result = await api.runtime.mediaUnderstanding.runFile({
filePath: "/tmp/inbound-file.pdf",
cfg: api.config,
});
```
Returns `{ text: undefined }` when no output is produced (e.g. skipped input).
<Info>
`api.runtime.stt.transcribeAudioFile(...)` remains as a compatibility alias
for `api.runtime.mediaUnderstanding.transcribeAudioFile(...)`.
</Info>
### `api.runtime.imageGeneration`
Image generation.
```typescript
const result = await api.runtime.imageGeneration.generate({
prompt: "A robot painting a sunset",
cfg: api.config,
});
const providers = api.runtime.imageGeneration.listProviders({ cfg: api.config });
```
### `api.runtime.webSearch`
Web search.
```typescript
const providers = api.runtime.webSearch.listProviders({ config: api.config });
const result = await api.runtime.webSearch.search({
config: api.config,
args: { query: "OpenClaw plugin SDK", count: 5 },
});
```
### `api.runtime.media`
Low-level media utilities.
```typescript
const webMedia = await api.runtime.media.loadWebMedia(url);
const mime = await api.runtime.media.detectMime(buffer);
const kind = api.runtime.media.mediaKindFromMime("image/jpeg"); // "image"
const isVoice = api.runtime.media.isVoiceCompatibleAudio(filePath);
const metadata = await api.runtime.media.getImageMetadata(filePath);
const resized = await api.runtime.media.resizeToJpeg(buffer, { maxWidth: 800 });
const terminalQr = await api.runtime.media.renderQrTerminal("https://openclaw.ai");
const pngQr = await api.runtime.media.renderQrPngBase64("https://openclaw.ai", {
scale: 6, // 1-12
marginModules: 4, // 0-16
});
const pngQrDataUrl = await api.runtime.media.renderQrPngDataUrl("https://openclaw.ai");
const tmpRoot = resolvePreferredOpenClawTmpDir();
const pngQrFile = await api.runtime.media.writeQrPngTempFile("https://openclaw.ai", {
tmpRoot,
dirPrefix: "my-plugin-qr-",
fileName: "qr.png",
});
```
### `api.runtime.config`
Config load and write.
```typescript
const cfg = await api.runtime.config.loadConfig();
await api.runtime.config.writeConfigFile(cfg);
```
### `api.runtime.system`
System-level utilities.
```typescript
await api.runtime.system.enqueueSystemEvent(event);
api.runtime.system.requestHeartbeatNow();
const output = await api.runtime.system.runCommandWithTimeout(cmd, args, opts);
const hint = api.runtime.system.formatNativeDependencyHint(pkg);
```
### `api.runtime.events`
Event subscriptions.
```typescript
api.runtime.events.onAgentEvent((event) => {
/* ... */
});
api.runtime.events.onSessionTranscriptUpdate((update) => {
/* ... */
});
```
### `api.runtime.logging`
Logging.
```typescript
const verbose = api.runtime.logging.shouldLogVerbose();
const childLogger = api.runtime.logging.getChildLogger({ plugin: "my-plugin" }, { level: "debug" });
```
### `api.runtime.modelAuth`
Model and provider auth resolution.
```typescript
const auth = await api.runtime.modelAuth.getApiKeyForModel({ model, cfg });
const providerAuth = await api.runtime.modelAuth.resolveApiKeyForProvider({
provider: "openai",
cfg,
});
```
### `api.runtime.state`
State directory resolution.
```typescript
const stateDir = api.runtime.state.resolveStateDir();
```
### `api.runtime.tools`
Memory tool factories and CLI.
```typescript
const getTool = api.runtime.tools.createMemoryGetTool(/* ... */);
const searchTool = api.runtime.tools.createMemorySearchTool(/* ... */);
api.runtime.tools.registerMemoryCli(/* ... */);
```
### `api.runtime.channel`
Channel-specific runtime helpers (available when a channel plugin is loaded).
`api.runtime.channel.mentions` is the shared inbound mention-policy surface for
bundled channel plugins that use runtime injection:
```typescript
const mentionMatch = api.runtime.channel.mentions.matchesMentionWithExplicit(text, {
mentionRegexes,
mentionPatterns,
});
const decision = api.runtime.channel.mentions.resolveInboundMentionDecision({
facts: {
canDetectMention: true,
wasMentioned: mentionMatch.matched,
implicitMentionKinds: api.runtime.channel.mentions.implicitMentionKindWhen(
"reply_to_bot",
isReplyToBot,
),
},
policy: {
isGroup,
requireMention,
allowTextCommands,
hasControlCommand,
commandAuthorized,
},
});
```
Available mention helpers:
- `buildMentionRegexes`
- `matchesMentionPatterns`
- `matchesMentionWithExplicit`
- `implicitMentionKindWhen`
- `resolveInboundMentionDecision`
`api.runtime.channel.mentions` intentionally does not expose the older
`resolveMentionGating*` compatibility helpers. Prefer the normalized
`{ facts, policy }` path.
<AccordionGroup>
<Accordion title="api.runtime.agent">
Agent identity, directories, and session management.
```typescript
// Resolve the agent's working directory
const agentDir = api.runtime.agent.resolveAgentDir(cfg);
// Resolve agent workspace
const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(cfg);
// Get agent identity
const identity = api.runtime.agent.resolveAgentIdentity(cfg);
// Get default thinking level
const thinking = api.runtime.agent.resolveThinkingDefault(cfg, provider, model);
// Get agent timeout
const timeoutMs = api.runtime.agent.resolveAgentTimeoutMs(cfg);
// Ensure workspace exists
await api.runtime.agent.ensureAgentWorkspace(cfg);
// Run an embedded agent turn
const agentDir = api.runtime.agent.resolveAgentDir(cfg);
const result = await api.runtime.agent.runEmbeddedAgent({
sessionId: "my-plugin:task-1",
runId: crypto.randomUUID(),
sessionFile: path.join(agentDir, "sessions", "my-plugin-task-1.jsonl"),
workspaceDir: api.runtime.agent.resolveAgentWorkspaceDir(cfg),
prompt: "Summarize the latest changes",
timeoutMs: api.runtime.agent.resolveAgentTimeoutMs(cfg),
});
```
`runEmbeddedAgent(...)` is the neutral helper for starting a normal OpenClaw agent turn from plugin code. It uses the same provider/model resolution and agent-harness selection as channel-triggered replies.
`runEmbeddedPiAgent(...)` remains as a compatibility alias.
**Session store helpers** are under `api.runtime.agent.session`:
```typescript
const storePath = api.runtime.agent.session.resolveStorePath(cfg);
const store = api.runtime.agent.session.loadSessionStore(cfg);
await api.runtime.agent.session.saveSessionStore(cfg, store);
const filePath = api.runtime.agent.session.resolveSessionFilePath(cfg, sessionId);
```
</Accordion>
<Accordion title="api.runtime.agent.defaults">
Default model and provider constants:
```typescript
const model = api.runtime.agent.defaults.model; // e.g. "anthropic/claude-sonnet-4-6"
const provider = api.runtime.agent.defaults.provider; // e.g. "anthropic"
```
</Accordion>
<Accordion title="api.runtime.subagent">
Launch and manage background subagent runs.
```typescript
// Start a subagent run
const { runId } = await api.runtime.subagent.run({
sessionKey: "agent:main:subagent:search-helper",
message: "Expand this query into focused follow-up searches.",
provider: "openai", // optional override
model: "gpt-4.1-mini", // optional override
deliver: false,
});
// Wait for completion
const result = await api.runtime.subagent.waitForRun({ runId, timeoutMs: 30000 });
// Read session messages
const { messages } = await api.runtime.subagent.getSessionMessages({
sessionKey: "agent:main:subagent:search-helper",
limit: 10,
});
// Delete a session
await api.runtime.subagent.deleteSession({
sessionKey: "agent:main:subagent:search-helper",
});
```
<Warning>
Model overrides (`provider`/`model`) require operator opt-in via `plugins.entries.<id>.subagent.allowModelOverride: true` in config. Untrusted plugins can still run subagents, but override requests are rejected.
</Warning>
</Accordion>
<Accordion title="api.runtime.nodes">
List connected nodes and invoke a node-host command from Gateway-loaded plugin code or from plugin CLI commands. Use this when a plugin owns local work on a paired device, for example a browser or audio bridge on another Mac.
```typescript
const { nodes } = await api.runtime.nodes.list({ connected: true });
const result = await api.runtime.nodes.invoke({
nodeId: "mac-studio",
command: "my-plugin.command",
params: { action: "start" },
timeoutMs: 30000,
});
```
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, and node-local command handling.
</Accordion>
<Accordion title="api.runtime.taskFlow">
Bind a Task Flow runtime to an existing OpenClaw session key or trusted tool context, then create and manage Task Flows without passing an owner on every call.
```typescript
const taskFlow = api.runtime.taskFlow.fromToolContext(ctx);
const created = taskFlow.createManaged({
controllerId: "my-plugin/review-batch",
goal: "Review new pull requests",
});
const child = taskFlow.runTask({
flowId: created.flowId,
runtime: "acp",
childSessionKey: "agent:main:subagent:reviewer",
task: "Review PR #123",
status: "running",
startedAt: Date.now(),
});
const waiting = taskFlow.setWaiting({
flowId: created.flowId,
expectedRevision: created.revision,
currentStep: "await-human-reply",
waitJson: { kind: "reply", channel: "telegram" },
});
```
Use `bindSession({ sessionKey, requesterOrigin })` when you already have a trusted OpenClaw session key from your own binding layer. Do not bind from raw user input.
</Accordion>
<Accordion title="api.runtime.tts">
Text-to-speech synthesis.
```typescript
// Standard TTS
const clip = await api.runtime.tts.textToSpeech({
text: "Hello from OpenClaw",
cfg: api.config,
});
// Telephony-optimized TTS
const telephonyClip = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from OpenClaw",
cfg: api.config,
});
// List available voices
const voices = await api.runtime.tts.listVoices({
provider: "elevenlabs",
cfg: api.config,
});
```
Uses core `messages.tts` configuration and provider selection. Returns PCM audio buffer + sample rate.
</Accordion>
<Accordion title="api.runtime.mediaUnderstanding">
Image, audio, and video analysis.
```typescript
// Describe an image
const image = await api.runtime.mediaUnderstanding.describeImageFile({
filePath: "/tmp/inbound-photo.jpg",
cfg: api.config,
agentDir: "/tmp/agent",
});
// Transcribe audio
const { text } = await api.runtime.mediaUnderstanding.transcribeAudioFile({
filePath: "/tmp/inbound-audio.ogg",
cfg: api.config,
mime: "audio/ogg", // optional, for when MIME cannot be inferred
});
// Describe a video
const video = await api.runtime.mediaUnderstanding.describeVideoFile({
filePath: "/tmp/inbound-video.mp4",
cfg: api.config,
});
// Generic file analysis
const result = await api.runtime.mediaUnderstanding.runFile({
filePath: "/tmp/inbound-file.pdf",
cfg: api.config,
});
```
Returns `{ text: undefined }` when no output is produced (e.g. skipped input).
<Info>
`api.runtime.stt.transcribeAudioFile(...)` remains as a compatibility alias for `api.runtime.mediaUnderstanding.transcribeAudioFile(...)`.
</Info>
</Accordion>
<Accordion title="api.runtime.imageGeneration">
Image generation.
```typescript
const result = await api.runtime.imageGeneration.generate({
prompt: "A robot painting a sunset",
cfg: api.config,
});
const providers = api.runtime.imageGeneration.listProviders({ cfg: api.config });
```
</Accordion>
<Accordion title="api.runtime.webSearch">
Web search.
```typescript
const providers = api.runtime.webSearch.listProviders({ config: api.config });
const result = await api.runtime.webSearch.search({
config: api.config,
args: { query: "OpenClaw plugin SDK", count: 5 },
});
```
</Accordion>
<Accordion title="api.runtime.media">
Low-level media utilities.
```typescript
const webMedia = await api.runtime.media.loadWebMedia(url);
const mime = await api.runtime.media.detectMime(buffer);
const kind = api.runtime.media.mediaKindFromMime("image/jpeg"); // "image"
const isVoice = api.runtime.media.isVoiceCompatibleAudio(filePath);
const metadata = await api.runtime.media.getImageMetadata(filePath);
const resized = await api.runtime.media.resizeToJpeg(buffer, { maxWidth: 800 });
const terminalQr = await api.runtime.media.renderQrTerminal("https://openclaw.ai");
const pngQr = await api.runtime.media.renderQrPngBase64("https://openclaw.ai", {
scale: 6, // 1-12
marginModules: 4, // 0-16
});
const pngQrDataUrl = await api.runtime.media.renderQrPngDataUrl("https://openclaw.ai");
const tmpRoot = resolvePreferredOpenClawTmpDir();
const pngQrFile = await api.runtime.media.writeQrPngTempFile("https://openclaw.ai", {
tmpRoot,
dirPrefix: "my-plugin-qr-",
fileName: "qr.png",
});
```
</Accordion>
<Accordion title="api.runtime.config">
Config load and write.
```typescript
const cfg = await api.runtime.config.loadConfig();
await api.runtime.config.writeConfigFile(cfg);
```
</Accordion>
<Accordion title="api.runtime.system">
System-level utilities.
```typescript
await api.runtime.system.enqueueSystemEvent(event);
api.runtime.system.requestHeartbeatNow();
const output = await api.runtime.system.runCommandWithTimeout(cmd, args, opts);
const hint = api.runtime.system.formatNativeDependencyHint(pkg);
```
</Accordion>
<Accordion title="api.runtime.events">
Event subscriptions.
```typescript
api.runtime.events.onAgentEvent((event) => {
/* ... */
});
api.runtime.events.onSessionTranscriptUpdate((update) => {
/* ... */
});
```
</Accordion>
<Accordion title="api.runtime.logging">
Logging.
```typescript
const verbose = api.runtime.logging.shouldLogVerbose();
const childLogger = api.runtime.logging.getChildLogger({ plugin: "my-plugin" }, { level: "debug" });
```
</Accordion>
<Accordion title="api.runtime.modelAuth">
Model and provider auth resolution.
```typescript
const auth = await api.runtime.modelAuth.getApiKeyForModel({ model, cfg });
const providerAuth = await api.runtime.modelAuth.resolveApiKeyForProvider({
provider: "openai",
cfg,
});
```
</Accordion>
<Accordion title="api.runtime.state">
State directory resolution.
```typescript
const stateDir = api.runtime.state.resolveStateDir();
```
</Accordion>
<Accordion title="api.runtime.tools">
Memory tool factories and CLI.
```typescript
const getTool = api.runtime.tools.createMemoryGetTool(/* ... */);
const searchTool = api.runtime.tools.createMemorySearchTool(/* ... */);
api.runtime.tools.registerMemoryCli(/* ... */);
```
</Accordion>
<Accordion title="api.runtime.channel">
Channel-specific runtime helpers (available when a channel plugin is loaded).
`api.runtime.channel.mentions` is the shared inbound mention-policy surface for bundled channel plugins that use runtime injection:
```typescript
const mentionMatch = api.runtime.channel.mentions.matchesMentionWithExplicit(text, {
mentionRegexes,
mentionPatterns,
});
const decision = api.runtime.channel.mentions.resolveInboundMentionDecision({
facts: {
canDetectMention: true,
wasMentioned: mentionMatch.matched,
implicitMentionKinds: api.runtime.channel.mentions.implicitMentionKindWhen(
"reply_to_bot",
isReplyToBot,
),
},
policy: {
isGroup,
requireMention,
allowTextCommands,
hasControlCommand,
commandAuthorized,
},
});
```
Available mention helpers:
- `buildMentionRegexes`
- `matchesMentionPatterns`
- `matchesMentionWithExplicit`
- `implicitMentionKindWhen`
- `resolveInboundMentionDecision`
`api.runtime.channel.mentions` intentionally does not expose the older `resolveMentionGating*` compatibility helpers. Prefer the normalized `{ facts, policy }` path.
</Accordion>
</AccordionGroup>
## Storing runtime references
Use `createPluginRuntimeStore` to store the runtime reference for use outside
the `register` callback:
Use `createPluginRuntimeStore` to store the runtime reference for use outside the `register` callback:
```typescript
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
<Steps>
<Step title="Create the store">
```typescript
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
const store = createPluginRuntimeStore<PluginRuntime>({
pluginId: "my-plugin",
errorMessage: "my-plugin runtime not initialized",
});
const store = createPluginRuntimeStore<PluginRuntime>({
pluginId: "my-plugin",
errorMessage: "my-plugin runtime not initialized",
});
```
// In your entry point
export default defineChannelPluginEntry({
id: "my-plugin",
name: "My Plugin",
description: "Example",
plugin: myPlugin,
setRuntime: store.setRuntime,
});
</Step>
<Step title="Wire into the entry point">
```typescript
export default defineChannelPluginEntry({
id: "my-plugin",
name: "My Plugin",
description: "Example",
plugin: myPlugin,
setRuntime: store.setRuntime,
});
```
</Step>
<Step title="Access from other files">
```typescript
export function getRuntime() {
return store.getRuntime(); // throws if not initialized
}
// In other files
export function getRuntime() {
return store.getRuntime(); // throws if not initialized
}
export function tryGetRuntime() {
return store.tryGetRuntime(); // returns null if not initialized
}
```
export function tryGetRuntime() {
return store.tryGetRuntime(); // returns null if not initialized
}
```
</Step>
</Steps>
Prefer `pluginId` for the runtime-store identity. The lower-level `key` form is
for uncommon cases where one plugin intentionally needs more than one runtime
slot.
<Note>
Prefer `pluginId` for the runtime-store identity. The lower-level `key` form is for uncommon cases where one plugin intentionally needs more than one runtime slot.
</Note>
## Other top-level `api` fields
Beyond `api.runtime`, the API object also provides:
| Field | Type | Description |
| ------------------------ | ------------------------- | ------------------------------------------------------------------------------------------- |
| `api.id` | `string` | Plugin id |
| `api.name` | `string` | Plugin display name |
| `api.config` | `OpenClawConfig` | Current config snapshot (active in-memory runtime snapshot when available) |
| `api.pluginConfig` | `Record<string, unknown>` | Plugin-specific config from `plugins.entries.<id>.config` |
| `api.logger` | `PluginLogger` | Scoped logger (`debug`, `info`, `warn`, `error`) |
| `api.registrationMode` | `PluginRegistrationMode` | Current load mode; `"setup-runtime"` is the lightweight pre-full-entry startup/setup window |
| `api.resolvePath(input)` | `(string) => string` | Resolve a path relative to the plugin root |
<ParamField path="api.id" type="string">
Plugin id.
</ParamField>
<ParamField path="api.name" type="string">
Plugin display name.
</ParamField>
<ParamField path="api.config" type="OpenClawConfig">
Current config snapshot (active in-memory runtime snapshot when available).
</ParamField>
<ParamField path="api.pluginConfig" type="Record<string, unknown>">
Plugin-specific config from `plugins.entries.<id>.config`.
</ParamField>
<ParamField path="api.logger" type="PluginLogger">
Scoped logger (`debug`, `info`, `warn`, `error`).
</ParamField>
<ParamField path="api.registrationMode" type="PluginRegistrationMode">
Current load mode; `"setup-runtime"` is the lightweight pre-full-entry startup/setup window.
</ParamField>
<ParamField path="api.resolvePath(input)" type="(string) => string">
Resolve a path relative to the plugin root.
</ParamField>
## Related
- [SDK overview](/plugins/sdk-overview) — subpath reference
- [SDK entry points](/plugins/sdk-entrypoints) — `definePluginEntry` options
- [Plugin internals](/plugins/architecture) — capability model and registry
- [SDK entry points](/plugins/sdk-entrypoints) — `definePluginEntry` options
- [SDK overview](/plugins/sdk-overview) — subpath reference

View File

@@ -1,86 +1,92 @@
---
summary: "Setup wizards, setup-entry.ts, config schemas, and package.json metadata"
title: "Plugin setup and config"
sidebarTitle: "Setup and Config"
sidebarTitle: "Setup and config"
read_when:
- You are adding a setup wizard to a plugin
- You need to understand setup-entry.ts vs index.ts
- You are defining plugin config schemas or package.json openclaw metadata
---
Reference for plugin packaging (`package.json` metadata), manifests
(`openclaw.plugin.json`), setup entries, and config schemas.
Reference for plugin packaging (`package.json` metadata), manifests (`openclaw.plugin.json`), setup entries, and config schemas.
<Tip>
**Looking for a walkthrough?** The how-to guides cover packaging in context:
[Channel Plugins](/plugins/sdk-channel-plugins#step-1-package-and-manifest) and
[Provider Plugins](/plugins/sdk-provider-plugins#step-1-package-and-manifest).
**Looking for a walkthrough?** The how-to guides cover packaging in context: [Channel plugins](/plugins/sdk-channel-plugins#step-1-package-and-manifest) and [Provider plugins](/plugins/sdk-provider-plugins#step-1-package-and-manifest).
</Tip>
## Package metadata
Your `package.json` needs an `openclaw` field that tells the plugin system what
your plugin provides:
Your `package.json` needs an `openclaw` field that tells the plugin system what your plugin provides:
**Channel plugin:**
```json
{
"name": "@myorg/openclaw-my-channel",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "my-channel",
"label": "My Channel",
"blurb": "Short description of the channel."
<Tabs>
<Tab title="Channel plugin">
```json
{
"name": "@myorg/openclaw-my-channel",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "my-channel",
"label": "My Channel",
"blurb": "Short description of the channel."
}
}
}
}
}
```
**Provider plugin / ClawHub publish baseline:**
```json openclaw-clawhub-package.json
{
"name": "@myorg/openclaw-my-plugin",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
```
</Tab>
<Tab title="Provider plugin / ClawHub baseline">
```json openclaw-clawhub-package.json
{
"name": "@myorg/openclaw-my-plugin",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
}
}
}
}
}
```
```
</Tab>
</Tabs>
If you publish the plugin externally on ClawHub, those `compat` and `build`
fields are required. The canonical publish snippets live in
`docs/snippets/plugin-publish/`.
<Note>
If you publish the plugin externally on ClawHub, those `compat` and `build` fields are required. The canonical publish snippets live in `docs/snippets/plugin-publish/`.
</Note>
### `openclaw` fields
| Field | Type | Description |
| ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `extensions` | `string[]` | Entry point files (relative to package root) |
| `setupEntry` | `string` | Lightweight setup-only entry (optional) |
| `channel` | `object` | Channel catalog metadata for setup, picker, quickstart, and status surfaces |
| `providers` | `string[]` | Provider ids registered by this plugin |
| `install` | `object` | Install hints: `npmSpec`, `localPath`, `defaultChoice`, `minHostVersion`, `expectedIntegrity`, `allowInvalidConfigRecovery` |
| `startup` | `object` | Startup behavior flags |
<ParamField path="extensions" type="string[]">
Entry point files (relative to package root).
</ParamField>
<ParamField path="setupEntry" type="string">
Lightweight setup-only entry (optional).
</ParamField>
<ParamField path="channel" type="object">
Channel catalog metadata for setup, picker, quickstart, and status surfaces.
</ParamField>
<ParamField path="providers" type="string[]">
Provider ids registered by this plugin.
</ParamField>
<ParamField path="install" type="object">
Install hints: `npmSpec`, `localPath`, `defaultChoice`, `minHostVersion`, `expectedIntegrity`, `allowInvalidConfigRecovery`.
</ParamField>
<ParamField path="startup" type="object">
Startup behavior flags.
</ParamField>
### `openclaw.channel`
`openclaw.channel` is cheap package metadata for channel discovery and setup
surfaces before runtime loads.
`openclaw.channel` is cheap package metadata for channel discovery and setup surfaces before runtime loads.
| Field | Type | What it means |
| -------------------------------------- | ---------- | ----------------------------------------------------------------------------- |
@@ -140,8 +146,9 @@ Example:
- `setup`: include the channel in interactive setup/configure pickers
- `docs`: mark the channel as public-facing in docs/navigation surfaces
`showConfigured` and `showInSetup` remain supported as legacy aliases. Prefer
`exposure`.
<Note>
`showConfigured` and `showInSetup` remain supported as legacy aliases. Prefer `exposure`.
</Note>
### `openclaw.install`
@@ -156,39 +163,33 @@ Example:
| `expectedIntegrity` | `string` | Expected npm dist integrity string, usually `sha512-...`, for pinned installs. |
| `allowInvalidConfigRecovery` | `boolean` | Lets bundled-plugin reinstall flows recover from specific stale-config failures. |
Interactive onboarding also uses `openclaw.install` for install-on-demand
surfaces. If your plugin exposes provider auth choices or channel setup/catalog
metadata before runtime loads, onboarding can show that choice, prompt for npm
vs local install, install or enable the plugin, then continue the selected
flow. Npm onboarding choices require trusted catalog metadata with a registry
`npmSpec`; exact versions and `expectedIntegrity` are optional pins. If
`expectedIntegrity` is present, install/update flows enforce it. Keep the "what
to show" metadata in `openclaw.plugin.json` and the "how to install it"
metadata in `package.json`.
<AccordionGroup>
<Accordion title="Onboarding behavior">
Interactive onboarding also uses `openclaw.install` for install-on-demand surfaces. If your plugin exposes provider auth choices or channel setup/catalog metadata before runtime loads, onboarding can show that choice, prompt for npm vs local install, install or enable the plugin, then continue the selected flow. Npm onboarding choices require trusted catalog metadata with a registry `npmSpec`; exact versions and `expectedIntegrity` are optional pins. If `expectedIntegrity` is present, install/update flows enforce it. Keep the "what to show" metadata in `openclaw.plugin.json` and the "how to install it" metadata in `package.json`.
</Accordion>
<Accordion title="minHostVersion enforcement">
If `minHostVersion` is set, install and manifest-registry loading both enforce it. Older hosts skip the plugin; invalid version strings are rejected.
</Accordion>
<Accordion title="Pinned npm installs">
For pinned npm installs, keep the exact version in `npmSpec` and add the expected artifact integrity:
If `minHostVersion` is set, install and manifest-registry loading both enforce
it. Older hosts skip the plugin; invalid version strings are rejected.
For pinned npm installs, keep the exact version in `npmSpec` and add the
expected artifact integrity:
```json
{
"openclaw": {
"install": {
"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3",
"expectedIntegrity": "sha512-REPLACE_WITH_NPM_DIST_INTEGRITY",
"defaultChoice": "npm"
```json
{
"openclaw": {
"install": {
"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3",
"expectedIntegrity": "sha512-REPLACE_WITH_NPM_DIST_INTEGRITY",
"defaultChoice": "npm"
}
}
}
}
}
```
```
`allowInvalidConfigRecovery` is not a general bypass for broken configs. It is
for narrow bundled-plugin recovery only, so reinstall/setup can repair known
upgrade leftovers like a missing bundled plugin path or stale `channels.<id>`
entry for that same plugin. If config is broken for unrelated reasons, install
still fails closed and tells the operator to run `openclaw doctor --fix`.
</Accordion>
<Accordion title="allowInvalidConfigRecovery scope">
`allowInvalidConfigRecovery` is not a general bypass for broken configs. It is for narrow bundled-plugin recovery only, so reinstall/setup can repair known upgrade leftovers like a missing bundled plugin path or stale `channels.<id>` entry for that same plugin. If config is broken for unrelated reasons, install still fails closed and tells the operator to run `openclaw doctor --fix`.
</Accordion>
</AccordionGroup>
### Deferred full load
@@ -206,26 +207,17 @@ Channel plugins can opt into deferred loading with:
}
```
When enabled, OpenClaw loads only `setupEntry` during the pre-listen startup
phase, even for already-configured channels. The full entry loads after the
gateway starts listening.
When enabled, OpenClaw loads only `setupEntry` during the pre-listen startup phase, even for already-configured channels. The full entry loads after the gateway starts listening.
<Warning>
Only enable deferred loading when your `setupEntry` registers everything the
gateway needs before it starts listening (channel registration, HTTP routes,
gateway methods). If the full entry owns required startup capabilities, keep
the default behavior.
Only enable deferred loading when your `setupEntry` registers everything the gateway needs before it starts listening (channel registration, HTTP routes, gateway methods). If the full entry owns required startup capabilities, keep the default behavior.
</Warning>
If your setup/full entry registers gateway RPC methods, keep them on a
plugin-specific prefix. Reserved core admin namespaces (`config.*`,
`exec.approvals.*`, `wizard.*`, `update.*`) stay core-owned and always resolve
to `operator.admin`.
If your setup/full entry registers gateway RPC methods, keep them on a plugin-specific prefix. Reserved core admin namespaces (`config.*`, `exec.approvals.*`, `wizard.*`, `update.*`) stay core-owned and always resolve to `operator.admin`.
## Plugin manifest
Every native plugin must ship an `openclaw.plugin.json` in the package root.
OpenClaw uses this to validate config without executing plugin code.
Every native plugin must ship an `openclaw.plugin.json` in the package root. OpenClaw uses this to validate config without executing plugin code.
```json
{
@@ -272,7 +264,7 @@ Even plugins with no config must ship a schema. An empty schema is valid:
}
```
See [Plugin Manifest](/plugins/manifest) for the full schema reference.
See [Plugin manifest](/plugins/manifest) for the full schema reference.
## ClawHub publishing
@@ -283,14 +275,13 @@ clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
```
The legacy skill-only publish alias is for skills. Plugin packages should
always use `clawhub package publish`.
<Note>
The legacy skill-only publish alias is for skills. Plugin packages should always use `clawhub package publish`.
</Note>
## Setup entry
The `setup-entry.ts` file is a lightweight alternative to `index.ts` that
OpenClaw loads when it only needs setup surfaces (onboarding, config repair,
disabled channel inspection).
The `setup-entry.ts` file is a lightweight alternative to `index.ts` that OpenClaw loads when it only needs setup surfaces (onboarding, config repair, disabled channel inspection).
```typescript
// setup-entry.ts
@@ -300,41 +291,35 @@ import { myChannelPlugin } from "./src/channel.js";
export default defineSetupPluginEntry(myChannelPlugin);
```
This avoids loading heavy runtime code (crypto libraries, CLI registrations,
background services) during setup flows.
This avoids loading heavy runtime code (crypto libraries, CLI registrations, background services) during setup flows.
Bundled workspace channels that keep setup-safe exports in sidecar modules can
use `defineBundledChannelSetupEntry(...)` from
`openclaw/plugin-sdk/channel-entry-contract` instead of
`defineSetupPluginEntry(...)`. That bundled contract also supports an optional
`runtime` export so setup-time runtime wiring can stay lightweight and explicit.
Bundled workspace channels that keep setup-safe exports in sidecar modules can use `defineBundledChannelSetupEntry(...)` from `openclaw/plugin-sdk/channel-entry-contract` instead of `defineSetupPluginEntry(...)`. That bundled contract also supports an optional `runtime` export so setup-time runtime wiring can stay lightweight and explicit.
**When OpenClaw uses `setupEntry` instead of the full entry:**
<AccordionGroup>
<Accordion title="When OpenClaw uses setupEntry instead of the full entry">
- The channel is disabled but needs setup/onboarding surfaces.
- The channel is enabled but unconfigured.
- Deferred loading is enabled (`deferConfiguredChannelFullLoadUntilAfterListen`).
</Accordion>
<Accordion title="What setupEntry must register">
- The channel plugin object (via `defineSetupPluginEntry`).
- Any HTTP routes required before gateway listen.
- Any gateway methods needed during startup.
- The channel is disabled but needs setup/onboarding surfaces
- The channel is enabled but unconfigured
- Deferred loading is enabled (`deferConfiguredChannelFullLoadUntilAfterListen`)
Those startup gateway methods should still avoid reserved core admin namespaces such as `config.*` or `update.*`.
**What `setupEntry` must register:**
- The channel plugin object (via `defineSetupPluginEntry`)
- Any HTTP routes required before gateway listen
- Any gateway methods needed during startup
Those startup gateway methods should still avoid reserved core admin
namespaces such as `config.*` or `update.*`.
**What `setupEntry` should NOT include:**
- CLI registrations
- Background services
- Heavy runtime imports (crypto, SDKs)
- Gateway methods only needed after startup
</Accordion>
<Accordion title="What setupEntry should NOT include">
- CLI registrations.
- Background services.
- Heavy runtime imports (crypto, SDKs).
- Gateway methods only needed after startup.
</Accordion>
</AccordionGroup>
### Narrow setup helper imports
For hot setup-only paths, prefer the narrow setup helper seams over the broader
`plugin-sdk/setup` umbrella when you only need part of the setup surface:
For hot setup-only paths, prefer the narrow setup helper seams over the broader `plugin-sdk/setup` umbrella when you only need part of the setup surface:
| Import path | Use it for | Key exports |
| ---------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -342,41 +327,27 @@ For hot setup-only paths, prefer the narrow setup helper seams over the broader
| `plugin-sdk/setup-adapter-runtime` | environment-aware account setup adapters | `createEnvPatchedAccountSetupAdapter` |
| `plugin-sdk/setup-tools` | setup/install CLI/archive/docs helpers | `formatCliCommand`, `detectBinary`, `extractArchive`, `resolveBrewExecutable`, `formatDocsLink`, `CONFIG_DIR` |
Use the broader `plugin-sdk/setup` seam when you want the full shared setup
toolbox, including config-patch helpers such as
`moveSingleAccountChannelSectionToDefaultAccount(...)`.
Use the broader `plugin-sdk/setup` seam when you want the full shared setup toolbox, including config-patch helpers such as `moveSingleAccountChannelSectionToDefaultAccount(...)`.
The setup patch adapters stay hot-path safe on import. Their bundled
single-account promotion contract-surface lookup is lazy, so importing
`plugin-sdk/setup-runtime` does not eagerly load bundled contract-surface
discovery before the adapter is actually used.
The setup patch adapters stay hot-path safe on import. Their bundled single-account promotion contract-surface lookup is lazy, so importing `plugin-sdk/setup-runtime` does not eagerly load bundled contract-surface discovery before the adapter is actually used.
### Channel-owned single-account promotion
When a channel upgrades from a single-account top-level config to
`channels.<id>.accounts.*`, the default shared behavior is to move promoted
account-scoped values into `accounts.default`.
When a channel upgrades from a single-account top-level config to `channels.<id>.accounts.*`, the default shared behavior is to move promoted account-scoped values into `accounts.default`.
Bundled channels can narrow or override that promotion through their setup
contract surface:
Bundled channels can narrow or override that promotion through their setup contract surface:
- `singleAccountKeysToMove`: extra top-level keys that should move into the
promoted account
- `namedAccountPromotionKeys`: when named accounts already exist, only these
keys move into the promoted account; shared policy/delivery keys stay at the
channel root
- `resolveSingleAccountPromotionTarget(...)`: choose which existing account
receives promoted values
- `singleAccountKeysToMove`: extra top-level keys that should move into the promoted account
- `namedAccountPromotionKeys`: when named accounts already exist, only these keys move into the promoted account; shared policy/delivery keys stay at the channel root
- `resolveSingleAccountPromotionTarget(...)`: choose which existing account receives promoted values
Matrix is the current bundled example. If exactly one named Matrix account
already exists, or if `defaultAccount` points at an existing non-canonical key
such as `Ops`, promotion preserves that account instead of creating a new
`accounts.default` entry.
<Note>
Matrix is the current bundled example. If exactly one named Matrix account already exists, or if `defaultAccount` points at an existing non-canonical key such as `Ops`, promotion preserves that account instead of creating a new `accounts.default` entry.
</Note>
## Config schema
Plugin config is validated against the JSON Schema in your manifest. Users
configure plugins via:
Plugin config is validated against the JSON Schema in your manifest. Users configure plugins via:
```json5
{
@@ -409,8 +380,7 @@ For channel-specific config, use the channel config section instead:
### Building channel config schemas
Use `buildChannelConfigSchema` to convert a Zod schema into the
`ChannelConfigSchema` wrapper used by plugin-owned config artifacts:
Use `buildChannelConfigSchema` to convert a Zod schema into the `ChannelConfigSchema` wrapper used by plugin-owned config artifacts:
```typescript
import { z } from "zod";
@@ -426,15 +396,11 @@ const accountSchema = z.object({
const configSchema = buildChannelConfigSchema(accountSchema);
```
For third-party plugins, the cold-path contract is still the plugin manifest:
mirror the generated JSON Schema into `openclaw.plugin.json#channelConfigs` so
config schema, setup, and UI surfaces can inspect `channels.<id>` without
loading runtime code.
For third-party plugins, the cold-path contract is still the plugin manifest: mirror the generated JSON Schema into `openclaw.plugin.json#channelConfigs` so config schema, setup, and UI surfaces can inspect `channels.<id>` without loading runtime code.
## Setup wizards
Channel plugins can provide interactive setup wizards for `openclaw onboard`.
The wizard is a `ChannelSetupWizard` object on the `ChannelPlugin`:
Channel plugins can provide interactive setup wizards for `openclaw onboard`. The wizard is a `ChannelSetupWizard` object on the `ChannelPlugin`:
```typescript
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/channel-setup";
@@ -467,84 +433,75 @@ const setupWizard: ChannelSetupWizard = {
};
```
The `ChannelSetupWizard` type supports `credentials`, `textInputs`,
`dmPolicy`, `allowFrom`, `groupAccess`, `prepare`, `finalize`, and more.
See bundled plugin packages (for example the Discord plugin `src/channel.setup.ts`) for
full examples.
The `ChannelSetupWizard` type supports `credentials`, `textInputs`, `dmPolicy`, `allowFrom`, `groupAccess`, `prepare`, `finalize`, and more. See bundled plugin packages (for example the Discord plugin `src/channel.setup.ts`) for full examples.
For DM allowlist prompts that only need the standard
`note -> prompt -> parse -> merge -> patch` flow, prefer the shared setup
helpers from `openclaw/plugin-sdk/setup`: `createPromptParsedAllowFromForAccount(...)`,
`createTopLevelChannelParsedAllowFromPrompt(...)`, and
`createNestedChannelParsedAllowFromPrompt(...)`.
<AccordionGroup>
<Accordion title="Shared allowFrom prompts">
For DM allowlist prompts that only need the standard `note -> prompt -> parse -> merge -> patch` flow, prefer the shared setup helpers from `openclaw/plugin-sdk/setup`: `createPromptParsedAllowFromForAccount(...)`, `createTopLevelChannelParsedAllowFromPrompt(...)`, and `createNestedChannelParsedAllowFromPrompt(...)`.
</Accordion>
<Accordion title="Standard channel setup status">
For channel setup status blocks that only vary by labels, scores, and optional extra lines, prefer `createStandardChannelSetupStatus(...)` from `openclaw/plugin-sdk/setup` instead of hand-rolling the same `status` object in each plugin.
</Accordion>
<Accordion title="Optional channel setup surface">
For optional setup surfaces that should only appear in certain contexts, use `createOptionalChannelSetupSurface` from `openclaw/plugin-sdk/channel-setup`:
For channel setup status blocks that only vary by labels, scores, and optional
extra lines, prefer `createStandardChannelSetupStatus(...)` from
`openclaw/plugin-sdk/setup` instead of hand-rolling the same `status` object in
each plugin.
```typescript
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
For optional setup surfaces that should only appear in certain contexts, use
`createOptionalChannelSetupSurface` from `openclaw/plugin-sdk/channel-setup`:
const setupSurface = createOptionalChannelSetupSurface({
channel: "my-channel",
label: "My Channel",
npmSpec: "@myorg/openclaw-my-channel",
docsPath: "/channels/my-channel",
});
// Returns { setupAdapter, setupWizard }
```
```typescript
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
`plugin-sdk/channel-setup` also exposes the lower-level `createOptionalChannelSetupAdapter(...)` and `createOptionalChannelSetupWizard(...)` builders when you only need one half of that optional-install surface.
const setupSurface = createOptionalChannelSetupSurface({
channel: "my-channel",
label: "My Channel",
npmSpec: "@myorg/openclaw-my-channel",
docsPath: "/channels/my-channel",
});
// Returns { setupAdapter, setupWizard }
```
The generated optional adapter/wizard fail closed on real config writes. They reuse one install-required message across `validateInput`, `applyAccountConfig`, and `finalize`, and append a docs link when `docsPath` is set.
`plugin-sdk/channel-setup` also exposes the lower-level
`createOptionalChannelSetupAdapter(...)` and
`createOptionalChannelSetupWizard(...)` builders when you only need one half of
that optional-install surface.
</Accordion>
<Accordion title="Binary-backed setup helpers">
For binary-backed setup UIs, prefer the shared delegated helpers instead of copying the same binary/status glue into every channel:
The generated optional adapter/wizard fail closed on real config writes. They
reuse one install-required message across `validateInput`,
`applyAccountConfig`, and `finalize`, and append a docs link when `docsPath` is
set.
- `createDetectedBinaryStatus(...)` for status blocks that vary only by labels, hints, scores, and binary detection
- `createCliPathTextInput(...)` for path-backed text inputs
- `createDelegatedSetupWizardStatusResolvers(...)`, `createDelegatedPrepare(...)`, `createDelegatedFinalize(...)`, and `createDelegatedResolveConfigured(...)` when `setupEntry` needs to forward to a heavier full wizard lazily
- `createDelegatedTextInputShouldPrompt(...)` when `setupEntry` only needs to delegate a `textInputs[*].shouldPrompt` decision
For binary-backed setup UIs, prefer the shared delegated helpers instead of
copying the same binary/status glue into every channel:
- `createDetectedBinaryStatus(...)` for status blocks that vary only by labels,
hints, scores, and binary detection
- `createCliPathTextInput(...)` for path-backed text inputs
- `createDelegatedSetupWizardStatusResolvers(...)`,
`createDelegatedPrepare(...)`, `createDelegatedFinalize(...)`, and
`createDelegatedResolveConfigured(...)` when `setupEntry` needs to forward to
a heavier full wizard lazily
- `createDelegatedTextInputShouldPrompt(...)` when `setupEntry` only needs to
delegate a `textInputs[*].shouldPrompt` decision
</Accordion>
</AccordionGroup>
## Publishing and installing
**External plugins:** publish to [ClawHub](/tools/clawhub) or npm, then install:
```bash
openclaw plugins install @myorg/openclaw-my-plugin
```
<Tabs>
<Tab title="Auto (ClawHub then npm)">
```bash
openclaw plugins install @myorg/openclaw-my-plugin
```
OpenClaw tries ClawHub first and falls back to npm automatically. You can also
force ClawHub explicitly:
OpenClaw tries ClawHub first and falls back to npm automatically.
```bash
openclaw plugins install clawhub:@myorg/openclaw-my-plugin # ClawHub only
```
</Tab>
<Tab title="ClawHub only">
```bash
openclaw plugins install clawhub:@myorg/openclaw-my-plugin
```
</Tab>
<Tab title="npm package spec">
There is no matching `npm:` override. Use the normal npm package spec when you want the npm path after ClawHub fallback:
There is no matching `npm:` override. Use the normal npm package spec when you
want the npm path after ClawHub fallback:
```bash
openclaw plugins install @myorg/openclaw-my-plugin
```
```bash
openclaw plugins install @myorg/openclaw-my-plugin
```
</Tab>
</Tabs>
**In-repo plugins:** place under the bundled plugin workspace tree and they are automatically
discovered during build.
**In-repo plugins:** place under the bundled plugin workspace tree and they are automatically discovered during build.
**Users can install:**
@@ -553,20 +510,15 @@ openclaw plugins install <package-name>
```
<Info>
For npm-sourced installs, `openclaw plugins install` runs
project-local `npm install --ignore-scripts` (no lifecycle scripts), ignoring
inherited global npm install settings. Keep plugin dependency trees pure JS/TS
and avoid packages that require `postinstall` builds.
For npm-sourced installs, `openclaw plugins install` runs project-local `npm install --ignore-scripts` (no lifecycle scripts), ignoring inherited global npm install settings. Keep plugin dependency trees pure JS/TS and avoid packages that require `postinstall` builds.
</Info>
Bundled OpenClaw-owned plugins are the only startup repair exception: when a
packaged install sees one enabled by plugin config, legacy channel config, or
its bundled default-enabled manifest, startup installs that plugin's missing
runtime dependencies before import. Third-party plugins should not rely on
startup installs; keep using the explicit plugin installer.
<Note>
Bundled OpenClaw-owned plugins are the only startup repair exception: when a packaged install sees one enabled by plugin config, legacy channel config, or its bundled default-enabled manifest, startup installs that plugin's missing runtime dependencies before import. Third-party plugins should not rely on startup installs; keep using the explicit plugin installer.
</Note>
## Related
- [SDK entry points](/plugins/sdk-entrypoints) — `definePluginEntry` and `defineChannelPluginEntry`
- [Plugin manifest](/plugins/manifest) — full manifest schema reference
- [Building plugins](/plugins/building-plugins) — step-by-step getting started guide
- [Plugin manifest](/plugins/manifest) — full manifest schema reference
- [SDK entry points](/plugins/sdk-entrypoints) — `definePluginEntry` and `defineChannelPluginEntry`

View File

@@ -271,7 +271,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| Line | `plugin-sdk/line`, `plugin-sdk/line-core`, `plugin-sdk/line-runtime`, `plugin-sdk/line-surface` | Bundled LINE helper/runtime surface |
| IRC | `plugin-sdk/irc`, `plugin-sdk/irc-surface` | Bundled IRC helper surface |
| Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu-conversation`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch` | Bundled channel compatibility/helper seams |
| Auth/plugin-specific helpers | `plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`, `plugin-sdk/diagnostics-otel`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/thread-ownership`, `plugin-sdk/voice-call` | Bundled feature/plugin helper seams; `plugin-sdk/github-copilot-token` currently exports `DEFAULT_COPILOT_API_BASE_URL`, `deriveCopilotApiBaseUrlFromToken`, and `resolveCopilotApiToken` |
| Auth/plugin-specific helpers | `plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`, `plugin-sdk/diagnostics-otel`, `plugin-sdk/diagnostics-prometheus`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/thread-ownership`, `plugin-sdk/voice-call` | Bundled feature/plugin helper seams; `plugin-sdk/github-copilot-token` currently exports `DEFAULT_COPILOT_API_BASE_URL`, `deriveCopilotApiBaseUrlFromToken`, and `resolveCopilotApiToken` |
</Accordion>
</AccordionGroup>

View File

@@ -461,7 +461,7 @@ For the full setup and behavior details, see [Ollama Web Search](/tools/ollama-s
<Accordion title="Streaming configuration">
OpenClaw's Ollama integration uses the **native Ollama API** (`/api/chat`) by default, which fully supports streaming and tool calling simultaneously. No special configuration is needed.
For native `/api/chat` requests, OpenClaw also forwards thinking control directly to Ollama: `/think off` and `openclaw agent --thinking off` send top-level `think: false`, while non-`off` thinking levels send `think: true`.
For native `/api/chat` requests, OpenClaw also forwards thinking control directly to Ollama: `/think off` and `openclaw agent --thinking off` send top-level `think: false`, while `/think low|medium|high` send the matching top-level `think` effort string. `/think max` maps to Ollama's highest native effort, `think: "high"`.
<Tip>
If you need to use the OpenAI-compatible endpoint, see the "Legacy OpenAI-compatible mode" section above. Streaming and tool calling may not work simultaneously in that mode.

View File

@@ -1,133 +0,0 @@
---
summary: "Investigation notes for duplicate async exec completion injection"
read_when:
- Debugging repeated node exec completion events
- Working on heartbeat/system-event dedupe
title: "Async exec duplicate completion investigation"
---
## Scope
- Session: `agent:main:telegram:group:-1003774691294:topic:1`
- Symptom: the same async exec completion for session/run `keen-nexus` was recorded twice in LCM as user turns.
- Goal: identify whether this is most likely duplicate session injection or plain outbound delivery retry.
## Conclusion
Most likely this is **duplicate session injection**, not a pure outbound delivery retry.
The strongest gateway-side gap is in the **node exec completion path**:
1. A node-side exec finish emits `exec.finished` with the full `runId`.
2. Gateway `server-node-events` converts that into a system event and requests a heartbeat.
3. The heartbeat run injects the drained system event block into the agent prompt.
4. The embedded runner persists that prompt as a new user turn in the session transcript.
If the same `exec.finished` reaches the gateway twice for the same `runId` for any reason (replay, reconnect duplicate, upstream resend, duplicated producer), OpenClaw currently has **no idempotency check keyed by `runId`/`contextKey`** on this path. The second copy will become a second user message with the same content.
## Exact Code Path
### 1. Producer: node exec completion event
- `src/node-host/invoke.ts:340-360`
- `sendExecFinishedEvent(...)` emits `node.event` with event `exec.finished`.
- Payload includes `sessionKey` and full `runId`.
### 2. Gateway event ingestion
- `src/gateway/server-node-events.ts:574-640`
- Handles `exec.finished`.
- Builds text:
- `Exec finished (node=..., id=<runId>, code ...)`
- Enqueues it via:
- `enqueueSystemEvent(text, { sessionKey, contextKey: runId ? \`exec:${runId}\` : "exec", trusted: false })`
- Immediately requests a wake:
- `requestHeartbeatNow(scopedHeartbeatWakeOptions(sessionKey, { reason: "exec-event" }))`
### 3. System event dedupe weakness
- `src/infra/system-events.ts:90-115`
- `enqueueSystemEvent(...)` only suppresses **consecutive duplicate text**:
- `if (entry.lastText === cleaned) return false`
- It stores `contextKey`, but does **not** use `contextKey` for idempotency.
- After drain, duplicate suppression resets.
This means a replayed `exec.finished` with the same `runId` can be accepted again later, even though the code already had a stable idempotency candidate (`exec:<runId>`).
### 4. Wake handling is not the primary duplicator
- `src/infra/heartbeat-wake.ts:79-117`
- Wakes are coalesced by `(agentId, sessionKey)`.
- Duplicate wake requests for the same target collapse to one pending wake entry.
This makes **duplicate wake handling alone** a weaker explanation than duplicate event ingestion.
### 5. Heartbeat consumes the event and turns it into prompt input
- `src/infra/heartbeat-runner.ts:535-574`
- Preflight peeks pending system events and classifies exec-event runs.
- `src/auto-reply/reply/session-system-events.ts:86-90`
- `drainFormattedSystemEvents(...)` drains the queue for the session.
- `src/auto-reply/reply/get-reply-run.ts:400-427`
- The drained system event block is prepended into the agent prompt body.
### 6. Transcript injection point
- `src/agents/pi-embedded-runner/run/attempt.ts:2000-2017`
- `activeSession.prompt(effectivePrompt)` submits the full prompt to the embedded PI session.
- That is the point where the completion-derived prompt becomes a persisted user turn.
So once the same system event is rebuilt into the prompt twice, duplicate LCM user messages are expected.
## Why plain outbound delivery retry is less likely
There is a real outbound failure path in the heartbeat runner:
- `src/infra/heartbeat-runner.ts:1194-1242`
- The reply is generated first.
- Outbound delivery happens later via `deliverOutboundPayloads(...)`.
- Failure there returns `{ status: "failed" }`.
However, for the same system event queue entry, this alone is **not sufficient** to explain the duplicate user turns:
- `src/auto-reply/reply/session-system-events.ts:86-90`
- The system event queue is already drained before outbound delivery.
So a channel send retry by itself would not recreate the exact same queued event. It could explain missing/failed external delivery, but not by itself a second identical session user message.
## Secondary, lower-confidence possibility
There is a full-run retry loop in the agent runner:
- `src/auto-reply/reply/agent-runner-execution.ts:741-1473`
- Certain transient failures can retry the whole run and resubmit the same `commandBody`.
That can duplicate a persisted user prompt **within the same reply execution** if the prompt was already appended before the retry condition triggered.
I rank this lower than duplicate `exec.finished` ingestion because:
- the observed gap was around 51 seconds, which looks more like a second wake/turn than an in-process retry;
- the report already mentions repeated message send failures, which points more toward a separate later turn than an immediate model/runtime retry.
## Root Cause Hypothesis
Highest-confidence hypothesis:
- The `keen-nexus` completion came through the **node exec event path**.
- The same `exec.finished` was delivered to `server-node-events` twice.
- Gateway accepted both because `enqueueSystemEvent(...)` does not dedupe by `contextKey` / `runId`.
- Each accepted event triggered a heartbeat and was injected as a user turn into the PI transcript.
## Proposed Tiny Surgical Fix
If a fix is wanted, the smallest high-value change is:
- make exec/system-event idempotency honor `contextKey` for a short horizon, at least for exact `(sessionKey, contextKey, text)` repeats;
- or add a dedicated dedupe in `server-node-events` for `exec.finished` keyed by `(sessionKey, runId, event kind)`.
That would directly block replayed `exec.finished` duplicates before they become session turns.
## Related
- [Exec tool](/tools/exec)
- [Session management](/concepts/session)

View File

@@ -1,540 +0,0 @@
---
summary: "QA refactor plan for scenario catalog and harness consolidation"
read_when:
- Refactoring QA scenario definitions or qa-lab harness code
- Moving QA behavior between markdown scenarios and TypeScript harness logic
title: "QA refactor"
---
Status: foundational migration landed.
## Goal
Move OpenClaw QA from a split-definition model to a single source of truth:
- scenario metadata
- prompts sent to the model
- setup and teardown
- harness logic
- assertions and success criteria
- artifacts and report hints
The desired end state is a generic QA harness that loads powerful scenario definition files instead of hardcoding most behavior in TypeScript.
## Current State
Primary source of truth now lives in `qa/scenarios/index.md` plus one file per
scenario under `qa/scenarios/<theme>/*.md`.
Implemented:
- `qa/scenarios/index.md`
- canonical QA pack metadata
- operator identity
- kickoff mission
- `qa/scenarios/<theme>/*.md`
- one markdown file per scenario
- scenario metadata
- handler bindings
- scenario-specific execution config
- `extensions/qa-lab/src/scenario-catalog.ts`
- markdown pack parser + zod validation
- `extensions/qa-lab/src/qa-agent-bootstrap.ts`
- plan rendering from the markdown pack
- `extensions/qa-lab/src/qa-agent-workspace.ts`
- seeds generated compatibility files plus `QA_SCENARIOS.md`
- `extensions/qa-lab/src/suite.ts`
- selects executable scenarios through markdown-defined handler bindings
- QA bus protocol + UI
- generic inline attachments for image/video/audio/file rendering
Remaining split surfaces:
- `extensions/qa-lab/src/suite.ts`
- still owns most executable custom handler logic
- `extensions/qa-lab/src/report.ts`
- still derives report structure from runtime outputs
So the source-of-truth split is fixed, but execution is still mostly handler-backed rather than fully declarative.
## What The Real Scenario Surface Looks Like
Reading the current suite shows a few distinct scenario classes.
### Simple interaction
- channel baseline
- DM baseline
- threaded follow-up
- model switch
- approval followthrough
- reaction/edit/delete
### Config and runtime mutation
- config patch skill disable
- config apply restart wake-up
- config restart capability flip
- runtime inventory drift check
### Filesystem and repo assertions
- source/docs discovery report
- build Lobster Invaders
- generated image artifact lookup
### Memory orchestration
- memory recall
- memory tools in channel context
- memory failure fallback
- session memory ranking
- thread memory isolation
- memory dreaming sweep
### Tool and plugin integration
- MCP plugin-tools call
- skill visibility
- skill hot install
- native image generation
- image roundtrip
- image understanding from attachment
### Multi-turn and multi-actor
- subagent handoff
- subagent fanout synthesis
- restart recovery style flows
These categories matter because they drive DSL requirements. A flat list of prompt + expected text is not enough.
## Direction
### Single source of truth
Use `qa/scenarios/index.md` plus `qa/scenarios/<theme>/*.md` as the authored
source of truth.
The pack should stay:
- human-readable in review
- machine-parseable
- rich enough to drive:
- suite execution
- QA workspace bootstrap
- QA Lab UI metadata
- docs/discovery prompts
- report generation
### Preferred authoring format
Use markdown as the top-level format, with structured YAML inside it.
Recommended shape:
- YAML frontmatter
- id
- title
- surface
- tags
- docs refs
- code refs
- model/provider overrides
- prerequisites
- prose sections
- objective
- notes
- debugging hints
- fenced YAML blocks
- setup
- steps
- assertions
- cleanup
This gives:
- better PR readability than giant JSON
- richer context than pure YAML
- strict parsing and zod validation
Raw JSON is acceptable only as an intermediate generated form.
## Proposed Scenario File Shape
Example:
````md
---
id: image-generation-roundtrip
title: Image generation roundtrip
surface: image
tags: [media, image, roundtrip]
models:
primary: openai/gpt-5.4
requires:
tools: [image_generate]
plugins: [openai, qa-channel]
docsRefs:
- docs/help/testing.md
- docs/concepts/model-providers.md
codeRefs:
- extensions/qa-lab/src/suite.ts
- src/gateway/chat-attachments.ts
---
# Objective
Verify generated media is reattached on the follow-up turn.
# Setup
```yaml scenario.setup
- action: config.patch
patch:
agents:
defaults:
imageGenerationModel:
primary: openai/gpt-image-1
- action: session.create
key: agent:qa:image-roundtrip
```
# Steps
```yaml scenario.steps
- action: agent.send
session: agent:qa:image-roundtrip
message: |
Image generation check: generate a QA lighthouse image and summarize it in one short sentence.
- action: artifact.capture
kind: generated-image
promptSnippet: Image generation check
saveAs: lighthouseImage
- action: agent.send
session: agent:qa:image-roundtrip
message: |
Roundtrip image inspection check: describe the generated lighthouse attachment in one short sentence.
attachments:
- fromArtifact: lighthouseImage
```
# Expect
```yaml scenario.expect
- assert: outbound.textIncludes
value: lighthouse
- assert: requestLog.matches
where:
promptIncludes: Roundtrip image inspection check
imageInputCountGte: 1
- assert: artifact.exists
ref: lighthouseImage
```
````
## Runner Capabilities The DSL Must Cover
Based on the current suite, the generic runner needs more than prompt execution.
### Environment and setup actions
- `bus.reset`
- `gateway.waitHealthy`
- `channel.waitReady`
- `session.create`
- `thread.create`
- `workspace.writeSkill`
### Agent turn actions
- `agent.send`
- `agent.wait`
- `bus.injectInbound`
- `bus.injectOutbound`
### Config and runtime actions
- `config.get`
- `config.patch`
- `config.apply`
- `gateway.restart`
- `tools.effective`
- `skills.status`
### File and artifact actions
- `file.write`
- `file.read`
- `file.delete`
- `file.touchTime`
- `artifact.captureGeneratedImage`
- `artifact.capturePath`
### Memory and cron actions
- `memory.indexForce`
- `memory.searchCli`
- `doctor.memory.status`
- `cron.list`
- `cron.run`
- `cron.waitCompletion`
- `sessionTranscript.write`
### MCP actions
- `mcp.callTool`
### Assertions
- `outbound.textIncludes`
- `outbound.inThread`
- `outbound.notInRoot`
- `tool.called`
- `tool.notPresent`
- `skill.visible`
- `skill.disabled`
- `file.contains`
- `memory.contains`
- `requestLog.matches`
- `sessionStore.matches`
- `cron.managedPresent`
- `artifact.exists`
## Variables and Artifact References
The DSL must support saved outputs and later references.
Examples from the current suite:
- create a thread, then reuse `threadId`
- create a session, then reuse `sessionKey`
- generate an image, then attach the file on the next turn
- generate a wake marker string, then assert that it appears later
Needed capabilities:
- `saveAs`
- `${vars.name}`
- `${artifacts.name}`
- typed references for paths, session keys, thread ids, markers, tool outputs
Without variable support, the harness will keep leaking scenario logic back into TypeScript.
## What Should Stay As Escape Hatches
A fully pure declarative runner is not realistic in phase 1.
Some scenarios are inherently orchestration-heavy:
- memory dreaming sweep
- config apply restart wake-up
- config restart capability flip
- generated image artifact resolution by timestamp/path
- discovery-report evaluation
These should use explicit custom handlers for now.
Recommended rule:
- 85-90% declarative
- explicit `customHandler` steps for the hard remainder
- named and documented custom handlers only
- no anonymous inline code in the scenario file
That keeps the generic engine clean while still allowing progress.
## Architecture Change
### Current
Scenario markdown already is the source of truth for:
- suite execution
- workspace bootstrap files
- QA Lab UI scenario catalog
- report metadata
- discovery prompts
Generated compatibility:
- seeded workspace still includes `QA_KICKOFF_TASK.md`
- seeded workspace still includes `QA_SCENARIO_PLAN.md`
- seeded workspace now also includes `QA_SCENARIOS.md`
## Refactor Plan
### Phase 1: loader and schema
Done.
- added `qa/scenarios/index.md`
- split scenarios into `qa/scenarios/<theme>/*.md`
- added parser for named markdown YAML pack content
- validated with zod
- switched consumers to the parsed pack
- removed repo-level `qa/seed-scenarios.json` and `qa/QA_KICKOFF_TASK.md`
### Phase 2: generic engine
- split `extensions/qa-lab/src/suite.ts` into:
- loader
- engine
- action registry
- assertion registry
- custom handlers
- keep existing helper functions as engine operations
Deliverable:
- engine executes simple declarative scenarios
Start with scenarios that are mostly prompt + wait + assert:
- threaded follow-up
- image understanding from attachment
- skill visibility and invocation
- channel baseline
Deliverable:
- first real markdown-defined scenarios shipping through the generic engine
### Phase 4: migrate medium scenarios
- image generation roundtrip
- memory tools in channel context
- session memory ranking
- subagent handoff
- subagent fanout synthesis
Deliverable:
- variables, artifacts, tool assertions, request-log assertions proven out
### Phase 5: keep hard scenarios on custom handlers
- memory dreaming sweep
- config apply restart wake-up
- config restart capability flip
- runtime inventory drift
Deliverable:
- same authoring format, but with explicit custom-step blocks where needed
### Phase 6: delete hardcoded scenario map
Once the pack coverage is good enough:
- remove most scenario-specific TypeScript branching from `extensions/qa-lab/src/suite.ts`
## Fake Slack / Rich Media Support
The current QA bus is text-first.
Relevant files:
- `extensions/qa-channel/src/protocol.ts`
- `extensions/qa-lab/src/bus-state.ts`
- `extensions/qa-lab/src/bus-queries.ts`
- `extensions/qa-lab/src/bus-server.ts`
- `extensions/qa-lab/web/src/ui-render.ts`
Today the QA bus supports:
- text
- reactions
- threads
It does not yet model inline media attachments.
### Needed transport contract
Add a generic QA bus attachment model:
```ts
type QaBusAttachment = {
id: string;
kind: "image" | "video" | "audio" | "file";
mimeType: string;
fileName?: string;
inline?: boolean;
url?: string;
contentBase64?: string;
width?: number;
height?: number;
durationMs?: number;
altText?: string;
transcript?: string;
};
```
Then add `attachments?: QaBusAttachment[]` to:
- `QaBusMessage`
- `QaBusInboundMessageInput`
- `QaBusOutboundMessageInput`
### Why generic first
Do not build a Slack-only media model.
Instead:
- one generic QA transport model
- multiple renderers on top of it
- current QA Lab chat
- future fake Slack web
- any other fake transport views
This prevents duplicate logic and lets media scenarios stay transport-agnostic.
### UI work needed
Update the QA UI to render:
- inline image preview
- inline audio player
- inline video player
- file attachment chip
The current UI can already render threads and reactions, so attachment rendering should layer onto the same message card model.
### Scenario work enabled by media transport
Once attachments flow through QA bus, we can add richer fake-chat scenarios:
- inline image reply in fake Slack
- audio attachment understanding
- video attachment understanding
- mixed attachment ordering
- thread reply with media retained
## Recommendation
The next implementation chunk should be:
1. add markdown scenario loader + zod schema
2. generate the current catalog from markdown
3. migrate a few simple scenarios first
4. add generic QA bus attachment support
5. render inline image in the QA UI
6. then expand to audio and video
This is the smallest path that proves both goals:
- generic markdown-defined QA
- richer fake messaging surfaces
## Open Questions
- whether scenario files should allow embedded markdown prompt templates with variable interpolation
- whether setup/cleanup should be named sections or just ordered action lists
- whether artifact references should be strongly typed in schema or string-based
- whether custom handlers should live in one registry or per-surface registries
- whether the generated JSON compatibility file should remain checked in during migration
## Related
- [QA E2E automation](/concepts/qa-e2e-automation)

View File

@@ -49,6 +49,12 @@ OpenClaw has three public release lanes:
- Run `pnpm build && pnpm ui:build` before `pnpm release:check` so the expected
`dist/*` release artifacts and Control UI bundle exist for the pack
validation step
- Run the manual `CI` workflow before release approval when you need full normal
CI coverage for the release candidate. Manual CI dispatches bypass changed
scoping and force the Linux Node shards, bundled-plugin shards, channel
contracts, `check`, `check-additional`, build smoke, docs checks, Python
skills, Windows, macOS, Android, and Control UI i18n lanes.
Example: `gh workflow run ci.yml --ref release/YYYY.M.D`
- Run `pnpm qa:otel:smoke` when validating release telemetry. It exercises
QA-lab through a local OTLP/HTTP receiver and verifies the exported trace
span names, bounded attributes, and content/identifier redaction without
@@ -182,18 +188,20 @@ When cutting a stable npm release:
SHA for a validation-only dry run of the preflight workflow
2. Choose `npm_dist_tag=beta` for the normal beta-first flow, or `latest` only
when you intentionally want a direct stable publish
3. Run `OpenClaw Release Checks` separately with the same tag or the
3. Run the manual `CI` workflow on the release ref when you want full normal CI
coverage instead of smart-scoped merge coverage
4. Run `OpenClaw Release Checks` separately with the same tag or the
full current workflow-branch commit SHA when you want live prompt cache,
QA Lab parity, Matrix, and Telegram coverage
- This is separate on purpose so live coverage stays available without
recoupling long-running or flaky checks to the publish workflow
4. Save the successful `preflight_run_id`
5. Run `OpenClaw NPM Release` again with `preflight_only=false`, the same
5. Save the successful `preflight_run_id`
6. Run `OpenClaw NPM Release` again with `preflight_only=false`, the same
`tag`, the same `npm_dist_tag`, and the saved `preflight_run_id`
6. If the release landed on `beta`, use the private
7. If the release landed on `beta`, use the private
`openclaw/releases-private/.github/workflows/openclaw-npm-dist-tags.yml`
workflow to promote that stable version from `beta` to `latest`
7. If the release intentionally published directly to `latest` and `beta`
8. If the release intentionally published directly to `latest` and `beta`
should follow the same stable build immediately, use that same private
workflow to point both dist-tags at the stable version, or let its scheduled
self-healing sync move `beta` later

View File

@@ -193,7 +193,12 @@ Notable entry types:
- `compaction`: persisted compaction summary with `firstKeptEntryId` and `tokensBefore`
- `branch_summary`: persisted summary when navigating a tree branch
OpenClaw intentionally does **not** “fix up” transcripts; the Gateway uses `SessionManager` to read/write them.
OpenClaw uses `SessionManager` for normal transcript reads/writes. After
compaction, the Gateway now defaults to a bounded transcript rewrite that drops
message entries already covered by the persisted compaction summary while
keeping non-message session state and the recent unsummarized tail. Set
`agents.defaults.compaction.truncateAfterCompaction` to `false` to preserve the
legacy append-only behavior.
---

View File

@@ -10,13 +10,15 @@ title: "Tests"
- `pnpm test:force`: Kills any lingering gateway process holding the default control port, then runs the full Vitest suite with an isolated gateway port so server tests dont collide with a running instance. Use this when a prior gateway run left port 18789 occupied.
- `pnpm test:coverage`: Runs the unit suite with V8 coverage (via `vitest.unit.config.ts`). This is a loaded-file unit coverage gate, not whole-repo all-file coverage. Thresholds are 70% lines/functions/statements and 55% branches. Because `coverage.all` is false, the gate measures files loaded by the unit coverage suite instead of treating every split-lane source file as uncovered.
- `pnpm test:coverage:changed`: Runs unit coverage only for files changed since `origin/main`.
- `pnpm test:changed`: expands changed git paths into scoped Vitest lanes when the diff only touches routable source/test files. Config/setup changes still fall back to the native root projects run so wiring edits rerun broadly when needed.
- `pnpm test:changed`: cheap smart changed test run. It runs precise targets from direct test edits, sibling `*.test.ts` files, explicit source mappings, and the local import graph. Broad/config/package changes are skipped unless they map to precise 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 gate for the diff against `origin/main`. It runs core work with core test lanes, extension work with extension test lanes, test-only work with test typecheck/tests only, expands public Plugin SDK or plugin-contract changes to one extension validation pass, and keeps release metadata-only version bumps on targeted version/config/root-dependency checks.
- `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.
- `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.
- Full, extension, and include-pattern shard runs update local timing data in `.artifacts/vitest-shard-timings.json`; later whole-config runs use those timings to balance slow and fast shards. Include-pattern CI shards append the shard name to the timing key, which keeps filtered shard timings visible without replacing whole-config timing data. Set `OPENCLAW_TEST_PROJECTS_TIMINGS=0` to ignore the local timing artifact.
- Selected `plugin-sdk` and `commands` test files now route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes.
- Selected `plugin-sdk` and `commands` helper source files also map `pnpm test:changed` to explicit sibling tests in those light lanes, so small helper edits avoid rerunning the heavy runtime-backed suites.
- Source files with sibling tests map to that sibling before falling back to wider directory globs. Helper edits under `test/helpers/channels` and `test/helpers/plugins` use a local import graph to run importing tests instead of broad-running every shard when the dependency path is precise.
- `auto-reply` now also splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests.
- Base Vitest config now defaults to `pool: "threads"` and `isolate: false`, with the shared non-isolated runner enabled across the repo configs.
- `pnpm test:channels` runs `vitest.channels.config.ts`.
@@ -32,7 +34,7 @@ title: "Tests"
- Gateway integration: opt-in via `OPENCLAW_TEST_INCLUDE_GATEWAY=1 pnpm test` or `pnpm test:gateway`.
- `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `threads` + `isolate: false` with adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=<n>` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs.
- `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip.
- `pnpm test:docker:all`: Builds the shared live-test image and Docker E2E image once, then runs the Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. `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. 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 are written under `.artifacts/docker-tests/<run-id>/`.
- `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, 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. 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.
- 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.
- `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 (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.

View File

@@ -1,177 +1,176 @@
---
summary: Per-agent sandbox + tool restrictions, precedence, and examples
title: Multi-agent sandbox & tools
read_when: “You want per-agent sandboxing or per-agent tool allow/deny policies in a multi-agent gateway.”
summary: "Per-agent sandbox + tool restrictions, precedence, and examples"
title: "Multi-agent sandbox and tools"
sidebarTitle: "Multi-agent sandbox and tools"
read_when: "You want per-agent sandboxing or per-agent tool allow/deny policies in a multi-agent gateway."
status: active
---
# Multi-Agent Sandbox & Tools Configuration
Each agent in a multi-agent setup can override the global sandbox and tool policy. This page covers per-agent configuration, precedence rules, and examples.
Each agent in a multi-agent setup can override the global sandbox and tool
policy. This page covers per-agent configuration, precedence rules, and
examples.
<CardGroup cols={3}>
<Card title="Sandboxing" href="/gateway/sandboxing">
Backends and modes — full sandbox reference.
</Card>
<Card title="Sandbox vs tool policy vs elevated" href="/gateway/sandbox-vs-tool-policy-vs-elevated">
Debug "why is this blocked?"
</Card>
<Card title="Elevated mode" href="/tools/elevated">
Elevated exec for trusted senders.
</Card>
</CardGroup>
- **Sandbox backends and modes**: see [Sandboxing](/gateway/sandboxing).
- **Debugging blocked tools**: see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) and `openclaw sandbox explain`.
- **Elevated exec**: see [Elevated Mode](/tools/elevated).
Auth is per-agent: each agent reads from its own `agentDir` auth store at
`~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
Credentials are **not** shared between agents. Never reuse `agentDir` across agents.
If you want to share creds, copy `auth-profiles.json` into the other agent's `agentDir`.
<Warning>
Auth is per-agent: each agent reads from its own `agentDir` auth store at `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`. Credentials are **not** shared between agents. Never reuse `agentDir` across agents. If you want to share creds, copy `auth-profiles.json` into the other agent's `agentDir`.
</Warning>
---
## Configuration Examples
## Configuration examples
### Example 1: Personal + Restricted Family Agent
```json
{
"agents": {
"list": [
{
"id": "main",
"default": true,
"name": "Personal Assistant",
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
},
{
"id": "family",
"name": "Family Bot",
"workspace": "~/.openclaw/workspace-family",
"sandbox": {
"mode": "all",
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process", "browser"]
}
}
]
},
"bindings": [
<AccordionGroup>
<Accordion title="Example 1: Personal + restricted family agent">
```json
{
"agentId": "family",
"match": {
"provider": "whatsapp",
"accountId": "*",
"peer": {
"kind": "group",
"id": "120363424282127706@g.us"
"agents": {
"list": [
{
"id": "main",
"default": true,
"name": "Personal Assistant",
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
},
{
"id": "family",
"name": "Family Bot",
"workspace": "~/.openclaw/workspace-family",
"sandbox": {
"mode": "all",
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process", "browser"]
}
}
]
},
"bindings": [
{
"agentId": "family",
"match": {
"provider": "whatsapp",
"accountId": "*",
"peer": {
"kind": "group",
"id": "120363424282127706@g.us"
}
}
}
]
}
```
**Result:**
- `main` agent: runs on host, full tool access.
- `family` agent: runs in Docker (one container per agent), only `read` tool.
</Accordion>
<Accordion title="Example 2: Work agent with shared sandbox">
```json
{
"agents": {
"list": [
{
"id": "personal",
"workspace": "~/.openclaw/workspace-personal",
"sandbox": { "mode": "off" }
},
{
"id": "work",
"workspace": "~/.openclaw/workspace-work",
"sandbox": {
"mode": "all",
"scope": "shared",
"workspaceRoot": "/tmp/work-sandboxes"
},
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": ["browser", "gateway", "discord"]
}
}
]
}
}
]
}
```
```
</Accordion>
<Accordion title="Example 2b: Global coding profile + messaging-only agent">
```json
{
"tools": { "profile": "coding" },
"agents": {
"list": [
{
"id": "support",
"tools": { "profile": "messaging", "allow": ["slack"] }
}
]
}
}
```
**Result:**
**Result:**
- `main` agent: Runs on host, full tool access
- `family` agent: Runs in Docker (one container per agent), only `read` tool
- default agents get coding tools.
- `support` agent is messaging-only (+ Slack tool).
---
### Example 2: Work Agent with Shared Sandbox
```json
{
"agents": {
"list": [
{
"id": "personal",
"workspace": "~/.openclaw/workspace-personal",
"sandbox": { "mode": "off" }
},
{
"id": "work",
"workspace": "~/.openclaw/workspace-work",
"sandbox": {
"mode": "all",
"scope": "shared",
"workspaceRoot": "/tmp/work-sandboxes"
</Accordion>
<Accordion title="Example 3: Different sandbox modes per agent">
```json
{
"agents": {
"defaults": {
"sandbox": {
"mode": "non-main",
"scope": "session"
}
},
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": ["browser", "gateway", "discord"]
}
"list": [
{
"id": "main",
"workspace": "~/.openclaw/workspace",
"sandbox": {
"mode": "off"
}
},
{
"id": "public",
"workspace": "~/.openclaw/workspace-public",
"sandbox": {
"mode": "all",
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch"]
}
}
]
}
]
}
}
```
}
```
</Accordion>
</AccordionGroup>
---
### Example 2b: Global coding profile + messaging-only agent
```json
{
"tools": { "profile": "coding" },
"agents": {
"list": [
{
"id": "support",
"tools": { "profile": "messaging", "allow": ["slack"] }
}
]
}
}
```
**Result:**
- default agents get coding tools
- `support` agent is messaging-only (+ Slack tool)
---
### Example 3: Different Sandbox Modes per Agent
```json
{
"agents": {
"defaults": {
"sandbox": {
"mode": "non-main", // Global default
"scope": "session"
}
},
"list": [
{
"id": "main",
"workspace": "~/.openclaw/workspace",
"sandbox": {
"mode": "off" // Override: main never sandboxed
}
},
{
"id": "public",
"workspace": "~/.openclaw/workspace-public",
"sandbox": {
"mode": "all", // Override: public always sandboxed
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch"]
}
}
]
}
}
```
---
## Configuration Precedence
## Configuration precedence
When both global (`agents.defaults.*`) and agent-specific (`agents.list[].*`) configs exist:
### Sandbox Config
### Sandbox config
Agent-specific settings override global:
@@ -185,139 +184,154 @@ agents.list[].sandbox.browser.* > agents.defaults.sandbox.browser.*
agents.list[].sandbox.prune.* > agents.defaults.sandbox.prune.*
```
**Notes:**
<Note>
`agents.list[].sandbox.{docker,browser,prune}.*` overrides `agents.defaults.sandbox.{docker,browser,prune}.*` for that agent (ignored when sandbox scope resolves to `"shared"`).
</Note>
- `agents.list[].sandbox.{docker,browser,prune}.*` overrides `agents.defaults.sandbox.{docker,browser,prune}.*` for that agent (ignored when sandbox scope resolves to `"shared"`).
### Tool Restrictions
### Tool restrictions
The filtering order is:
1. **Tool profile** (`tools.profile` or `agents.list[].tools.profile`)
2. **Provider tool profile** (`tools.byProvider[provider].profile` or `agents.list[].tools.byProvider[provider].profile`)
3. **Global tool policy** (`tools.allow` / `tools.deny`)
4. **Provider tool policy** (`tools.byProvider[provider].allow/deny`)
5. **Agent-specific tool policy** (`agents.list[].tools.allow/deny`)
6. **Agent provider policy** (`agents.list[].tools.byProvider[provider].allow/deny`)
7. **Sandbox tool policy** (`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`)
8. **Subagent tool policy** (`tools.subagents.tools`, if applicable)
<Steps>
<Step title="Tool profile">
`tools.profile` or `agents.list[].tools.profile`.
</Step>
<Step title="Provider tool profile">
`tools.byProvider[provider].profile` or `agents.list[].tools.byProvider[provider].profile`.
</Step>
<Step title="Global tool policy">
`tools.allow` / `tools.deny`.
</Step>
<Step title="Provider tool policy">
`tools.byProvider[provider].allow/deny`.
</Step>
<Step title="Agent-specific tool policy">
`agents.list[].tools.allow/deny`.
</Step>
<Step title="Agent provider policy">
`agents.list[].tools.byProvider[provider].allow/deny`.
</Step>
<Step title="Sandbox tool policy">
`tools.sandbox.tools` or `agents.list[].tools.sandbox.tools`.
</Step>
<Step title="Subagent tool policy">
`tools.subagents.tools`, if applicable.
</Step>
</Steps>
Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent.
If `agents.list[].tools.profile` is set, it overrides `tools.profile` for that agent.
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.4`).
If any explicit allowlist in that chain leaves the run with no callable tools,
OpenClaw stops before submitting the prompt to the model. This is intentional:
an agent configured with a missing tool such as
`agents.list[].tools.allow: ["query_db"]` should fail loudly until the plugin
that registers `query_db` is enabled, not continue as a text-only agent.
<AccordionGroup>
<Accordion title="Precedence rules">
- Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
- If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent.
- If `agents.list[].tools.profile` is set, it overrides `tools.profile` for that agent.
- Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.4`).
</Accordion>
<Accordion title="Empty allowlist behavior">
If any explicit allowlist in that chain leaves the run with no callable tools, OpenClaw stops before submitting the prompt to the model. This is intentional: an agent configured with a missing tool such as `agents.list[].tools.allow: ["query_db"]` should fail loudly until the plugin that registers `query_db` is enabled, not continue as a text-only agent.
</Accordion>
</AccordionGroup>
Tool policies support `group:*` shorthands that expand to multiple tools. See [Tool groups](/gateway/sandbox-vs-tool-policy-vs-elevated#tool-groups-shorthands) for the full list.
Per-agent elevated overrides (`agents.list[].tools.elevated`) can further restrict elevated exec for specific agents. See [Elevated Mode](/tools/elevated) for details.
Per-agent elevated overrides (`agents.list[].tools.elevated`) can further restrict elevated exec for specific agents. See [Elevated mode](/tools/elevated) for details.
---
## Migration from Single Agent
## Migration from single agent
**Before (single agent):**
```json
{
"agents": {
"defaults": {
"workspace": "~/.openclaw/workspace",
"sandbox": {
"mode": "non-main"
}
}
},
"tools": {
"sandbox": {
<Tabs>
<Tab title="Before (single agent)">
```json
{
"agents": {
"defaults": {
"workspace": "~/.openclaw/workspace",
"sandbox": {
"mode": "non-main"
}
}
},
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": []
"sandbox": {
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": []
}
}
}
}
}
}
```
**After (multi-agent with different profiles):**
```json
{
"agents": {
"list": [
{
"id": "main",
"default": true,
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
```
</Tab>
<Tab title="After (multi-agent)">
```json
{
"agents": {
"list": [
{
"id": "main",
"default": true,
"workspace": "~/.openclaw/workspace",
"sandbox": { "mode": "off" }
}
]
}
]
}
}
```
}
```
</Tab>
</Tabs>
<Note>
Legacy `agent.*` configs are migrated by `openclaw doctor`; prefer `agents.defaults` + `agents.list` going forward.
</Note>
---
## Tool Restriction Examples
## Tool restriction examples
### Read-only Agent
<Tabs>
<Tab title="Read-only agent">
```json
{
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process"]
}
}
```
</Tab>
<Tab title="Safe execution (no file modifications)">
```json
{
"tools": {
"allow": ["read", "exec", "process"],
"deny": ["write", "edit", "apply_patch", "browser", "gateway"]
}
}
```
</Tab>
<Tab title="Communication-only">
```json
{
"tools": {
"sessions": { "visibility": "tree" },
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
"deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
}
}
```
```json
{
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process"]
}
}
```
`sessions_history` in this profile still returns a bounded, sanitized recall view rather than a raw transcript dump. Assistant recall strips thinking tags, `<relevant-memories>` scaffolding, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks), downgraded tool-call scaffolding, leaked ASCII/full-width model control tokens, and malformed MiniMax tool-call XML before redaction/truncation.
### Safe Execution Agent (no file modifications)
```json
{
"tools": {
"allow": ["read", "exec", "process"],
"deny": ["write", "edit", "apply_patch", "browser", "gateway"]
}
}
```
### Communication-only Agent
```json
{
"tools": {
"sessions": { "visibility": "tree" },
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
"deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
}
}
```
`sessions_history` in this profile still returns a bounded, sanitized recall
view rather than a raw transcript dump. Assistant recall strips thinking tags,
`<relevant-memories>` scaffolding, plain-text tool-call XML payloads
(including `<tool_call>...</tool_call>`,
`<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`,
`<function_calls>...</function_calls>`, and truncated tool-call blocks),
downgraded tool-call scaffolding, leaked ASCII/full-width model control
tokens, and malformed MiniMax tool-call XML before redaction/truncation.
</Tab>
</Tabs>
---
## Common Pitfall: "non-main"
## Common pitfall: "non-main"
`agents.defaults.sandbox.mode: "non-main"` is based on `session.mainKey` (default `"main"`),
not the agent id. Group/channel sessions always get their own keys, so they
are treated as non-main and will be sandboxed. If you want an agent to never
sandbox, set `agents.list[].sandbox.mode: "off"`.
<Warning>
`agents.defaults.sandbox.mode: "non-main"` is based on `session.mainKey` (default `"main"`), not the agent id. Group/channel sessions always get their own keys, so they are treated as non-main and will be sandboxed. If you want an agent to never sandbox, set `agents.list[].sandbox.mode: "off"`.
</Warning>
---
@@ -325,55 +339,55 @@ sandbox, set `agents.list[].sandbox.mode: "off"`.
After configuring multi-agent sandbox and tools:
1. **Check agent resolution:**
```exec
openclaw agents list --bindings
```
2. **Verify sandbox containers:**
```exec
docker ps --filter "name=openclaw-sbx-"
```
3. **Test tool restrictions:**
- Send a message requiring restricted tools
- Verify the agent cannot use denied tools
4. **Monitor logs:**
```exec
tail -f "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/logs/gateway.log" | grep -E "routing|sandbox|tools"
```
<Steps>
<Step title="Check agent resolution">
```bash
openclaw agents list --bindings
```
</Step>
<Step title="Verify sandbox containers">
```bash
docker ps --filter "name=openclaw-sbx-"
```
</Step>
<Step title="Test tool restrictions">
- Send a message requiring restricted tools.
- Verify the agent cannot use denied tools.
</Step>
<Step title="Monitor logs">
```bash
tail -f "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}/logs/gateway.log" | grep -E "routing|sandbox|tools"
```
</Step>
</Steps>
---
## Troubleshooting
### Agent not sandboxed despite `mode: "all"`
- Check if there's a global `agents.defaults.sandbox.mode` that overrides it
- Agent-specific config takes precedence, so set `agents.list[].sandbox.mode: "all"`
### Tools still available despite deny list
- Check tool filtering order: global → agent → sandbox → subagent
- Each level can only further restrict, not grant back
- Verify with logs: `[tools] filtering tools for agent:${agentId}`
### Container not isolated per agent
- Set `scope: "agent"` in agent-specific sandbox config
- Default is `"session"` which creates one container per session
<AccordionGroup>
<Accordion title="Agent not sandboxed despite `mode: 'all'`">
- Check if there's a global `agents.defaults.sandbox.mode` that overrides it.
- Agent-specific config takes precedence, so set `agents.list[].sandbox.mode: "all"`.
</Accordion>
<Accordion title="Tools still available despite deny list">
- Check tool filtering order: global → agent → sandbox → subagent.
- Each level can only further restrict, not grant back.
- Verify with logs: `[tools] filtering tools for agent:${agentId}`.
</Accordion>
<Accordion title="Container not isolated per agent">
- Set `scope: "agent"` in agent-specific sandbox config.
- Default is `"session"` which creates one container per session.
</Accordion>
</AccordionGroup>
---
## Related
- [Sandboxing](/gateway/sandboxing) -- full sandbox reference (modes, scopes, backends, images)
- [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) -- debugging "why is this blocked?"
- [Elevated Mode](/tools/elevated)
- [Multi-Agent Routing](/concepts/multi-agent)
- [Sandbox Configuration](/gateway/config-agents#agentsdefaultssandbox)
- [Session Management](/concepts/session)
- [Elevated mode](/tools/elevated)
- [Multi-agent routing](/concepts/multi-agent)
- [Sandbox configuration](/gateway/config-agents#agentsdefaultssandbox)
- [Sandbox vs tool policy vs elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) — debugging "why is this blocked?"
- [Sandboxing](/gateway/sandboxing) — full sandbox reference (modes, scopes, backends, images)
- [Session management](/concepts/session)

View File

@@ -70,6 +70,9 @@ Gateway startup runtime-dependency repair.
Explicit disablement still wins: `plugins.entries.<id>.enabled: false`,
`plugins.deny`, `plugins.enabled: false`, and `channels.<id>.enabled: false`
prevent automatic bundled runtime-dependency repair for that plugin/channel.
A non-empty `plugins.allow` also bounds default-enabled bundled runtime-dependency
repair; explicit bundled channel enablement (`channels.<id>.enabled: true`) can
still repair that channel's plugin dependencies.
External plugins and custom load paths must still be installed through
`openclaw plugins install`.
@@ -87,6 +90,28 @@ Both show up under `openclaw plugins list`. See [Plugin Bundles](/plugins/bundle
If you are writing a native plugin, start with [Building Plugins](/plugins/building-plugins)
and the [Plugin SDK Overview](/plugins/sdk-overview).
## Package Entrypoints
Native plugin npm packages must declare `openclaw.extensions` in `package.json`.
Each entry must stay inside the package directory and resolve to a readable
runtime file, or to a TypeScript source file with an inferred built JavaScript
peer such as `src/index.ts` to `dist/index.js`.
Use `openclaw.runtimeExtensions` when published runtime files do not live at the
same paths as the source entries. When present, `runtimeExtensions` must contain
exactly one entry for every `extensions` entry. Mismatched lists fail install and
plugin discovery rather than silently falling back to source paths.
```json
{
"name": "@acme/openclaw-plugin",
"openclaw": {
"extensions": ["./src/index.ts"],
"runtimeExtensions": ["./dist/index.js"]
}
}
```
## Official plugins
### Installable (npm)
@@ -199,6 +224,16 @@ OpenClaw scans for plugins in this order (first match wins):
</Step>
</Steps>
Packaged installs and Docker images normally resolve bundled plugins from the
compiled `dist/extensions` tree. If a bundled plugin source directory is
bind-mounted over the matching packaged source path, for example
`/app/extensions/synology-chat`, OpenClaw treats that mounted source directory
as a bundled source overlay and discovers it before the packaged
`/app/dist/extensions/synology-chat` bundle. This keeps maintainer container
loops working without switching every bundled plugin back to TypeScript source.
Set `OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS=1` to force packaged dist bundles
even when source overlay mounts are present.
### Enablement rules
- `plugins.enabled: false` disables all plugins
@@ -337,8 +372,9 @@ plugins. It is not supported with `--link`, which reuses the source path instead
of copying over a managed install target.
When `plugins.allow` is already set, `openclaw plugins install` adds the
installed plugin id to that allowlist before enabling it, so installs are
immediately loadable after restart.
installed plugin id to that allowlist before enabling it. If the same plugin id
is present in `plugins.deny`, install removes that stale deny entry so the
explicit install is immediately loadable after restart.
OpenClaw keeps a persisted local plugin registry as the cold read model for
plugin inventory, contribution ownership, and startup planning. Install, update,

View File

@@ -4,29 +4,35 @@ read_when:
- Using or configuring chat commands
- Debugging command routing or permissions
title: "Slash commands"
sidebarTitle: "Slash commands"
---
Commands are handled by the Gateway. Most commands must be sent as a **standalone** message that starts with `/`.
The host-only bash chat command uses `! <cmd>` (with `/bash <cmd>` as an alias).
Commands are handled by the Gateway. Most commands must be sent as a **standalone** message that starts with `/`. The host-only bash chat command uses `! <cmd>` (with `/bash <cmd>` as an alias).
When a conversation or thread is bound to an ACP session, normal follow-up text
routes to that ACP harness. Gateway management commands still stay local:
`/acp ...` always reaches the OpenClaw ACP command handler, and `/status` plus
`/unfocus` stay local whenever command handling is enabled for the surface.
When a conversation or thread is bound to an ACP session, normal follow-up text routes to that ACP harness. Gateway management commands still stay local: `/acp ...` always reaches the OpenClaw ACP command handler, and `/status` plus `/unfocus` stay local whenever command handling is enabled for the surface.
There are two related systems:
- **Commands**: standalone `/...` messages.
- **Directives**: `/think`, `/fast`, `/verbose`, `/trace`, `/reasoning`, `/elevated`, `/exec`, `/model`, `/queue`.
- Directives are stripped from the message before the model sees it.
- In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings.
- In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement.
- Directives are only applied for **authorized senders**. If `commands.allowFrom` is set, it is the only
allowlist used; otherwise authorization comes from channel allowlists/pairing plus `commands.useAccessGroups`.
Unauthorized senders see directives treated as plain text.
<AccordionGroup>
<Accordion title="Commands">
Standalone `/...` messages.
</Accordion>
<Accordion title="Directives">
`/think`, `/fast`, `/verbose`, `/trace`, `/reasoning`, `/elevated`, `/exec`, `/model`, `/queue`.
There are also a few **inline shortcuts** (allowlisted/authorized senders only): `/help`, `/commands`, `/status`, `/whoami` (`/id`).
They run immediately, are stripped before the model sees the message, and the remaining text continues through the normal flow.
- Directives are stripped from the message before the model sees it.
- In normal chat messages (not directive-only), they are treated as "inline hints" and do **not** persist session settings.
- In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement.
- Directives are only applied for **authorized senders**. If `commands.allowFrom` is set, it is the only allowlist used; otherwise authorization comes from channel allowlists/pairing plus `commands.useAccessGroups`. Unauthorized senders see directives treated as plain text.
</Accordion>
<Accordion title="Inline shortcuts">
Allowlisted/authorized senders only: `/help`, `/commands`, `/status`, `/whoami` (`/id`).
They run immediately, are stripped before the model sees the message, and the remaining text continues through the normal flow.
</Accordion>
</AccordionGroup>
## Config
@@ -55,30 +61,54 @@ They run immediately, are stripped before the model sees the message, and the re
}
```
- `commands.text` (default `true`) enables parsing `/...` in chat messages.
- On surfaces without native commands (WhatsApp/WebChat/Signal/iMessage/Google Chat/Microsoft Teams), text commands still work even if you set this to `false`.
- `commands.native` (default `"auto"`) registers native commands.
- Auto: on for Discord/Telegram; off for Slack (until you add slash commands); ignored for providers without native support.
- Set `channels.discord.commands.native`, `channels.telegram.commands.native`, or `channels.slack.commands.native` to override per provider (bool or `"auto"`).
- `false` clears previously registered commands on Discord/Telegram at startup. Slack commands are managed in the Slack app and are not removed automatically.
- `commands.nativeSkills` (default `"auto"`) registers **skill** commands natively when supported.
- Auto: on for Discord/Telegram; off for Slack (Slack requires creating a slash command per skill).
- Set `channels.discord.commands.nativeSkills`, `channels.telegram.commands.nativeSkills`, or `channels.slack.commands.nativeSkills` to override per provider (bool or `"auto"`).
- `commands.bash` (default `false`) enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists).
- `commands.bashForegroundMs` (default `2000`) controls how long bash waits before switching to background mode (`0` backgrounds immediately).
- `commands.config` (default `false`) enables `/config` (reads/writes `openclaw.json`).
- `commands.mcp` (default `false`) enables `/mcp` (reads/writes OpenClaw-managed MCP config under `mcp.servers`).
- `commands.plugins` (default `false`) enables `/plugins` (plugin discovery/status plus install + enable/disable controls).
- `commands.debug` (default `false`) enables `/debug` (runtime-only overrides).
- `commands.restart` (default `true`) enables `/restart` plus gateway restart tool actions.
- `commands.ownerAllowFrom` (optional) sets the explicit owner allowlist for owner-only command/tool surfaces. This is separate from `commands.allowFrom`.
- Per-channel `channels.<channel>.commands.enforceOwnerForCommands` (optional, default `false`) makes owner-only commands require **owner identity** to run on that surface. When `true`, the sender must either match a resolved owner candidate (for example an entry in `commands.ownerAllowFrom` or provider-native owner metadata) or hold internal `operator.admin` scope on an internal message channel. A wildcard entry in channel `allowFrom`, or an empty/unresolved owner-candidate list, is **not** sufficient — owner-only commands fail closed on that channel. Leave this off if you want owner-only commands gated only by `ownerAllowFrom` and the standard command allowlists.
- `commands.ownerDisplay` controls how owner ids appear in the system prompt: `raw` or `hash`.
- `commands.ownerDisplaySecret` optionally sets the HMAC secret used when `commands.ownerDisplay="hash"`.
- `commands.allowFrom` (optional) sets a per-provider allowlist for command authorization. When configured, it is the
only authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups`
are ignored). Use `"*"` for a global default; provider-specific keys override it.
- `commands.useAccessGroups` (default `true`) enforces allowlists/policies for commands when `commands.allowFrom` is not set.
<ParamField path="commands.text" type="boolean" default="true">
Enables parsing `/...` in chat messages. On surfaces without native commands (WhatsApp/WebChat/Signal/iMessage/Google Chat/Microsoft Teams), text commands still work even if you set this to `false`.
</ParamField>
<ParamField path="commands.native" type='boolean | "auto"' default='"auto"'>
Registers native commands. Auto: on for Discord/Telegram; off for Slack (until you add slash commands); ignored for providers without native support. Set `channels.discord.commands.native`, `channels.telegram.commands.native`, or `channels.slack.commands.native` to override per provider (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup. Slack commands are managed in the Slack app and are not removed automatically.
</ParamField>
<ParamField path="commands.nativeSkills" type='boolean | "auto"' default='"auto"'>
Registers **skill** commands natively when supported. Auto: on for Discord/Telegram; off for Slack (Slack requires creating a slash command per skill). Set `channels.discord.commands.nativeSkills`, `channels.telegram.commands.nativeSkills`, or `channels.slack.commands.nativeSkills` to override per provider (bool or `"auto"`).
</ParamField>
<ParamField path="commands.bash" type="boolean" default="false">
Enables `! <cmd>` to run host shell commands (`/bash <cmd>` is an alias; requires `tools.elevated` allowlists).
</ParamField>
<ParamField path="commands.bashForegroundMs" type="number" default="2000">
Controls how long bash waits before switching to background mode (`0` backgrounds immediately).
</ParamField>
<ParamField path="commands.config" type="boolean" default="false">
Enables `/config` (reads/writes `openclaw.json`).
</ParamField>
<ParamField path="commands.mcp" type="boolean" default="false">
Enables `/mcp` (reads/writes OpenClaw-managed MCP config under `mcp.servers`).
</ParamField>
<ParamField path="commands.plugins" type="boolean" default="false">
Enables `/plugins` (plugin discovery/status plus install + enable/disable controls).
</ParamField>
<ParamField path="commands.debug" type="boolean" default="false">
Enables `/debug` (runtime-only overrides).
</ParamField>
<ParamField path="commands.restart" type="boolean" default="true">
Enables `/restart` plus gateway restart tool actions.
</ParamField>
<ParamField path="commands.ownerAllowFrom" type="string[]">
Sets the explicit owner allowlist for owner-only command/tool surfaces. Separate from `commands.allowFrom`.
</ParamField>
<ParamField path="channels.<channel>.commands.enforceOwnerForCommands" type="boolean" default="false">
Per-channel: makes owner-only commands require **owner identity** to run on that surface. When `true`, the sender must either match a resolved owner candidate (for example an entry in `commands.ownerAllowFrom` or provider-native owner metadata) or hold internal `operator.admin` scope on an internal message channel. A wildcard entry in channel `allowFrom`, or an empty/unresolved owner-candidate list, is **not** sufficient — owner-only commands fail closed on that channel. Leave this off if you want owner-only commands gated only by `ownerAllowFrom` and the standard command allowlists.
</ParamField>
<ParamField path="commands.ownerDisplay" type='"raw" | "hash"'>
Controls how owner ids appear in the system prompt.
</ParamField>
<ParamField path="commands.ownerDisplaySecret" type="string">
Optionally sets the HMAC secret used when `commands.ownerDisplay="hash"`.
</ParamField>
<ParamField path="commands.allowFrom" type="object">
Per-provider allowlist for command authorization. When configured, it is the only authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups` are ignored). Use `"*"` for a global default; provider-specific keys override it.
</ParamField>
<ParamField path="commands.useAccessGroups" type="boolean" default="true">
Enforces allowlists/policies for commands when `commands.allowFrom` is not set.
</ParamField>
## Command list
@@ -91,56 +121,70 @@ Current source-of-truth:
### Core built-in commands
Built-in commands available today:
- `/new [model]` starts a new session; `/reset` is the reset alias.
- `/reset soft [message]` keeps the current transcript, drops reused CLI backend session ids, and reruns startup/system-prompt loading in-place.
- `/compact [instructions]` compacts the session context. See [/concepts/compaction](/concepts/compaction).
- `/stop` aborts the current run.
- `/session idle <duration|off>` and `/session max-age <duration|off>` manage thread-binding expiry.
- `/think <level>` sets the thinking level. Options come from the active model's provider profile; common levels are `off`, `minimal`, `low`, `medium`, and `high`, with custom levels such as `xhigh`, `adaptive`, `max`, or binary `on` only where supported. Aliases: `/thinking`, `/t`.
- `/verbose on|off|full` toggles verbose output. Alias: `/v`.
- `/trace on|off` toggles plugin trace output for the current session.
- `/fast [status|on|off]` shows or sets fast mode.
- `/reasoning [on|off|stream]` toggles reasoning visibility. Alias: `/reason`.
- `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`.
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` shows or sets exec defaults.
- `/model [name|#|status]` shows or sets the model.
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists providers or models for a provider.
- `/queue <mode>` manages queue behavior (`steer`, `interrupt`, `followup`, `collect`, `steer-backlog`) plus options like `debounce:2s cap:25 drop:summarize`.
- `/help` shows the short help summary.
- `/commands` shows the generated command catalog.
- `/tools [compact|verbose]` shows what the current agent can use right now.
- `/status` shows execution/runtime status, including `Execution`/`Runtime` labels and provider usage/quota when available.
- `/crestodian <request>` runs the Crestodian setup and repair helper from an owner DM.
- `/tasks` lists active/recent background tasks for the current session.
- `/context [list|detail|json]` explains how context is assembled.
- `/export-session [path]` exports the current session to HTML. Alias: `/export`.
- `/export-trajectory [path]` exports a JSONL [trajectory bundle](/tools/trajectory) for the current session. Alias: `/trajectory`.
- `/whoami` shows your sender id. Alias: `/id`.
- `/skill <name> [input]` runs a skill by name.
- `/allowlist [list|add|remove] ...` manages allowlist entries. Text-only.
- `/approve <id> <decision>` resolves exec approval prompts.
- `/btw <question>` asks a side question without changing future session context. See [/tools/btw](/tools/btw).
- `/subagents list|kill|log|info|send|steer|spawn` manages sub-agent runs for the current session.
- `/acp spawn|cancel|steer|close|sessions|status|set-mode|set|cwd|permissions|timeout|model|reset-options|doctor|install|help` manages ACP sessions and runtime options.
- `/focus <target>` binds the current Discord thread or Telegram topic/conversation to a session target.
- `/unfocus` removes the current binding.
- `/agents` lists thread-bound agents for the current session.
- `/kill <id|#|all>` aborts one or all running sub-agents.
- `/steer <id|#> <message>` sends steering to a running sub-agent. Alias: `/tell`.
- `/config show|get|set|unset` reads or writes `openclaw.json`. Owner-only. Requires `commands.config: true`.
- `/mcp show|get|set|unset` reads or writes OpenClaw-managed MCP server config under `mcp.servers`. Owner-only. Requires `commands.mcp: true`.
- `/plugins list|inspect|show|get|install|enable|disable` inspects or mutates plugin state. `/plugin` is an alias. Owner-only for writes. Requires `commands.plugins: true`.
- `/debug show|set|unset|reset` manages runtime-only config overrides. Owner-only. Requires `commands.debug: true`.
- `/usage off|tokens|full|cost` controls the per-response usage footer or prints a local cost summary.
- `/tts on|off|status|chat|latest|provider|limit|summary|audio|help` controls TTS. See [/tools/tts](/tools/tts).
- `/restart` restarts OpenClaw when enabled. Default: enabled; set `commands.restart: false` to disable it.
- `/activation mention|always` sets group activation mode.
- `/send on|off|inherit` sets send policy. Owner-only.
- `/bash <command>` runs a host shell command. Text-only. Alias: `! <command>`. Requires `commands.bash: true` plus `tools.elevated` allowlists.
- `!poll [sessionId]` checks a background bash job.
- `!stop [sessionId]` stops a background bash job.
<AccordionGroup>
<Accordion title="Sessions and runs">
- `/new [model]` starts a new session; `/reset` is the reset alias.
- `/reset soft [message]` keeps the current transcript, drops reused CLI backend session ids, and reruns startup/system-prompt loading in-place.
- `/compact [instructions]` compacts the session context. See [Compaction](/concepts/compaction).
- `/stop` aborts the current run.
- `/session idle <duration|off>` and `/session max-age <duration|off>` manage thread-binding expiry.
- `/export-session [path]` exports the current session to HTML. Alias: `/export`.
- `/export-trajectory [path]` exports a JSONL [trajectory bundle](/tools/trajectory) for the current session. Alias: `/trajectory`.
</Accordion>
<Accordion title="Model and run controls">
- `/think <level>` sets the thinking level. Options come from the active model's provider profile; common levels are `off`, `minimal`, `low`, `medium`, and `high`, with custom levels such as `xhigh`, `adaptive`, `max`, or binary `on` only where supported. Aliases: `/thinking`, `/t`.
- `/verbose on|off|full` toggles verbose output. Alias: `/v`.
- `/trace on|off` toggles plugin trace output for the current session.
- `/fast [status|on|off]` shows or sets fast mode.
- `/reasoning [on|off|stream]` toggles reasoning visibility. Alias: `/reason`.
- `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`.
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` shows or sets exec defaults.
- `/model [name|#|status]` shows or sets the model.
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists providers or models for a provider.
- `/queue <mode>` manages queue behavior (`steer`, `interrupt`, `followup`, `collect`, `steer-backlog`) plus options like `debounce:2s cap:25 drop:summarize`.
</Accordion>
<Accordion title="Discovery and status">
- `/help` shows the short help summary.
- `/commands` shows the generated command catalog.
- `/tools [compact|verbose]` shows what the current agent can use right now.
- `/status` shows execution/runtime status, including `Execution`/`Runtime` labels and provider usage/quota when available.
- `/crestodian <request>` runs the Crestodian setup and repair helper from an owner DM.
- `/tasks` lists active/recent background tasks for the current session.
- `/context [list|detail|json]` explains how context is assembled.
- `/whoami` shows your sender id. Alias: `/id`.
- `/usage off|tokens|full|cost` controls the per-response usage footer or prints a local cost summary.
</Accordion>
<Accordion title="Skills, allowlists, approvals">
- `/skill <name> [input]` runs a skill by name.
- `/allowlist [list|add|remove] ...` manages allowlist entries. Text-only.
- `/approve <id> <decision>` resolves exec approval prompts.
- `/btw <question>` asks a side question without changing future session context. See [BTW](/tools/btw).
</Accordion>
<Accordion title="Subagents and ACP">
- `/subagents list|kill|log|info|send|steer|spawn` manages sub-agent runs for the current session.
- `/acp spawn|cancel|steer|close|sessions|status|set-mode|set|cwd|permissions|timeout|model|reset-options|doctor|install|help` manages ACP sessions and runtime options.
- `/focus <target>` binds the current Discord thread or Telegram topic/conversation to a session target.
- `/unfocus` removes the current binding.
- `/agents` lists thread-bound agents for the current session.
- `/kill <id|#|all>` aborts one or all running sub-agents.
- `/steer <id|#> <message>` sends steering to a running sub-agent. Alias: `/tell`.
</Accordion>
<Accordion title="Owner-only writes and admin">
- `/config show|get|set|unset` reads or writes `openclaw.json`. Owner-only. Requires `commands.config: true`.
- `/mcp show|get|set|unset` reads or writes OpenClaw-managed MCP server config under `mcp.servers`. Owner-only. Requires `commands.mcp: true`.
- `/plugins list|inspect|show|get|install|enable|disable` inspects or mutates plugin state. `/plugin` is an alias. Owner-only for writes. Requires `commands.plugins: true`.
- `/debug show|set|unset|reset` manages runtime-only config overrides. Owner-only. Requires `commands.debug: true`.
- `/restart` restarts OpenClaw when enabled. Default: enabled; set `commands.restart: false` to disable it.
- `/send on|off|inherit` sets send policy. Owner-only.
</Accordion>
<Accordion title="Voice, TTS, channel control">
- `/tts on|off|status|chat|latest|provider|limit|summary|audio|help` controls TTS. See [TTS](/tools/tts).
- `/activation mention|always` sets group activation mode.
- `/bash <command>` runs a host shell command. Text-only. Alias: `! <command>`. Requires `commands.bash: true` plus `tools.elevated` allowlists.
- `!poll [sessionId]` checks a background bash job.
- `!stop [sessionId]` stops a background bash job.
</Accordion>
</AccordionGroup>
### Generated dock commands
@@ -160,7 +204,7 @@ Bundled plugins can add more slash commands. Current bundled commands in this re
- `/phone status|arm <camera|screen|writes|all> [duration]|disarm` temporarily arms high-risk phone node commands.
- `/voice status|list [limit]|set <voiceId|name>` manages Talk voice config. On Discord, the native command name is `/talkvoice`.
- `/card ...` sends LINE rich card presets. See [LINE](/channels/line).
- `/codex status|models|threads|resume|compact|review|account|mcp|skills` inspects and controls the bundled Codex app-server harness. See [Codex Harness](/plugins/codex-harness).
- `/codex status|models|threads|resume|compact|review|account|mcp|skills` inspects and controls the bundled Codex app-server harness. See [Codex harness](/plugins/codex-harness).
- QQBot-only commands:
- `/bot-ping`
- `/bot-version`
@@ -176,65 +220,71 @@ User-invocable skills are also exposed as slash commands:
- skills may also appear as direct commands like `/prose` when the skill/plugin registers them.
- native skill-command registration is controlled by `commands.nativeSkills` and `channels.<provider>.commands.nativeSkills`.
Notes:
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
- `/new <model>` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body.
- For full provider usage breakdown, use `openclaw status --usage`.
- `/allowlist add|remove` requires `commands.config=true` and honors channel `configWrites`.
- In multi-account channels, config-targeted `/allowlist --account <id>` and `/config set channels.<provider>.accounts.<id>...` also honor the target account's `configWrites`.
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from OpenClaw session logs.
- `/restart` is enabled by default; set `commands.restart: false` to disable it.
- `/plugins install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
- `/plugins enable|disable` updates plugin config and may prompt for a restart.
- Discord-only native command: `/vc join|leave|status` controls voice channels (not available as text). `join` requires a guild and selected voice/stage channel. Requires `channels.discord.voice` and native commands.
- Discord thread-binding commands (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`) require effective thread bindings to be enabled (`session.threadBindings.enabled` and/or `channels.discord.threadBindings.enabled`).
- ACP command reference and runtime behavior: [ACP Agents](/tools/acp-agents).
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
- `/trace` is narrower than `/verbose`: it only reveals plugin-owned trace/debug lines and keeps normal verbose tool chatter off.
- `/fast on|off` persists a session override. Use the Sessions UI `inherit` option to clear it and fall back to config defaults.
- `/fast` is provider-specific: OpenAI/OpenAI Codex map it to `service_tier=priority` on native Responses endpoints, while direct public Anthropic requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, map it to `service_tier=auto` or `standard_only`. See [OpenAI](/providers/openai) and [Anthropic](/providers/anthropic).
- Tool failure summaries are still shown when relevant, but detailed failure text is only included when `/verbose` is `on` or `full`.
- `/reasoning`, `/verbose`, and `/trace` are risky in group settings: they may reveal internal reasoning, tool output, or plugin diagnostics you did not intend to expose. Prefer leaving them off, especially in group chats.
- `/model` persists the new session model immediately.
- If the agent is idle, the next run uses it right away.
- If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point.
- If tool activity or reply output has already started, the pending switch can stay queued until a later retry opportunity or the next user turn.
- In the local TUI, `/crestodian [request]` returns from the normal agent TUI to
Crestodian. This is separate from message-channel rescue mode and does not
grant remote config authority.
- **Fast path:** command-only messages from allowlisted senders are handled immediately (bypass queue + model).
- **Group mention gating:** command-only messages from allowlisted senders bypass mention requirements.
- **Inline shortcuts (allowlisted senders only):** certain commands also work when embedded in a normal message and are stripped before the model sees the remaining text.
- Example: `hey /status` triggers a status reply, and the remaining text continues through the normal flow.
- Currently: `/help`, `/commands`, `/status`, `/whoami` (`/id`).
- Unauthorized command-only messages are silently ignored, and inline `/...` tokens are treated as plain text.
- **Skill commands:** `user-invocable` skills are exposed as slash commands. Names are sanitized to `a-z0-9_` (max 32 chars); collisions get numeric suffixes (e.g. `_2`).
- `/skill <name> [input]` runs a skill by name (useful when native command limits prevent per-skill commands).
- By default, skill commands are forwarded to the model as a normal request.
- Skills may optionally declare `command-dispatch: tool` to route the command directly to a tool (deterministic, no model).
- Example: `/prose` (OpenProse plugin) — see [OpenProse](/prose).
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg. Dynamic choices are resolved against the target session model, so model-specific options such as `/think` levels follow that session's `/model` override.
<AccordionGroup>
<Accordion title="Argument and parser notes">
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
- `/new <model>` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body.
- For full provider usage breakdown, use `openclaw status --usage`.
- `/allowlist add|remove` requires `commands.config=true` and honors channel `configWrites`.
- In multi-account channels, config-targeted `/allowlist --account <id>` and `/config set channels.<provider>.accounts.<id>...` also honor the target account's `configWrites`.
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from OpenClaw session logs.
- `/restart` is enabled by default; set `commands.restart: false` to disable it.
- `/plugins install <spec>` accepts the same plugin specs as `openclaw plugins install`: local path/archive, npm package, or `clawhub:<pkg>`.
- `/plugins enable|disable` updates plugin config and may prompt for a restart.
</Accordion>
<Accordion title="Channel-specific behavior">
- Discord-only native command: `/vc join|leave|status` controls voice channels (not available as text). `join` requires a guild and selected voice/stage channel. Requires `channels.discord.voice` and native commands.
- Discord thread-binding commands (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`) require effective thread bindings to be enabled (`session.threadBindings.enabled` and/or `channels.discord.threadBindings.enabled`).
- ACP command reference and runtime behavior: [ACP agents](/tools/acp-agents).
</Accordion>
<Accordion title="Verbose / trace / fast / reasoning safety">
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
- `/trace` is narrower than `/verbose`: it only reveals plugin-owned trace/debug lines and keeps normal verbose tool chatter off.
- `/fast on|off` persists a session override. Use the Sessions UI `inherit` option to clear it and fall back to config defaults.
- `/fast` is provider-specific: OpenAI/OpenAI Codex map it to `service_tier=priority` on native Responses endpoints, while direct public Anthropic requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, map it to `service_tier=auto` or `standard_only`. See [OpenAI](/providers/openai) and [Anthropic](/providers/anthropic).
- Tool failure summaries are still shown when relevant, but detailed failure text is only included when `/verbose` is `on` or `full`.
- `/reasoning`, `/verbose`, and `/trace` are risky in group settings: they may reveal internal reasoning, tool output, or plugin diagnostics you did not intend to expose. Prefer leaving them off, especially in group chats.
</Accordion>
<Accordion title="Model switching">
- `/model` persists the new session model immediately.
- If the agent is idle, the next run uses it right away.
- If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point.
- If tool activity or reply output has already started, the pending switch can stay queued until a later retry opportunity or the next user turn.
- In the local TUI, `/crestodian [request]` returns from the normal agent TUI to Crestodian. This is separate from message-channel rescue mode and does not grant remote config authority.
</Accordion>
<Accordion title="Fast path and inline shortcuts">
- **Fast path:** command-only messages from allowlisted senders are handled immediately (bypass queue + model).
- **Group mention gating:** command-only messages from allowlisted senders bypass mention requirements.
- **Inline shortcuts (allowlisted senders only):** certain commands also work when embedded in a normal message and are stripped before the model sees the remaining text.
- Example: `hey /status` triggers a status reply, and the remaining text continues through the normal flow.
- Currently: `/help`, `/commands`, `/status`, `/whoami` (`/id`).
- Unauthorized command-only messages are silently ignored, and inline `/...` tokens are treated as plain text.
</Accordion>
<Accordion title="Skill commands and native arguments">
- **Skill commands:** `user-invocable` skills are exposed as slash commands. Names are sanitized to `a-z0-9_` (max 32 chars); collisions get numeric suffixes (e.g. `_2`).
- `/skill <name> [input]` runs a skill by name (useful when native command limits prevent per-skill commands).
- By default, skill commands are forwarded to the model as a normal request.
- Skills may optionally declare `command-dispatch: tool` to route the command directly to a tool (deterministic, no model).
- Example: `/prose` (OpenProse plugin) — see [OpenProse](/prose).
- **Native command arguments:** Discord uses autocomplete for dynamic options (and button menus when you omit required args). Telegram and Slack show a button menu when a command supports choices and you omit the arg. Dynamic choices are resolved against the target session model, so model-specific options such as `/think` levels follow that session's `/model` override.
</Accordion>
</AccordionGroup>
## `/tools`
`/tools` answers a runtime question, not a config question: **what this agent can use right now in
this conversation**.
`/tools` answers a runtime question, not a config question: **what this agent can use right now in this conversation**.
- Default `/tools` is compact and optimized for quick scanning.
- `/tools verbose` adds short descriptions.
- Native-command surfaces that support arguments expose the same mode switch as `compact|verbose`.
- Results are session-scoped, so changing agent, channel, thread, sender authorization, or model can
change the output.
- `/tools` includes tools that are actually reachable at runtime, including core tools, connected
plugin tools, and channel-owned tools.
- Results are session-scoped, so changing agent, channel, thread, sender authorization, or model can change the output.
- `/tools` includes tools that are actually reachable at runtime, including core tools, connected plugin tools, and channel-owned tools.
For profile and override editing, use the Control UI Tools panel or config/catalog surfaces instead
of treating `/tools` as a static catalog.
For profile and override editing, use the Control UI Tools panel or config/catalog surfaces instead of treating `/tools` as a static catalog.
## Usage surfaces (what shows where)
- **Provider usage/quota** (example: Claude 80% left) shows up in `/status` for the current model provider when usage tracking is enabled. OpenClaw normalizes provider windows to `% left`; for MiniMax, remaining-only percent fields are inverted before display, and `model_remains` responses prefer the chat-model entry plus a model-tagged plan label.
- **Provider usage/quota** (example: "Claude 80% left") shows up in `/status` for the current model provider when usage tracking is enabled. OpenClaw normalizes provider windows to `% left`; for MiniMax, remaining-only percent fields are inverted before display, and `model_remains` responses prefer the chat-model entry plus a model-tagged plan label.
- **Token/cache lines** in `/status` can fall back to the latest transcript usage entry when the live session snapshot is sparse. Existing nonzero live values still win, and transcript fallback can also recover the active runtime model label plus a larger prompt-oriented total when stored totals are missing or smaller.
- **Execution vs runtime:** `/status` reports `Execution` for the effective sandbox path and `Runtime` for who is actually running the session: `OpenClaw Pi Default`, `OpenAI Codex`, a CLI backend, or an ACP backend.
- **Per-response tokens/cost** is controlled by `/usage off|tokens|full` (appended to normal replies).
@@ -276,10 +326,9 @@ Examples:
/debug reset
```
Notes:
- Overrides apply immediately to new config reads, but do **not** write to `openclaw.json`.
- Use `/debug reset` to clear all overrides and return to the on-disk config.
<Note>
Overrides apply immediately to new config reads, but do **not** write to `openclaw.json`. Use `/debug reset` to clear all overrides and return to the on-disk config.
</Note>
## Plugin trace output
@@ -316,10 +365,9 @@ Examples:
/config unset messages.responsePrefix
```
Notes:
- Config is validated before write; invalid changes are rejected.
- `/config` updates persist across restarts.
<Note>
Config is validated before write; invalid changes are rejected. `/config` updates persist across restarts.
</Note>
## MCP updates
@@ -334,10 +382,9 @@ Examples:
/mcp unset context7
```
Notes:
- `/mcp` stores config in OpenClaw config, not Pi-owned project settings.
- Runtime adapters decide which transports are actually executable.
<Note>
`/mcp` stores config in OpenClaw config, not Pi-owned project settings. Runtime adapters decide which transports are actually executable.
</Note>
## Plugin updates
@@ -353,22 +400,30 @@ Examples:
/plugins disable context7
```
Notes:
<Note>
- `/plugins list` and `/plugins show` use real plugin discovery against the current workspace plus on-disk config.
- `/plugins enable|disable` updates plugin config only; it does not install or uninstall plugins.
- After enable/disable changes, restart the gateway to apply them.
</Note>
## Surface notes
- **Text commands** run in the normal chat session (DMs share `main`, groups have their own session).
- **Native commands** use isolated sessions:
- Discord: `agent:<agentId>:discord:slash:<userId>`
- Slack: `agent:<agentId>:slack:slash:<userId>` (prefix configurable via `channels.slack.slashCommand.sessionPrefix`)
- Telegram: `telegram:slash:<userId>` (targets the chat session via `CommandTargetSessionKey`)
- **`/stop`** targets the active chat session so it can abort the current run.
- **Slack:** `channels.slack.slashCommand` is still supported for a single `/openclaw`-style command. If you enable `commands.native`, you must create one Slack slash command per built-in command (same names as `/help`). Command argument menus for Slack are delivered as ephemeral Block Kit buttons.
- Slack native exception: register `/agentstatus` (not `/status`) because Slack reserves `/status`. Text `/status` still works in Slack messages.
<AccordionGroup>
<Accordion title="Sessions per surface">
- **Text commands** run in the normal chat session (DMs share `main`, groups have their own session).
- **Native commands** use isolated sessions:
- Discord: `agent:<agentId>:discord:slash:<userId>`
- Slack: `agent:<agentId>:slack:slash:<userId>` (prefix configurable via `channels.slack.slashCommand.sessionPrefix`)
- Telegram: `telegram:slash:<userId>` (targets the chat session via `CommandTargetSessionKey`)
- **`/stop`** targets the active chat session so it can abort the current run.
</Accordion>
<Accordion title="Slack specifics">
`channels.slack.slashCommand` is still supported for a single `/openclaw`-style command. If you enable `commands.native`, you must create one Slack slash command per built-in command (same names as `/help`). Command argument menus for Slack are delivered as ephemeral Block Kit buttons.
Slack native exception: register `/agentstatus` (not `/status`) because Slack reserves `/status`. Text `/status` still works in Slack messages.
</Accordion>
</AccordionGroup>
## BTW side questions
@@ -382,8 +437,7 @@ Unlike normal chat:
- it is not written to transcript history,
- it is delivered as a live side result instead of a normal assistant message.
That makes `/btw` useful when you want a temporary clarification while the main
task keeps going.
That makes `/btw` useful when you want a temporary clarification while the main task keeps going.
Example:
@@ -391,11 +445,10 @@ Example:
/btw what are we doing right now?
```
See [BTW Side Questions](/tools/btw) for the full behavior and client UX
details.
See [BTW Side Questions](/tools/btw) for the full behavior and client UX details.
## Related
- [Creating skills](/tools/creating-skills)
- [Skills](/tools/skills)
- [Skills config](/tools/skills-config)
- [Creating skills](/tools/creating-skills)

View File

@@ -15,7 +15,7 @@ title: "Thinking levels"
- high → “ultrathink” (max budget)
- xhigh → “ultrathink+” (GPT-5.2+ and Codex models, plus Anthropic Claude Opus 4.7 effort)
- adaptive → provider-managed adaptive thinking (supported for Claude 4.6 on Anthropic/Bedrock, Anthropic Claude Opus 4.7, and Google Gemini dynamic thinking)
- max → provider max reasoning (currently Anthropic Claude Opus 4.7)
- max → provider max reasoning (Anthropic Claude Opus 4.7; Ollama maps this to its highest native `think` effort)
- `x-high`, `x_high`, `extra-high`, `extra high`, and `extra_high` map to `xhigh`.
- `highest` maps to `high`.
- Provider notes:
@@ -26,6 +26,7 @@ title: "Thinking levels"
- Anthropic Claude Opus 4.7 does not default to adaptive thinking. Its API effort default remains provider-owned unless you explicitly set a thinking level.
- Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting.
- Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path.
- Ollama thinking-capable models expose `/think low|medium|high|max`; `max` maps to native `think: "high"` because Ollama's native API accepts `low`, `medium`, and `high` effort strings.
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
- MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format.

View File

@@ -33,7 +33,7 @@ The dashboard settings panel keeps a token for the current browser tab session a
## Device pairing (first connection)
When you connect to the Control UI from a new browser or device, the Gateway requires a **one-time pairing approval** — even if you're on the same Tailnet with `gateway.auth.allowTailscale: true`. This is a security measure to prevent unauthorized access.
When you connect to the Control UI from a new browser or device, the Gateway usually requires a **one-time pairing approval**. This is a security measure to prevent unauthorized access.
**What you'll see:** "disconnected (1008): pairing required"
@@ -58,7 +58,8 @@ Once approved, the device is remembered and won't require re-approval unless you
<Note>
- Direct local loopback browser connections (`127.0.0.1` / `localhost`) are auto-approved.
- Tailnet and LAN browser connects still require explicit approval, even when they originate from the same machine.
- Tailscale Serve can skip the pairing round trip for Control UI operator sessions when `gateway.auth.allowTailscale: true`, Tailscale identity verifies, and the browser presents its device identity.
- Direct Tailnet binds, LAN browser connects, and browser profiles without device identity still require explicit approval.
- Each browser profile generates a unique device ID, so switching browsers or clearing browser data will require re-pairing.
</Note>
@@ -133,6 +134,7 @@ The Control UI can localize itself on first load based on your browser locale. T
<AccordionGroup>
<Accordion title="Send and history semantics">
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events.
- Chat uploads accept images plus non-video files. Images keep the native image path; other files are stored as managed media and shown in history as attachment links.
- Re-sending with the same `idempotencyKey` returns `{ status: "in_flight" }` while running, and `{ status: "ok" }` after completion.
- `chat.history` responses are size-bounded for UI safety. When transcript entries are too large, Gateway may truncate long text fields, omit heavy metadata blocks, and replace oversized messages with a placeholder (`[chat.history omitted: message too large]`).
- Assistant/generated images are persisted as managed media references and served back through authenticated Gateway media URLs, so reloads do not depend on raw base64 image payloads staying in the chat history response.
@@ -237,7 +239,7 @@ Absolute external `http(s)` embed URLs stay blocked by default. If you intention
- `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
By default, Control UI/WebSocket Serve requests can authenticate via Tailscale identity headers (`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. OpenClaw verifies the identity by resolving the `x-forwarded-for` address with `tailscale whois` and matching it to the header, and only accepts these when the request hits loopback with Tailscale's `x-forwarded-*` headers. Set `gateway.auth.allowTailscale: false` if you want to require explicit shared-secret credentials even for Serve traffic. Then use `gateway.auth.mode: "token"` or `"password"`.
By default, Control UI/WebSocket Serve requests can authenticate via Tailscale identity headers (`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. OpenClaw verifies the identity by resolving the `x-forwarded-for` address with `tailscale whois` and matching it to the header, and only accepts these when the request hits loopback with Tailscale's `x-forwarded-*` headers. For Control UI operator sessions with browser device identity, this verified Serve path also skips the device-pairing round trip; device-less browsers and node-role connections still follow the normal device checks. Set `gateway.auth.allowTailscale: false` if you want to require explicit shared-secret credentials even for Serve traffic. Then use `gateway.auth.mode: "token"` or `"password"`.
For that async Serve identity path, failed auth attempts for the same client IP and auth scope are serialized before rate-limit writes. Concurrent bad retries from the same browser can therefore show `retry later` on the second request instead of two plain mismatches racing in parallel.

View File

@@ -33,6 +33,7 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
leaked ASCII/full-width model control tokens are stripped from visible text,
and assistant entries whose whole visible text is only the exact silent
token `NO_REPLY` / `no_reply` are omitted.
- Reasoning-flagged reply payloads (`isReasoning: true`) are excluded from WebChat assistant content, transcript replay text, and audio content blocks, so thinking-only payloads do not surface as visible assistant messages or playable audio.
- `chat.inject` appends an assistant note directly to the transcript and broadcasts it to the UI (no agent run).
- Aborted runs can keep partial assistant output visible in the UI.
- Gateway persists aborted partial assistant text into transcript history when buffered output exists, and marks those entries with abort metadata.

View File

@@ -2,6 +2,7 @@
"id": "anthropic",
"enabledByDefault": true,
"providers": ["anthropic"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"modelSupport": {
"modelPrefixes": ["claude-"]
},

View File

@@ -0,0 +1,35 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { readClaudeCliCredentialsForRuntime } from "./cli-auth-seam.js";
const CLAUDE_CLI_BACKEND_ID = "claude-cli";
function resolveClaudeCliSyntheticAuth() {
const credential = readClaudeCliCredentialsForRuntime();
if (!credential) {
return undefined;
}
return credential.type === "oauth"
? {
apiKey: credential.access,
source: "Claude CLI native auth",
mode: "oauth" as const,
expiresAt: credential.expires,
}
: {
apiKey: credential.token,
source: "Claude CLI native auth",
mode: "token" as const,
expiresAt: credential.expires,
};
}
export const anthropicProviderDiscovery: ProviderPlugin = {
id: CLAUDE_CLI_BACKEND_ID,
label: "Claude CLI",
docsPath: "/providers/models",
auth: [],
resolveSyntheticAuth: ({ provider }) =>
provider === CLAUDE_CLI_BACKEND_ID ? resolveClaudeCliSyntheticAuth() : undefined,
};
export default anthropicProviderDiscovery;

View File

@@ -1,4 +1,5 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
import { sendBlueBubblesAttachment } from "./attachments.js";
import { editBlueBubblesMessage, setGroupIconBlueBubbles } from "./chat.js";
import { resolveBlueBubblesMessageId } from "./monitor-reply-cache.js";
@@ -44,8 +45,10 @@ vi.mock("./probe.js", () => ({
getCachedBlueBubblesPrivateApiStatus: vi.fn().mockReturnValue(null),
}));
const freshActionsModulePath = "./actions.js?actions-test";
const { bluebubblesMessageActions } = await import(freshActionsModulePath);
const { bluebubblesMessageActions } = await importFreshModule<typeof import("./actions.js")>(
import.meta.url,
"./actions.js?actions-test",
);
describe("bluebubblesMessageActions", () => {
const describeMessageTool = bluebubblesMessageActions.describeMessageTool!;

View File

@@ -1,5 +1,8 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { registerUnhandledRejectionHandler } from "openclaw/plugin-sdk/runtime";
import {
registerUncaughtExceptionHandler,
registerUnhandledRejectionHandler,
} from "openclaw/plugin-sdk/runtime";
import { startGatewayBonjourAdvertiser } from "./src/advertiser.js";
function formatBonjourInstanceName(displayName: string) {
@@ -33,7 +36,11 @@ export default definePluginEntry({
cliPath: ctx.cliPath,
minimal: ctx.minimal,
},
{ logger: api.logger, registerUnhandledRejectionHandler },
{
logger: api.logger,
registerUncaughtExceptionHandler,
registerUnhandledRejectionHandler,
},
);
return { stop: advertiser.stop };
},

View File

@@ -1,3 +1,4 @@
import fs from "node:fs";
import os from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -5,6 +6,7 @@ const mocks = vi.hoisted(() => ({
createService: vi.fn(),
getResponder: vi.fn(),
shutdown: vi.fn(),
registerUncaughtExceptionHandler: vi.fn(),
registerUnhandledRejectionHandler: vi.fn(),
logger: {
info: vi.fn(),
@@ -12,7 +14,14 @@ const mocks = vi.hoisted(() => ({
debug: vi.fn(),
},
}));
const { createService, getResponder, shutdown, registerUnhandledRejectionHandler, logger } = mocks;
const {
createService,
getResponder,
shutdown,
registerUncaughtExceptionHandler,
registerUnhandledRejectionHandler,
logger,
} = mocks;
const asString = (value: unknown, fallback: string) =>
typeof value === "string" && value.trim() ? value : fallback;
@@ -77,6 +86,7 @@ const startAdvertiser = (
): ReturnType<StartGatewayBonjourAdvertiser> =>
startGatewayBonjourAdvertiser(opts, {
logger,
registerUncaughtExceptionHandler: (handler) => registerUncaughtExceptionHandler(handler),
registerUnhandledRejectionHandler: (handler) => registerUnhandledRejectionHandler(handler),
});
@@ -103,6 +113,7 @@ describe("gateway bonjour advertiser", () => {
createService.mockClear();
getResponder.mockReset();
shutdown.mockClear();
registerUncaughtExceptionHandler.mockClear();
registerUnhandledRejectionHandler.mockClear();
logger.info.mockClear();
logger.warn.mockClear();
@@ -197,6 +208,38 @@ describe("gateway bonjour advertiser", () => {
await expect(started.stop()).resolves.toBeUndefined();
});
it("auto-disables Bonjour in detected containers", async () => {
enableAdvertiserUnitMode();
vi.spyOn(fs, "existsSync").mockImplementation((filePath) => String(filePath) === "/.dockerenv");
const started = await startAdvertiser({
gatewayPort: 18789,
sshPort: 2222,
});
expect(createService).not.toHaveBeenCalled();
await expect(started.stop()).resolves.toBeUndefined();
});
it("honors explicit Bonjour opt-in inside detected containers", async () => {
enableAdvertiserUnitMode();
process.env.OPENCLAW_DISABLE_BONJOUR = "0";
vi.spyOn(fs, "existsSync").mockImplementation((filePath) => String(filePath) === "/.dockerenv");
const destroy = vi.fn().mockResolvedValue(undefined);
const advertise = vi.fn().mockResolvedValue(undefined);
mockCiaoService({ advertise, destroy });
const started = await startAdvertiser({
gatewayPort: 18789,
sshPort: 2222,
});
expect(createService).toHaveBeenCalledTimes(1);
await started.stop();
});
it("attaches conflict listeners for services", async () => {
enableAdvertiserUnitMode();
@@ -220,7 +263,7 @@ describe("gateway bonjour advertiser", () => {
await started.stop();
});
it("does not install a process-level unhandled rejection handler by default", async () => {
it("does not install process-level ciao handlers by default", async () => {
enableAdvertiserUnitMode();
const destroy = vi.fn().mockResolvedValue(undefined);
@@ -237,11 +280,12 @@ describe("gateway bonjour advertiser", () => {
);
expect(processOn).not.toHaveBeenCalledWith("unhandledRejection", expect.any(Function));
expect(processOn).not.toHaveBeenCalledWith("uncaughtException", expect.any(Function));
await started.stop();
});
it("cleans up unhandled rejection handler after shutdown", async () => {
it("cleans up ciao process handlers after shutdown", async () => {
enableAdvertiserUnitMode();
const destroy = vi.fn().mockResolvedValue(undefined);
@@ -252,10 +296,14 @@ describe("gateway bonjour advertiser", () => {
});
mockCiaoService({ advertise, destroy });
const cleanup = vi.fn(() => {
order.push("cleanup");
const cleanupException = vi.fn(() => {
order.push("cleanup-exception");
});
registerUnhandledRejectionHandler.mockImplementation(() => cleanup);
const cleanupRejection = vi.fn(() => {
order.push("cleanup-rejection");
});
registerUncaughtExceptionHandler.mockImplementation(() => cleanupException);
registerUnhandledRejectionHandler.mockImplementation(() => cleanupRejection);
const started = await startAdvertiser({
gatewayPort: 18789,
@@ -264,9 +312,11 @@ describe("gateway bonjour advertiser", () => {
await started.stop();
expect(registerUncaughtExceptionHandler).toHaveBeenCalledTimes(1);
expect(registerUnhandledRejectionHandler).toHaveBeenCalledTimes(1);
expect(cleanup).toHaveBeenCalledTimes(1);
expect(order).toEqual(["shutdown", "cleanup"]);
expect(cleanupException).toHaveBeenCalledTimes(1);
expect(cleanupRejection).toHaveBeenCalledTimes(1);
expect(order).toEqual(["shutdown", "cleanup-exception", "cleanup-rejection"]);
});
it("logs ciao handler classifications at the bonjour caller", async () => {
@@ -284,7 +334,11 @@ describe("gateway bonjour advertiser", () => {
const handler = registerUnhandledRejectionHandler.mock.calls[0]?.[0] as
| ((reason: unknown) => boolean)
| undefined;
const exceptionHandler = registerUncaughtExceptionHandler.mock.calls[0]?.[0] as
| ((reason: unknown) => boolean)
| undefined;
expect(handler).toBeTypeOf("function");
expect(exceptionHandler).toBeTypeOf("function");
expect(handler?.(new Error("CIAO PROBING CANCELLED"))).toBe(true);
expect(logger.debug).toHaveBeenCalledWith(
@@ -299,6 +353,21 @@ describe("gateway bonjour advertiser", () => {
expect.stringContaining("suppressing ciao interface assertion"),
);
logger.warn.mockClear();
expect(
exceptionHandler?.(
Object.assign(
new Error(
"IP address version must match. Netmask cannot have a version different from the address!",
),
{ name: "AssertionError" },
),
),
).toBe(true);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining("suppressing ciao netmask assertion"),
);
await started.stop();
});
@@ -454,6 +523,8 @@ describe("gateway bonjour advertiser", () => {
const stateRef = { value: "announcing" };
const events: string[] = [];
const cleanupException = vi.fn();
const cleanupRejection = vi.fn();
let advertiseCount = 0;
const destroy = vi.fn().mockImplementation(async () => {
events.push("destroy");
@@ -469,6 +540,8 @@ describe("gateway bonjour advertiser", () => {
return Promise.resolve();
});
mockCiaoService({ advertise, destroy, stateRef });
registerUncaughtExceptionHandler.mockImplementation(() => cleanupException);
registerUnhandledRejectionHandler.mockImplementation(() => cleanupRejection);
const started = await startAdvertiser({
gatewayPort: 18789,
@@ -477,6 +550,8 @@ describe("gateway bonjour advertiser", () => {
expect(createService).toHaveBeenCalledTimes(1);
expect(advertise).toHaveBeenCalledTimes(1);
expect(registerUncaughtExceptionHandler).toHaveBeenCalledTimes(1);
expect(registerUnhandledRejectionHandler).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(15_000);
@@ -485,11 +560,15 @@ describe("gateway bonjour advertiser", () => {
expect(advertise).toHaveBeenCalledTimes(2);
expect(destroy).toHaveBeenCalledTimes(1);
expect(shutdown).not.toHaveBeenCalled();
expect(cleanupException).not.toHaveBeenCalled();
expect(cleanupRejection).not.toHaveBeenCalled();
expect(events).toEqual(["advertise:1", "destroy", "advertise:2"]);
await started.stop();
expect(destroy).toHaveBeenCalledTimes(2);
expect(shutdown).toHaveBeenCalledTimes(1);
expect(cleanupException).toHaveBeenCalledTimes(1);
expect(cleanupRejection).toHaveBeenCalledTimes(1);
});
it("treats probing-to-announcing churn as one unhealthy window", async () => {

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import type { PluginLogger } from "openclaw/plugin-sdk/plugin-entry";
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
import { classifyCiaoUnhandledRejection } from "./ciao.js";
import { classifyCiaoProcessError, type CiaoProcessErrorClassification } from "./ciao.js";
import { formatBonjourError } from "./errors.js";
export type GatewayBonjourAdvertiser = {
@@ -50,7 +51,6 @@ type CiaoModule = {
type BonjourCycle = {
responder: BonjourResponder;
services: Array<{ label: string; svc: BonjourService }>;
cleanupUnhandledRejection?: () => void;
};
type ServiceStateTracker = {
@@ -59,10 +59,12 @@ type ServiceStateTracker = {
};
type ConsoleLogFn = (...args: unknown[]) => void;
type UncaughtExceptionHandler = (error: unknown) => boolean;
type UnhandledRejectionHandler = (reason: unknown) => boolean;
type BonjourAdvertiserDeps = {
logger?: Pick<PluginLogger, "info" | "warn" | "debug">;
registerUncaughtExceptionHandler?: (handler: UncaughtExceptionHandler) => () => void;
registerUnhandledRejectionHandler?: (handler: UnhandledRejectionHandler) => () => void;
};
@@ -88,16 +90,61 @@ async function loadCiaoModule(): Promise<CiaoModule> {
return ciaoModulePromise;
}
function isDisabledByEnv() {
if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_BONJOUR)) {
function readBonjourDisableOverride(): boolean | null {
const raw = process.env.OPENCLAW_DISABLE_BONJOUR;
const normalized = raw?.trim().toLowerCase();
if (!normalized) {
return null;
}
if (isTruthyEnvValue(raw)) {
return true;
}
switch (normalized) {
case "0":
case "false":
case "no":
case "off":
return false;
default:
return null;
}
}
function isContainerEnvironment() {
for (const sentinelPath of ["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"]) {
try {
if (fs.existsSync(sentinelPath)) {
return true;
}
} catch {
// ignore
}
}
try {
const cgroup = fs.readFileSync("/proc/1/cgroup", "utf8");
return /\/docker\/|cri-containerd-[0-9a-f]|containerd\/[0-9a-f]{64}|\/kubepods[/.]|\blxc\b/u.test(
cgroup,
);
} catch {
return false;
}
}
function isDisabledByEnv() {
if (process.env.NODE_ENV === "test") {
return true;
}
if (process.env.VITEST) {
return true;
}
const envOverride = readBonjourDisableOverride();
if (envOverride !== null) {
return envOverride;
}
if (isContainerEnvironment()) {
return true;
}
return false;
}
@@ -175,21 +222,38 @@ export async function startGatewayBonjourAdvertiser(
};
const { getResponder, Protocol } = await loadCiaoModule();
const restoreConsoleLog = installCiaoConsoleNoiseFilter();
let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined;
let cleanupUnhandledRejection: (() => void) | undefined;
let cleanupUncaughtException: (() => void) | undefined;
let processHandlersCleaned = false;
const handleCiaoUnhandledRejection = (reason: unknown): boolean => {
const classification = classifyCiaoUnhandledRejection(reason);
function cleanupProcessHandlers() {
if (processHandlersCleaned) {
return;
}
processHandlersCleaned = true;
cleanupUncaughtException?.();
cleanupUnhandledRejection?.();
}
const handleCiaoProcessError = (reason: unknown): boolean => {
const classification = classifyCiaoProcessError(reason);
if (!classification) {
return false;
}
if (classification.kind === "interface-assertion") {
logger.warn(`bonjour: suppressing ciao interface assertion: ${classification.formatted}`);
return true;
if (classification.kind === "cancellation") {
logger.debug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`);
} else {
const label =
classification.kind === "netmask-assertion" ? "netmask assertion" : "interface assertion";
logger.warn(`bonjour: suppressing ciao ${label}: ${classification.formatted}`);
requestCiaoRecovery?.(classification);
}
logger.debug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`);
return true;
};
cleanupUnhandledRejection = deps.registerUnhandledRejectionHandler?.(handleCiaoProcessError);
cleanupUncaughtException = deps.registerUncaughtExceptionHandler?.(handleCiaoProcessError);
try {
const hostnameRaw = process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || "openclaw";
@@ -253,12 +317,7 @@ export async function startGatewayBonjourAdvertiser(
svc: gateway as unknown as BonjourService,
});
const cleanupUnhandledRejection =
services.length > 0 && deps.registerUnhandledRejectionHandler
? deps.registerUnhandledRejectionHandler(handleCiaoUnhandledRejection)
: undefined;
return { responder, services, cleanupUnhandledRejection };
return { responder, services };
}
async function stopCycle(cycle: BonjourCycle | null, opts?: { shutdownResponder?: boolean }) {
@@ -278,8 +337,6 @@ export async function startGatewayBonjourAdvertiser(
}
} catch {
/* ignore */
} finally {
cycle.cleanupUnhandledRejection?.();
}
}
@@ -388,6 +445,9 @@ export async function startGatewayBonjourAdvertiser(
});
return recreatePromise;
};
requestCiaoRecovery = (classification) => {
void recreateAdvertiser(`ciao ${classification.kind}: ${classification.formatted}`);
};
const lastRepairAttempt = new Map<string, number>();
const watchdog = setInterval(() => {
@@ -469,10 +529,12 @@ export async function startGatewayBonjourAdvertiser(
}
await stopCycle(cycle, { shutdownResponder: true });
restoreConsoleLog();
cleanupProcessHandlers();
},
};
} catch (err) {
restoreConsoleLog();
cleanupProcessHandlers();
throw err;
}
}

View File

@@ -21,6 +21,23 @@ describe("bonjour-ciao", () => {
});
});
it("classifies ciao netmask assertions separately from side effects", () => {
expect(
classifyCiaoUnhandledRejection(
Object.assign(
new Error(
"IP address version must match. Netmask cannot have a version different from the address!",
),
{ name: "AssertionError" },
),
),
).toEqual({
kind: "netmask-assertion",
formatted:
"AssertionError: IP address version must match. Netmask cannot have a version different from the address!",
});
});
it("suppresses ciao announcement cancellation rejections", () => {
expect(ignoreCiaoUnhandledRejection(new Error("Ciao announcement cancelled by shutdown"))).toBe(
true,
@@ -31,6 +48,34 @@ describe("bonjour-ciao", () => {
expect(ignoreCiaoUnhandledRejection(new Error("CIAO PROBING CANCELLED"))).toBe(true);
});
it("suppresses wrapped ciao cancellation rejections", () => {
expect(
classifyCiaoUnhandledRejection({
reason: new Error("CIAO ANNOUNCEMENT CANCELLED"),
}),
).toEqual({
kind: "cancellation",
formatted: "CIAO ANNOUNCEMENT CANCELLED",
});
});
it("suppresses aggregate ciao assertion rejections", () => {
expect(
classifyCiaoUnhandledRejection(
new AggregateError([
Object.assign(
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
{ name: "AssertionError" },
),
]),
),
).toEqual({
kind: "interface-assertion",
formatted:
"AssertionError: Reached illegal state! IPV4 address change from defined to undefined!",
});
});
it("suppresses lower-case string cancellation reasons too", () => {
expect(ignoreCiaoUnhandledRejection("ciao announcement cancelled during cleanup")).toBe(true);
});
@@ -44,6 +89,17 @@ describe("bonjour-ciao", () => {
expect(ignoreCiaoUnhandledRejection(error)).toBe(true);
});
it("suppresses ciao netmask assertion errors as non-fatal", () => {
const error = Object.assign(
new Error(
"IP address version must match. Netmask cannot have a version different from the address!",
),
{ name: "AssertionError" },
);
expect(ignoreCiaoUnhandledRejection(error)).toBe(true);
});
it("keeps unrelated rejections visible", () => {
expect(ignoreCiaoUnhandledRejection(new Error("boom"))).toBe(false);
});

View File

@@ -2,26 +2,74 @@ import { formatBonjourError } from "./errors.js";
const CIAO_CANCELLATION_MESSAGE_RE = /^CIAO (?:ANNOUNCEMENT|PROBING) CANCELLED\b/u;
const CIAO_INTERFACE_ASSERTION_MESSAGE_RE =
/REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM DEFINED TO UNDEFINED!?/u;
/REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM (?:DEFINED TO UNDEFINED|UNDEFINED TO DEFINED)!?/u;
const CIAO_NETMASK_ASSERTION_MESSAGE_RE =
/IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u;
export type CiaoUnhandledRejectionClassification =
export type CiaoProcessErrorClassification =
| { kind: "cancellation"; formatted: string }
| { kind: "interface-assertion"; formatted: string };
| { kind: "interface-assertion"; formatted: string }
| { kind: "netmask-assertion"; formatted: string };
export function classifyCiaoUnhandledRejection(
reason: unknown,
): CiaoUnhandledRejectionClassification | null {
const formatted = formatBonjourError(reason);
const message = formatted.toUpperCase();
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
return { kind: "cancellation", formatted };
function collectCiaoProcessErrorCandidates(reason: unknown): unknown[] {
const queue: unknown[] = [reason];
const seen = new Set<unknown>();
const candidates: unknown[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current == null || seen.has(current)) {
continue;
}
seen.add(current);
candidates.push(current);
if (!current || typeof current !== "object") {
continue;
}
const record = current as Record<string, unknown>;
for (const nested of [
record.cause,
record.reason,
record.original,
record.error,
record.data,
]) {
if (nested != null && !seen.has(nested)) {
queue.push(nested);
}
}
if (Array.isArray(record.errors)) {
for (const nested of record.errors) {
if (nested != null && !seen.has(nested)) {
queue.push(nested);
}
}
}
}
if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "interface-assertion", formatted };
return candidates;
}
export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClassification | null {
for (const candidate of collectCiaoProcessErrorCandidates(reason)) {
const formatted = formatBonjourError(candidate);
const message = formatted.toUpperCase();
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
return { kind: "cancellation", formatted };
}
if (CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "interface-assertion", formatted };
}
if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "netmask-assertion", formatted };
}
}
return null;
}
export const classifyCiaoUnhandledRejection = classifyCiaoProcessError;
export function ignoreCiaoUnhandledRejection(reason: unknown): boolean {
return classifyCiaoUnhandledRejection(reason) !== null;
return classifyCiaoProcessError(reason) !== null;
}

View File

@@ -5,7 +5,7 @@
"description": "OpenClaw Brave plugin",
"type": "module",
"dependencies": {
"typebox": "1.1.32"
"typebox": "1.1.33"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"

View File

@@ -1,6 +1,7 @@
{
"id": "browser",
"enabledByDefault": true,
"commandAliases": [{ "name": "browser" }],
"skills": ["./skills"],
"configSchema": {
"type": "object",

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