Compare commits

..

131 Commits

Author SHA1 Message Date
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
Peter Steinberger
4c7a94aac4 fix: quote Windows UI runner paths 2026-04-26 08:31:00 +01:00
Vincent Koc
434c8a1c91 docs(heartbeat): rewrite with Steps for quick start, ParamField for field notes, AccordionGroup for delivery and tasks behavior 2026-04-26 00:30:47 -07:00
Shakker
04575333d3 chore: ignore local agent skills 2026-04-26 08:26:28 +01:00
Shakker
50558e0d56 docs: note channel runtime laziness fixes 2026-04-26 08:26:28 +01:00
Shakker
8fe449c883 fix: avoid channel runtime in format summaries 2026-04-26 08:26:27 +01:00
Shakker
8b32c31252 fix: keep thread placement metadata cold 2026-04-26 08:26:27 +01:00
Shakker
2e101e8413 fix: keep channel security checks cold 2026-04-26 08:26:27 +01:00
Vincent Koc
a77996dc56 fix(diagnostics): propagate trusted traceparent headers 2026-04-26 00:24:47 -07:00
Vincent Koc
5e8fda4c64 docs(memory-config): rewrite with CardGroup overview links, Steps for auto-detect, AccordionGroup for provider configs and QMD subsections 2026-04-26 00:21:28 -07:00
Peter Steinberger
76cf013df5 test: remove slow reply bypass from docker smoke 2026-04-26 08:19:23 +01:00
Vincent Koc
450dc3a206 docs(control-ui): rewrite with Steps for pairing, AccordionGroup for capabilities and chat behavior, Tabs for tailnet access 2026-04-26 00:18:47 -07:00
Peter Steinberger
7b438965bd test: stabilize docker mcp readiness smokes 2026-04-26 08:17:28 +01:00
Peter Steinberger
c5bbf83904 fix: skip unresolved delivery targets for no-deliver cron 2026-04-26 08:17:28 +01:00
Peter Steinberger
f4f74a2391 docs(changelog): organize unreleased notes 2026-04-26 08:15:34 +01:00
Vincent Koc
0c8f0aacf5 docs(plugin-architecture): rewrite with AccordionGroup for shapes and ownership, Steps for the architecture pipeline, Tabs for layering 2026-04-26 00:15:25 -07:00
Peter Steinberger
1de4aff06d fix: cover Windows pnpm and Lobster install regressions 2026-04-26 08:14:28 +01:00
Peter Steinberger
5b9be2cdb1 fix: migrate agent runtime config 2026-04-26 08:12:44 +01:00
Vincent Koc
9d6e79019f docs(secrets): rewrite with Tabs for SecretRef sources, AccordionGroup for providers and exec examples, Steps for the audit flow 2026-04-26 00:12:05 -07:00
Shakker
b5e4e2f257 Revert "fix(plugins): persist registry contribution metadata"
This reverts commit 1ee5654220.
2026-04-26 08:11:09 +01:00
Shakker
59d1fa65df docs: note plugin uninstall file cleanup 2026-04-26 08:11:09 +01:00
Shakker
6428440086 fix: remove plugins from recorded install roots 2026-04-26 08:11:09 +01:00
Peter Steinberger
d419fb561d feat(tts): resolve channel account config generically 2026-04-26 08:10:36 +01:00
Vincent Koc
6c60cd2b72 docs(mcp): rewrite with Steps for lifecycle, Tabs for client modes, ParamField for serve options, AccordionGroup for tools 2026-04-26 00:08:16 -07:00
Vincent Koc
1ee5654220 fix(plugins): persist registry contribution metadata 2026-04-26 00:03:21 -07:00
Peter Steinberger
54f8e4145e test: speed up provider and security tests 2026-04-26 07:59:32 +01:00
Peter Steinberger
d1e5f4bd3c fix(update): bound Windows scheduled task stop 2026-04-26 07:56:46 +01:00
Shakker
3ad29972d0 docs: note read-only channel command discovery 2026-04-26 07:55:00 +01:00
Shakker
43557b16a6 fix: keep channel command discovery read-only 2026-04-26 07:55:00 +01:00
Shakker
fd97f530e3 docs: note cold session metadata fix 2026-04-26 07:55:00 +01:00
Shakker
bbed91bf71 fix: avoid session metadata channel runtime fallback 2026-04-26 07:55:00 +01:00
Shakker
49b106d357 docs: note cold native command defaults 2026-04-26 07:55:00 +01:00
Shakker
7a7728db13 fix: keep native command auto defaults cold 2026-04-26 07:55:00 +01:00
Shakker
aee4c92344 docs: note provider index disablement fix 2026-04-26 07:55:00 +01:00
Shakker
78fb0ade09 fix: honor plugin disablement in provider index rows 2026-04-26 07:55:00 +01:00
Vincent Koc
f48dc96d43 docs(opentelemetry): document harness lifecycle metric, span, and diagnostic events from 82ddcf24f5 2026-04-25 23:54:30 -07:00
Vincent Koc
ff7f0df871 docs(config-tools): rewrite with AccordionGroup for provider examples and field details, ParamField for loop detectors 2026-04-25 23:51:26 -07:00
Peter Steinberger
4ee537a04a fix(node-runtime): keep node-host recovering after gateway restarts 2026-04-26 07:49:45 +01:00
Vincent Koc
c7ead7d8a9 fix(cli): lazy-load model auth runtime 2026-04-25 23:49:06 -07:00
Vincent Koc
62869c8502 docs(bluebubbles): rewrite with Steps for setup, Tabs for DM/groups and coalescing, AccordionGroup for actions and config 2026-04-25 23:48:13 -07:00
Vincent Koc
bb0ef5ef18 docs(troubleshooting): rewrite with AccordionGroup for symptom signatures, Steps for fix flows, and Warning callouts 2026-04-25 23:44:25 -07:00
Harry Xie
77719899f3 fix(gateway): refresh stale embedded service tokens
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.

Co-authored-by: Harry Xie <harryhsieh963@yahoo.com>
2026-04-26 07:42:14 +01:00
Vincent Koc
8c87a637e9 docs(doctor): rewrite with Tabs for headless flags and AccordionGroup for the 19+ detailed behaviors 2026-04-25 23:40:24 -07:00
Vincent Koc
c4a39a6819 docs(model-providers): rewrite with AccordionGroup, CardGroup, Tabs, and Steps for cleaner provider scan 2026-04-25 23:36:01 -07:00
Vincent Koc
82ddcf24f5 feat(diagnostics): add harness lifecycle telemetry 2026-04-25 23:34:34 -07:00
Peter Steinberger
8bbb143ab8 fix: enforce device token scope containment 2026-04-26 07:28:21 +01:00
Peter Steinberger
26e4eb8e40 fix(update): ignore plugin install stages in dist verify 2026-04-26 07:28:02 +01:00
Peter Steinberger
8368026986 fix(installer): preserve PowerShell host on failure 2026-04-26 07:23:48 +01:00
Peter Steinberger
1fae716a04 fix: recover stale cron task records 2026-04-26 07:23:39 +01:00
Ayaan Zaidi
9d21200049 test(e2e): cover npm onboarding runtime deps 2026-04-26 11:53:17 +05:30
Peter Steinberger
7091dbe2bf docs: prefer ghcrawl for OpenClaw issue triage 2026-04-26 07:19:00 +01:00
417 changed files with 17645 additions and 8795 deletions

View File

@@ -7,6 +7,22 @@ description: Review, triage, close, label, comment on, or land OpenClaw PRs/issu
Use this skill for maintainer-facing GitHub workflow, not for ordinary code changes.
## Start issue and PR triage with ghcrawl
- Anytime you inspect OpenClaw issues or PRs, check local `ghcrawl` data first for related threads, duplicate attempts, and already-landed fixes.
- Use `ghcrawl` for candidate discovery and clustering; use `gh`, `gh api`, and the current checkout to verify live state before commenting, labeling, closing, or landing.
- If `ghcrawl` is missing, stale, lacks the target thread, or has no embeddings for neighbor/search commands, fall back to the GitHub search workflow below.
- Do not run expensive/update commands such as `ghcrawl refresh`, `ghcrawl embed`, or `ghcrawl cluster` unless the user asked to update the local store or the stale data is blocking the decision.
Common read-only path:
```bash
ghcrawl threads openclaw/openclaw --numbers <issue-or-pr-number> --include-closed --json
ghcrawl neighbors openclaw/openclaw --number <issue-or-pr-number> --limit 12 --json
ghcrawl search openclaw/openclaw --query "<scope or title keywords>" --mode hybrid --json
ghcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --body-chars 280 --json
```
## Apply close and triage labels correctly
- If an issue or PR matches an auto-close reason, apply the label and let `.github/workflows/auto-response.yml` handle the comment/close/lock flow.
@@ -59,9 +75,9 @@ Use this skill for maintainer-facing GitHub workflow, not for ordinary code chan
## Search broadly before deciding
- Prefer targeted keyword search before proposing new work or closing something as duplicate.
- Use `--repo openclaw/openclaw` with `--match title,body` first.
- Add `--match comments` when triaging follow-up discussion.
- Prefer `ghcrawl` first. Then use targeted GitHub keyword search to verify gaps, live status, comments, and candidates not present in the local store.
- Use `--repo openclaw/openclaw` with `--match title,body` first when using `gh search`.
- Add `--match comments` when triaging follow-up discussion or closed-as-duplicate chains.
- Do not stop at the first 500 results when the task requires a full search.
Examples:

View File

@@ -28,12 +28,17 @@ Use this skill for release and publish-time workflow. Keep ordinary development
- 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 +80,10 @@ 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, run the expensive Docker, Parallels, and QA-Lab
release rosters in parallel instead of serializing them. 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
@@ -491,8 +500,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 +540,15 @@ 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. Start Docker, Parallels, and QA-Lab in parallel once the
beta is live. 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

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

@@ -59,16 +59,14 @@ jobs:
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
needs: approve_release_manager
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"
@@ -79,61 +77,20 @@ jobs:
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 }}
- 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 +135,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

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:

30
.gitignore vendored
View File

@@ -97,6 +97,36 @@ USER.md
# local tooling
.serena/
# Local project-agent skill installs. Only repo-owned skills are visible by
# default; promoting a new repo skill should require an intentional `git add -f`.
.agents/skills/*
!.agents/skills/blacksmith-testbox/
!.agents/skills/blacksmith-testbox/**
!.agents/skills/openclaw-ghsa-maintainer/
!.agents/skills/openclaw-ghsa-maintainer/**
!.agents/skills/openclaw-parallels-smoke/
!.agents/skills/openclaw-parallels-smoke/**
!.agents/skills/openclaw-pr-maintainer/
!.agents/skills/openclaw-pr-maintainer/**
!.agents/skills/openclaw-qa-testing/
!.agents/skills/openclaw-qa-testing/**
!.agents/skills/openclaw-release-maintainer/
!.agents/skills/openclaw-release-maintainer/**
!.agents/skills/openclaw-secret-scanning-maintainer/
!.agents/skills/openclaw-secret-scanning-maintainer/**
!.agents/skills/openclaw-test-heap-leaks/
!.agents/skills/openclaw-test-heap-leaks/**
!.agents/skills/openclaw-test-performance/
!.agents/skills/openclaw-test-performance/**
!.agents/skills/optimizetests/
!.agents/skills/optimizetests/**
!.agents/skills/parallels-discord-roundtrip/
!.agents/skills/parallels-discord-roundtrip/**
!.agents/skills/security-triage/
!.agents/skills/security-triage/**
!.agents/skills/tag-duplicate-prs-issues/
!.agents/skills/tag-duplicate-prs-issues/**
# Agent credentials and memory (NEVER COMMIT)
/memory/
.agent/*.json

View File

@@ -85,7 +85,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

View File

@@ -4,28 +4,52 @@ Docs: https://docs.openclaw.ai
## Unreleased
## 2026.4.25 (Unreleased)
### Fixes
- 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.
## 2026.4.26
### Fixes
- 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.
## 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.
- Plugins/tokenjuice: bump the bundled tokenjuice runtime to 0.6.3. Thanks @vincentkoc.
- TTS/agents: allow `agents.list[].tts` to override global `messages.tts` for per-agent voices while keeping shared provider credentials and preferences in the existing TTS config surface.
- TTS/agents: make `/tts audio`, `/tts status`, and the `tts` agent tool honor the active `agents.list[].tts` voice/provider override.
- 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.
- Providers/Azure Speech: add Azure Speech as a bundled TTS provider with Speech-resource auth, voice listing, SSML escaping, native Ogg/Opus voice-note output, and telephony output. (#51776) Thanks @leonchui.
- Browser automation: add 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.
- 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`.
- 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.
- CLI/Crestodian: open interactive Crestodian in the full OpenClaw TUI shell instead of a basic readline prompt.
- CLI/Crestodian: shorten the startup greeting to the active planner/model, config state, Gateway probe result, and next debug action instead of dumping every discovered backend.
- CLI/Crestodian/TUI: add the first-run setup helper, local planner fallback, full-TUI interactive Crestodian, startup progress indicators, context mode selector, and a shorter startup greeting. (#71720, #71760) Thanks @SebTardif and @kevinlin-openai.
- Plugins: migrate the local plugin registry automatically during package install/update, keeping install metadata in the plugin index while indexing existing plugin manifests for the new cold registry path. Thanks @vincentkoc and @shakkernerd.
- Plugins/doctor: make `openclaw doctor --fix` refresh the plugin index and cold registry index when needed without treating plugin install records as authored config. Thanks @vincentkoc and @shakkernerd.
- Plugins/hooks: add before-agent-finalize hooks, cron `jobId` hook context, bounded native permission fingerprints, and Codex MCP hook relay support. (#71765, #71758, #71707) Thanks @vincentkoc and @pashpashpash.
- Plugins/tokenjuice: bump the bundled tokenjuice runtime to 0.6.3. Thanks @vincentkoc.
- Diagnostics/OTEL: align model-call GenAI span attributes with OpenTelemetry stability opt-in semantics, keeping legacy `gen_ai.system` by default while emitting `gen_ai.provider.name` under `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`. Thanks @vincentkoc.
- Diagnostics/OTEL: support signal-specific OTLP endpoint overrides for traces, metrics, and logs via config or standard OTEL environment variables. Thanks @vincentkoc.
- 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.
@@ -37,9 +61,14 @@ Docs: https://docs.openclaw.ai
- CLI/models: use OpenClaw Provider Index preview rows as the final cold fallback for installable providers, while keeping user config, installed manifests, and refreshed cache rows above provider-index metadata. Thanks @vincentkoc.
- 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.
@@ -54,6 +83,7 @@ Docs: https://docs.openclaw.ai
- Diagnostics/OTEL: keep model-usage span GenAI provider attributes aligned with the existing semantic-convention opt-in policy, using legacy `gen_ai.system` unless latest experimental GenAI conventions are enabled. Thanks @vincentkoc.
- Diagnostics/OTEL: keep `gen_ai.request.model` present on GenAI token usage metrics with a bounded `unknown` fallback when model usage events do not include a model. Thanks @vincentkoc.
- Docs/OTEL: document the GenAI token and model-call duration metrics, model-usage span attributes, and `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` provider-attribute behavior. Thanks @vincentkoc.
- Docs: refresh the MCP, model provider, doctor, troubleshooting, BlueBubbles, media generation, TTS, subagents, skills, cron/tasks, exec approvals, and voice-call guides with structured Steps, Tabs, and Accordion content.
- Diagnostics/trace: add an internal traceparent propagation helper that only formats trusted dispatcher metadata, keeping plugin-emitted diagnostic traces out of outbound propagation by default. Thanks @vincentkoc.
- Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.
@@ -74,20 +104,49 @@ 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`, 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.
- CLI/models: keep Provider Index preview rows out of `models list --all --provider <id>` when the owning provider plugin is disabled, preserving config authority for cold catalog fallbacks. Thanks @shakkernerd.
- CLI/model runs: keep `openclaw infer model run` on explicit OpenRouter models from loading the full provider catalog or inheriting chat-agent silent-reply policy, restoring non-empty one-shot probe output. Fixes #68791. Thanks @limpredator.
- Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io.
- Installer: load nvm before Node.js detection so `curl | bash` installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj.
- Installer/Windows: route PowerShell install failures through a top-level handler so `iwr ... | iex` returns control to the current shell while direct script-file runs still exit non-zero. Fixes #38054. Thanks @PwrSrg.
- CLI/Volta: respawn raw `openclaw` CLI runs through the named `node` shim when the current Node executable resolves to `volta-shim`, avoiding direct shim execution failures in non-interactive shells. Fixes #68672. Thanks @sanchezm86.
- 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.
@@ -95,27 +154,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.
@@ -129,52 +174,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.
@@ -186,13 +204,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.
@@ -211,9 +224,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.
@@ -227,8 +238,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.
@@ -238,12 +249,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.
@@ -257,16 +268,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.
@@ -789,16 +800,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.
@@ -837,9 +841,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

@@ -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

@@ -1,4 +1,4 @@
f1eefb91a486188915373b09199959f0f1a7cd01dc75ef923832741f72a12543 config-baseline.json
9f0e386d5118cbca785a2e8e9c8b170d844faf1b7ef5e82e6b15d9e1c39f3796 config-baseline.core.json
d8e7866e0c3f633222f75a35defed3c3a03d849f4aa4f70871e3436e80074e76 config-baseline.json
5f5fb87fd46f9cbb84d8af17e00ae3c4b74062e8ad517bc2260ba83da2e9014f config-baseline.core.json
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
a5479c182ec987bb21e814b8a4e7b3bda7190ae5c2b35fd5ca403dfa48afa115 config-baseline.plugin.json

View File

@@ -51,7 +51,7 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
<a id="maintenance"></a>
<Note>
Task reconciliation for cron is runtime-owned: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and the 5-minute grace window expires, maintenance can mark the task `lost`.
Task reconciliation for cron is runtime-owned first, durable-history-backed second: an active cron task stays live while the cron runtime still tracks that job as running, even if an old child session row still exists. Once the runtime stops owning the job and the 5-minute grace window expires, maintenance checks persisted run logs and job state for the matching `cron:<jobId>:<startedAt>` run. If that durable history shows a terminal result, the task ledger is finalized from it; otherwise Gateway-owned maintenance can mark the task `lost`. Offline CLI audit can recover from durable history, but it does not treat its own empty in-process active-job set as proof that a Gateway-owned cron run is gone.
</Note>
## Schedule types

View File

@@ -25,8 +25,12 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
- Tasks are **records**, not schedulers — cron and heartbeat decide _when_ work runs, tasks track _what happened_.
- ACP, subagents, all cron jobs, and CLI operations create tasks. Heartbeat turns do not.
- Each task moves through `queued → running → terminal` (succeeded, failed, timed_out, cancelled, or lost).
- Cron tasks stay live while the cron runtime still owns the job; chat-backed CLI tasks stay live only while their owning run context is still active.
- Completion is push-driven: detached work can notify directly or wake the requester session/heartbeat when it finishes, so status polling loops are usually the wrong shape.
- Cron tasks stay live while the cron runtime still owns the job; if the
in-memory runtime state is gone, task maintenance first checks durable cron
run history before marking a task lost.
- Completion is push-driven: detached work can notify directly or wake the
requester session/heartbeat when it finishes, so status polling loops are
usually the wrong shape.
- Isolated cron runs and subagent completions best-effort clean up tracked browser tabs/processes for their child session before final cleanup bookkeeping.
- Isolated cron delivery suppresses stale interim parent replies while descendant subagent work is still draining, and it prefers final descendant output when that arrives before delivery.
- Completion notifications are delivered directly to a channel or queued for the next heartbeat.
@@ -143,8 +147,14 @@ Agent run completion is authoritative for active task records. A successful deta
- ACP tasks: backing ACP child session metadata disappeared.
- Subagent tasks: backing child session disappeared from the target agent store.
- Cron tasks: the cron runtime no longer tracks the job as active.
- CLI tasks: isolated child-session tasks use the child session; chat-backed CLI tasks use the live run context instead, so lingering channel/group/direct session rows do not keep them alive. Gateway-backed `openclaw agent` runs also finalize from their run result, so completed runs do not sit active until the sweeper marks them `lost`.
- Cron tasks: the cron runtime no longer tracks the job as active and durable
cron run history does not show a terminal result for that run. Offline CLI
audit does not treat its own empty in-process cron runtime state as authority.
- CLI tasks: isolated child-session tasks use the child session; chat-backed
CLI tasks use the live run context instead, so lingering
channel/group/direct session rows do not keep them alive. Gateway-backed
`openclaw agent` runs also finalize from their run result, so completed runs
do not sit active until the sweeper marks them `lost`.
## Delivery and notifications
@@ -236,7 +246,7 @@ openclaw tasks notify <lookup> state_changes
Reconciliation is runtime-aware:
- ACP/subagent tasks check their backing child session.
- Cron tasks check whether the cron runtime still owns the job.
- Cron tasks check whether the cron runtime still owns the job, then recover terminal status from persisted cron run logs/job state before falling back to `lost`. Only the Gateway process is authoritative for the in-memory cron active-job set; offline CLI audit uses durable history but does not mark a cron task lost solely because that local Set is empty.
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
Completion cleanup is also runtime-aware:

View File

@@ -5,14 +5,14 @@ read_when:
- Troubleshooting webhook pairing
- Configuring iMessage on macOS
title: "BlueBubbles"
sidebarTitle: "BlueBubbles"
---
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
## Bundled plugin
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not
need a separate `openclaw plugins install` step.
<Note>
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not need a separate `openclaw plugins install` step.
</Note>
## Overview
@@ -21,113 +21,119 @@ need a separate `openclaw plugins install` step.
- OpenClaw talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`).
- Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls.
- Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible).
- Auto-TTS replies that synthesize MP3 or CAF audio are delivered as iMessage
voice memo bubbles instead of plain file attachments.
- Auto-TTS replies that synthesize MP3 or CAF audio are delivered as iMessage voice memo bubbles instead of plain file attachments.
- Pairing/allowlist works the same way as other channels (`/channels/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes.
- Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying.
- Advanced features: edit, unsend, reply threading, message effects, group management.
## Quick start
1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
2. In the BlueBubbles config, enable the web API and set a password.
3. Run `openclaw onboard` and select BlueBubbles, or configure manually:
<Steps>
<Step title="Install BlueBubbles">
Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
</Step>
<Step title="Enable the web API">
In the BlueBubbles config, enable the web API and set a password.
</Step>
<Step title="Configure OpenClaw">
Run `openclaw onboard` and select BlueBubbles, or configure manually:
```json5
{
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://192.168.1.100:1234",
password: "example-password",
webhookPath: "/bluebubbles-webhook",
},
},
}
```
```json5
{
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://192.168.1.100:1234",
password: "example-password",
webhookPath: "/bluebubbles-webhook",
},
},
}
```
4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=<password>`).
5. Start the gateway; it will register the webhook handler and start pairing.
</Step>
<Step title="Point webhooks at the gateway">
Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=<password>`).
</Step>
<Step title="Start the gateway">
Start the gateway; it will register the webhook handler and start pairing.
</Step>
</Steps>
Security note:
<Warning>
**Security**
- Always set a webhook password.
- Webhook authentication is always required. OpenClaw rejects BlueBubbles webhook requests unless they include a password/guid that matches `channels.bluebubbles.password` (for example `?password=<password>` or `x-password`), regardless of loopback/proxy topology.
- Password authentication is checked before reading/parsing full webhook bodies.
</Warning>
## Keeping Messages.app alive (VM / headless setups)
Some macOS VM / always-on setups can end up with Messages.app going idle (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent.
Some macOS VM / always-on setups can end up with Messages.app going "idle" (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent.
### 1) Save the AppleScript
<Steps>
<Step title="Save the AppleScript">
Save this as `~/Scripts/poke-messages.scpt`:
Save this as:
```applescript
try
tell application "Messages"
if not running then
launch
end if
- `~/Scripts/poke-messages.scpt`
-- Touch the scripting interface to keep the process responsive.
set _chatCount to (count of chats)
end tell
on error
-- Ignore transient failures (first-run prompts, locked session, etc).
end try
```
Example script (non-interactive; does not steal focus):
</Step>
<Step title="Install a LaunchAgent">
Save this as `~/Library/LaunchAgents/com.user.poke-messages.plist`:
```applescript
try
tell application "Messages"
if not running then
launch
end if
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.poke-messages</string>
-- Touch the scripting interface to keep the process responsive.
set _chatCount to (count of chats)
end tell
on error
-- Ignore transient failures (first-run prompts, locked session, etc).
end try
```
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>/usr/bin/osascript &quot;$HOME/Scripts/poke-messages.scpt&quot;</string>
</array>
### 2) Install a LaunchAgent
<key>RunAtLoad</key>
<true/>
Save this as:
<key>StartInterval</key>
<integer>300</integer>
- `~/Library/LaunchAgents/com.user.poke-messages.plist`
<key>StandardOutPath</key>
<string>/tmp/poke-messages.log</string>
<key>StandardErrorPath</key>
<string>/tmp/poke-messages.err</string>
</dict>
</plist>
```
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.poke-messages</string>
This runs **every 300 seconds** and **on login**. The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-lc</string>
<string>/usr/bin/osascript &quot;$HOME/Scripts/poke-messages.scpt&quot;</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>300</integer>
<key>StandardOutPath</key>
<string>/tmp/poke-messages.log</string>
<key>StandardErrorPath</key>
<string>/tmp/poke-messages.err</string>
</dict>
</plist>
```
Notes:
- This runs **every 300 seconds** and **on login**.
- The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
Load it:
```bash
launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
```
</Step>
<Step title="Load it">
```bash
launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true
launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
```
</Step>
</Steps>
## Onboarding
@@ -139,11 +145,21 @@ openclaw onboard
The wizard prompts for:
- **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`)
- **Password** (required): API password from BlueBubbles Server settings
- **Webhook path** (optional): Defaults to `/bluebubbles-webhook`
- **DM policy**: pairing, allowlist, open, or disabled
- **Allow list**: Phone numbers, emails, or chat targets
<ParamField path="Server URL" type="string" required>
BlueBubbles server address (e.g., `http://192.168.1.100:1234`).
</ParamField>
<ParamField path="Password" type="string" required>
API password from BlueBubbles Server settings.
</ParamField>
<ParamField path="Webhook path" type="string" default="/bluebubbles-webhook">
Webhook endpoint path.
</ParamField>
<ParamField path="DM policy" type="string">
`pairing`, `allowlist`, `open`, or `disabled`.
</ParamField>
<ParamField path="Allow list" type="string[]">
Phone numbers, emails, or chat targets.
</ParamField>
You can also add BlueBubbles via CLI:
@@ -153,19 +169,20 @@ openclaw channels add bluebubbles --http-url http://192.168.1.100:1234 --passwor
## Access control (DMs + groups)
DMs:
- Default: `channels.bluebubbles.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
- `openclaw pairing list bluebubbles`
- `openclaw pairing approve bluebubbles <CODE>`
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
Groups:
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
<Tabs>
<Tab title="DMs">
- Default: `channels.bluebubbles.dmPolicy = "pairing"`.
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via:
- `openclaw pairing list bluebubbles`
- `openclaw pairing approve bluebubbles <CODE>`
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
</Tab>
<Tab title="Groups">
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
</Tab>
</Tabs>
### Contact name enrichment (macOS, optional)
@@ -361,21 +378,23 @@ BlueBubbles supports advanced message actions when enabled in config:
}
```
Available actions:
- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`). iMessage's native tapback set is `love`, `like`, `dislike`, `laugh`, `emphasize`, and `question`. When an agent picks an emoji outside that set (for example `👀`), the reaction tool falls back to `love` so the tapback still renders instead of failing the whole request. Configured ack reactions still validate strictly and error on unknown values.
- **edit**: Edit a sent message (`messageId`, `text`)
- **unsend**: Unsend a message (`messageId`)
- **reply**: Reply to a specific message (`messageId`, `text`, `to`)
- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`)
- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`)
- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
- **addParticipant**: Add someone to a group (`chatGuid`, `address`)
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`)
- **leaveGroup**: Leave a group chat (`chatGuid`)
- **upload-file**: Send media/files (`to`, `buffer`, `filename`, `asVoice`)
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
- Legacy alias: `sendAttachment` still works, but `upload-file` is the canonical action name.
<AccordionGroup>
<Accordion title="Available actions">
- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`). iMessage's native tapback set is `love`, `like`, `dislike`, `laugh`, `emphasize`, and `question`. When an agent picks an emoji outside that set (for example `👀`), the reaction tool falls back to `love` so the tapback still renders instead of failing the whole request. Configured ack reactions still validate strictly and error on unknown values.
- **edit**: Edit a sent message (`messageId`, `text`).
- **unsend**: Unsend a message (`messageId`).
- **reply**: Reply to a specific message (`messageId`, `text`, `to`).
- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`).
- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`).
- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
- **addParticipant**: Add someone to a group (`chatGuid`, `address`).
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`).
- **leaveGroup**: Leave a group chat (`chatGuid`).
- **upload-file**: Send media/files (`to`, `buffer`, `filename`, `asVoice`).
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
- Legacy alias: `sendAttachment` still works, but `upload-file` is the canonical action name.
</Accordion>
</AccordionGroup>
### Message IDs (short vs full)
@@ -406,54 +425,56 @@ The two webhooks arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coa
`channels.bluebubbles.coalesceSameSenderDms` opts a DM into merging consecutive same-sender webhooks into a single agent turn. Group chats continue to key per-message so multi-user turn structure is preserved.
### When to enable
<Tabs>
<Tab title="When to enable">
Enable when:
Enable when:
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
- Your users paste URLs, images, or long content alongside commands.
- You can accept the added DM turn latency (see below).
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
- Your users paste URLs, images, or long content alongside commands.
- You can accept the added DM turn latency (see below).
Leave disabled when:
Leave disabled when:
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
### Enabling
```json5
{
channels: {
bluebubbles: {
coalesceSameSenderDms: true, // opt in (default: false)
},
},
}
```
With the flag on and no explicit `messages.inbound.byChannel.bluebubbles`, the debounce window widens to **2500 ms** (the default for non-coalescing is 500 ms). The wider window is required — Apple's split-send cadence of 0.8-2.0 s does not fit in the tighter default.
To tune the window yourself:
```json5
{
messages: {
inbound: {
byChannel: {
// 2500 ms works for most setups; raise to 4000 ms if your Mac is slow
// or under memory pressure (observed gap can stretch past 2 s then).
bluebubbles: 2500,
</Tab>
<Tab title="Enabling">
```json5
{
channels: {
bluebubbles: {
coalesceSameSenderDms: true, // opt in (default: false)
},
},
},
},
}
```
}
```
### Trade-offs
With the flag on and no explicit `messages.inbound.byChannel.bluebubbles`, the debounce window widens to **2500 ms** (the default for non-coalescing is 500 ms). The wider window is required — Apple's split-send cadence of 0.8-2.0 s does not fit in the tighter default.
- **Added latency for DM control commands.** With the flag on, DM control-command messages (like `Dump`, `Save`, etc.) now wait up to the debounce window before dispatching, in case a payload webhook is coming. Group-chat commands keep instant dispatch.
- **Merged output is bounded** — merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source `messageId` still reaches inbound-dedupe so a later MessagePoller replay of any individual event is recognized as a duplicate.
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected.
To tune the window yourself:
```json5
{
messages: {
inbound: {
byChannel: {
// 2500 ms works for most setups; raise to 4000 ms if your Mac is slow
// or under memory pressure (observed gap can stretch past 2 s then).
bluebubbles: 2500,
},
},
},
}
```
</Tab>
<Tab title="Trade-offs">
- **Added latency for DM control commands.** With the flag on, DM control-command messages (like `Dump`, `Save`, etc.) now wait up to the debounce window before dispatching, in case a payload webhook is coming. Group-chat commands keep instant dispatch.
- **Merged output is bounded** — merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source `messageId` still reaches inbound-dedupe so a later MessagePoller replay of any individual event is recognized as a duplicate.
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected.
</Tab>
</Tabs>
### Scenarios and what the agent sees
@@ -470,27 +491,35 @@ To tune the window yourself:
If the flag is on and split-sends still arrive as two turns, check each layer:
1. **Config actually loaded.**
<AccordionGroup>
<Accordion title="Config actually loaded">
```
grep coalesceSameSenderDms ~/.openclaw/openclaw.json
```
```
grep coalesceSameSenderDms ~/.openclaw/openclaw.json
```
Then `openclaw gateway restart` — the flag is read at debouncer-registry creation.
Then `openclaw gateway restart` — the flag is read at debouncer-registry creation.
</Accordion>
<Accordion title="Debounce window wide enough for your setup">
Look at the BlueBubbles server log under `~/Library/Logs/bluebubbles-server/main.log`:
2. **Debounce window wide enough for your setup.** Look at the BlueBubbles server log under `~/Library/Logs/bluebubbles-server/main.log`:
```
grep -E "Dispatching event to webhook" main.log | tail -20
```
```
grep -E "Dispatching event to webhook" main.log | tail -20
```
Measure the gap between the `"Dump"`-style text dispatch and the `"https://..."; Attachments:` dispatch that follows. Raise `messages.inbound.byChannel.bluebubbles` to comfortably cover that gap.
Measure the gap between the `"Dump"`-style text dispatch and the `"https://..."; Attachments:` dispatch that follows. Raise `messages.inbound.byChannel.bluebubbles` to comfortably cover that gap.
3. **Session JSONL timestamps ≠ webhook arrival.** Session event timestamps (`~/.openclaw/agents/<id>/sessions/*.jsonl`) reflect when the gateway hands a message to the agent, **not** when the webhook arrived. A queued-second message tagged `[Queued messages while agent was busy]` means the first turn was still running when the second webhook arrived — the coalesce bucket had already flushed. Tune the window against the BB server log, not the session log.
4. **Memory pressure slowing reply dispatch.** On smaller machines (8 GB), agent turns can take long enough that the coalesce bucket flushes before the reply completes, and the URL lands as a queued second turn. Check `memory_pressure` and `ps -o rss -p $(pgrep openclaw-gateway)`; if the gateway is over ~500 MB RSS and the compressor is active, close other heavy processes or bump to a larger host.
5. **Reply-quote sends are a different path.** If the user tapped `Dump` as a **reply** to an existing URL-balloon (iMessage shows a "1 Reply" badge on the Dump bubble), the URL lives in `replyToBody`, not in a second webhook. Coalescing does not apply — that's a skill/prompt concern, not a debouncer concern.
</Accordion>
<Accordion title="Session JSONL timestamps ≠ webhook arrival">
Session event timestamps (`~/.openclaw/agents/<id>/sessions/*.jsonl`) reflect when the gateway hands a message to the agent, **not** when the webhook arrived. A queued-second message tagged `[Queued messages while agent was busy]` means the first turn was still running when the second webhook arrived — the coalesce bucket had already flushed. Tune the window against the BB server log, not the session log.
</Accordion>
<Accordion title="Memory pressure slowing reply dispatch">
On smaller machines (8 GB), agent turns can take long enough that the coalesce bucket flushes before the reply completes, and the URL lands as a queued second turn. Check `memory_pressure` and `ps -o rss -p $(pgrep openclaw-gateway)`; if the gateway is over ~500 MB RSS and the compressor is active, close other heavy processes or bump to a larger host.
</Accordion>
<Accordion title="Reply-quote sends are a different path">
If the user tapped `Dump` as a **reply** to an existing URL-balloon (iMessage shows a "1 Reply" badge on the Dump bubble), the URL lives in `replyToBody`, not in a second webhook. Coalescing does not apply — that's a skill/prompt concern, not a debouncer concern.
</Accordion>
</AccordionGroup>
## Block streaming
@@ -516,30 +545,40 @@ Control whether responses are sent as a single message or streamed in blocks:
Full configuration: [Configuration](/gateway/configuration)
Provider options:
- `channels.bluebubbles.enabled`: Enable/disable the channel.
- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
- `channels.bluebubbles.password`: API password.
- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
- `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`.
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
- `channels.bluebubbles.sendTimeoutMs`: Per-request timeout in ms for outbound text sends via `/api/v1/message/text` (default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example `45000` or `60000`. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override: `channels.bluebubbles.accounts.<accountId>.sendTimeoutMs`.
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
- `channels.bluebubbles.mediaMaxMb`: Inbound/outbound media cap in MB (default: 8).
- `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts.<accountId>.mediaLocalRoots`.
- `channels.bluebubbles.coalesceSameSenderDms`: Merge consecutive same-sender DM webhooks into one agent turn so Apple's text+URL split-send arrives as a single message (default: `false`). See [Coalescing split-send DMs](#coalescing-split-send-dms-command--url-in-one-composition) for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicit `messages.inbound.byChannel.bluebubbles`.
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
- `channels.bluebubbles.actions`: Enable/disable specific actions.
- `channels.bluebubbles.accounts`: Multi-account configuration.
<AccordionGroup>
<Accordion title="Connection and webhook">
- `channels.bluebubbles.enabled`: Enable/disable the channel.
- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
- `channels.bluebubbles.password`: API password.
- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
</Accordion>
<Accordion title="Access policy">
- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
- `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`.
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
</Accordion>
<Accordion title="Delivery and chunking">
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
- `channels.bluebubbles.sendTimeoutMs`: Per-request timeout in ms for outbound text sends via `/api/v1/message/text` (default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example `45000` or `60000`. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override: `channels.bluebubbles.accounts.<accountId>.sendTimeoutMs`.
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
</Accordion>
<Accordion title="Media and history">
- `channels.bluebubbles.mediaMaxMb`: Inbound/outbound media cap in MB (default: 8).
- `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts.<accountId>.mediaLocalRoots`.
- `channels.bluebubbles.coalesceSameSenderDms`: Merge consecutive same-sender DM webhooks into one agent turn so Apple's text+URL split-send arrives as a single message (default: `false`). See [Coalescing split-send DMs](#coalescing-split-send-dms-command--url-in-one-composition) for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicit `messages.inbound.byChannel.bluebubbles`.
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
</Accordion>
<Accordion title="Actions and accounts">
- `channels.bluebubbles.actions`: Enable/disable specific actions.
- `channels.bluebubbles.accounts`: Multi-account configuration.
</Accordion>
</AccordionGroup>
Related global options:
@@ -582,8 +621,8 @@ For general channel workflow reference, see [Channels](/channels) and the [Plugi
## 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

@@ -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

@@ -213,6 +213,11 @@ openclaw pairing list feishu
appId: "cli_xxx",
appSecret: "xxx",
name: "Primary bot",
tts: {
providers: {
openai: { voice: "shimmer" },
},
},
},
backup: {
appId: "cli_yyy",
@@ -227,6 +232,10 @@ openclaw pairing list feishu
```
`defaultAccount` controls which account is used when outbound APIs do not specify an `accountId`.
`accounts.<id>.tts` uses the same shape as `messages.tts` and deep-merges over
global TTS config, so multi-bot Feishu setups can keep shared provider
credentials globally while overriding only voice, model, persona, or auto mode
per account.
### Message limits
@@ -386,6 +395,7 @@ Full configuration: [Gateway configuration](/gateway/configuration)
| `channels.feishu.accounts.<id>.appId` | App ID | — |
| `channels.feishu.accounts.<id>.appSecret` | App Secret | — |
| `channels.feishu.accounts.<id>.domain` | Per-account domain override | `feishu` |
| `channels.feishu.accounts.<id>.tts` | Per-account TTS override | `messages.tts` |
| `channels.feishu.dmPolicy` | DM policy | `allowlist` |
| `channels.feishu.allowFrom` | DM allowlist (open_id list) | [BotOwnerId] |
| `channels.feishu.groupPolicy` | Group policy | `allowlist` |

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

@@ -83,6 +83,8 @@ That bootstrap token carries the built-in pairing bootstrap profile:
- bootstrap scope checks are role-prefixed, not one flat scope pool:
operator scope entries only satisfy operator requests, and non-operator roles
must still request scopes under their own role prefix
- later token rotation/revocation remains bounded by both the device's approved
role contract and the caller session's operator scopes
Treat the setup code like a password while it is valid.

View File

@@ -122,10 +122,10 @@ openclaw channels add --channel qqbot --account bot2 --token "222222222:secret-o
STT and TTS support two-level configuration with priority fallback:
| Setting | Plugin-specific | Framework fallback |
| ------- | -------------------- | ----------------------------- |
| STT | `channels.qqbot.stt` | `tools.media.audio.models[0]` |
| TTS | `channels.qqbot.tts` | `messages.tts` |
| Setting | Plugin-specific | Framework fallback |
| ------- | -------------------------------------------------------- | ----------------------------- |
| STT | `channels.qqbot.stt` | `tools.media.audio.models[0]` |
| TTS | `channels.qqbot.tts`, `channels.qqbot.accounts.<id>.tts` | `messages.tts` |
```json5
{
@@ -140,12 +140,23 @@ STT and TTS support two-level configuration with priority fallback:
model: "your-tts-model",
voice: "your-voice",
},
accounts: {
qq-main: {
tts: {
providers: {
openai: { voice: "shimmer" },
},
},
},
},
},
},
}
```
Set `enabled: false` on either to disable.
Account-level TTS overrides use the same shape as `messages.tts` and deep-merge
over the channel/global TTS config.
Inbound QQ voice attachments are exposed to agents as audio media metadata while
keeping raw voice files out of generic `MediaPaths`. `[[audio_as_voice]]` plain

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

@@ -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

@@ -162,7 +162,7 @@ configured OpenClaw model. If no configured model is usable yet, it can fall
back to local runtimes already present on the machine:
- Claude Code CLI: `claude-cli/claude-opus-4-7`
- Codex app-server harness: `openai/gpt-5.5` with `embeddedHarness.runtime: "codex"`
- Codex app-server harness: `openai/gpt-5.5` with `agentRuntime.id: "codex"`
- Codex CLI: `codex-cli/gpt-5.5`
The model-assisted planner cannot mutate config directly. It must translate the

View File

@@ -95,9 +95,9 @@ If you omit `--scope`, later reconnects with the stored rotated token reuse that
token's cached approved scopes. If you pass explicit `--scope` values, those
become the stored scope set for future cached-token reconnects.
Non-admin paired-device callers can rotate only their **own** device token.
Also, any explicit `--scope` values must stay within the caller session's own
operator scopes; rotation cannot mint a broader operator token than the caller
already has.
The target token scope set must stay within the caller session's own operator
scopes; rotation cannot mint or preserve a broader operator token than the
caller already has.
```
openclaw devices rotate --device <deviceId> --role operator --scope operator.read --scope operator.write
@@ -111,6 +111,8 @@ Revoke a device token for a specific role.
Non-admin paired-device callers can revoke only their **own** device token.
Revoking some other device's token requires `operator.admin`.
The target token scope set must also fit within the caller session's own
operator scopes; pairing-only callers cannot revoke admin/write operator tokens.
```
openclaw devices revoke --device <deviceId> --role node
@@ -135,12 +137,15 @@ Pass `--token` or `--password` explicitly. Missing explicit credentials is an er
- These commands require `operator.pairing` (or `operator.admin`) scope.
- `gateway.nodes.pairing.autoApproveCidrs` is an opt-in Gateway policy for
fresh node device pairing only; it does not change CLI approval authority.
- Token rotation stays inside the approved pairing role set and approved scope
baseline for that device. A stray cached token entry does not grant a new
rotate target.
- Token rotation and revocation stay inside the approved pairing role set and
approved scope baseline for that device. A stray cached token entry does not
grant a token-management target.
- For paired-device token sessions, cross-device management is admin-only:
`remove`, `rotate`, and `revoke` are self-only unless the caller has
`operator.admin`.
- Token mutation is also caller-scope contained: a pairing-only session cannot
rotate or revoke a token that currently carries `operator.admin` or
`operator.write`.
- `devices clear` is intentionally gated by `--yes`.
- If pairing scope is unavailable on local loopback (and no explicit `--url` is passed), list/approve can use a local pairing fallback.
- `devices approve` requires an explicit request ID before minting tokens; omitting `requestId` or passing `--latest` only previews the newest pending request.

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

@@ -5,91 +5,89 @@ read_when:
- Running `openclaw mcp serve`
- Managing OpenClaw-saved MCP server definitions
title: "MCP"
sidebarTitle: "MCP"
---
`openclaw mcp` has two jobs:
- run OpenClaw as an MCP server with `openclaw mcp serve`
- manage OpenClaw-owned outbound MCP server definitions with `list`, `show`,
`set`, and `unset`
- manage OpenClaw-owned outbound MCP server definitions with `list`, `show`, `set`, and `unset`
In other words:
- `serve` is OpenClaw acting as an MCP server
- `list` / `show` / `set` / `unset` is OpenClaw acting as an MCP client-side
registry for other MCP servers its runtimes may consume later
- `list` / `show` / `set` / `unset` is OpenClaw acting as an MCP client-side registry for other MCP servers its runtimes may consume later
Use [`openclaw acp`](/cli/acp) when OpenClaw should host a coding harness
session itself and route that runtime through ACP.
Use [`openclaw acp`](/cli/acp) when OpenClaw should host a coding harness session itself and route that runtime through ACP.
## OpenClaw as an MCP server
This is the `openclaw mcp serve` path.
## When to use `serve`
### When to use `serve`
Use `openclaw mcp serve` when:
- Codex, Claude Code, or another MCP client should talk directly to
OpenClaw-backed channel conversations
- Codex, Claude Code, or another MCP client should talk directly to OpenClaw-backed channel conversations
- you already have a local or remote OpenClaw Gateway with routed sessions
- you want one MCP server that works across OpenClaw's channel backends instead
of running separate per-channel bridges
- you want one MCP server that works across OpenClaw's channel backends instead of running separate per-channel bridges
Use [`openclaw acp`](/cli/acp) instead when OpenClaw should host the coding
runtime itself and keep the agent session inside OpenClaw.
Use [`openclaw acp`](/cli/acp) instead when OpenClaw should host the coding runtime itself and keep the agent session inside OpenClaw.
## How it works
### How it works
`openclaw mcp serve` starts a stdio MCP server. The MCP client owns that
process. While the client keeps the stdio session open, the bridge connects to a
local or remote OpenClaw Gateway over WebSocket and exposes routed channel
conversations over MCP.
`openclaw mcp serve` starts a stdio MCP server. The MCP client owns that process. While the client keeps the stdio session open, the bridge connects to a local or remote OpenClaw Gateway over WebSocket and exposes routed channel conversations over MCP.
Lifecycle:
<Steps>
<Step title="Client spawns the bridge">
The MCP client spawns `openclaw mcp serve`.
</Step>
<Step title="Bridge connects to Gateway">
The bridge connects to the OpenClaw Gateway over WebSocket.
</Step>
<Step title="Sessions become MCP conversations">
Routed sessions become MCP conversations and transcript/history tools.
</Step>
<Step title="Live events queue">
Live events are queued in memory while the bridge is connected.
</Step>
<Step title="Optional Claude push">
If Claude channel mode is enabled, the same session can also receive Claude-specific push notifications.
</Step>
</Steps>
1. the MCP client spawns `openclaw mcp serve`
2. the bridge connects to Gateway
3. routed sessions become MCP conversations and transcript/history tools
4. live events are queued in memory while the bridge is connected
5. if Claude channel mode is enabled, the same session can also receive
Claude-specific push notifications
<AccordionGroup>
<Accordion title="Important behavior">
- live queue state starts when the bridge connects
- older transcript history is read with `messages_read`
- Claude push notifications only exist while the MCP session is alive
- when the client disconnects, the bridge exits and the live queue is gone
- one-shot agent entry points such as `openclaw agent` and `openclaw infer model run` retire any bundled MCP runtimes they open when the reply completes, so repeated scripted runs do not accumulate stdio MCP child processes
- stdio MCP servers launched by OpenClaw (bundled or user-configured) are torn down as a process tree on shutdown, so child subprocesses started by the server do not survive after the parent stdio client exits
- deleting or resetting a session disposes that session's MCP clients through the shared runtime cleanup path, so there are no lingering stdio connections tied to a removed session
</Accordion>
</AccordionGroup>
Important behavior:
- live queue state starts when the bridge connects
- older transcript history is read with `messages_read`
- Claude push notifications only exist while the MCP session is alive
- when the client disconnects, the bridge exits and the live queue is gone
- one-shot agent entry points such as `openclaw agent` and
`openclaw infer model run` retire any bundled MCP runtimes they open when the
reply completes, so repeated scripted runs do not accumulate stdio MCP child
processes
- stdio MCP servers launched by OpenClaw (bundled or user-configured) are torn
down as a process tree on shutdown, so child subprocesses started by the
server do not survive after the parent stdio client exits
- deleting or resetting a session disposes that session's MCP clients through
the shared runtime cleanup path, so there are no lingering stdio connections
tied to a removed session
## Choose a client mode
### Choose a client mode
Use the same bridge in two different ways:
- Generic MCP clients: standard MCP tools only. Use `conversations_list`,
`messages_read`, `events_poll`, `events_wait`, `messages_send`, and the
approval tools.
- Claude Code: standard MCP tools plus the Claude-specific channel adapter.
Enable `--claude-channel-mode on` or leave the default `auto`.
<Tabs>
<Tab title="Generic MCP clients">
Standard MCP tools only. Use `conversations_list`, `messages_read`, `events_poll`, `events_wait`, `messages_send`, and the approval tools.
</Tab>
<Tab title="Claude Code">
Standard MCP tools plus the Claude-specific channel adapter. Enable `--claude-channel-mode on` or leave the default `auto`.
</Tab>
</Tabs>
Today, `auto` behaves the same as `on`. There is no client capability detection
yet.
<Note>
Today, `auto` behaves the same as `on`. There is no client capability detection yet.
</Note>
## What `serve` exposes
### What `serve` exposes
The bridge uses existing Gateway session route metadata to expose channel-backed
conversations. A conversation appears when OpenClaw already has session state
with a known route such as:
The bridge uses existing Gateway session route metadata to expose channel-backed conversations. A conversation appears when OpenClaw already has session state with a known route such as:
- `channel`
- recipient or destination metadata
@@ -104,101 +102,91 @@ This gives MCP clients one place to:
- send a reply back through the same route
- see approval requests that arrive while the bridge is connected
## Usage
### Usage
```bash
# Local Gateway
openclaw mcp serve
<Tabs>
<Tab title="Local Gateway">
```bash
openclaw mcp serve
```
</Tab>
<Tab title="Remote Gateway (token)">
```bash
openclaw mcp serve --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.token
```
</Tab>
<Tab title="Remote Gateway (password)">
```bash
openclaw mcp serve --url wss://gateway-host:18789 --password-file ~/.openclaw/gateway.password
```
</Tab>
<Tab title="Verbose / Claude off">
```bash
openclaw mcp serve --verbose
openclaw mcp serve --claude-channel-mode off
```
</Tab>
</Tabs>
# Remote Gateway
openclaw mcp serve --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.token
# Remote Gateway with password auth
openclaw mcp serve --url wss://gateway-host:18789 --password-file ~/.openclaw/gateway.password
# Enable verbose bridge logs
openclaw mcp serve --verbose
# Disable Claude-specific push notifications
openclaw mcp serve --claude-channel-mode off
```
## Bridge tools
### Bridge tools
The current bridge exposes these MCP tools:
- `conversations_list`
- `conversation_get`
- `messages_read`
- `attachments_fetch`
- `events_poll`
- `events_wait`
- `messages_send`
- `permissions_list_open`
- `permissions_respond`
<AccordionGroup>
<Accordion title="conversations_list">
Lists recent session-backed conversations that already have route metadata in Gateway session state.
### `conversations_list`
Useful filters:
Lists recent session-backed conversations that already have route metadata in
Gateway session state.
- `limit`
- `search`
- `channel`
- `includeDerivedTitles`
- `includeLastMessage`
Useful filters:
</Accordion>
<Accordion title="conversation_get">
Returns one conversation by `session_key`.
</Accordion>
<Accordion title="messages_read">
Reads recent transcript messages for one session-backed conversation.
</Accordion>
<Accordion title="attachments_fetch">
Extracts non-text message content blocks from one transcript message. This is a metadata view over transcript content, not a standalone durable attachment blob store.
</Accordion>
<Accordion title="events_poll">
Reads queued live events since a numeric cursor.
</Accordion>
<Accordion title="events_wait">
Long-polls until the next matching queued event arrives or a timeout expires.
- `limit`
- `search`
- `channel`
- `includeDerivedTitles`
- `includeLastMessage`
Use this when a generic MCP client needs near-real-time delivery without a Claude-specific push protocol.
### `conversation_get`
</Accordion>
<Accordion title="messages_send">
Sends text back through the same route already recorded on the session.
Returns one conversation by `session_key`.
Current behavior:
### `messages_read`
- requires an existing conversation route
- uses the session's channel, recipient, account id, and thread id
- sends text only
Reads recent transcript messages for one session-backed conversation.
</Accordion>
<Accordion title="permissions_list_open">
Lists pending exec/plugin approval requests the bridge has observed since it connected to the Gateway.
</Accordion>
<Accordion title="permissions_respond">
Resolves one pending exec/plugin approval request with:
### `attachments_fetch`
- `allow-once`
- `allow-always`
- `deny`
Extracts non-text message content blocks from one transcript message. This is a
metadata view over transcript content, not a standalone durable attachment blob
store.
</Accordion>
</AccordionGroup>
### `events_poll`
Reads queued live events since a numeric cursor.
### `events_wait`
Long-polls until the next matching queued event arrives or a timeout expires.
Use this when a generic MCP client needs near-real-time delivery without a
Claude-specific push protocol.
### `messages_send`
Sends text back through the same route already recorded on the session.
Current behavior:
- requires an existing conversation route
- uses the session's channel, recipient, account id, and thread id
- sends text only
### `permissions_list_open`
Lists pending exec/plugin approval requests the bridge has observed since it
connected to the Gateway.
### `permissions_respond`
Resolves one pending exec/plugin approval request with:
- `allow-once`
- `allow-always`
- `deny`
## Event model
### Event model
The bridge keeps an in-memory event queue while it is connected.
@@ -211,46 +199,43 @@ Current event types:
- `plugin_approval_resolved`
- `claude_permission_request`
Important limits:
<Warning>
- the queue is live-only; it starts when the MCP bridge starts
- `events_poll` and `events_wait` do not replay older Gateway history by
themselves
- `events_poll` and `events_wait` do not replay older Gateway history by themselves
- durable backlog should be read with `messages_read`
</Warning>
## Claude channel notifications
### Claude channel notifications
The bridge can also expose Claude-specific channel notifications. This is the
OpenClaw equivalent of a Claude Code channel adapter: standard MCP tools remain
available, but live inbound messages can also arrive as Claude-specific MCP
notifications.
The bridge can also expose Claude-specific channel notifications. This is the OpenClaw equivalent of a Claude Code channel adapter: standard MCP tools remain available, but live inbound messages can also arrive as Claude-specific MCP notifications.
Flags:
<Tabs>
<Tab title="off">
`--claude-channel-mode off`: standard MCP tools only.
</Tab>
<Tab title="on">
`--claude-channel-mode on`: enable Claude channel notifications.
</Tab>
<Tab title="auto (default)">
`--claude-channel-mode auto`: current default; same bridge behavior as `on`.
</Tab>
</Tabs>
- `--claude-channel-mode off`: standard MCP tools only
- `--claude-channel-mode on`: enable Claude channel notifications
- `--claude-channel-mode auto`: current default; same bridge behavior as `on`
When Claude channel mode is enabled, the server advertises Claude experimental
capabilities and can emit:
When Claude channel mode is enabled, the server advertises Claude experimental capabilities and can emit:
- `notifications/claude/channel`
- `notifications/claude/channel/permission`
Current bridge behavior:
- inbound `user` transcript messages are forwarded as
`notifications/claude/channel`
- inbound `user` transcript messages are forwarded as `notifications/claude/channel`
- Claude permission requests received over MCP are tracked in-memory
- if the linked conversation later sends `yes abcde` or `no abcde`, the bridge
converts that to `notifications/claude/channel/permission`
- these notifications are live-session only; if the MCP client disconnects,
there is no push target
- if the linked conversation later sends `yes abcde` or `no abcde`, the bridge converts that to `notifications/claude/channel/permission`
- these notifications are live-session only; if the MCP client disconnects, there is no push target
This is intentionally client-specific. Generic MCP clients should rely on the
standard polling tools.
This is intentionally client-specific. Generic MCP clients should rely on the standard polling tools.
## MCP client config
### MCP client config
Example stdio client config:
@@ -272,43 +257,52 @@ Example stdio client config:
}
```
For most generic MCP clients, start with the standard tool surface and ignore
Claude mode. Turn Claude mode on only for clients that actually understand the
Claude-specific notification methods.
For most generic MCP clients, start with the standard tool surface and ignore Claude mode. Turn Claude mode on only for clients that actually understand the Claude-specific notification methods.
## Options
### Options
`openclaw mcp serve` supports:
- `--url <url>`: Gateway WebSocket URL
- `--token <token>`: Gateway token
- `--token-file <path>`: read token from file
- `--password <password>`: Gateway password
- `--password-file <path>`: read password from file
- `--claude-channel-mode <auto|on|off>`: Claude notification mode
- `-v`, `--verbose`: verbose logs on stderr
<ParamField path="--url" type="string">
Gateway WebSocket URL.
</ParamField>
<ParamField path="--token" type="string">
Gateway token.
</ParamField>
<ParamField path="--token-file" type="string">
Read token from file.
</ParamField>
<ParamField path="--password" type="string">
Gateway password.
</ParamField>
<ParamField path="--password-file" type="string">
Read password from file.
</ParamField>
<ParamField path="--claude-channel-mode" type='"auto" | "on" | "off"'>
Claude notification mode.
</ParamField>
<ParamField path="-v, --verbose" type="boolean">
Verbose logs on stderr.
</ParamField>
<Tip>
Prefer `--token-file` or `--password-file` over inline secrets when possible.
</Tip>
## Security and trust boundary
### Security and trust boundary
The bridge does not invent routing. It only exposes conversations that Gateway
already knows how to route.
The bridge does not invent routing. It only exposes conversations that Gateway already knows how to route.
That means:
- sender allowlists, pairing, and channel-level trust still belong to the
underlying OpenClaw channel configuration
- sender allowlists, pairing, and channel-level trust still belong to the underlying OpenClaw channel configuration
- `messages_send` can only reply through an existing stored route
- approval state is live/in-memory only for the current bridge session
- bridge auth should use the same Gateway token or password controls you would
trust for any other remote Gateway client
- bridge auth should use the same Gateway token or password controls you would trust for any other remote Gateway client
If a conversation is missing from `conversations_list`, the usual cause is not
MCP configuration. It is missing or incomplete route metadata in the underlying
Gateway session.
If a conversation is missing from `conversations_list`, the usual cause is not MCP configuration. It is missing or incomplete route metadata in the underlying Gateway session.
## Testing
### Testing
OpenClaw ships a deterministic Docker smoke for this bridge:
@@ -320,79 +314,60 @@ That smoke:
- starts a seeded Gateway container
- starts a second container that spawns `openclaw mcp serve`
- verifies conversation discovery, transcript reads, attachment metadata reads,
live event queue behavior, and outbound send routing
- validates Claude-style channel and permission notifications over the real
stdio MCP bridge
- verifies conversation discovery, transcript reads, attachment metadata reads, live event queue behavior, and outbound send routing
- validates Claude-style channel and permission notifications over the real stdio MCP bridge
This is the fastest way to prove the bridge works without wiring a real
Telegram, Discord, or iMessage account into the test run.
This is the fastest way to prove the bridge works without wiring a real Telegram, Discord, or iMessage account into the test run.
For broader testing context, see [Testing](/help/testing).
## Troubleshooting
### Troubleshooting
### No conversations returned
<AccordionGroup>
<Accordion title="No conversations returned">
Usually means the Gateway session is not already routable. Confirm that the underlying session has stored channel/provider, recipient, and optional account/thread route metadata.
</Accordion>
<Accordion title="events_poll or events_wait misses older messages">
Expected. The live queue starts when the bridge connects. Read older transcript history with `messages_read`.
</Accordion>
<Accordion title="Claude notifications do not show up">
Check all of these:
Usually means the Gateway session is not already routable. Confirm that the
underlying session has stored channel/provider, recipient, and optional
account/thread route metadata.
- the client kept the stdio MCP session open
- `--claude-channel-mode` is `on` or `auto`
- the client actually understands the Claude-specific notification methods
- the inbound message happened after the bridge connected
### `events_poll` or `events_wait` misses older messages
Expected. The live queue starts when the bridge connects. Read older transcript
history with `messages_read`.
### Claude notifications do not show up
Check all of these:
- the client kept the stdio MCP session open
- `--claude-channel-mode` is `on` or `auto`
- the client actually understands the Claude-specific notification methods
- the inbound message happened after the bridge connected
### Approvals are missing
`permissions_list_open` only shows approval requests observed while the bridge
was connected. It is not a durable approval history API.
</Accordion>
<Accordion title="Approvals are missing">
`permissions_list_open` only shows approval requests observed while the bridge was connected. It is not a durable approval history API.
</Accordion>
</AccordionGroup>
## OpenClaw as an MCP client registry
This is the `openclaw mcp list`, `show`, `set`, and `unset` path.
These commands do not expose OpenClaw over MCP. They manage OpenClaw-owned MCP
server definitions under `mcp.servers` in OpenClaw config.
These commands do not expose OpenClaw over MCP. They manage OpenClaw-owned MCP server definitions under `mcp.servers` in OpenClaw config.
Those saved definitions are for runtimes that OpenClaw launches or configures
later, such as embedded Pi and other runtime adapters. OpenClaw stores the
definitions centrally so those runtimes do not need to keep their own duplicate
MCP server lists.
Those saved definitions are for runtimes that OpenClaw launches or configures later, such as embedded Pi and other runtime adapters. OpenClaw stores the definitions centrally so those runtimes do not need to keep their own duplicate MCP server lists.
Important behavior:
<AccordionGroup>
<Accordion title="Important behavior">
- these commands only read or write OpenClaw config
- they do not connect to the target MCP server
- they do not validate whether the command, URL, or remote transport is reachable right now
- runtime adapters decide which transport shapes they actually support at execution time
- embedded Pi exposes configured MCP tools in normal `coding` and `messaging` tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]` disables them explicitly
- session-scoped bundled MCP runtimes are reaped after `mcp.sessionIdleTtlMs` milliseconds of idle time (default 10 minutes; set `0` to disable) and one-shot embedded runs clean them up at run end
</Accordion>
</AccordionGroup>
- these commands only read or write OpenClaw config
- they do not connect to the target MCP server
- they do not validate whether the command, URL, or remote transport is
reachable right now
- runtime adapters decide which transport shapes they actually support at
execution time
- embedded Pi exposes configured MCP tools in normal `coding` and `messaging`
tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]`
disables them explicitly
- session-scoped bundled MCP runtimes are reaped after `mcp.sessionIdleTtlMs`
milliseconds of idle time (default 10 minutes; set `0` to disable) and
one-shot embedded runs clean them up at run end
Runtime adapters may normalize this shared registry into the shape their downstream client expects. For example, embedded Pi consumes OpenClaw `transport` values directly, while Claude Code and Gemini receive CLI-native `type` values such as `http`, `sse`, or `stdio`.
Runtime adapters may normalize this shared registry into the shape their
downstream client expects. For example, embedded Pi consumes OpenClaw
`transport` values directly, while Claude Code and Gemini receive CLI-native
`type` values such as `http`, `sse`, or `stdio`.
### Saved MCP server definitions
## Saved MCP server definitions
OpenClaw also stores a lightweight MCP server registry in config for surfaces
that want OpenClaw-managed MCP definitions.
OpenClaw also stores a lightweight MCP server registry in config for surfaces that want OpenClaw-managed MCP definitions.
Commands:
@@ -447,11 +422,13 @@ Launches a local child process and communicates over stdin/stdout.
| `env` | Extra environment variables |
| `cwd` / `workingDirectory` | Working directory for the process |
#### Stdio env safety filter
<Warning>
**Stdio env safety filter**
OpenClaw rejects interpreter-startup env keys that can alter how a stdio MCP server starts up before the first RPC, even if they appear in a server's `env` block. Blocked keys include `NODE_OPTIONS`, `PYTHONSTARTUP`, `PYTHONPATH`, `PERL5OPT`, `RUBYOPT`, `SHELLOPTS`, `PS4`, and similar runtime-control variables. Startup rejects these with a configuration error so they cannot inject an implicit prelude, swap the interpreter, or enable a debugger against the stdio process. Ordinary credential, proxy, and server-specific env vars (`GITHUB_TOKEN`, `HTTP_PROXY`, custom `*_API_KEY`, etc.) are unaffected.
If your MCP server genuinely needs one of the blocked variables, set it on the gateway host process instead of under the stdio server's `env`.
</Warning>
### SSE / HTTP transport
@@ -480,8 +457,7 @@ Example:
}
```
Sensitive values in `url` (userinfo) and `headers` are redacted in logs and
status output.
Sensitive values in `url` (userinfo) and `headers` are redacted in logs and status output.
### Streamable HTTP transport
@@ -513,8 +489,9 @@ Example:
}
```
These commands manage saved config only. They do not start the channel bridge,
open a live MCP client session, or prove the target server is reachable.
<Note>
These commands manage saved config only. They do not start the channel bridge, open a live MCP client session, or prove the target server is reachable.
</Note>
## Current limits
@@ -526,8 +503,7 @@ Current limits:
- no generic push protocol beyond the Claude-specific adapter
- no message edit or react tools yet
- HTTP/SSE/streamable-http transport connects to a single remote server; no multiplexed upstream yet
- `permissions_list_open` only includes approvals observed while the bridge is
connected
- `permissions_list_open` only includes approvals observed while the bridge is connected
## Related

View File

@@ -114,6 +114,12 @@ Use `openclaw node run` for a foreground node host (no service).
Service commands accept `--json` for machine-readable output.
The node host retries Gateway restart and network closes in-process. If the
Gateway reports a terminal token/password/bootstrap auth pause, the node host
logs the close detail and exits non-zero so launchd/systemd can restart it with
fresh config and credentials. Pairing-required pauses stay in the foreground
flow so the pending request can be approved.
## Pairing
The first connection creates a pending device pairing request (`role: node`) on the Gateway.

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,25 @@ 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 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 +206,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 +226,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 +242,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 +271,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 +282,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 +292,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 +304,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 +319,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

@@ -84,6 +84,10 @@ openclaw tasks maintenance [--apply] [--json]
```
Previews or applies task and Task Flow reconciliation, cleanup stamping, and pruning.
For cron tasks, reconciliation uses persisted run logs/job state before marking an
old active task `lost`, so completed cron runs do not become false audit errors
just because the in-memory Gateway runtime state is gone. Offline CLI audit is
not authoritative for the Gateway's process-local cron active-job set.
### `flow`

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

@@ -18,14 +18,24 @@ configuration. They are different layers:
| ------------- | ------------------------------------- | ------------------------------------------------------------------- |
| Provider | `openai`, `anthropic`, `openai-codex` | How OpenClaw authenticates, discovers models, and names model refs. |
| Model | `gpt-5.5`, `claude-opus-4-6` | The model selected for the agent turn. |
| Agent runtime | `pi`, `codex`, ACP-backed runtimes | The low level loop that executes the prepared turn. |
| Agent runtime | `pi`, `codex`, `claude-cli` | The low level loop or backend that executes the prepared turn. |
| Channel | Telegram, Discord, Slack, WhatsApp | Where messages enter and leave OpenClaw. |
You will also see the word **harness** in code and config. A harness is the
implementation that provides an agent runtime. For example, the bundled Codex
harness implements the `codex` runtime. The config key is still named
`embeddedHarness` for compatibility, but user-facing docs and status output
should generally say runtime.
You will also see the word **harness** in code. A harness is the implementation
that provides an agent runtime. For example, the bundled Codex harness
implements the `codex` runtime. Public config uses `agentRuntime.id`; `openclaw
doctor --fix` rewrites older runtime-policy keys to that shape.
There are two runtime families:
- **Embedded harnesses** run inside OpenClaw's prepared agent loop. Today this
is the built-in `pi` runtime plus registered plugin harnesses such as
`codex`.
- **CLI backends** run a local CLI process while keeping the model ref
canonical. For example, `anthropic/claude-opus-4-7` with
`agentRuntime.id: "claude-cli"` means "select the Anthropic model, execute
through Claude CLI." `claude-cli` is not an embedded harness id and must not
be passed to AgentHarness selection.
## Three things named Codex
@@ -34,7 +44,7 @@ Most confusion comes from three different surfaces sharing the Codex name:
| Surface | OpenClaw name/config | What it does |
| ---------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Codex OAuth provider route | `openai-codex/*` model refs | Uses ChatGPT/Codex subscription OAuth through the normal OpenClaw PI runner. |
| Native Codex app-server runtime | `embeddedHarness.runtime: "codex"` | Runs the embedded agent turn through the bundled Codex app-server harness. |
| Native Codex app-server runtime | `agentRuntime.id: "codex"` | Runs the embedded agent turn through the bundled Codex app-server harness. |
| Codex ACP adapter | `runtime: "acp"`, `agentId: "codex"` | Runs Codex through the external ACP/acpx control plane. Use only when ACP/acpx is explicitly asked. |
| Native Codex chat-control command set | `/codex ...` | Binds, resumes, steers, stops, and inspects Codex app-server threads from chat. |
| OpenAI Platform API route for GPT/Codex-style models | `openai/*` model refs | Uses OpenAI API-key auth unless a runtime override, such as `runtime: "codex"`, runs the turn. |
@@ -52,8 +62,8 @@ The common Codex setup uses the `openai` provider with the `codex` runtime:
agents: {
defaults: {
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
},
},
},
@@ -76,7 +86,7 @@ This is the agent-facing decision tree:
1. If the user asks for **Codex bind/control/thread/resume/steer/stop**, use the
native `/codex` command surface when the bundled `codex` plugin is enabled.
2. If the user asks for **Codex as the embedded runtime**, use
`openai/<model>` with `embeddedHarness.runtime: "codex"`.
`openai/<model>` with `agentRuntime.id: "codex"`.
3. If the user asks for **Codex OAuth/subscription auth on the normal OpenClaw
runner**, use `openai-codex/<model>` and leave the runtime as PI.
4. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
@@ -87,7 +97,7 @@ This is the agent-facing decision tree:
| You mean... | Use... |
| --------------------------------------- | -------------------------------------------- |
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
| Codex app-server embedded agent runtime | `embeddedHarness.runtime: "codex"` |
| Codex app-server embedded agent runtime | `agentRuntime.id: "codex"` |
| OpenAI Codex OAuth on the PI runner | `openai-codex/*` model refs |
| Claude Code or other external harness | ACP/acpx |
@@ -122,9 +132,9 @@ OpenClaw chooses an embedded runtime after provider and model resolution:
1. A session's recorded runtime wins. Config changes do not hot-switch an
existing transcript to a different native thread system.
2. `OPENCLAW_AGENT_RUNTIME=<id>` forces that runtime for new or reset sessions.
3. `agents.defaults.embeddedHarness.runtime` or
`agents.list[].embeddedHarness.runtime` can set `auto`, `pi`, or a registered
runtime id such as `codex`.
3. `agents.defaults.agentRuntime.id` or `agents.list[].agentRuntime.id` can set
`auto`, `pi`, a registered embedded harness id such as `codex`, or a
supported CLI backend alias such as `claude-cli`.
4. In `auto` mode, registered plugin runtimes can claim supported provider/model
pairs.
5. If no runtime claims a turn in `auto` mode and `fallback: "pi"` is set
@@ -137,6 +147,24 @@ Explicit plugin runtimes fail closed by default. For example,
a broader fallback setting, so an agent-level `runtime: "codex"` is not silently
routed back to PI just because defaults used `fallback: "pi"`.
CLI backend aliases are different from embedded harness ids. The preferred
Claude CLI form is:
```json5
{
agents: {
defaults: {
model: "anthropic/claude-opus-4-7",
agentRuntime: { id: "claude-cli" },
},
},
}
```
Legacy refs such as `claude-cli/claude-opus-4-7` remain supported for
compatibility, but new config should keep the provider/model canonical and put
the execution backend in `agentRuntime.id`.
`auto` mode is intentionally conservative. Plugin runtimes can claim
provider/model pairs they understand, but the Codex plugin does not claim the
`openai-codex` provider in `auto` mode. That keeps
@@ -146,7 +174,7 @@ moving subscription-auth configs onto the native app-server harness.
If `openclaw doctor` warns that the `codex` plugin is enabled while
`openai-codex/*` still routes through PI, treat that as a diagnosis, not a
migration. Keep the config unchanged when PI Codex OAuth is what you want.
Switch to `openai/<model>` plus `runtime: "codex"` only when you want native
Switch to `openai/<model>` plus `agentRuntime.id: "codex"` only when you want native
Codex app-server execution.
## Compatibility contract

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

@@ -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

@@ -4,35 +4,42 @@ read_when:
- You need a provider-by-provider model setup reference
- You want example configs or CLI onboarding commands for model providers
title: "Model providers"
sidebarTitle: "Model providers"
---
Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram). For model selection rules, see [Models](/concepts/models).
## Quick rules
- Model refs use `provider/model` (example: `opencode/claude-opus-4-6`).
- `agents.defaults.models` acts as an allowlist when set.
- CLI helpers: `openclaw onboard`, `openclaw models list`, `openclaw models set <provider/model>`.
- `models.providers.*.models[].contextWindow` is native model metadata; `contextTokens` is the effective runtime cap.
- Fallback rules, cooldown probes, and session-override persistence: [Model failover](/concepts/model-failover).
- OpenAI-family routes are prefix-specific: `openai/<model>` uses the direct
OpenAI API-key provider in PI, `openai-codex/<model>` uses Codex OAuth in PI,
and `openai/<model>` plus `agents.defaults.embeddedHarness.runtime: "codex"`
uses the native Codex app-server harness. See [OpenAI](/providers/openai)
and [Codex harness](/plugins/codex-harness). If the provider/runtime split is
confusing, read [Agent runtimes](/concepts/agent-runtimes) first.
- Plugin auto-enable follows that same boundary: `openai-codex/<model>` belongs
to the OpenAI plugin, while the Codex plugin is enabled by
`embeddedHarness.runtime: "codex"` or legacy `codex/<model>` refs.
- CLI runtimes use the same split: choose canonical model refs such as
`anthropic/claude-*`, `google/gemini-*`, or `openai/gpt-*`, then set
`agents.defaults.embeddedHarness.runtime` to `claude-cli`,
`google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate
back to canonical provider refs with the runtime recorded separately.
- GPT-5.5 is available through `openai/gpt-5.5` for direct API-key traffic,
`openai-codex/gpt-5.5` in PI for Codex OAuth, and the native Codex
app-server harness when `embeddedHarness.runtime: "codex"` is set.
<AccordionGroup>
<Accordion title="Model refs and CLI helpers">
- Model refs use `provider/model` (example: `opencode/claude-opus-4-6`).
- `agents.defaults.models` acts as an allowlist when set.
- CLI helpers: `openclaw onboard`, `openclaw models list`, `openclaw models set <provider/model>`.
- `models.providers.*.models[].contextWindow` is native model metadata; `contextTokens` is the effective runtime cap.
- Fallback rules, cooldown probes, and session-override persistence: [Model failover](/concepts/model-failover).
</Accordion>
<Accordion title="OpenAI provider/runtime split">
OpenAI-family routes are prefix-specific:
- `openai/<model>` uses the direct OpenAI API-key provider in PI.
- `openai-codex/<model>` uses Codex OAuth in PI.
- `openai/<model>` plus `agents.defaults.agentRuntime.id: "codex"` uses the native Codex app-server harness.
See [OpenAI](/providers/openai) and [Codex harness](/plugins/codex-harness). If the provider/runtime split is confusing, read [Agent runtimes](/concepts/agent-runtimes) first.
Plugin auto-enable follows the same boundary: `openai-codex/<model>` belongs to the OpenAI plugin, while the Codex plugin is enabled by `agentRuntime.id: "codex"` or legacy `codex/<model>` refs.
GPT-5.5 is available through `openai/gpt-5.5` for direct API-key traffic, `openai-codex/gpt-5.5` in PI for Codex OAuth, and the native Codex app-server harness when `agentRuntime.id: "codex"` is set.
</Accordion>
<Accordion title="CLI runtimes">
CLI runtimes use the same split: choose canonical model refs such as `anthropic/claude-*`, `google/gemini-*`, or `openai/gpt-*`, then set `agents.defaults.agentRuntime.id` to `claude-cli`, `google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate back to canonical provider refs with the runtime recorded separately.
</Accordion>
</AccordionGroup>
## Plugin-owned provider behavior
@@ -46,25 +53,28 @@ Provider runtime `capabilities` is shared runner metadata (provider family, tran
## API key rotation
- Supports generic provider rotation for selected providers.
- Configure multiple keys via:
- `OPENCLAW_LIVE_<PROVIDER>_KEY` (single live override, highest priority)
- `<PROVIDER>_API_KEYS` (comma or semicolon list)
- `<PROVIDER>_API_KEY` (primary key)
- `<PROVIDER>_API_KEY_*` (numbered list, e.g. `<PROVIDER>_API_KEY_1`)
- For Google providers, `GOOGLE_API_KEY` is also included as fallback.
- Key selection order preserves priority and deduplicates values.
- Requests are retried with the next key only on rate-limit responses (for
example `429`, `rate_limit`, `quota`, `resource exhausted`, `Too many
concurrent requests`, `ThrottlingException`, `concurrency limit reached`,
`workers_ai ... quota limit exceeded`, or periodic usage-limit messages).
- Non-rate-limit failures fail immediately; no key rotation is attempted.
- When all candidate keys fail, the final error is returned from the last attempt.
<AccordionGroup>
<Accordion title="Key sources and priority">
Configure multiple keys via:
- `OPENCLAW_LIVE_<PROVIDER>_KEY` (single live override, highest priority)
- `<PROVIDER>_API_KEYS` (comma or semicolon list)
- `<PROVIDER>_API_KEY` (primary key)
- `<PROVIDER>_API_KEY_*` (numbered list, e.g. `<PROVIDER>_API_KEY_1`)
For Google providers, `GOOGLE_API_KEY` is also included as fallback. Key selection order preserves priority and deduplicates values.
</Accordion>
<Accordion title="When rotation kicks in">
- Requests are retried with the next key only on rate-limit responses (for example `429`, `rate_limit`, `quota`, `resource exhausted`, `Too many concurrent requests`, `ThrottlingException`, `concurrency limit reached`, `workers_ai ... quota limit exceeded`, or periodic usage-limit messages).
- Non-rate-limit failures fail immediately; no key rotation is attempted.
- When all candidate keys fail, the final error is returned from the last attempt.
</Accordion>
</AccordionGroup>
## Built-in providers (pi-ai catalog)
OpenClaw ships with the piai catalog. These providers require **no**
`models.providers` config; just set auth + pick a model.
OpenClaw ships with the piai catalog. These providers require **no** `models.providers` config; just set auth + pick a model.
### OpenAI
@@ -72,8 +82,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Auth: `OPENAI_API_KEY`
- Optional rotation: `OPENAI_API_KEYS`, `OPENAI_API_KEY_1`, `OPENAI_API_KEY_2`, plus `OPENCLAW_LIVE_OPENAI_KEY` (single override)
- Example models: `openai/gpt-5.5`, `openai/gpt-5.4-mini`
- Verify account/model availability with `openclaw models list --provider openai`
if a specific install or API key behaves differently.
- Verify account/model availability with `openclaw models list --provider openai` if a specific install or API key behaves differently.
- CLI: `openclaw onboard --auth-choice openai-api-key`
- Default transport is `auto` (WebSocket-first, SSE fallback)
- Override per model via `agents.defaults.models["openai/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
@@ -81,11 +90,8 @@ OpenClaw ships with the piai catalog. These providers require **no**
- OpenAI priority processing can be enabled via `agents.defaults.models["openai/<model>"].params.serviceTier`
- `/fast` and `params.fastMode` map direct `openai/*` Responses requests to `service_tier=priority` on `api.openai.com`
- Use `params.serviceTier` when you want an explicit tier instead of the shared `/fast` toggle
- Hidden OpenClaw attribution headers (`originator`, `version`,
`User-Agent`) apply only on native OpenAI traffic to `api.openai.com`, not
generic OpenAI-compatible proxies
- Native OpenAI routes also keep Responses `store`, prompt-cache hints, and
OpenAI reasoning-compat payload shaping; proxy routes do not
- Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) apply only on native OpenAI traffic to `api.openai.com`, not generic OpenAI-compatible proxies
- Native OpenAI routes also keep Responses `store`, prompt-cache hints, and OpenAI reasoning-compat payload shaping; proxy routes do not
- `openai/gpt-5.3-codex-spark` is intentionally suppressed in OpenClaw because live OpenAI API requests reject it and the current Codex catalog does not expose it
```json5
@@ -102,8 +108,14 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Example model: `anthropic/claude-opus-4-6`
- CLI: `openclaw onboard --auth-choice apiKey`
- Direct public Anthropic requests support the shared `/fast` toggle and `params.fastMode`, including API-key and OAuth-authenticated traffic sent to `api.anthropic.com`; OpenClaw maps that to Anthropic `service_tier` (`auto` vs `standard_only`)
- Anthropic note: Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats Claude CLI reuse and `claude -p` usage as sanctioned for this integration unless Anthropic publishes a new policy.
- Anthropic setup-token remains available as a supported OpenClaw token path, but OpenClaw now prefers Claude CLI reuse and `claude -p` when available.
- Preferred Claude CLI config keeps the model ref canonical and selects the CLI
backend separately: `anthropic/claude-opus-4-7` with
`agents.defaults.agentRuntime.id: "claude-cli"`. Legacy
`claude-cli/claude-opus-4-7` refs still work for compatibility.
<Note>
Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats Claude CLI reuse and `claude -p` usage as sanctioned for this integration unless Anthropic publishes a new policy. Anthropic setup-token remains available as a supported OpenClaw token path, but OpenClaw now prefers Claude CLI reuse and `claude -p` when available.
</Note>
```json5
{
@@ -116,19 +128,15 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Provider: `openai-codex`
- Auth: OAuth (ChatGPT)
- PI model ref: `openai-codex/gpt-5.5`
- Native Codex app-server harness ref: `openai/gpt-5.5` with `agents.defaults.embeddedHarness.runtime: "codex"`
- Native Codex app-server harness ref: `openai/gpt-5.5` with `agents.defaults.agentRuntime.id: "codex"`
- Native Codex app-server harness docs: [Codex harness](/plugins/codex-harness)
- Legacy model refs: `codex/gpt-*`
- Plugin boundary: `openai-codex/*` loads the OpenAI plugin; the native Codex
app-server plugin is selected only by the Codex harness runtime or legacy
`codex/*` refs.
- Plugin boundary: `openai-codex/*` loads the OpenAI plugin; the native Codex app-server plugin is selected only by the Codex harness runtime or legacy `codex/*` refs.
- CLI: `openclaw onboard --auth-choice openai-codex` or `openclaw models auth login --provider openai-codex`
- Default transport is `auto` (WebSocket-first, SSE fallback)
- Override per PI model via `agents.defaults.models["openai-codex/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
- `params.serviceTier` is also forwarded on native Codex Responses requests (`chatgpt.com/backend-api`)
- Hidden OpenClaw attribution headers (`originator`, `version`,
`User-Agent`) are only attached on native Codex traffic to
`chatgpt.com/backend-api`, not generic OpenAI-compatible proxies
- Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) are only attached on native Codex traffic to `chatgpt.com/backend-api`, not generic OpenAI-compatible proxies
- Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`; OpenClaw maps that to `service_tier=priority`
- `openai-codex/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai-codex.models[].contextTokens`
- Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw.
@@ -154,9 +162,17 @@ OpenClaw ships with the piai catalog. These providers require **no**
### Other subscription-style hosted options
- [Qwen Cloud](/providers/qwen): Qwen Cloud provider surface plus Alibaba DashScope and Coding Plan endpoint mapping
- [MiniMax](/providers/minimax): MiniMax Coding Plan OAuth or API key access
- [GLM models](/providers/glm): Z.AI Coding Plan or general API endpoints
<CardGroup cols={3}>
<Card title="GLM models" href="/providers/glm">
Z.AI Coding Plan or general API endpoints.
</Card>
<Card title="MiniMax" href="/providers/minimax">
MiniMax Coding Plan OAuth or API key access.
</Card>
<Card title="Qwen Cloud" href="/providers/qwen">
Qwen Cloud provider surface plus Alibaba DashScope and Coding Plan endpoint mapping.
</Card>
</CardGroup>
### OpenCode
@@ -180,29 +196,54 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Example models: `google/gemini-3.1-pro-preview`, `google/gemini-3-flash-preview`
- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview`
- CLI: `openclaw onboard --auth-choice gemini-api-key`
- Thinking: `/think adaptive` uses Google dynamic thinking. Gemini 3/3.1 omit a fixed
`thinkingLevel`; Gemini 2.5 sends `thinkingBudget: -1`.
- Direct Gemini runs also accept `agents.defaults.models["google/<model>"].params.cachedContent`
(or legacy `cached_content`) to forward a provider-native
`cachedContents/...` handle; Gemini cache hits surface as OpenClaw `cacheRead`
- Thinking: `/think adaptive` uses Google dynamic thinking. Gemini 3/3.1 omit a fixed `thinkingLevel`; Gemini 2.5 sends `thinkingBudget: -1`.
- Direct Gemini runs also accept `agents.defaults.models["google/<model>"].params.cachedContent` (or legacy `cached_content`) to forward a provider-native `cachedContents/...` handle; Gemini cache hits surface as OpenClaw `cacheRead`
### Google Vertex and Gemini CLI
- Providers: `google-vertex`, `google-gemini-cli`
- Auth: Vertex uses gcloud ADC; Gemini CLI uses its OAuth flow
- Caution: Gemini CLI OAuth in OpenClaw is an unofficial integration. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
- Gemini CLI OAuth is shipped as part of the bundled `google` plugin.
- Install Gemini CLI first:
- `brew install gemini-cli`
- or `npm install -g @google/gemini-cli`
- Enable: `openclaw plugins enable google`
- Login: `openclaw models auth login --provider google-gemini-cli --set-default`
- Default model: `google-gemini-cli/gemini-3-flash-preview`
- Note: you do **not** paste a client id or secret into `openclaw.json`. The CLI login flow stores
tokens in auth profiles on the gateway host.
- If requests fail after login, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` on the gateway host.
- Gemini CLI JSON replies are parsed from `response`; usage falls back to
`stats`, with `stats.cached` normalized into OpenClaw `cacheRead`.
<Warning>
Gemini CLI OAuth in OpenClaw is an unofficial integration. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
</Warning>
Gemini CLI OAuth is shipped as part of the bundled `google` plugin.
<Steps>
<Step title="Install Gemini CLI">
<Tabs>
<Tab title="brew">
```bash
brew install gemini-cli
```
</Tab>
<Tab title="npm">
```bash
npm install -g @google/gemini-cli
```
</Tab>
</Tabs>
</Step>
<Step title="Enable plugin">
```bash
openclaw plugins enable google
```
</Step>
<Step title="Login">
```bash
openclaw models auth login --provider google-gemini-cli --set-default
```
Default model: `google-gemini-cli/gemini-3-flash-preview`. You do **not** paste a client id or secret into `openclaw.json`. The CLI login flow stores tokens in auth profiles on the gateway host.
</Step>
<Step title="Set project (if needed)">
If requests fail after login, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` on the gateway host.
</Step>
</Steps>
Gemini CLI JSON replies are parsed from `response`; usage falls back to `stats`, with `stats.cached` normalized into OpenClaw `cacheRead`.
### Z.AI (GLM)
@@ -217,8 +258,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Provider: `vercel-ai-gateway`
- Auth: `AI_GATEWAY_API_KEY`
- Example models: `vercel-ai-gateway/anthropic/claude-opus-4.6`,
`vercel-ai-gateway/moonshotai/kimi-k2.6`
- Example models: `vercel-ai-gateway/anthropic/claude-opus-4.6`, `vercel-ai-gateway/moonshotai/kimi-k2.6`
- CLI: `openclaw onboard --auth-choice ai-gateway-api-key`
### Kilo Gateway
@@ -228,11 +268,8 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Example model: `kilocode/kilo/auto`
- CLI: `openclaw onboard --auth-choice kilocode-api-key`
- Base URL: `https://api.kilo.ai/api/gateway/`
- Static fallback catalog ships `kilocode/kilo/auto`; live
`https://api.kilo.ai/api/gateway/models` discovery can expand the runtime
catalog further.
- Exact upstream routing behind `kilocode/kilo/auto` is owned by Kilo Gateway,
not hard-coded in OpenClaw.
- Static fallback catalog ships `kilocode/kilo/auto`; live `https://api.kilo.ai/api/gateway/models` discovery can expand the runtime catalog further.
- Exact upstream routing behind `kilocode/kilo/auto` is owned by Kilo Gateway, not hard-coded in OpenClaw.
See [/providers/kilocode](/providers/kilocode) for setup details.
@@ -264,28 +301,35 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
| xAI | `xai` | `XAI_API_KEY` | `xai/grok-4` |
| Xiaomi | `xiaomi` | `XIAOMI_API_KEY` | `xiaomi/mimo-v2-flash` |
Quirks worth knowing:
#### Quirks worth knowing
- **OpenRouter** applies its app-attribution headers and Anthropic `cache_control` markers only on verified `openrouter.ai` routes. DeepSeek, Moonshot, and ZAI refs are cache-TTL eligible for OpenRouter-managed prompt caching but do not receive Anthropic cache markers. As a proxy-style OpenAI-compatible path, it skips native-OpenAI-only shaping (`serviceTier`, Responses `store`, prompt-cache hints, OpenAI reasoning-compat). Gemini-backed refs keep proxy-Gemini thought-signature sanitation only.
- **Kilo Gateway** Gemini-backed refs follow the same proxy-Gemini sanitation path; `kilocode/kilo/auto` and other proxy-reasoning-unsupported refs skip proxy reasoning injection.
- **MiniMax** API-key onboarding writes explicit text-only M2.7 chat model definitions; image understanding stays on the plugin-owned `MiniMax-VL-01` media provider.
- **xAI** uses the xAI Responses path. `/fast` or `params.fastMode: true` rewrites `grok-3`, `grok-3-mini`, `grok-4`, and `grok-4-0709` to their `*-fast` variants. `tool_stream` defaults on; disable via `agents.defaults.models["xai/<model>"].params.tool_stream=false`.
- **Cerebras** GLM models use `zai-glm-4.7` / `zai-glm-4.6`; OpenAI-compatible base URL is `https://api.cerebras.ai/v1`.
<AccordionGroup>
<Accordion title="OpenRouter">
Applies its app-attribution headers and Anthropic `cache_control` markers only on verified `openrouter.ai` routes. DeepSeek, Moonshot, and ZAI refs are cache-TTL eligible for OpenRouter-managed prompt caching but do not receive Anthropic cache markers. As a proxy-style OpenAI-compatible path, it skips native-OpenAI-only shaping (`serviceTier`, Responses `store`, prompt-cache hints, OpenAI reasoning-compat). Gemini-backed refs keep proxy-Gemini thought-signature sanitation only.
</Accordion>
<Accordion title="Kilo Gateway">
Gemini-backed refs follow the same proxy-Gemini sanitation path; `kilocode/kilo/auto` and other proxy-reasoning-unsupported refs skip proxy reasoning injection.
</Accordion>
<Accordion title="MiniMax">
API-key onboarding writes explicit text-only M2.7 chat model definitions; image understanding stays on the plugin-owned `MiniMax-VL-01` media provider.
</Accordion>
<Accordion title="xAI">
Uses the xAI Responses path. `/fast` or `params.fastMode: true` rewrites `grok-3`, `grok-3-mini`, `grok-4`, and `grok-4-0709` to their `*-fast` variants. `tool_stream` defaults on; disable via `agents.defaults.models["xai/<model>"].params.tool_stream=false`.
</Accordion>
<Accordion title="Cerebras">
GLM models use `zai-glm-4.7` / `zai-glm-4.6`; OpenAI-compatible base URL is `https://api.cerebras.ai/v1`.
</Accordion>
</AccordionGroup>
## Providers via `models.providers` (custom/base URL)
Use `models.providers` (or `models.json`) to add **custom** providers or
OpenAI/Anthropiccompatible proxies.
Use `models.providers` (or `models.json`) to add **custom** providers or OpenAI/Anthropiccompatible proxies.
Many of the bundled provider plugins below already publish a default catalog.
Use explicit `models.providers.<id>` entries only when you want to override the
default base URL, headers, or model list.
Many of the bundled provider plugins below already publish a default catalog. Use explicit `models.providers.<id>` entries only when you want to override the default base URL, headers, or model list.
### Moonshot AI (Kimi)
Moonshot ships as a bundled provider plugin. Use the built-in provider by
default, and add an explicit `models.providers.moonshot` entry only when you
need to override the base URL or model metadata:
Moonshot ships as a bundled provider plugin. Use the built-in provider by default, and add an explicit `models.providers.moonshot` entry only when you need to override the base URL or model metadata:
- Provider: `moonshot`
- Auth: `MOONSHOT_API_KEY`
@@ -359,29 +403,26 @@ Volcano Engine (火山引擎) provides access to Doubao and other models in Chin
}
```
Onboarding defaults to the coding surface, but the general `volcengine/*`
catalog is registered at the same time.
Onboarding defaults to the coding surface, but the general `volcengine/*` catalog is registered at the same time.
In onboarding/configure model pickers, the Volcengine auth choice prefers both
`volcengine/*` and `volcengine-plan/*` rows. If those models are not loaded yet,
OpenClaw falls back to the unfiltered catalog instead of showing an empty
provider-scoped picker.
In onboarding/configure model pickers, the Volcengine auth choice prefers both `volcengine/*` and `volcengine-plan/*` rows. If those models are not loaded yet, OpenClaw falls back to the unfiltered catalog instead of showing an empty provider-scoped picker.
Available models:
- `volcengine/doubao-seed-1-8-251228` (Doubao Seed 1.8)
- `volcengine/doubao-seed-code-preview-251028`
- `volcengine/kimi-k2-5-260127` (Kimi K2.5)
- `volcengine/glm-4-7-251222` (GLM 4.7)
- `volcengine/deepseek-v3-2-251201` (DeepSeek V3.2 128K)
Coding models (`volcengine-plan`):
- `volcengine-plan/ark-code-latest`
- `volcengine-plan/doubao-seed-code`
- `volcengine-plan/kimi-k2.5`
- `volcengine-plan/kimi-k2-thinking`
- `volcengine-plan/glm-4.7`
<Tabs>
<Tab title="Standard models">
- `volcengine/doubao-seed-1-8-251228` (Doubao Seed 1.8)
- `volcengine/doubao-seed-code-preview-251028`
- `volcengine/kimi-k2-5-260127` (Kimi K2.5)
- `volcengine/glm-4-7-251222` (GLM 4.7)
- `volcengine/deepseek-v3-2-251201` (DeepSeek V3.2 128K)
</Tab>
<Tab title="Coding models (volcengine-plan)">
- `volcengine-plan/ark-code-latest`
- `volcengine-plan/doubao-seed-code`
- `volcengine-plan/kimi-k2.5`
- `volcengine-plan/kimi-k2-thinking`
- `volcengine-plan/glm-4.7`
</Tab>
</Tabs>
### BytePlus (International)
@@ -400,27 +441,24 @@ BytePlus ARK provides access to the same models as Volcano Engine for internatio
}
```
Onboarding defaults to the coding surface, but the general `byteplus/*`
catalog is registered at the same time.
Onboarding defaults to the coding surface, but the general `byteplus/*` catalog is registered at the same time.
In onboarding/configure model pickers, the BytePlus auth choice prefers both
`byteplus/*` and `byteplus-plan/*` rows. If those models are not loaded yet,
OpenClaw falls back to the unfiltered catalog instead of showing an empty
provider-scoped picker.
In onboarding/configure model pickers, the BytePlus auth choice prefers both `byteplus/*` and `byteplus-plan/*` rows. If those models are not loaded yet, OpenClaw falls back to the unfiltered catalog instead of showing an empty provider-scoped picker.
Available models:
- `byteplus/seed-1-8-251228` (Seed 1.8)
- `byteplus/kimi-k2-5-260127` (Kimi K2.5)
- `byteplus/glm-4-7-251222` (GLM 4.7)
Coding models (`byteplus-plan`):
- `byteplus-plan/ark-code-latest`
- `byteplus-plan/doubao-seed-code`
- `byteplus-plan/kimi-k2.5`
- `byteplus-plan/kimi-k2-thinking`
- `byteplus-plan/glm-4.7`
<Tabs>
<Tab title="Standard models">
- `byteplus/seed-1-8-251228` (Seed 1.8)
- `byteplus/kimi-k2-5-260127` (Kimi K2.5)
- `byteplus/glm-4-7-251222` (GLM 4.7)
</Tab>
<Tab title="Coding models (byteplus-plan)">
- `byteplus-plan/ark-code-latest`
- `byteplus-plan/doubao-seed-code`
- `byteplus-plan/kimi-k2.5`
- `byteplus-plan/kimi-k2-thinking`
- `byteplus-plan/glm-4.7`
</Tab>
</Tabs>
### Synthetic
@@ -458,14 +496,13 @@ MiniMax is configured via `models.providers` because it uses custom endpoints:
- MiniMax OAuth (CN): `--auth-choice minimax-cn-oauth`
- MiniMax API key (Global): `--auth-choice minimax-global-api`
- MiniMax API key (CN): `--auth-choice minimax-cn-api`
- Auth: `MINIMAX_API_KEY` for `minimax`; `MINIMAX_OAUTH_TOKEN` or
`MINIMAX_API_KEY` for `minimax-portal`
- Auth: `MINIMAX_API_KEY` for `minimax`; `MINIMAX_OAUTH_TOKEN` or `MINIMAX_API_KEY` for `minimax-portal`
See [/providers/minimax](/providers/minimax) for setup details, model options, and config snippets.
On MiniMax's Anthropic-compatible streaming path, OpenClaw disables thinking by
default unless you explicitly set it, and `/fast on` rewrites
`MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
<Note>
On MiniMax's Anthropic-compatible streaming path, OpenClaw disables thinking by default unless you explicitly set it, and `/fast on` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
</Note>
Plugin-owned capability split:
@@ -492,9 +529,7 @@ Then set a model (replace with one of the IDs returned by `http://localhost:1234
}
```
OpenClaw uses LM Studio's native `/api/v1/models` and `/api/v1/models/load`
for discovery + auto-load, with `/v1/chat/completions` for inference by default.
See [/providers/lmstudio](/providers/lmstudio) for setup and troubleshooting.
OpenClaw uses LM Studio's native `/api/v1/models` and `/api/v1/models/load` for discovery + auto-load, with `/v1/chat/completions` for inference by default. See [/providers/lmstudio](/providers/lmstudio) for setup and troubleshooting.
### Ollama
@@ -518,21 +553,17 @@ ollama pull llama3.3
}
```
Ollama is detected locally at `http://127.0.0.1:11434` when you opt in with
`OLLAMA_API_KEY`, and the bundled provider plugin adds Ollama directly to
`openclaw onboard` and the model picker. See [/providers/ollama](/providers/ollama)
for onboarding, cloud/local mode, and custom configuration.
Ollama is detected locally at `http://127.0.0.1:11434` when you opt in with `OLLAMA_API_KEY`, and the bundled provider plugin adds Ollama directly to `openclaw onboard` and the model picker. See [/providers/ollama](/providers/ollama) for onboarding, cloud/local mode, and custom configuration.
### vLLM
vLLM ships as a bundled provider plugin for local/self-hosted OpenAI-compatible
servers:
vLLM ships as a bundled provider plugin for local/self-hosted OpenAI-compatible servers:
- Provider: `vllm`
- Auth: Optional (depends on your server)
- Default base URL: `http://127.0.0.1:8000/v1`
To opt in to auto-discovery locally (any value works if your server doesnt enforce auth):
To opt in to auto-discovery locally (any value works if your server doesn't enforce auth):
```bash
export VLLM_API_KEY="vllm-local"
@@ -552,15 +583,13 @@ See [/providers/vllm](/providers/vllm) for details.
### SGLang
SGLang ships as a bundled provider plugin for fast self-hosted
OpenAI-compatible servers:
SGLang ships as a bundled provider plugin for fast self-hosted OpenAI-compatible servers:
- Provider: `sglang`
- Auth: Optional (depends on your server)
- Default base URL: `http://127.0.0.1:30000/v1`
To opt in to auto-discovery locally (any value works if your server does not
enforce auth):
To opt in to auto-discovery locally (any value works if your server does not enforce auth):
```bash
export SGLANG_API_KEY="sglang-local"
@@ -613,31 +642,28 @@ Example (OpenAIcompatible):
}
```
Notes:
<AccordionGroup>
<Accordion title="Default optional fields">
For custom providers, `reasoning`, `input`, `cost`, `contextWindow`, and `maxTokens` are optional. When omitted, OpenClaw defaults to:
- For custom providers, `reasoning`, `input`, `cost`, `contextWindow`, and `maxTokens` are optional.
When omitted, OpenClaw defaults to:
- `reasoning: false`
- `input: ["text"]`
- `cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }`
- `contextWindow: 200000`
- `maxTokens: 8192`
- Recommended: set explicit values that match your proxy/model limits.
- For `api: "openai-completions"` on non-native endpoints (any non-empty `baseUrl` whose host is not `api.openai.com`), OpenClaw forces `compat.supportsDeveloperRole: false` to avoid provider 400 errors for unsupported `developer` roles.
- Proxy-style OpenAI-compatible routes also skip native OpenAI-only request
shaping: no `service_tier`, no Responses `store`, no Completions `store`, no
prompt-cache hints, no OpenAI reasoning-compat payload shaping, and no hidden
OpenClaw attribution headers.
- For OpenAI-compatible Completions proxies that need vendor-specific fields,
set `agents.defaults.models["provider/model"].params.extra_body` (or
`extraBody`) to merge extra JSON into the outbound request body.
- For vLLM chat-template controls, set
`agents.defaults.models["provider/model"].params.chat_template_kwargs`.
OpenClaw automatically sends `enable_thinking: false` and
`force_nonempty_content: true` for `vllm/nemotron-3-*` when the session
thinking level is off.
- If `baseUrl` is empty/omitted, OpenClaw keeps the default OpenAI behavior (which resolves to `api.openai.com`).
- For safety, an explicit `compat.supportsDeveloperRole: true` is still overridden on non-native `openai-completions` endpoints.
- `reasoning: false`
- `input: ["text"]`
- `cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }`
- `contextWindow: 200000`
- `maxTokens: 8192`
Recommended: set explicit values that match your proxy/model limits.
</Accordion>
<Accordion title="Proxy-route shaping rules">
- For `api: "openai-completions"` on non-native endpoints (any non-empty `baseUrl` whose host is not `api.openai.com`), OpenClaw forces `compat.supportsDeveloperRole: false` to avoid provider 400 errors for unsupported `developer` roles.
- Proxy-style OpenAI-compatible routes also skip native OpenAI-only request shaping: no `service_tier`, no Responses `store`, no Completions `store`, no prompt-cache hints, no OpenAI reasoning-compat payload shaping, and no hidden OpenClaw attribution headers.
- For OpenAI-compatible Completions proxies that need vendor-specific fields, set `agents.defaults.models["provider/model"].params.extra_body` (or `extraBody`) to merge extra JSON into the outbound request body.
- For vLLM chat-template controls, set `agents.defaults.models["provider/model"].params.chat_template_kwargs`. OpenClaw automatically sends `enable_thinking: false` and `force_nonempty_content: true` for `vllm/nemotron-3-*` when the session thinking level is off.
- If `baseUrl` is empty/omitted, OpenClaw keeps the default OpenAI behavior (which resolves to `api.openai.com`).
- For safety, an explicit `compat.supportsDeveloperRole: true` is still overridden on non-native `openai-completions` endpoints.
</Accordion>
</AccordionGroup>
## CLI examples
@@ -651,7 +677,7 @@ See also: [Configuration](/gateway/configuration) for full configuration example
## Related
- [Models](/concepts/models) — model configuration and aliases
- [Model failover](/concepts/model-failover) — fallback chains and retry behavior
- [Configuration reference](/gateway/config-agents#agent-defaults) — model config keys
- [Model failover](/concepts/model-failover) — fallback chains and retry behavior
- [Models](/concepts/models) — model configuration and aliases
- [Providers](/providers) — per-provider setup guides

View File

@@ -13,7 +13,7 @@ Quick provider overview + examples: [/concepts/model-providers](/concepts/model-
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.embeddedHarness.runtime`. See
on `agents.defaults.agentRuntime.id`. See
[/concepts/agent-runtimes](/concepts/agent-runtimes).
## How model selection works

View File

@@ -59,9 +59,9 @@ pnpm qa:otel:smoke
That script starts a local OTLP/HTTP trace receiver, runs the
`otel-trace-smoke` QA scenario with the `diagnostics-otel` plugin enabled, then
decodes the exported protobuf spans and asserts the release-critical shape:
`openclaw.run`, `openclaw.model.call`, `openclaw.context.assembled`, and
`openclaw.message.delivery` must be present; model calls must not export
`StreamAbandoned` on successful turns; raw diagnostic IDs and
`openclaw.run`, `openclaw.harness.run`, `openclaw.model.call`,
`openclaw.context.assembled`, and `openclaw.message.delivery` must be present;
model calls must not export `StreamAbandoned` on successful turns; raw diagnostic IDs and
`openclaw.content.*` attributes must stay out of the trace. It writes
`otel-smoke-summary.json` next to the QA suite artifacts.

View File

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

View File

@@ -316,8 +316,8 @@ Time format in system prompt. Default: `auto` (OS preference).
fallbacks: ["openai/gpt-5.4-mini"],
},
params: { cacheRetention: "long" }, // global default provider params
embeddedHarness: {
runtime: "pi", // pi | auto | registered harness id, e.g. codex
agentRuntime: {
id: "pi", // pi | auto | registered harness id, e.g. codex
fallback: "pi", // pi | none
},
pdfMaxBytesMb: 10,
@@ -373,25 +373,25 @@ Time format in system prompt. Default: `auto` (OS preference).
- `params.extra_body`/`params.extraBody`: advanced pass-through JSON merged into `api: "openai-completions"` request bodies for OpenAI-compatible proxies. If it collides with generated request keys, the extra body wins; non-native completions routes still strip OpenAI-only `store` afterward.
- `params.chat_template_kwargs`: vLLM/OpenAI-compatible chat-template arguments merged into top-level `api: "openai-completions"` request bodies. For `vllm/nemotron-3-*` with thinking off, OpenClaw automatically sends `enable_thinking: false` and `force_nonempty_content: true`; explicit `chat_template_kwargs` override those defaults, and `extra_body.chat_template_kwargs` still has final precedence.
- `params.preserveThinking`: Z.AI-only opt-in for preserved thinking. When enabled and thinking is on, OpenClaw sends `thinking.clear_thinking: false` and replays prior `reasoning_content`; see [Z.AI thinking and preserved thinking](/providers/zai#thinking-and-preserved-thinking).
- `embeddedHarness`: default low-level embedded agent runtime policy. Omitted runtime defaults to OpenClaw Pi. Use `runtime: "pi"` to force the built-in PI harness, `runtime: "auto"` to let registered plugin harnesses claim supported models, or a registered harness id such as `runtime: "codex"`. Set `fallback: "none"` to disable automatic PI fallback. Explicit plugin runtimes such as `codex` fail closed by default unless you set `fallback: "pi"` in the same override scope. Keep model refs canonical as `provider/model`; select Codex, Claude CLI, Gemini CLI, and other execution backends through runtime config instead of legacy runtime provider prefixes. See [Agent runtimes](/concepts/agent-runtimes) for how this differs from provider/model selection.
- `agentRuntime`: default low-level agent runtime policy. Omitted id defaults to OpenClaw Pi. Use `id: "pi"` to force the built-in PI harness, `id: "auto"` to let registered plugin harnesses claim supported models, a registered harness id such as `id: "codex"`, or a supported CLI backend alias such as `id: "claude-cli"`. Set `fallback: "none"` to disable automatic PI fallback. Explicit plugin runtimes such as `codex` fail closed by default unless you set `fallback: "pi"` in the same override scope. Keep model refs canonical as `provider/model`; select Codex, Claude CLI, Gemini CLI, and other execution backends through runtime config instead of legacy runtime provider prefixes. See [Agent runtimes](/concepts/agent-runtimes) for how this differs from provider/model selection.
- Config writers that mutate these fields (for example `/models set`, `/models set-image`, and fallback add/remove commands) save canonical object form and preserve existing fallback lists when possible.
- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). Default: 4.
### `agents.defaults.embeddedHarness`
### `agents.defaults.agentRuntime`
`embeddedHarness` controls which low-level executor runs embedded agent turns.
Most deployments should keep the default OpenClaw Pi runtime.
Use it when a trusted plugin provides a native harness, such as the bundled
Codex app-server harness. For the mental model, see
[Agent runtimes](/concepts/agent-runtimes).
`agentRuntime` controls which low-level executor runs agent turns. Most
deployments should keep the default OpenClaw Pi runtime. Use it when a trusted
plugin provides a native harness, such as the bundled Codex app-server harness,
or when you want a supported CLI backend such as Claude CLI. For the mental
model, see [Agent runtimes](/concepts/agent-runtimes).
```json5
{
agents: {
defaults: {
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
fallback: "none",
},
},
@@ -399,12 +399,14 @@ Codex app-server harness. For the mental model, see
}
```
- `runtime`: `"auto"`, `"pi"`, or a registered plugin harness id. The bundled Codex plugin registers `codex`.
- `fallback`: `"pi"` or `"none"`. In `runtime: "auto"`, omitted fallback defaults to `"pi"` so old configs can keep using PI when no plugin harness claims a run. In explicit plugin runtime mode, such as `runtime: "codex"`, omitted fallback defaults to `"none"` so a missing harness fails instead of silently using PI. Runtime overrides do not inherit fallback from a broader scope; set `fallback: "pi"` alongside the explicit runtime when you intentionally want that compatibility fallback. Selected plugin harness failures always surface directly.
- Environment overrides: `OPENCLAW_AGENT_RUNTIME=<id|auto|pi>` overrides `runtime`; `OPENCLAW_AGENT_HARNESS_FALLBACK=pi|none` overrides fallback for that process.
- For Codex-only deployments, set `model: "openai/gpt-5.5"` and `embeddedHarness.runtime: "codex"`. You may also set `embeddedHarness.fallback: "none"` explicitly for readability; it is the default for explicit plugin runtimes.
- `id`: `"auto"`, `"pi"`, a registered plugin harness id, or a supported CLI backend alias. The bundled Codex plugin registers `codex`; the bundled Anthropic plugin provides the `claude-cli` CLI backend.
- `fallback`: `"pi"` or `"none"`. In `id: "auto"`, omitted fallback defaults to `"pi"` so old configs can keep using PI when no plugin harness claims a run. In explicit plugin runtime mode, such as `id: "codex"`, omitted fallback defaults to `"none"` so a missing harness fails instead of silently using PI. Runtime overrides do not inherit fallback from a broader scope; set `fallback: "pi"` alongside the explicit runtime when you intentionally want that compatibility fallback. Selected plugin harness failures always surface directly.
- Environment overrides: `OPENCLAW_AGENT_RUNTIME=<id|auto|pi>` overrides `id`; `OPENCLAW_AGENT_HARNESS_FALLBACK=pi|none` overrides fallback for that process.
- For Codex-only deployments, set `model: "openai/gpt-5.5"` and `agentRuntime.id: "codex"`. You may also set `agentRuntime.fallback: "none"` explicitly for readability; it is the default for explicit plugin runtimes.
- For Claude CLI deployments, prefer `model: "anthropic/claude-opus-4-7"` plus `agentRuntime.id: "claude-cli"`. Legacy `claude-cli/claude-opus-4-7` model refs still work for compatibility, but new config should keep provider/model selection canonical and put the execution backend in `agentRuntime.id`.
- Older runtime-policy keys are rewritten to `agentRuntime` by `openclaw doctor --fix`.
- Harness choice is pinned per session id after the first embedded run. Config/env changes affect new or reset sessions, not an existing transcript. Legacy sessions with transcript history but no recorded pin are treated as PI-pinned. `/status` reports the effective runtime, for example `Runtime: OpenClaw Pi Default` or `Runtime: OpenAI Codex`.
- This only controls the embedded chat harness. Media generation, vision, PDF, music, video, and TTS still use their provider/model settings.
- This only controls text agent-turn execution. Media generation, vision, PDF, music, video, and TTS still use their provider/model settings.
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
@@ -923,7 +925,7 @@ for provider examples and precedence.
thinkingDefault: "high", // per-agent thinking level override
reasoningDefault: "on", // per-agent reasoning visibility override
fastModeDefault: false, // per-agent fast mode override
embeddedHarness: { runtime: "auto", fallback: "pi" },
agentRuntime: { id: "auto", fallback: "pi" },
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
tts: {
providers: {
@@ -970,7 +972,7 @@ for provider examples and precedence.
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5).
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Applies when no per-message or session reasoning override is set.
- `fastModeDefault`: optional per-agent default for fast mode (`true | false`). Applies when no per-message or session fast-mode override is set.
- `embeddedHarness`: optional per-agent low-level harness policy override. Use `{ runtime: "codex" }` to make one agent Codex-only while other agents keep the default PI fallback in `auto` mode.
- `agentRuntime`: optional per-agent low-level runtime policy override. Use `{ id: "codex" }` to make one agent Codex-only while other agents keep the default PI fallback in `auto` mode.
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.

View File

@@ -5,11 +5,10 @@ read_when:
- Registering custom providers or overriding base URLs
- Setting up OpenAI-compatible self-hosted endpoints
title: "Configuration — tools and custom providers"
sidebarTitle: "Tools and custom providers"
---
`tools.*` config keys and custom provider / base-URL setup. For agents,
channels, and other top-level config keys, see
[Configuration reference](/gateway/configuration-reference).
`tools.*` config keys and custom provider / base-URL setup. For agents, channels, and other top-level config keys, see [Configuration reference](/gateway/configuration-reference).
## Tools
@@ -17,7 +16,9 @@ channels, and other top-level config keys, see
`tools.profile` sets a base allowlist before `tools.allow`/`tools.deny`:
<Note>
Local onboarding defaults new local configs to `tools.profile: "coding"` when unset (existing explicit profiles are preserved).
</Note>
| Profile | Includes |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
@@ -113,8 +114,7 @@ Controls elevated exec access outside the sandbox:
### `tools.loopDetection`
Tool-loop safety checks are **disabled by default**. Set `enabled: true` to activate detection.
Settings can be defined globally in `tools.loopDetection` and overridden per-agent at `agents.list[].tools.loopDetection`.
Tool-loop safety checks are **disabled by default**. Set `enabled: true` to activate detection. Settings can be defined globally in `tools.loopDetection` and overridden per-agent at `agents.list[].tools.loopDetection`.
```json5
{
@@ -135,14 +135,31 @@ Settings can be defined globally in `tools.loopDetection` and overridden per-age
}
```
- `historySize`: max tool-call history retained for loop analysis.
- `warningThreshold`: repeating no-progress pattern threshold for warnings.
- `criticalThreshold`: higher repeating threshold for blocking critical loops.
- `globalCircuitBreakerThreshold`: hard stop threshold for any no-progress run.
- `detectors.genericRepeat`: warn on repeated same-tool/same-args calls.
- `detectors.knownPollNoProgress`: warn/block on known poll tools (`process.poll`, `command_status`, etc.).
- `detectors.pingPong`: warn/block on alternating no-progress pair patterns.
- If `warningThreshold >= criticalThreshold` or `criticalThreshold >= globalCircuitBreakerThreshold`, validation fails.
<ParamField path="historySize" type="number">
Max tool-call history retained for loop analysis.
</ParamField>
<ParamField path="warningThreshold" type="number">
Repeating no-progress pattern threshold for warnings.
</ParamField>
<ParamField path="criticalThreshold" type="number">
Higher repeating threshold for blocking critical loops.
</ParamField>
<ParamField path="globalCircuitBreakerThreshold" type="number">
Hard stop threshold for any no-progress run.
</ParamField>
<ParamField path="detectors.genericRepeat" type="boolean">
Warn on repeated same-tool/same-args calls.
</ParamField>
<ParamField path="detectors.knownPollNoProgress" type="boolean">
Warn/block on known poll tools (`process.poll`, `command_status`, etc.).
</ParamField>
<ParamField path="detectors.pingPong" type="boolean">
Warn/block on alternating no-progress pair patterns.
</ParamField>
<Warning>
If `warningThreshold >= criticalThreshold` or `criticalThreshold >= globalCircuitBreakerThreshold`, validation fails.
</Warning>
### `tools.web`
@@ -208,34 +225,33 @@ Configures inbound media understanding (image/audio/video):
}
```
<Accordion title="Media model entry fields">
<AccordionGroup>
<Accordion title="Media model entry fields">
**Provider entry** (`type: "provider"` or omitted):
**Provider entry** (`type: "provider"` or omitted):
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc.)
- `model`: model id override
- `profile` / `preferredProfile`: `auth-profiles.json` profile selection
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc.)
- `model`: model id override
- `profile` / `preferredProfile`: `auth-profiles.json` profile selection
**CLI entry** (`type: "cli"`):
**CLI entry** (`type: "cli"`):
- `command`: executable to run
- `args`: templated args (supports `{{MediaPath}}`, `{{Prompt}}`, `{{MaxChars}}`, etc.)
- `command`: executable to run
- `args`: templated args (supports `{{MediaPath}}`, `{{Prompt}}`, `{{MaxChars}}`, etc.)
**Common fields:**
**Common fields:**
- `capabilities`: optional list (`image`, `audio`, `video`). Defaults: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
- Failures fall back to the next entry.
- `capabilities`: optional list (`image`, `audio`, `video`). Defaults: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
- Failures fall back to the next entry.
Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`.
Provider auth follows standard order: `auth-profiles.json` → env vars → `models.providers.*.apiKey`.
**Async completion fields:**
**Async completion fields:**
- `asyncCompletion.directSend`: when `true`, completed async `music_generate` and `video_generate` tasks try direct channel delivery first. Default: `false` (legacy requester-session wake/model-delivery path).
- `asyncCompletion.directSend`: when `true`, completed async `music_generate`
and `video_generate` tasks try direct channel delivery first. Default: `false`
(legacy requester-session wake/model-delivery path).
</Accordion>
</Accordion>
</AccordionGroup>
### `tools.agentToAgent`
@@ -267,13 +283,15 @@ Default: `tree` (current session + sessions spawned by it, such as subagents).
}
```
Notes:
- `self`: only the current session key.
- `tree`: current session + sessions spawned by the current session (subagents).
- `agent`: any session belonging to the current agent id (can include other users if you run per-sender sessions under the same agent id).
- `all`: any session. Cross-agent targeting still requires `tools.agentToAgent`.
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility="spawned"`, visibility is forced to `tree` even if `tools.sessions.visibility="all"`.
<AccordionGroup>
<Accordion title="Visibility scopes">
- `self`: only the current session key.
- `tree`: current session + sessions spawned by the current session (subagents).
- `agent`: any session belonging to the current agent id (can include other users if you run per-sender sessions under the same agent id).
- `all`: any session. Cross-agent targeting still requires `tools.agentToAgent`.
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility="spawned"`, visibility is forced to `tree` even if `tools.sessions.visibility="all"`.
</Accordion>
</AccordionGroup>
### `tools.sessions_spawn`
@@ -295,14 +313,16 @@ Controls inline attachment support for `sessions_spawn`.
}
```
Notes:
- Attachments are only supported for `runtime: "subagent"`. ACP runtime rejects them.
- Files are materialized into the child workspace at `.openclaw/attachments/<uuid>/` with a `.manifest.json`.
- Attachment content is automatically redacted from transcript persistence.
- Base64 inputs are validated with strict alphabet/padding checks and a pre-decode size guard.
- File permissions are `0700` for directories and `0600` for files.
- Cleanup follows the `cleanup` policy: `delete` always removes attachments; `keep` retains them only when `retainOnSessionKeep: true`.
<AccordionGroup>
<Accordion title="Attachment notes">
- Attachments are only supported for `runtime: "subagent"`. ACP runtime rejects them.
- Files are materialized into the child workspace at `.openclaw/attachments/<uuid>/` with a `.manifest.json`.
- Attachment content is automatically redacted from transcript persistence.
- Base64 inputs are validated with strict alphabet/padding checks and a pre-decode size guard.
- File permissions are `0700` for directories and `0600` for files.
- Cleanup follows the `cleanup` policy: `delete` always removes attachments; `keep` retains them only when `retainOnSessionKeep: true`.
</Accordion>
</AccordionGroup>
<a id="toolsexperimental"></a>
@@ -320,8 +340,6 @@ Experimental built-in tool flags. Default off unless a strict-agentic GPT-5 auto
}
```
Notes:
- `planTool`: enables the structured `update_plan` tool for non-trivial multi-step work tracking.
- Default: `false` unless `agents.defaults.embeddedPi.executionContract` (or a per-agent override) is set to `"strict-agentic"` for an OpenAI or OpenAI Codex GPT-5-family run. Set `true` to force the tool on outside that scope, or `false` to keep it off even for strict-agentic GPT-5 runs.
- When enabled, the system prompt also adds usage guidance so the model only uses it for substantial work and keeps at most one step `in_progress`.
@@ -382,286 +400,281 @@ OpenClaw uses the built-in model catalog. Add custom providers via `models.provi
}
```
- Use `authHeader: true` + `headers` for custom auth needs.
- Override agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`, a legacy environment variable alias).
- Merge precedence for matching provider IDs:
- Non-empty agent `models.json` `baseUrl` values win.
- Non-empty agent `apiKey` values win 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 `models.providers` in config.
- Matching model `contextWindow`/`maxTokens` use the higher value between explicit config and implicit catalog values.
- Matching model `contextTokens` preserves an explicit runtime cap when present; use it to limit effective context without changing native model metadata.
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json`.
- Marker persistence is source-authoritative: markers are written from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
<AccordionGroup>
<Accordion title="Auth and merge precedence">
- Use `authHeader: true` + `headers` for custom auth needs.
- Override agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`, a legacy environment variable alias).
- Merge precedence for matching provider IDs:
- Non-empty agent `models.json` `baseUrl` values win.
- Non-empty agent `apiKey` values win 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 `models.providers` in config.
- Matching model `contextWindow`/`maxTokens` use the higher value between explicit config and implicit catalog values.
- Matching model `contextTokens` preserves an explicit runtime cap when present; use it to limit effective context without changing native model metadata.
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json`.
- Marker persistence is source-authoritative: markers are written from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
</Accordion>
</AccordionGroup>
### Provider field details
- `models.mode`: provider catalog behavior (`merge` or `replace`).
- `models.providers`: custom provider map keyed by provider id.
- Safe edits: use `openclaw config set models.providers.<id> '<json>' --strict-json --merge` or `openclaw config set models.providers.<id>.models '<json-array>' --strict-json --merge` for additive updates. `config set` refuses destructive replacements unless you pass `--replace`.
- `models.providers.*.api`: request adapter (`openai-completions`, `openai-responses`, `anthropic-messages`, `google-generative-ai`, etc).
- `models.providers.*.apiKey`: provider credential (prefer SecretRef/env substitution).
- `models.providers.*.auth`: auth strategy (`api-key`, `token`, `oauth`, `aws-sdk`).
- `models.providers.*.injectNumCtxForOpenAICompat`: for Ollama + `openai-completions`, inject `options.num_ctx` into requests (default: `true`).
- `models.providers.*.authHeader`: force credential transport in the `Authorization` header when required.
- `models.providers.*.baseUrl`: upstream API base URL.
- `models.providers.*.headers`: extra static headers for proxy/tenant routing.
- `models.providers.*.request`: transport overrides for model-provider HTTP requests.
- `request.headers`: extra headers (merged with provider defaults). Values accept SecretRef.
- `request.auth`: auth strategy override. Modes: `"provider-default"` (use provider's built-in auth), `"authorization-bearer"` (with `token`), `"header"` (with `headerName`, `value`, optional `prefix`).
- `request.proxy`: HTTP proxy override. Modes: `"env-proxy"` (use `HTTP_PROXY`/`HTTPS_PROXY` env vars), `"explicit-proxy"` (with `url`). Both modes accept an optional `tls` sub-object.
- `request.tls`: TLS override for direct connections. Fields: `ca`, `cert`, `key`, `passphrase` (all accept SecretRef), `serverName`, `insecureSkipVerify`.
- `request.allowPrivateNetwork`: when `true`, allow HTTPS to `baseUrl` when DNS resolves to private, CGNAT, or similar ranges, via the provider HTTP fetch guard (operator opt-in for trusted self-hosted OpenAI-compatible endpoints). WebSocket uses the same `request` for headers/TLS but not that fetch SSRF gate. Default `false`.
- `models.providers.*.models`: explicit provider model catalog entries.
- `models.providers.*.models.*.contextWindow`: native model context window metadata.
- `models.providers.*.models.*.contextTokens`: optional runtime context cap. Use this when you want a smaller effective context budget than the model's native `contextWindow`; `openclaw models list` shows both values when they differ.
- `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior.
- `models.providers.*.models.*.compat.requiresStringContent`: optional compatibility hint for string-only OpenAI-compatible chat endpoints. When `true`, OpenClaw flattens pure text `messages[].content` arrays into plain strings before sending the request.
- `plugins.entries.amazon-bedrock.config.discovery`: Bedrock auto-discovery settings root.
- `plugins.entries.amazon-bedrock.config.discovery.enabled`: turn implicit discovery on/off.
- `plugins.entries.amazon-bedrock.config.discovery.region`: AWS region for discovery.
- `plugins.entries.amazon-bedrock.config.discovery.providerFilter`: optional provider-id filter for targeted discovery.
- `plugins.entries.amazon-bedrock.config.discovery.refreshInterval`: polling interval for discovery refresh.
- `plugins.entries.amazon-bedrock.config.discovery.defaultContextWindow`: fallback context window for discovered models.
- `plugins.entries.amazon-bedrock.config.discovery.defaultMaxTokens`: fallback max output tokens for discovered models.
<AccordionGroup>
<Accordion title="Top-level catalog">
- `models.mode`: provider catalog behavior (`merge` or `replace`).
- `models.providers`: custom provider map keyed by provider id.
- Safe edits: use `openclaw config set models.providers.<id> '<json>' --strict-json --merge` or `openclaw config set models.providers.<id>.models '<json-array>' --strict-json --merge` for additive updates. `config set` refuses destructive replacements unless you pass `--replace`.
</Accordion>
<Accordion title="Provider connection and auth">
- `models.providers.*.api`: request adapter (`openai-completions`, `openai-responses`, `anthropic-messages`, `google-generative-ai`, etc).
- `models.providers.*.apiKey`: provider credential (prefer SecretRef/env substitution).
- `models.providers.*.auth`: auth strategy (`api-key`, `token`, `oauth`, `aws-sdk`).
- `models.providers.*.injectNumCtxForOpenAICompat`: for Ollama + `openai-completions`, inject `options.num_ctx` into requests (default: `true`).
- `models.providers.*.authHeader`: force credential transport in the `Authorization` header when required.
- `models.providers.*.baseUrl`: upstream API base URL.
- `models.providers.*.headers`: extra static headers for proxy/tenant routing.
</Accordion>
<Accordion title="Request transport overrides">
`models.providers.*.request`: transport overrides for model-provider HTTP requests.
- `request.headers`: extra headers (merged with provider defaults). Values accept SecretRef.
- `request.auth`: auth strategy override. Modes: `"provider-default"` (use provider's built-in auth), `"authorization-bearer"` (with `token`), `"header"` (with `headerName`, `value`, optional `prefix`).
- `request.proxy`: HTTP proxy override. Modes: `"env-proxy"` (use `HTTP_PROXY`/`HTTPS_PROXY` env vars), `"explicit-proxy"` (with `url`). Both modes accept an optional `tls` sub-object.
- `request.tls`: TLS override for direct connections. Fields: `ca`, `cert`, `key`, `passphrase` (all accept SecretRef), `serverName`, `insecureSkipVerify`.
- `request.allowPrivateNetwork`: when `true`, allow HTTPS to `baseUrl` when DNS resolves to private, CGNAT, or similar ranges, via the provider HTTP fetch guard (operator opt-in for trusted self-hosted OpenAI-compatible endpoints). WebSocket uses the same `request` for headers/TLS but not that fetch SSRF gate. Default `false`.
</Accordion>
<Accordion title="Model catalog entries">
- `models.providers.*.models`: explicit provider model catalog entries.
- `models.providers.*.models.*.contextWindow`: native model context window metadata.
- `models.providers.*.models.*.contextTokens`: optional runtime context cap. Use this when you want a smaller effective context budget than the model's native `contextWindow`; `openclaw models list` shows both values when they differ.
- `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior.
- `models.providers.*.models.*.compat.requiresStringContent`: optional compatibility hint for string-only OpenAI-compatible chat endpoints. When `true`, OpenClaw flattens pure text `messages[].content` arrays into plain strings before sending the request.
</Accordion>
<Accordion title="Amazon Bedrock discovery">
- `plugins.entries.amazon-bedrock.config.discovery`: Bedrock auto-discovery settings root.
- `plugins.entries.amazon-bedrock.config.discovery.enabled`: turn implicit discovery on/off.
- `plugins.entries.amazon-bedrock.config.discovery.region`: AWS region for discovery.
- `plugins.entries.amazon-bedrock.config.discovery.providerFilter`: optional provider-id filter for targeted discovery.
- `plugins.entries.amazon-bedrock.config.discovery.refreshInterval`: polling interval for discovery refresh.
- `plugins.entries.amazon-bedrock.config.discovery.defaultContextWindow`: fallback context window for discovered models.
- `plugins.entries.amazon-bedrock.config.discovery.defaultMaxTokens`: fallback max output tokens for discovered models.
</Accordion>
</AccordionGroup>
### Provider examples
<Accordion title="Cerebras (GLM 4.6 / 4.7)">
```json5
{
env: { CEREBRAS_API_KEY: "sk-..." },
agents: {
defaults: {
model: {
primary: "cerebras/zai-glm-4.7",
fallbacks: ["cerebras/zai-glm-4.6"],
<AccordionGroup>
<Accordion title="Cerebras (GLM 4.6 / 4.7)">
```json5
{
env: { CEREBRAS_API_KEY: "sk-..." },
agents: {
defaults: {
model: {
primary: "cerebras/zai-glm-4.7",
fallbacks: ["cerebras/zai-glm-4.6"],
},
models: {
"cerebras/zai-glm-4.7": { alias: "GLM 4.7 (Cerebras)" },
"cerebras/zai-glm-4.6": { alias: "GLM 4.6 (Cerebras)" },
},
},
},
models: {
"cerebras/zai-glm-4.7": { alias: "GLM 4.7 (Cerebras)" },
"cerebras/zai-glm-4.6": { alias: "GLM 4.6 (Cerebras)" },
},
},
},
models: {
mode: "merge",
providers: {
cerebras: {
baseUrl: "https://api.cerebras.ai/v1",
apiKey: "${CEREBRAS_API_KEY}",
api: "openai-completions",
models: [
{ id: "zai-glm-4.7", name: "GLM 4.7 (Cerebras)" },
{ id: "zai-glm-4.6", name: "GLM 4.6 (Cerebras)" },
],
},
},
},
}
```
Use `cerebras/zai-glm-4.7` for Cerebras; `zai/glm-4.7` for Z.AI direct.
</Accordion>
<Accordion title="OpenCode">
```json5
{
agents: {
defaults: {
model: { primary: "opencode/claude-opus-4-6" },
models: { "opencode/claude-opus-4-6": { alias: "Opus" } },
},
},
}
```
Set `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`). Use `opencode/...` refs for the Zen catalog or `opencode-go/...` refs for the Go catalog. Shortcut: `openclaw onboard --auth-choice opencode-zen` or `openclaw onboard --auth-choice opencode-go`.
</Accordion>
<Accordion title="Z.AI (GLM-4.7)">
```json5
{
agents: {
defaults: {
model: { primary: "zai/glm-4.7" },
models: { "zai/glm-4.7": {} },
},
},
}
```
Set `ZAI_API_KEY`. `z.ai/*` and `z-ai/*` are accepted aliases. Shortcut: `openclaw onboard --auth-choice zai-api-key`.
- General endpoint: `https://api.z.ai/api/paas/v4`
- Coding endpoint (default): `https://api.z.ai/api/coding/paas/v4`
- For the general endpoint, define a custom provider with the base URL override.
</Accordion>
<Accordion title="Moonshot AI (Kimi)">
```json5
{
env: { MOONSHOT_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "moonshot/kimi-k2.6" },
models: { "moonshot/kimi-k2.6": { alias: "Kimi K2.6" } },
},
},
models: {
mode: "merge",
providers: {
moonshot: {
baseUrl: "https://api.moonshot.ai/v1",
apiKey: "${MOONSHOT_API_KEY}",
api: "openai-completions",
models: [
{
id: "kimi-k2.6",
name: "Kimi K2.6",
reasoning: false,
input: ["text", "image"],
cost: { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
contextWindow: 262144,
maxTokens: 262144,
mode: "merge",
providers: {
cerebras: {
baseUrl: "https://api.cerebras.ai/v1",
apiKey: "${CEREBRAS_API_KEY}",
api: "openai-completions",
models: [
{ id: "zai-glm-4.7", name: "GLM 4.7 (Cerebras)" },
{ id: "zai-glm-4.6", name: "GLM 4.6 (Cerebras)" },
],
},
],
},
},
},
},
}
```
}
```
For the China endpoint: `baseUrl: "https://api.moonshot.cn/v1"` or `openclaw onboard --auth-choice moonshot-api-key-cn`.
Use `cerebras/zai-glm-4.7` for Cerebras; `zai/glm-4.7` for Z.AI direct.
Native Moonshot endpoints advertise streaming usage compatibility on the shared
`openai-completions` transport, and OpenClaw keys that off endpoint capabilities
rather than the built-in provider id alone.
</Accordion>
<Accordion title="Kimi Coding">
```json5
{
env: { KIMI_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "kimi/kimi-code" },
models: { "kimi/kimi-code": { alias: "Kimi Code" } },
},
},
}
```
</Accordion>
Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choice kimi-code-api-key`.
<Accordion title="Kimi Coding">
```json5
{
env: { KIMI_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "kimi/kimi-code" },
models: { "kimi/kimi-code": { alias: "Kimi Code" } },
},
},
}
```
Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choice kimi-code-api-key`.
</Accordion>
<Accordion title="Synthetic (Anthropic-compatible)">
```json5
{
env: { SYNTHETIC_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.5" },
models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax M2.5" } },
},
},
models: {
mode: "merge",
providers: {
synthetic: {
baseUrl: "https://api.synthetic.new/anthropic",
apiKey: "${SYNTHETIC_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "hf:MiniMaxAI/MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 192000,
maxTokens: 65536,
</Accordion>
<Accordion title="Local models (LM Studio)">
See [Local Models](/gateway/local-models). TL;DR: run a large local model via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
</Accordion>
<Accordion title="MiniMax M2.7 (direct)">
```json5
{
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.7" },
models: {
"minimax/MiniMax-M2.7": { alias: "Minimax" },
},
],
},
},
},
},
}
```
Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw onboard --auth-choice synthetic-api-key`.
</Accordion>
<Accordion title="MiniMax M2.7 (direct)">
```json5
{
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.7" },
models: {
"minimax/MiniMax-M2.7": { alias: "Minimax" },
},
},
},
models: {
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "${MINIMAX_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.7",
name: "MiniMax M2.7",
reasoning: true,
input: ["text"],
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
contextWindow: 204800,
maxTokens: 131072,
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "${MINIMAX_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.7",
name: "MiniMax M2.7",
reasoning: true,
input: ["text"],
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 },
contextWindow: 204800,
maxTokens: 131072,
},
],
},
],
},
},
},
},
}
```
}
```
Set `MINIMAX_API_KEY`. Shortcuts:
`openclaw onboard --auth-choice minimax-global-api` or
`openclaw onboard --auth-choice minimax-cn-api`.
The model catalog defaults to M2.7 only.
On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking
by default unless you explicitly set `thinking` yourself. `/fast on` or
`params.fastMode: true` rewrites `MiniMax-M2.7` to
`MiniMax-M2.7-highspeed`.
Set `MINIMAX_API_KEY`. Shortcuts: `openclaw onboard --auth-choice minimax-global-api` or `openclaw onboard --auth-choice minimax-cn-api`. The model catalog defaults to M2.7 only. On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking by default unless you explicitly set `thinking` yourself. `/fast on` or `params.fastMode: true` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
</Accordion>
</Accordion>
<Accordion title="Moonshot AI (Kimi)">
```json5
{
env: { MOONSHOT_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "moonshot/kimi-k2.6" },
models: { "moonshot/kimi-k2.6": { alias: "Kimi K2.6" } },
},
},
models: {
mode: "merge",
providers: {
moonshot: {
baseUrl: "https://api.moonshot.ai/v1",
apiKey: "${MOONSHOT_API_KEY}",
api: "openai-completions",
models: [
{
id: "kimi-k2.6",
name: "Kimi K2.6",
reasoning: false,
input: ["text", "image"],
cost: { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
contextWindow: 262144,
maxTokens: 262144,
},
],
},
},
},
}
```
<Accordion title="Local models (LM Studio)">
For the China endpoint: `baseUrl: "https://api.moonshot.cn/v1"` or `openclaw onboard --auth-choice moonshot-api-key-cn`.
See [Local Models](/gateway/local-models). TL;DR: run a large local model via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
Native Moonshot endpoints advertise streaming usage compatibility on the shared `openai-completions` transport, and OpenClaw keys that off endpoint capabilities rather than the built-in provider id alone.
</Accordion>
</Accordion>
<Accordion title="OpenCode">
```json5
{
agents: {
defaults: {
model: { primary: "opencode/claude-opus-4-6" },
models: { "opencode/claude-opus-4-6": { alias: "Opus" } },
},
},
}
```
Set `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`). Use `opencode/...` refs for the Zen catalog or `opencode-go/...` refs for the Go catalog. Shortcut: `openclaw onboard --auth-choice opencode-zen` or `openclaw onboard --auth-choice opencode-go`.
</Accordion>
<Accordion title="Synthetic (Anthropic-compatible)">
```json5
{
env: { SYNTHETIC_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.5" },
models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax M2.5" } },
},
},
models: {
mode: "merge",
providers: {
synthetic: {
baseUrl: "https://api.synthetic.new/anthropic",
apiKey: "${SYNTHETIC_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "hf:MiniMaxAI/MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 192000,
maxTokens: 65536,
},
],
},
},
},
}
```
Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw onboard --auth-choice synthetic-api-key`.
</Accordion>
<Accordion title="Z.AI (GLM-4.7)">
```json5
{
agents: {
defaults: {
model: { primary: "zai/glm-4.7" },
models: { "zai/glm-4.7": {} },
},
},
}
```
Set `ZAI_API_KEY`. `z.ai/*` and `z-ai/*` are accepted aliases. Shortcut: `openclaw onboard --auth-choice zai-api-key`.
- General endpoint: `https://api.z.ai/api/paas/v4`
- Coding endpoint (default): `https://api.z.ai/api/coding/paas/v4`
- For the general endpoint, define a custom provider with the base URL override.
</Accordion>
</AccordionGroup>
---
## Related
- [Configuration reference](/gateway/configuration-reference) — other top-level keys
- [Configuration — agents](/gateway/config-agents)
- [Configuration — channels](/gateway/config-channels)
- [Configuration reference](/gateway/configuration-reference) — other top-level keys
- [Tools and plugins](/tools)

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,38 @@ read_when:
- Adjusting heartbeat cadence or messaging
- Deciding between heartbeat and cron for scheduled tasks
title: "Heartbeat"
sidebarTitle: "Heartbeat"
---
> **Heartbeat vs Cron?** See [Automation & Tasks](/automation) for guidance on when to use each.
<Note>
**Heartbeat vs cron?** See [Automation & Tasks](/automation) for guidance on when to use each.
</Note>
Heartbeat runs **periodic agent turns** in the main session so the model can
surface anything that needs attention without spamming you.
Heartbeat runs **periodic agent turns** in the main session so the model can surface anything that needs attention without spamming you.
Heartbeat is a scheduled main-session turn — it does **not** create [background task](/automation/tasks) records.
Task records are for detached work (ACP runs, subagents, isolated cron jobs).
Heartbeat is a scheduled main-session turn — it does **not** create [background task](/automation/tasks) records. Task records are for detached work (ACP runs, subagents, isolated cron jobs).
Troubleshooting: [Scheduled Tasks](/automation/cron-jobs#troubleshooting)
## Quick start (beginner)
1. Leave heartbeats enabled (default is `30m`, or `1h` for Anthropic OAuth/token auth, including Claude CLI reuse) or set your own cadence.
2. Create a tiny `HEARTBEAT.md` checklist or `tasks:` block in the agent workspace (optional but recommended).
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
4. Optional: enable heartbeat reasoning delivery for transparency.
5. Optional: use lightweight bootstrap context if heartbeat runs only need `HEARTBEAT.md`.
6. Optional: enable isolated sessions to avoid sending full conversation history each heartbeat.
7. Optional: restrict heartbeats to active hours (local time).
<Steps>
<Step title="Pick a cadence">
Leave heartbeats enabled (default is `30m`, or `1h` for Anthropic OAuth/token auth, including Claude CLI reuse) or set your own cadence.
</Step>
<Step title="Add HEARTBEAT.md (optional)">
Create a tiny `HEARTBEAT.md` checklist or `tasks:` block in the agent workspace.
</Step>
<Step title="Decide where heartbeat messages should go">
`target: "none"` is the default; set `target: "last"` to route to the last contact.
</Step>
<Step title="Optional tuning">
- Enable heartbeat reasoning delivery for transparency.
- Use lightweight bootstrap context if heartbeat runs only need `HEARTBEAT.md`.
- Enable isolated sessions to avoid sending full conversation history each heartbeat.
- Restrict heartbeats to active hours (local time).
</Step>
</Steps>
Example config:
@@ -49,44 +60,30 @@ Example config:
## Defaults
- Interval: `30m` (or `1h` when Anthropic OAuth/token auth is the detected auth mode, including Claude CLI reuse). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable.
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`):
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
- The heartbeat prompt is sent **verbatim** as the user message. The system
prompt includes a “Heartbeat” section only when heartbeats are enabled for the
default agent, and the run is flagged internally.
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md`
from bootstrap context so the model does not see heartbeat-only instructions.
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone.
Outside the window, heartbeats are skipped until the next tick inside the window.
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`): `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
- The heartbeat prompt is sent **verbatim** as the user message. The system prompt includes a "Heartbeat" section only when heartbeats are enabled for the default agent, and the run is flagged internally.
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md` from bootstrap context so the model does not see heartbeat-only instructions.
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone. Outside the window, heartbeats are skipped until the next tick inside the window.
## What the heartbeat prompt is for
The default prompt is intentionally broad:
- **Background tasks**: Consider outstanding tasks nudges the agent to review
follow-ups (inbox, calendar, reminders, queued work) and surface anything urgent.
- **Human check-in**: “Checkup sometimes on your human during day time” nudges an
occasional lightweight “anything you need?” message, but avoids night-time spam
by using your configured local timezone (see [/concepts/timezone](/concepts/timezone)).
- **Background tasks**: "Consider outstanding tasks" nudges the agent to review follow-ups (inbox, calendar, reminders, queued work) and surface anything urgent.
- **Human check-in**: "Checkup sometimes on your human during day time" nudges an occasional lightweight "anything you need?" message, but avoids night-time spam by using your configured local timezone (see [Timezone](/concepts/timezone)).
Heartbeat can react to completed [background tasks](/automation/tasks), but a heartbeat run itself does not create a task record.
If you want a heartbeat to do something very specific (e.g. check Gmail PubSub
stats” or “verify gateway health”), set `agents.defaults.heartbeat.prompt` (or
`agents.list[].heartbeat.prompt`) to a custom body (sent verbatim).
If you want a heartbeat to do something very specific (e.g. "check Gmail PubSub stats" or "verify gateway health"), set `agents.defaults.heartbeat.prompt` (or `agents.list[].heartbeat.prompt`) to a custom body (sent verbatim).
## Response contract
- If nothing needs attention, reply with **`HEARTBEAT_OK`**.
- During heartbeat runs, OpenClaw treats `HEARTBEAT_OK` as an ack when it appears
at the **start or end** of the reply. The token is stripped and the reply is
dropped if the remaining content is **`ackMaxChars`** (default: 300).
- If `HEARTBEAT_OK` appears in the **middle** of a reply, it is not treated
specially.
- During heartbeat runs, OpenClaw treats `HEARTBEAT_OK` as an ack when it appears at the **start or end** of the reply. The token is stripped and the reply is dropped if the remaining content is **`ackMaxChars`** (default: 300).
- If `HEARTBEAT_OK` appears in the **middle** of a reply, it is not treated specially.
- For alerts, **do not** include `HEARTBEAT_OK`; return only the alert text.
Outside heartbeats, stray `HEARTBEAT_OK` at the start/end of a message is stripped
and logged; a message that is only `HEARTBEAT_OK` is dropped.
Outside heartbeats, stray `HEARTBEAT_OK` at the start/end of a message is stripped and logged; a message that is only `HEARTBEAT_OK` is dropped.
## Config
@@ -121,9 +118,7 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
### Per-agent heartbeats
If any `agents.list[]` entry includes a `heartbeat` block, **only those agents**
run heartbeats. The per-agent block merges on top of `agents.defaults.heartbeat`
(so you can set shared defaults once and override per agent).
If any `agents.list[]` entry includes a `heartbeat` block, **only those agents** run heartbeats. The per-agent block merges on top of `agents.defaults.heartbeat` (so you can set shared defaults once and override per agent).
Example: two agents, only the second agent runs heartbeats.
@@ -184,10 +179,11 @@ If you want heartbeats to run all day, use one of these patterns:
- Omit `activeHours` entirely (no time-window restriction; this is the default behavior).
- Set a full-day window: `activeHours: { start: "00:00", end: "24:00" }`.
Do not set the same `start` and `end` time (for example `08:00` to `08:00`).
That is treated as a zero-width window, so heartbeats are always skipped.
<Warning>
Do not set the same `start` and `end` time (for example `08:00` to `08:00`). That is treated as a zero-width window, so heartbeats are always skipped.
</Warning>
### Multi account example
### Multi-account example
Use `accountId` to target a specific account on multi-account channels like Telegram:
@@ -218,63 +214,87 @@ Use `accountId` to target a specific account on multi-account channels like Tele
### Field notes
- `every`: heartbeat interval (duration string; default unit = minutes).
- `model`: optional model override for heartbeat runs (`provider/model`).
- `includeReasoning`: when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Uses the same isolation pattern as cron `sessionTarget: "isolated"`. Dramatically reduces per-heartbeat token cost. Combine with `lightContext: true` for maximum savings. Delivery routing still uses the main session context.
- `session`: optional session key for heartbeat runs.
- `main` (default): agent main session.
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
- Session key formats: see [Sessions](/concepts/session) and [Groups](/channels/groups).
- `target`:
- `last`: deliver to the last used external channel.
- explicit channel: any configured channel or plugin id, for example `discord`, `matrix`, `telegram`, or `whatsapp`.
- `none` (default): run the heartbeat but **do not deliver** externally.
- `directPolicy`: controls direct/DM delivery behavior:
- `allow` (default): allow direct/DM heartbeat delivery.
- `block`: suppress direct/DM delivery (`reason=dm-blocked`).
- `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp or a Telegram chat id). For Telegram topics/threads, use `<chatId>:topic:<messageThreadId>`.
- `accountId`: optional account id for multi-account channels. When `target: "last"`, the account id applies to the resolved last channel if it supports accounts; otherwise it is ignored. If the account id does not match a configured account for the resolved channel, delivery is skipped.
- `prompt`: overrides the default prompt body (not merged).
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- `activeHours`: restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
- Omitted or `"user"`: uses your `agents.defaults.userTimezone` if set, otherwise falls back to the host system timezone.
- `"local"`: always uses the host system timezone.
- Any IANA identifier (e.g. `America/New_York`): used directly; if invalid, falls back to the `"user"` behavior above.
- `start` and `end` must not be equal for an active window; equal values are treated as zero-width (always outside the window).
- Outside the active window, heartbeats are skipped until the next tick inside the window.
<ParamField path="every" type="string">
Heartbeat interval (duration string; default unit = minutes).
</ParamField>
<ParamField path="model" type="string">
Optional model override for heartbeat runs (`provider/model`).
</ParamField>
<ParamField path="includeReasoning" type="boolean" default="false">
When enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`).
</ParamField>
<ParamField path="lightContext" type="boolean" default="false">
When true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
</ParamField>
<ParamField path="isolatedSession" type="boolean" default="false">
When true, each heartbeat runs in a fresh session with no prior conversation history. Uses the same isolation pattern as cron `sessionTarget: "isolated"`. Dramatically reduces per-heartbeat token cost. Combine with `lightContext: true` for maximum savings. Delivery routing still uses the main session context.
</ParamField>
<ParamField path="session" type="string">
Optional session key for heartbeat runs.
- `main` (default): agent main session.
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI](/cli/sessions)).
- Session key formats: see [Sessions](/concepts/session) and [Groups](/channels/groups).
</ParamField>
<ParamField path="target" type="string">
- `last`: deliver to the last used external channel.
- explicit channel: any configured channel or plugin id, for example `discord`, `matrix`, `telegram`, or `whatsapp`.
- `none` (default): run the heartbeat but **do not deliver** externally.
</ParamField>
<ParamField path="directPolicy" type='"allow" | "block"' default="allow">
Controls direct/DM delivery behavior. `allow`: allow direct/DM heartbeat delivery. `block`: suppress direct/DM delivery (`reason=dm-blocked`).
</ParamField>
<ParamField path="to" type="string">
Optional recipient override (channel-specific id, e.g. E.164 for WhatsApp or a Telegram chat id). For Telegram topics/threads, use `<chatId>:topic:<messageThreadId>`.
</ParamField>
<ParamField path="accountId" type="string">
Optional account id for multi-account channels. When `target: "last"`, the account id applies to the resolved last channel if it supports accounts; otherwise it is ignored. If the account id does not match a configured account for the resolved channel, delivery is skipped.
</ParamField>
<ParamField path="prompt" type="string">
Overrides the default prompt body (not merged).
</ParamField>
<ParamField path="ackMaxChars" type="number" default="300">
Max chars allowed after `HEARTBEAT_OK` before delivery.
</ParamField>
<ParamField path="suppressToolErrorWarnings" type="boolean">
When true, suppresses tool error warning payloads during heartbeat runs.
</ParamField>
<ParamField path="activeHours" type="object">
Restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.
- Omitted or `"user"`: uses your `agents.defaults.userTimezone` if set, otherwise falls back to the host system timezone.
- `"local"`: always uses the host system timezone.
- Any IANA identifier (e.g. `America/New_York`): used directly; if invalid, falls back to the `"user"` behavior above.
- `start` and `end` must not be equal for an active window; equal values are treated as zero-width (always outside the window).
- Outside the active window, heartbeats are skipped until the next tick inside the window.
</ParamField>
## Delivery behavior
- Heartbeats run in the agents main session by default (`agent:<id>:<mainKey>`),
or `global` when `session.scope = "global"`. Set `session` to override to a
specific channel session (Discord/WhatsApp/etc.).
- `session` only affects the run context; delivery is controlled by `target` and `to`.
- To deliver to a specific channel/recipient, set `target` + `to`. With
`target: "last"`, delivery uses the last external channel for that session.
- Heartbeat deliveries allow direct/DM targets by default. Set `directPolicy: "block"` to suppress direct-target sends while still running the heartbeat turn.
- If the main queue is busy, the heartbeat is skipped and retried later.
- If `target` resolves to no external destination, the run still happens but no
outbound message is sent.
- If `showOk`, `showAlerts`, and `useIndicator` are all disabled, the run is skipped up front as `reason=alerts-disabled`.
- If only alert delivery is disabled, OpenClaw can still run the heartbeat, update due-task timestamps, restore the session idle timestamp, and suppress the outward alert payload.
- If the resolved heartbeat target supports typing, OpenClaw shows typing while
the heartbeat run is active. This uses the same target the heartbeat would
send chat output to, and it is disabled by `typingMode: "never"`.
- Heartbeat-only replies do **not** keep the session alive. Heartbeat metadata
may update the session row, but idle expiry uses `lastInteractionAt` from the
last real user/channel message, and daily expiry uses `sessionStartedAt`.
- Control UI and WebChat history hide heartbeat prompts and OK-only
acknowledgments. The underlying session transcript can still contain those
turns for audit/replay.
- Detached [background tasks](/automation/tasks) can enqueue a system event and wake heartbeat when the main session should notice something quickly. That wake does not make the heartbeat run a background task.
<AccordionGroup>
<Accordion title="Session and target routing">
- Heartbeats run in the agent's main session by default (`agent:<id>:<mainKey>`), or `global` when `session.scope = "global"`. Set `session` to override to a specific channel session (Discord/WhatsApp/etc.).
- `session` only affects the run context; delivery is controlled by `target` and `to`.
- To deliver to a specific channel/recipient, set `target` + `to`. With `target: "last"`, delivery uses the last external channel for that session.
- Heartbeat deliveries allow direct/DM targets by default. Set `directPolicy: "block"` to suppress direct-target sends while still running the heartbeat turn.
- If the main queue is busy, the heartbeat is skipped and retried later.
- If `target` resolves to no external destination, the run still happens but no outbound message is sent.
</Accordion>
<Accordion title="Visibility and skip behavior">
- If `showOk`, `showAlerts`, and `useIndicator` are all disabled, the run is skipped up front as `reason=alerts-disabled`.
- If only alert delivery is disabled, OpenClaw can still run the heartbeat, update due-task timestamps, restore the session idle timestamp, and suppress the outward alert payload.
- If the resolved heartbeat target supports typing, OpenClaw shows typing while the heartbeat run is active. This uses the same target the heartbeat would send chat output to, and it is disabled by `typingMode: "never"`.
</Accordion>
<Accordion title="Session lifecycle and audit">
- Heartbeat-only replies do **not** keep the session alive. Heartbeat metadata may update the session row, but idle expiry uses `lastInteractionAt` from the last real user/channel message, and daily expiry uses `sessionStartedAt`.
- Control UI and WebChat history hide heartbeat prompts and OK-only acknowledgments. The underlying session transcript can still contain those turns for audit/replay.
- Detached [background tasks](/automation/tasks) can enqueue a system event and wake heartbeat when the main session should notice something quickly. That wake does not make the heartbeat run a background task.
</Accordion>
</AccordionGroup>
## Visibility controls
By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is
delivered. You can adjust this per channel or per account:
By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is delivered. You can adjust this per channel or per account:
```yaml
channels:
@@ -335,19 +355,11 @@ channels:
## HEARTBEAT.md (optional)
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
safe to include every 30 minutes.
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the agent to read it. Think of it as your "heartbeat checklist": small, stable, and safe to include every 30 minutes.
On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is
enabled for the default agent. Disabling the heartbeat cadence with `0m` or
setting `includeSystemPromptSection: false` omits it from normal bootstrap
context.
On normal runs, `HEARTBEAT.md` is only injected when heartbeat guidance is enabled for the default agent. Disabling the heartbeat cadence with `0m` or setting `includeSystemPromptSection: false` omits it from normal bootstrap context.
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls.
That skip is reported as `reason=empty-heartbeat-file`.
If the file is missing, the heartbeat still runs and the model decides what to do.
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls. That skip is reported as `reason=empty-heartbeat-file`. If the file is missing, the heartbeat still runs and the model decides what to do.
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
@@ -357,14 +369,13 @@ Example `HEARTBEAT.md`:
# Heartbeat checklist
- Quick scan: anything urgent in inboxes?
- If its daytime, do a lightweight check-in if nothing else is pending.
- If it's daytime, do a lightweight check-in if nothing else is pending.
- If a task is blocked, write down _what is missing_ and ask Peter next time.
```
### `tasks:` blocks
`HEARTBEAT.md` also supports a small structured `tasks:` block for interval-based
checks inside heartbeat itself.
`HEARTBEAT.md` also supports a small structured `tasks:` block for interval-based checks inside heartbeat itself.
Example:
@@ -384,14 +395,16 @@ tasks:
- If nothing needs attention after all due tasks, reply HEARTBEAT_OK.
```
Behavior:
- OpenClaw parses the `tasks:` block and checks each task against its own `interval`.
- Only **due** tasks are included in the heartbeat prompt for that tick.
- If no tasks are due, the heartbeat is skipped entirely (`reason=no-tasks-due`) to avoid a wasted model call.
- Non-task content in `HEARTBEAT.md` is preserved and appended as additional context after the due-task list.
- Task last-run timestamps are stored in session state (`heartbeatTaskState`), so intervals survive normal restarts.
- Task timestamps are only advanced after a heartbeat run completes its normal reply path. Skipped `empty-heartbeat-file` / `no-tasks-due` runs do not mark tasks as completed.
<AccordionGroup>
<Accordion title="Behavior">
- OpenClaw parses the `tasks:` block and checks each task against its own `interval`.
- Only **due** tasks are included in the heartbeat prompt for that tick.
- If no tasks are due, the heartbeat is skipped entirely (`reason=no-tasks-due`) to avoid a wasted model call.
- Non-task content in `HEARTBEAT.md` is preserved and appended as additional context after the due-task list.
- Task last-run timestamps are stored in session state (`heartbeatTaskState`), so intervals survive normal restarts.
- Task timestamps are only advanced after a heartbeat run completes its normal reply path. Skipped `empty-heartbeat-file` / `no-tasks-due` runs do not mark tasks as completed.
</Accordion>
</AccordionGroup>
Task mode is useful when you want one heartbeat file to hold several periodic checks without paying for all of them every tick.
@@ -399,18 +412,16 @@ Task mode is useful when you want one heartbeat file to hold several periodic ch
Yes — if you ask it to.
`HEARTBEAT.md` is just a normal file in the agent workspace, so you can tell the
agent (in a normal chat) something like:
`HEARTBEAT.md` is just a normal file in the agent workspace, so you can tell the agent (in a normal chat) something like:
- Update `HEARTBEAT.md` to add a daily calendar check.
- Rewrite `HEARTBEAT.md` so its shorter and focused on inbox follow-ups.
- "Update `HEARTBEAT.md` to add a daily calendar check."
- "Rewrite `HEARTBEAT.md` so it's shorter and focused on inbox follow-ups."
If you want this to happen proactively, you can also include an explicit line in
your heartbeat prompt like: “If the checklist becomes stale, update HEARTBEAT.md
with a better one.”
If you want this to happen proactively, you can also include an explicit line in your heartbeat prompt like: "If the checklist becomes stale, update HEARTBEAT.md with a better one."
Safety note: dont put secrets (API keys, phone numbers, private tokens) into
`HEARTBEAT.md` — it becomes part of the prompt context.
<Warning>
Don't put secrets (API keys, phone numbers, private tokens) into `HEARTBEAT.md` — it becomes part of the prompt context.
</Warning>
## Manual wake (on-demand)
@@ -420,24 +431,19 @@ You can enqueue a system event and trigger an immediate heartbeat with:
openclaw system event --text "Check for urgent follow-ups" --mode now
```
If multiple agents have `heartbeat` configured, a manual wake runs each of those
agent heartbeats immediately.
If multiple agents have `heartbeat` configured, a manual wake runs each of those agent heartbeats immediately.
Use `--mode next-heartbeat` to wait for the next scheduled tick.
## Reasoning delivery (optional)
By default, heartbeats deliver only the final answer payload.
By default, heartbeats deliver only the final "answer" payload.
If you want transparency, enable:
- `agents.defaults.heartbeat.includeReasoning: true`
When enabled, heartbeats will also deliver a separate message prefixed
`Reasoning:` (same shape as `/reasoning on`). This can be useful when the agent
is managing multiple sessions/codexes and you want to see why it decided to ping
you — but it can also leak more internal detail than you want. Prefer keeping it
off in group chats.
When enabled, heartbeats will also deliver a separate message prefixed `Reasoning:` (same shape as `/reasoning on`). This can be useful when the agent is managing multiple sessions/codexes and you want to see why it decided to ping you — but it can also leak more internal detail than you want. Prefer keeping it off in group chats.
## Cost awareness

View File

@@ -19,6 +19,9 @@ works without code changes. For local file logs and how to read them, see
and exec.
- **`diagnostics-otel` plugin** subscribes to those events and exports them as
OpenTelemetry **metrics**, **traces**, and **logs** over OTLP/HTTP.
- **Provider calls** receive a W3C `traceparent` header from OpenClaw's
trusted model-call span context when the provider transport accepts custom
headers. Plugin-emitted trace context is not propagated.
- Exporters only attach when both the diagnostics surface and the plugin are
enabled, so the in-process cost stays near zero by default.
@@ -61,11 +64,11 @@ openclaw plugins enable diagnostics-otel
## Signals exported
| Signal | What goes in it |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Metrics** | Counters and histograms for token usage, cost, run duration, message flow, queue lanes, session state, exec, and memory pressure. |
| **Traces** | Spans for model usage, model calls, tool execution, exec, webhook/message processing, context assembly, and tool loops. |
| **Logs** | Structured `logging.file` records exported over OTLP when `diagnostics.otel.logs` is enabled. |
| Signal | What goes in it |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Metrics** | Counters and histograms for token usage, cost, run duration, message flow, queue lanes, session state, exec, and memory pressure. |
| **Traces** | Spans for model usage, model calls, harness lifecycle, tool execution, exec, webhook/message processing, context assembly, and tool loops. |
| **Logs** | Structured `logging.file` records exported over OTLP when `diagnostics.otel.logs` is enabled. |
Toggle `traces`, `metrics`, and `logs` independently. All three default to on
when `diagnostics.otel.enabled` is true.
@@ -121,6 +124,11 @@ identifiers (channel, provider, model, error category, hash-only request ids)
and never include prompt text, response text, tool inputs, tool outputs, or
session keys.
Outbound model requests may include a W3C `traceparent` header. That header is
generated only from OpenClaw-owned diagnostic trace context for the active model
call. Existing caller-supplied `traceparent` headers are replaced, so plugins or
custom provider options cannot spoof cross-service trace ancestry.
Set `diagnostics.otel.captureContent.*` to `true` only when your collector and
retention policy are approved for prompt, response, tool, or system-prompt
text. Each subkey is opt-in independently:
@@ -176,6 +184,10 @@ When any subkey is enabled, model and tool spans get bounded, redacted
- `openclaw.session.stuck_age_ms` (histogram, attrs: `openclaw.state`)
- `openclaw.run.attempt` (counter, attrs: `openclaw.attempt`)
### Harness lifecycle
- `openclaw.harness.duration_ms` (histogram, attrs: `openclaw.harness.id`, `openclaw.harness.plugin`, `openclaw.outcome`, `openclaw.harness.phase` on errors)
### Exec
- `openclaw.exec.duration_ms` (histogram, attrs: `openclaw.exec.target`, `openclaw.exec.mode`, `openclaw.outcome`, `openclaw.failureKind`)
@@ -201,6 +213,10 @@ When any subkey is enabled, model and tool spans get bounded, redacted
- `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.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`
- On completion: `openclaw.harness.result_classification`, `openclaw.harness.yield_detected`, `openclaw.harness.items.started`, `openclaw.harness.items.completed`, `openclaw.harness.items.active`
- On error: `openclaw.harness.phase`, `openclaw.errorCategory`, optional `openclaw.harness.cleanup_failed`
- `openclaw.tool.execution`
- `gen_ai.tool.name`, `openclaw.toolName`, `openclaw.errorCategory`, `openclaw.tool.params.*`
- `openclaw.exec`
@@ -251,6 +267,16 @@ to them directly without OTLP export.
- `run.attempt`
- `diagnostic.heartbeat` (aggregate counters: webhooks/queue/session)
**Harness lifecycle**
- `harness.run.started` / `harness.run.completed` / `harness.run.error`
per-run lifecycle for the agent harness. Includes `harnessId`, optional
`pluginId`, provider/model/channel, and run id. Completion adds
`durationMs`, `outcome`, optional `resultClassification`, `yieldDetected`,
and `itemLifecycle` counts. Errors add `phase`
(`prepare`/`start`/`send`/`resolve`/`cleanup`), `errorCategory`, and
optional `cleanupFailed`.
**Exec**
- `exec.process.completed` — terminal outcome, duration, target, mode, exit

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

@@ -360,8 +360,8 @@ enumeration of `src/gateway/server-methods/*.ts`.
<Accordion title="Device pairing and device tokens">
- `device.pair.list` returns pending and approved paired devices.
- `device.pair.approve`, `device.pair.reject`, and `device.pair.remove` manage device-pairing records.
- `device.token.rotate` rotates a paired device token within its approved role and scope bounds.
- `device.token.revoke` revokes a paired device token.
- `device.token.rotate` rotates a paired device token within its approved role and caller scope bounds.
- `device.token.revoke` revokes a paired device token within its approved role and caller scope bounds.
</Accordion>
<Accordion title="Node pairing, invoke, and pending work">
@@ -549,15 +549,15 @@ rather than the pre-handshake defaults.
reused when the client is reusing the stored per-device token.
- Device tokens can be rotated/revoked via `device.token.rotate` and
`device.token.revoke` (requires `operator.pairing` scope).
- Token issuance/rotation stays bounded to the approved role set recorded in
that device's pairing entry; rotating a token cannot expand the device into a
role that pairing approval never granted.
- Token issuance, rotation, and revocation stay bounded to the approved role set
recorded in that device's pairing entry; token mutation cannot expand or
target a device role that pairing approval never granted.
- For paired-device token sessions, device management is self-scoped unless the
caller also has `operator.admin`: non-admin callers can remove/revoke/rotate
only their **own** device entry.
- `device.token.rotate` also checks the requested operator scope set against the
caller's current session scopes. Non-admin callers cannot rotate a token into
a broader operator scope set than they already hold.
- `device.token.rotate` and `device.token.revoke` also check the target operator
token scope set against the caller's current session scopes. Non-admin callers
cannot rotate or revoke a broader operator token than they already hold.
- Auth failures include `error.details.code` plus recovery hints:
- `error.details.canRetryWithDeviceToken` (boolean)
- `error.details.recommendedNextStep` (`retry_with_device_token`, `update_auth_configuration`, `update_auth_credentials`, `wait_then_retry`, `review_auth_configuration`)

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

@@ -5,11 +5,14 @@ read_when:
- Operating secrets reload, audit, configure, and apply safely in production
- Understanding startup fail-fast, inactive-surface filtering, and last-known-good behavior
title: "Secrets management"
sidebarTitle: "Secrets management"
---
OpenClaw supports additive SecretRefs so supported credentials do not need to be stored as plaintext in configuration.
<Note>
Plaintext still works. SecretRefs are opt-in per credential.
</Note>
## Goals and runtime model
@@ -33,38 +36,32 @@ SecretRefs are validated only on effectively active surfaces.
- Inactive surfaces: unresolved refs do not block startup/reload.
- Inactive refs emit non-fatal diagnostics with code `SECRETS_REF_IGNORED_INACTIVE_SURFACE`.
Examples of inactive surfaces:
- Disabled channel/account entries.
- Top-level channel credentials that no enabled account inherits.
- Disabled tool/feature surfaces.
- Web search provider-specific keys that are not selected by `tools.web.search.provider`.
In auto mode (provider unset), keys are consulted by precedence for provider auto-detection until one resolves.
After selection, non-selected provider keys are treated as inactive until selected.
- Sandbox SSH auth material (`agents.defaults.sandbox.ssh.identityData`,
`certificateData`, `knownHostsData`, plus per-agent overrides) is active only
when the effective sandbox backend is `ssh` for the default agent or an enabled agent.
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active if one of these is true:
- `gateway.mode=remote`
- `gateway.remote.url` is configured
- `gateway.tailscale.mode` is `serve` or `funnel`
- In local mode without those remote surfaces:
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
- `gateway.auth.token` SecretRef is inactive for startup auth resolution when `OPENCLAW_GATEWAY_TOKEN` is set, because env token input wins for that runtime.
<AccordionGroup>
<Accordion title="Examples of inactive surfaces">
- Disabled channel/account entries.
- Top-level channel credentials that no enabled account inherits.
- Disabled tool/feature surfaces.
- Web search provider-specific keys that are not selected by `tools.web.search.provider`. In auto mode (provider unset), keys are consulted by precedence for provider auto-detection until one resolves. After selection, non-selected provider keys are treated as inactive until selected.
- Sandbox SSH auth material (`agents.defaults.sandbox.ssh.identityData`, `certificateData`, `knownHostsData`, plus per-agent overrides) is active only when the effective sandbox backend is `ssh` for the default agent or an enabled agent.
- `gateway.remote.token` / `gateway.remote.password` SecretRefs are active if one of these is true:
- `gateway.mode=remote`
- `gateway.remote.url` is configured
- `gateway.tailscale.mode` is `serve` or `funnel`
- In local mode without those remote surfaces:
- `gateway.remote.token` is active when token auth can win and no env/auth token is configured.
- `gateway.remote.password` is active only when password auth can win and no env/auth password is configured.
- `gateway.auth.token` SecretRef is inactive for startup auth resolution when `OPENCLAW_GATEWAY_TOKEN` is set, because env token input wins for that runtime.
</Accordion>
</AccordionGroup>
## Gateway auth surface diagnostics
When a SecretRef is configured on `gateway.auth.token`, `gateway.auth.password`,
`gateway.remote.token`, or `gateway.remote.password`, gateway startup/reload logs the
surface state explicitly:
When a SecretRef is configured on `gateway.auth.token`, `gateway.auth.password`, `gateway.remote.token`, or `gateway.remote.password`, gateway startup/reload logs the surface state explicitly:
- `active`: the SecretRef is part of the effective auth surface and must resolve.
- `inactive`: the SecretRef is ignored for this runtime because another auth surface wins, or
because remote auth is disabled/not active.
- `inactive`: the SecretRef is ignored for this runtime because another auth surface wins, or because remote auth is disabled/not active.
These entries are logged with `SECRETS_GATEWAY_AUTH_SURFACE` and include the reason used by the
active-surface policy, so you can see why a credential was treated as active or inactive.
These entries are logged with `SECRETS_GATEWAY_AUTH_SURFACE` and include the reason used by the active-surface policy, so you can see why a credential was treated as active or inactive.
## Onboarding reference preflight
@@ -84,40 +81,43 @@ Use one object shape everywhere:
{ source: "env" | "file" | "exec", provider: "default", id: "..." }
```
### `source: "env"`
<Tabs>
<Tab title="env">
```json5
{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
```
```json5
{ source: "env", provider: "default", id: "OPENAI_API_KEY" }
```
Validation:
Validation:
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
- `id` must match `^[A-Z][A-Z0-9_]{0,127}$`
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
- `id` must match `^[A-Z][A-Z0-9_]{0,127}$`
</Tab>
<Tab title="file">
```json5
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" }
```
### `source: "file"`
Validation:
```json5
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" }
```
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
- `id` must be an absolute JSON pointer (`/...`)
- RFC6901 escaping in segments: `~` => `~0`, `/` => `~1`
Validation:
</Tab>
<Tab title="exec">
```json5
{ source: "exec", provider: "vault", id: "providers/openai/apiKey" }
```
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
- `id` must be an absolute JSON pointer (`/...`)
- RFC6901 escaping in segments: `~` => `~0`, `/` => `~1`
Validation:
### `source: "exec"`
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
- `id` must match `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
- `id` must not contain `.` or `..` as slash-delimited path segments (for example `a/../b` is rejected)
```json5
{ source: "exec", provider: "vault", id: "providers/openai/apiKey" }
```
Validation:
- `provider` must match `^[a-z][a-z0-9_-]{0,63}$`
- `id` must match `^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$`
- `id` must not contain `.` or `..` as slash-delimited path segments (for example `a/../b` is rejected)
</Tab>
</Tabs>
## Provider config
@@ -155,138 +155,139 @@ Define providers under `secrets.providers`:
}
```
### Env provider
<AccordionGroup>
<Accordion title="Env provider">
- Optional allowlist via `allowlist`.
- Missing/empty env values fail resolution.
</Accordion>
<Accordion title="File provider">
- Reads local file from `path`.
- `mode: "json"` expects JSON object payload and resolves `id` as pointer.
- `mode: "singleValue"` expects ref id `"value"` and returns file contents.
- Path must pass ownership/permission checks.
- Windows fail-closed note: if ACL verification is unavailable for a path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
</Accordion>
<Accordion title="Exec provider">
- Runs configured absolute binary path, no shell.
- By default, `command` must point to a regular file (not a symlink).
- Set `allowSymlinkCommand: true` to allow symlink command paths (for example Homebrew shims). OpenClaw validates the resolved target path.
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
- Optional allowlist via `allowlist`.
- Missing/empty env values fail resolution.
Request payload (stdin):
### File provider
```json
{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }
```
- Reads local file from `path`.
- `mode: "json"` expects JSON object payload and resolves `id` as pointer.
- `mode: "singleValue"` expects ref id `"value"` and returns file contents.
- Path must pass ownership/permission checks.
- Windows fail-closed note: if ACL verification is unavailable for a path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
Response payload (stdout):
### Exec provider
```jsonc
{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "<openai-api-key>" } } // pragma: allowlist secret
```
- Runs configured absolute binary path, no shell.
- By default, `command` must point to a regular file (not a symlink).
- Set `allowSymlinkCommand: true` to allow symlink command paths (for example Homebrew shims). OpenClaw validates the resolved target path.
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
Optional per-id errors:
Request payload (stdin):
```json
{
"protocolVersion": 1,
"values": {},
"errors": { "providers/openai/apiKey": { "message": "not found" } }
}
```
```json
{ "protocolVersion": 1, "provider": "vault", "ids": ["providers/openai/apiKey"] }
```
Response payload (stdout):
```jsonc
{ "protocolVersion": 1, "values": { "providers/openai/apiKey": "<openai-api-key>" } } // pragma: allowlist secret
```
Optional per-id errors:
```json
{
"protocolVersion": 1,
"values": {},
"errors": { "providers/openai/apiKey": { "message": "not found" } }
}
```
</Accordion>
</AccordionGroup>
## Exec integration examples
### 1Password CLI
```json5
{
secrets: {
providers: {
onepassword_openai: {
source: "exec",
command: "/opt/homebrew/bin/op",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["read", "op://Personal/OpenClaw QA API Key/password"],
passEnv: ["HOME"],
jsonOnly: false,
<AccordionGroup>
<Accordion title="1Password CLI">
```json5
{
secrets: {
providers: {
onepassword_openai: {
source: "exec",
command: "/opt/homebrew/bin/op",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["read", "op://Personal/OpenClaw QA API Key/password"],
passEnv: ["HOME"],
jsonOnly: false,
},
},
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "onepassword_openai", id: "value" },
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "onepassword_openai", id: "value" },
},
},
},
},
},
}
```
### HashiCorp Vault CLI
```json5
{
secrets: {
providers: {
vault_openai: {
source: "exec",
command: "/opt/homebrew/bin/vault",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["kv", "get", "-field=OPENAI_API_KEY", "secret/openclaw"],
passEnv: ["VAULT_ADDR", "VAULT_TOKEN"],
jsonOnly: false,
}
```
</Accordion>
<Accordion title="HashiCorp Vault CLI">
```json5
{
secrets: {
providers: {
vault_openai: {
source: "exec",
command: "/opt/homebrew/bin/vault",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["kv", "get", "-field=OPENAI_API_KEY", "secret/openclaw"],
passEnv: ["VAULT_ADDR", "VAULT_TOKEN"],
jsonOnly: false,
},
},
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "vault_openai", id: "value" },
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "vault_openai", id: "value" },
},
},
},
},
},
}
```
### `sops`
```json5
{
secrets: {
providers: {
sops_openai: {
source: "exec",
command: "/opt/homebrew/bin/sops",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["-d", "--extract", '["providers"]["openai"]["apiKey"]', "/path/to/secrets.enc.json"],
passEnv: ["SOPS_AGE_KEY_FILE"],
jsonOnly: false,
}
```
</Accordion>
<Accordion title="sops">
```json5
{
secrets: {
providers: {
sops_openai: {
source: "exec",
command: "/opt/homebrew/bin/sops",
allowSymlinkCommand: true, // required for Homebrew symlinked binaries
trustedDirs: ["/opt/homebrew"],
args: ["-d", "--extract", '["providers"]["openai"]["apiKey"]', "/path/to/secrets.enc.json"],
passEnv: ["SOPS_AGE_KEY_FILE"],
jsonOnly: false,
},
},
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "sops_openai", id: "value" },
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5", name: "gpt-5" }],
apiKey: { source: "exec", provider: "sops_openai", id: "value" },
},
},
},
},
},
}
```
}
```
</Accordion>
</AccordionGroup>
## MCP server environment variables
@@ -356,7 +357,9 @@ Canonical supported and unsupported credentials are listed in:
- [SecretRef Credential Surface](/reference/secretref-credential-surface)
<Note>
Runtime-minted or rotating credentials and OAuth refresh material are intentionally excluded from read-only SecretRef resolution.
</Note>
## Required behavior and precedence
@@ -415,15 +418,22 @@ Command paths can opt into supported SecretRef resolution via gateway snapshot R
There are two broad behaviors:
- Strict command paths (for example `openclaw memory` remote-memory paths and `openclaw qr --remote` when it needs remote shared-secret refs) read from the active snapshot and fail fast when a required SecretRef is unavailable.
- Read-only command paths (for example `openclaw status`, `openclaw status --all`, `openclaw channels status`, `openclaw channels resolve`, `openclaw security audit`, and read-only doctor/config repair flows) also prefer the active snapshot, but degrade instead of aborting when a targeted SecretRef is unavailable in that command path.
<Tabs>
<Tab title="Strict command paths">
For example `openclaw memory` remote-memory paths and `openclaw qr --remote` when it needs remote shared-secret refs. They read from the active snapshot and fail fast when a required SecretRef is unavailable.
</Tab>
<Tab title="Read-only command paths">
For example `openclaw status`, `openclaw status --all`, `openclaw channels status`, `openclaw channels resolve`, `openclaw security audit`, and read-only doctor/config repair flows. They also prefer the active snapshot, but degrade instead of aborting when a targeted SecretRef is unavailable in that command path.
Read-only behavior:
Read-only behavior:
- When the gateway is running, these commands read from the active snapshot first.
- If gateway resolution is incomplete or the gateway is unavailable, they attempt targeted local fallback for the specific command surface.
- If a targeted SecretRef is still unavailable, the command continues with degraded read-only output and explicit diagnostics such as configured but unavailable in this command path.
- This degraded behavior is command-local only. It does not weaken runtime startup, reload, or send/auth paths.
- When the gateway is running, these commands read from the active snapshot first.
- If gateway resolution is incomplete or the gateway is unavailable, they attempt targeted local fallback for the specific command surface.
- If a targeted SecretRef is still unavailable, the command continues with degraded read-only output and explicit diagnostics such as "configured but unavailable in this command path".
- This degraded behavior is command-local only. It does not weaken runtime startup, reload, or send/auth paths.
</Tab>
</Tabs>
Other notes:
@@ -434,82 +444,97 @@ Other notes:
Default operator flow:
```bash
openclaw secrets audit --check
openclaw secrets configure
openclaw secrets audit --check
```
<Steps>
<Step title="Audit current state">
```bash
openclaw secrets audit --check
```
</Step>
<Step title="Configure SecretRefs">
```bash
openclaw secrets configure
```
</Step>
<Step title="Re-audit">
```bash
openclaw secrets audit --check
```
</Step>
</Steps>
### `secrets audit`
<AccordionGroup>
<Accordion title="secrets audit">
Findings include:
Findings include:
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`, and generated `agents/*/agent/models.json`)
- plaintext sensitive provider header residues in generated `models.json` entries
- unresolved refs
- precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs)
- legacy residues (`auth.json`, OAuth reminders)
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`, and generated `agents/*/agent/models.json`)
- plaintext sensitive provider header residues in generated `models.json` entries
- unresolved refs
- precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs)
- legacy residues (`auth.json`, OAuth reminders)
Exec note:
Exec note:
- By default, audit skips exec SecretRef resolvability checks to avoid command side effects.
- Use `openclaw secrets audit --allow-exec` to execute exec providers during audit.
- By default, audit skips exec SecretRef resolvability checks to avoid command side effects.
- Use `openclaw secrets audit --allow-exec` to execute exec providers during audit.
Header residue note:
Header residue note:
- Sensitive provider header detection is name-heuristic based (common auth/credential header names and fragments such as `authorization`, `x-api-key`, `token`, `secret`, `password`, and `credential`).
- Sensitive provider header detection is name-heuristic based (common auth/credential header names and fragments such as `authorization`, `x-api-key`, `token`, `secret`, `password`, and `credential`).
</Accordion>
<Accordion title="secrets configure">
Interactive helper that:
### `secrets configure`
- configures `secrets.providers` first (`env`/`file`/`exec`, add/edit/remove)
- lets you select supported secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for one agent scope
- can create a new `auth-profiles.json` mapping directly in the target picker
- captures SecretRef details (`source`, `provider`, `id`)
- runs preflight resolution
- can apply immediately
Interactive helper that:
Exec note:
- configures `secrets.providers` first (`env`/`file`/`exec`, add/edit/remove)
- lets you select supported secret-bearing fields in `openclaw.json` plus `auth-profiles.json` for one agent scope
- can create a new `auth-profiles.json` mapping directly in the target picker
- captures SecretRef details (`source`, `provider`, `id`)
- runs preflight resolution
- can apply immediately
- Preflight skips exec SecretRef checks unless `--allow-exec` is set.
- If you apply directly from `configure --apply` and the plan includes exec refs/providers, keep `--allow-exec` set for the apply step too.
Exec note:
Helpful modes:
- Preflight skips exec SecretRef checks unless `--allow-exec` is set.
- If you apply directly from `configure --apply` and the plan includes exec refs/providers, keep `--allow-exec` set for the apply step too.
- `openclaw secrets configure --providers-only`
- `openclaw secrets configure --skip-provider-setup`
- `openclaw secrets configure --agent <id>`
Helpful modes:
`configure` apply defaults:
- `openclaw secrets configure --providers-only`
- `openclaw secrets configure --skip-provider-setup`
- `openclaw secrets configure --agent <id>`
- scrub matching static credentials from `auth-profiles.json` for targeted providers
- scrub legacy static `api_key` entries from `auth.json`
- scrub matching known secret lines from `<config-dir>/.env`
`configure` apply defaults:
</Accordion>
<Accordion title="secrets apply">
Apply a saved plan:
- scrub matching static credentials from `auth-profiles.json` for targeted providers
- scrub legacy static `api_key` entries from `auth.json`
- scrub matching known secret lines from `<config-dir>/.env`
```bash
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec
```
### `secrets apply`
Exec note:
Apply a saved plan:
- dry-run skips exec checks unless `--allow-exec` is set.
- write mode rejects plans containing exec SecretRefs/providers unless `--allow-exec` is set.
```bash
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --allow-exec
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run
openclaw secrets apply --from /tmp/openclaw-secrets-plan.json --dry-run --allow-exec
```
For strict target/path contract details and exact rejection rules, see [Secrets Apply Plan Contract](/gateway/secrets-plan-contract).
Exec note:
- dry-run skips exec checks unless `--allow-exec` is set.
- write mode rejects plans containing exec SecretRefs/providers unless `--allow-exec` is set.
For strict target/path contract details and exact rejection rules, see:
- [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
</Accordion>
</AccordionGroup>
## One-way safety policy
<Warning>
OpenClaw intentionally does not write rollback backups containing historical plaintext secret values.
</Warning>
Safety model:
@@ -529,11 +554,11 @@ For static credentials, runtime no longer depends on plaintext legacy auth stora
Some SecretInput unions are easier to configure in raw editor mode than in form mode.
## Related docs
## Related
- CLI commands: [secrets](/cli/secrets)
- Plan contract details: [Secrets Apply Plan Contract](/gateway/secrets-plan-contract)
- Credential surface: [SecretRef Credential Surface](/reference/secretref-credential-surface)
- Auth setup: [Authentication](/gateway/authentication)
- Security posture: [Security](/gateway/security)
- Environment precedence: [Environment Variables](/help/environment)
- [Authentication](/gateway/authentication) — auth setup
- [CLI: secrets](/cli/secrets) — CLI commands
- [Environment Variables](/help/environment) — environment precedence
- [SecretRef Credential Surface](/reference/secretref-credential-surface) — credential surface
- [Secrets Apply Plan Contract](/gateway/secrets-plan-contract) — plan contract details
- [Security](/gateway/security) — security posture

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

@@ -4,12 +4,10 @@ read_when:
- The troubleshooting hub pointed you here for deeper diagnosis
- You need stable symptom based runbook sections with exact commands
title: "Troubleshooting"
sidebarTitle: "Troubleshooting"
---
# Gateway troubleshooting
This page is the deep runbook.
Start at [/help/troubleshooting](/help/troubleshooting) if you want the fast triage flow first.
This page is the deep runbook. Start at [/help/troubleshooting](/help/troubleshooting) if you want the fast triage flow first.
## Command ladder
@@ -27,20 +25,13 @@ Expected healthy signals:
- `openclaw gateway status` shows `Runtime: running`, `Connectivity probe: ok`, and a `Capability: ...` line.
- `openclaw doctor` reports no blocking config/service issues.
- `openclaw channels status --probe` shows live per-account transport status and,
where supported, probe/audit results such as `works` or `audit ok`.
- `openclaw channels status --probe` shows live per-account transport status and, where supported, probe/audit results such as `works` or `audit ok`.
## Split brain installs and newer config guard
Use this when a gateway service unexpectedly stops after an update, or logs show
that one `openclaw` binary is older than the version that last wrote
`openclaw.json`.
Use this when a gateway service unexpectedly stops after an update, or logs show that one `openclaw` binary is older than the version that last wrote `openclaw.json`.
OpenClaw stamps config writes with `meta.lastTouchedVersion`. Read-only commands
can still inspect a config written by a newer OpenClaw, but process and service
mutations refuse to continue from an older binary. Blocked actions include
gateway service start, stop, restart, uninstall, forced service reinstall,
service-mode gateway startup, and `gateway --force` port cleanup.
OpenClaw stamps config writes with `meta.lastTouchedVersion`. Read-only commands can still inspect a config written by a newer OpenClaw, but process and service mutations refuse to continue from an older binary. Blocked actions include gateway service start, stop, restart, uninstall, forced service reinstall, service-mode gateway startup, and `gateway --force` port cleanup.
```bash
which openclaw
@@ -49,27 +40,31 @@ openclaw gateway status --deep
openclaw config get meta.lastTouchedVersion
```
Fix options:
<Steps>
<Step title="Fix PATH">
Fix `PATH` so `openclaw` resolves to the newer install, then rerun the action.
</Step>
<Step title="Reinstall the gateway service">
Reinstall the intended gateway service from the newer install:
1. Fix `PATH` so `openclaw` resolves to the newer install, then rerun the action.
2. Reinstall the intended gateway service from the newer install:
```bash
openclaw gateway install --force
openclaw gateway restart
```
```bash
openclaw gateway install --force
openclaw gateway restart
```
</Step>
<Step title="Remove stale wrappers">
Remove stale system package or old wrapper entries that still point at an old `openclaw` binary.
</Step>
</Steps>
3. Remove stale system package or old wrapper entries that still point at an old
`openclaw` binary.
For intentional downgrade or emergency recovery only, set
`OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1` for the single command.
Leave it unset for normal operation.
<Warning>
For intentional downgrade or emergency recovery only, set `OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1` for the single command. Leave it unset for normal operation.
</Warning>
## Anthropic 429 extra usage required for long context
Use this when logs/errors include:
`HTTP 429: rate_limit_error: Extra usage is required for long context requests`.
Use this when logs/errors include: `HTTP 429: rate_limit_error: Extra usage is required for long context requests`.
```bash
openclaw logs --follow
@@ -85,9 +80,17 @@ Look for:
Fix options:
1. Disable `context1m` for that model to fall back to the normal context window.
2. Use an Anthropic credential that is eligible for long-context requests, or switch to an Anthropic API key.
3. Configure fallback models so runs continue when Anthropic long-context requests are rejected.
<Steps>
<Step title="Disable context1m">
Disable `context1m` for that model to fall back to the normal context window.
</Step>
<Step title="Use an eligible credential">
Use an Anthropic credential that is eligible for long-context requests, or switch to an Anthropic API key.
</Step>
<Step title="Configure fallback models">
Configure fallback models so runs continue when Anthropic long-context requests are rejected.
</Step>
</Steps>
Related:
@@ -116,38 +119,26 @@ Look for:
- direct tiny calls succeed, but OpenClaw runs fail only on larger prompts
- backend errors about `messages[].content` expecting a string
- backend crashes that appear only with larger prompt-token counts or full agent
runtime prompts
- backend crashes that appear only with larger prompt-token counts or full agent runtime prompts
Common signatures:
- `messages[...].content: invalid type: sequence, expected a string` → backend
rejects structured Chat Completions content parts. Fix: set
`models.providers.<provider>.models[].compat.requiresStringContent: true`.
- direct tiny requests succeed, but OpenClaw agent runs fail with backend/model
crashes (for example Gemma on some `inferrs` builds) → OpenClaw transport is
likely already correct; the backend is failing on the larger agent-runtime
prompt shape.
- failures shrink after disabling tools but do not disappear → tool schemas were
part of the pressure, but the remaining issue is still upstream model/server
capacity or a backend bug.
Fix options:
1. Set `compat.requiresStringContent: true` for string-only Chat Completions backends.
2. Set `compat.supportsTools: false` for models/backends that cannot handle
OpenClaw's tool schema surface reliably.
3. Lower prompt pressure where possible: smaller workspace bootstrap, shorter
session history, lighter local model, or a backend with stronger long-context
support.
4. If tiny direct requests keep passing while OpenClaw agent turns still crash
inside the backend, treat it as an upstream server/model limitation and file
a repro there with the accepted payload shape.
<AccordionGroup>
<Accordion title="Common signatures">
- `messages[...].content: invalid type: sequence, expected a string` → backend rejects structured Chat Completions content parts. Fix: set `models.providers.<provider>.models[].compat.requiresStringContent: true`.
- direct tiny requests succeed, but OpenClaw agent runs fail with backend/model crashes (for example Gemma on some `inferrs` builds) → OpenClaw transport is likely already correct; the backend is failing on the larger agent-runtime prompt shape.
- failures shrink after disabling tools but do not disappear → tool schemas were part of the pressure, but the remaining issue is still upstream model/server capacity or a backend bug.
</Accordion>
<Accordion title="Fix options">
1. Set `compat.requiresStringContent: true` for string-only Chat Completions backends.
2. Set `compat.supportsTools: false` for models/backends that cannot handle OpenClaw's tool schema surface reliably.
3. Lower prompt pressure where possible: smaller workspace bootstrap, shorter session history, lighter local model, or a backend with stronger long-context support.
4. If tiny direct requests keep passing while OpenClaw agent turns still crash inside the backend, treat it as an upstream server/model limitation and file a repro there with the accepted payload shape.
</Accordion>
</AccordionGroup>
Related:
- [Local models](/gateway/local-models)
- [Configuration](/gateway/configuration)
- [Local models](/gateway/local-models)
- [OpenAI-compatible endpoints](/gateway/configuration-reference#openai-compatible-endpoints)
## No replies
@@ -177,10 +168,10 @@ Common signatures:
Related:
- [Channel troubleshooting](/channels/troubleshooting)
- [Pairing](/channels/pairing)
- [Groups](/channels/groups)
- [Pairing](/channels/pairing)
## Dashboard control ui connectivity
## Dashboard control UI connectivity
When dashboard/control UI will not connect, validate URL, auth mode, and secure context assumptions.
@@ -198,32 +189,21 @@ Look for:
- Auth mode/token mismatch between client and gateway.
- HTTP usage where device identity is required.
Common signatures:
- `device identity required` → non-secure context or missing device auth.
- `origin not allowed` → browser `Origin` is not in `gateway.controlUi.allowedOrigins`
(or you are connecting from a non-loopback browser origin without an explicit
allowlist).
- `device nonce required` / `device nonce mismatch` → client is not completing the
challenge-based device auth flow (`connect.challenge` + `device.nonce`).
- `device signature invalid` / `device signature expired` → client signed the wrong
payload (or stale timestamp) for the current handshake.
- `AUTH_TOKEN_MISMATCH` with `canRetryWithDeviceToken=true` → client can do one trusted retry with cached device token.
- That cached-token retry reuses the cached scope set stored with the paired
device token. Explicit `deviceToken` / explicit `scopes` callers keep their
requested scope set instead.
- Outside that retry path, connect auth precedence is explicit shared
token/password first, then explicit `deviceToken`, then stored device token,
then bootstrap token.
- On the async Tailscale Serve Control UI path, failed attempts for the same
`{scope, ip}` are serialized before the limiter records the failure. Two bad
concurrent retries from the same client can therefore surface `retry later`
on the second attempt instead of two plain mismatches.
- `too many failed authentication attempts (retry later)` from a browser-origin
loopback client → repeated failures from that same normalized `Origin` are
locked out temporarily; another localhost origin uses a separate bucket.
- repeated `unauthorized` after that retry → shared token/device token drift; refresh token config and re-approve/rotate device token if needed.
- `gateway connect failed:` → wrong host/port/url target.
<AccordionGroup>
<Accordion title="Connect / auth signatures">
- `device identity required` → non-secure context or missing device auth.
- `origin not allowed` → browser `Origin` is not in `gateway.controlUi.allowedOrigins` (or you are connecting from a non-loopback browser origin without an explicit allowlist).
- `device nonce required` / `device nonce mismatch` → client is not completing the challenge-based device auth flow (`connect.challenge` + `device.nonce`).
- `device signature invalid` / `device signature expired` → client signed the wrong payload (or stale timestamp) for the current handshake.
- `AUTH_TOKEN_MISMATCH` with `canRetryWithDeviceToken=true` → client can do one trusted retry with cached device token.
- That cached-token retry reuses the cached scope set stored with the paired device token. Explicit `deviceToken` / explicit `scopes` callers keep their requested scope set instead.
- Outside that retry path, connect auth precedence is explicit shared token/password first, then explicit `deviceToken`, then stored device token, then bootstrap token.
- On the async Tailscale Serve Control UI path, failed attempts for the same `{scope, ip}` are serialized before the limiter records the failure. Two bad concurrent retries from the same client can therefore surface `retry later` on the second attempt instead of two plain mismatches.
- `too many failed authentication attempts (retry later)` from a browser-origin loopback client → repeated failures from that same normalized `Origin` are locked out temporarily; another localhost origin uses a separate bucket.
- repeated `unauthorized` after that retry → shared token/device token drift; refresh token config and re-approve/rotate device token if needed.
- `gateway connect failed:` → wrong host/port/url target.
</Accordion>
</AccordionGroup>
### Auth detail codes quick map
@@ -236,11 +216,9 @@ Use `error.details.code` from the failed `connect` response to pick the next act
| `AUTH_DEVICE_TOKEN_MISMATCH` | Cached per-device token is stale or revoked. | Rotate/re-approve device token using [devices CLI](/cli/devices), then reconnect. |
| `PAIRING_REQUIRED` | Device identity needs approval. Check `error.details.reason` for `not-paired`, `scope-upgrade`, `role-upgrade`, or `metadata-upgrade`, and use `requestId` / `remediationHint` when present. | Approve pending request: `openclaw devices list` then `openclaw devices approve <requestId>`. Scope/role upgrades use the same flow after you review the requested access. |
Direct loopback backend RPCs authenticated with the shared gateway
token/password should not depend on the CLI's paired-device scope baseline. If
subagents or other internal calls still fail with `scope-upgrade`, verify the
caller is using `client.id: "gateway-client"` and `client.mode: "backend"` and
is not forcing an explicit `deviceIdentity` or device token.
<Note>
Direct loopback backend RPCs authenticated with the shared gateway token/password should not depend on the CLI's paired-device scope baseline. If subagents or other internal calls still fail with `scope-upgrade`, verify the caller is using `client.id: "gateway-client"` and `client.mode: "backend"` and is not forcing an explicit `deviceIdentity` or device token.
</Note>
Device auth v2 migration check:
@@ -252,24 +230,30 @@ openclaw gateway status
If logs show nonce/signature errors, update the connecting client and verify it:
1. waits for `connect.challenge`
2. signs the challenge-bound payload
3. sends `connect.params.device.nonce` with the same challenge nonce
<Steps>
<Step title="Wait for connect.challenge">
Client waits for the gateway-issued `connect.challenge`.
</Step>
<Step title="Sign the payload">
Client signs the challenge-bound payload.
</Step>
<Step title="Send the device nonce">
Client sends `connect.params.device.nonce` with the same challenge nonce.
</Step>
</Steps>
If `openclaw devices rotate` / `revoke` / `remove` is denied unexpectedly:
- paired-device token sessions can manage only **their own** device unless the
caller also has `operator.admin`
- `openclaw devices rotate --scope ...` can only request operator scopes that
the caller session already holds
- paired-device token sessions can manage only **their own** device unless the caller also has `operator.admin`
- `openclaw devices rotate --scope ...` can only request operator scopes that the caller session already holds
Related:
- [Control UI](/web/control-ui)
- [Configuration](/gateway/configuration) (gateway auth modes)
- [Trusted proxy auth](/gateway/trusted-proxy-auth)
- [Remote access](/gateway/remote)
- [Control UI](/web/control-ui)
- [Devices](/cli/devices)
- [Remote access](/gateway/remote)
- [Trusted proxy auth](/gateway/trusted-proxy-auth)
## Gateway service not running
@@ -291,12 +275,14 @@ Look for:
- Extra launchd/systemd/schtasks installs when `--deep` is used.
- `Other gateway-like services detected (best effort)` cleanup hints.
Common signatures:
- `Gateway start blocked: set gateway.mode=local` or `existing config is missing gateway.mode` → local gateway mode is not enabled, or the config file was clobbered and lost `gateway.mode`. Fix: set `gateway.mode="local"` in your config, or re-run `openclaw onboard --mode local` / `openclaw setup` to restamp the expected local-mode config. If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`.
- `refusing to bind gateway ... without auth` → non-loopback bind without a valid gateway auth path (token/password, or trusted-proxy where configured).
- `another gateway instance is already listening` / `EADDRINUSE` → port conflict.
- `Other gateway-like services detected (best effort)` → stale or parallel launchd/systemd/schtasks units exist. Most setups should keep one gateway per machine; if you do need more than one, isolate ports + config/state/workspace. See [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host).
<AccordionGroup>
<Accordion title="Common signatures">
- `Gateway start blocked: set gateway.mode=local` or `existing config is missing gateway.mode` → local gateway mode is not enabled, or the config file was clobbered and lost `gateway.mode`. Fix: set `gateway.mode="local"` in your config, or re-run `openclaw onboard --mode local` / `openclaw setup` to restamp the expected local-mode config. If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`.
- `refusing to bind gateway ... without auth` → non-loopback bind without a valid gateway auth path (token/password, or trusted-proxy where configured).
- `another gateway instance is already listening` / `EADDRINUSE` → port conflict.
- `Other gateway-like services detected (best effort)` → stale or parallel launchd/systemd/schtasks units exist. Most setups should keep one gateway per machine; if you do need more than one, isolate ports + config/state/workspace. See [/gateway#multiple-gateways-same-host](/gateway#multiple-gateways-same-host).
</Accordion>
</AccordionGroup>
Related:
@@ -323,46 +309,43 @@ Look for:
- A timestamped `openclaw.json.clobbered.*` file beside the active config
- A main-agent system event that starts with `Config recovery warning`
What happened:
- The rejected config did not validate during startup or hot reload.
- OpenClaw preserved the rejected payload as `.clobbered.*`.
- The active config was restored from the last validated last-known-good copy.
- The next main-agent turn is warned not to blindly rewrite the rejected config.
- If all validation issues were under `plugins.entries.<id>...`, OpenClaw would
not restore the whole file. Plugin-local failures stay loud while unrelated
user settings remain in the active config.
Inspect and repair:
```bash
CONFIG="$(openclaw config file)"
ls -lt "$CONFIG".clobbered.* "$CONFIG".rejected.* 2>/dev/null | head
diff -u "$CONFIG" "$(ls -t "$CONFIG".clobbered.* 2>/dev/null | head -n 1)"
openclaw config validate
openclaw doctor
```
Common signatures:
- `.clobbered.*` exists → an external direct edit or startup read was restored.
- `.rejected.*` exists → an OpenClaw-owned config write failed schema or clobber checks before commit.
- `Config write rejected:` → the write tried to drop required shape, shrink the file sharply, or persist invalid config.
- `missing-meta-vs-last-good`, `gateway-mode-missing-vs-last-good`, or `size-drop-vs-last-good:*` → startup treated the current file as clobbered because it lost fields or size compared with the last-known-good backup.
- `Config last-known-good promotion skipped` → the candidate contained redacted secret placeholders such as `***`.
Fix options:
1. Keep the restored active config if it is correct.
2. Copy only the intended keys from `.clobbered.*` or `.rejected.*`, then apply them with `openclaw config set` or `config.patch`.
3. Run `openclaw config validate` before restarting.
4. If you edit by hand, keep the full JSON5 config, not just the partial object you wanted to change.
<AccordionGroup>
<Accordion title="What happened">
- The rejected config did not validate during startup or hot reload.
- OpenClaw preserved the rejected payload as `.clobbered.*`.
- The active config was restored from the last validated last-known-good copy.
- The next main-agent turn is warned not to blindly rewrite the rejected config.
- If all validation issues were under `plugins.entries.<id>...`, OpenClaw would not restore the whole file. Plugin-local failures stay loud while unrelated user settings remain in the active config.
</Accordion>
<Accordion title="Inspect and repair">
```bash
CONFIG="$(openclaw config file)"
ls -lt "$CONFIG".clobbered.* "$CONFIG".rejected.* 2>/dev/null | head
diff -u "$CONFIG" "$(ls -t "$CONFIG".clobbered.* 2>/dev/null | head -n 1)"
openclaw config validate
openclaw doctor
```
</Accordion>
<Accordion title="Common signatures">
- `.clobbered.*` exists → an external direct edit or startup read was restored.
- `.rejected.*` exists → an OpenClaw-owned config write failed schema or clobber checks before commit.
- `Config write rejected:` → the write tried to drop required shape, shrink the file sharply, or persist invalid config.
- `missing-meta-vs-last-good`, `gateway-mode-missing-vs-last-good`, or `size-drop-vs-last-good:*` → startup treated the current file as clobbered because it lost fields or size compared with the last-known-good backup.
- `Config last-known-good promotion skipped` → the candidate contained redacted secret placeholders such as `***`.
</Accordion>
<Accordion title="Fix options">
1. Keep the restored active config if it is correct.
2. Copy only the intended keys from `.clobbered.*` or `.rejected.*`, then apply them with `openclaw config set` or `config.patch`.
3. Run `openclaw config validate` before restarting.
4. If you edit by hand, keep the full JSON5 config, not just the partial object you wanted to change.
</Accordion>
</AccordionGroup>
Related:
- [Configuration: strict validation](/gateway/configuration#strict-validation)
- [Configuration: hot reload](/gateway/configuration#config-hot-reload)
- [Config](/cli/config)
- [Configuration: hot reload](/gateway/configuration#config-hot-reload)
- [Configuration: strict validation](/gateway/configuration#strict-validation)
- [Doctor](/gateway/doctor)
## Gateway probe warnings
@@ -394,7 +377,7 @@ Related:
- [Multiple gateways on the same host](/gateway#multiple-gateways-same-host)
- [Remote access](/gateway/remote)
## Channel connected messages not flowing
## Channel connected, messages not flowing
If channel state is connected but message flow is dead, focus on policy, permissions, and channel specific delivery rules.
@@ -421,9 +404,9 @@ Common signatures:
Related:
- [Channel troubleshooting](/channels/troubleshooting)
- [WhatsApp](/channels/whatsapp)
- [Telegram](/channels/telegram)
- [Discord](/channels/discord)
- [Telegram](/channels/telegram)
- [WhatsApp](/channels/whatsapp)
## Cron and heartbeat delivery
@@ -443,23 +426,25 @@ Look for:
- Job run history status (`ok`, `skipped`, `error`).
- Heartbeat skip reasons (`quiet-hours`, `requests-in-flight`, `alerts-disabled`, `empty-heartbeat-file`, `no-tasks-due`).
Common signatures:
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled.
- `cron: timer tick failed` → scheduler tick failed; check file/log/runtime errors.
- `heartbeat skipped` with `reason=quiet-hours` → outside active hours window.
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank lines / markdown headers, so OpenClaw skips the model call.
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` contains a `tasks:` block, but none of the tasks are due on this tick.
- `heartbeat: unknown accountId` → invalid account id for heartbeat delivery target.
- `heartbeat skipped` with `reason=dm-blocked` → heartbeat target resolved to a DM-style destination while `agents.defaults.heartbeat.directPolicy` (or per-agent override) is set to `block`.
<AccordionGroup>
<Accordion title="Common signatures">
- `cron: scheduler disabled; jobs will not run automatically` → cron disabled.
- `cron: timer tick failed` → scheduler tick failed; check file/log/runtime errors.
- `heartbeat skipped` with `reason=quiet-hours` → outside active hours window.
- `heartbeat skipped` with `reason=empty-heartbeat-file` → `HEARTBEAT.md` exists but only contains blank lines / markdown headers, so OpenClaw skips the model call.
- `heartbeat skipped` with `reason=no-tasks-due` → `HEARTBEAT.md` contains a `tasks:` block, but none of the tasks are due on this tick.
- `heartbeat: unknown accountId` → invalid account id for heartbeat delivery target.
- `heartbeat skipped` with `reason=dm-blocked` → heartbeat target resolved to a DM-style destination while `agents.defaults.heartbeat.directPolicy` (or per-agent override) is set to `block`.
</Accordion>
</AccordionGroup>
Related:
- [Scheduled tasks: troubleshooting](/automation/cron-jobs#troubleshooting)
- [Scheduled tasks](/automation/cron-jobs)
- [Heartbeat](/gateway/heartbeat)
- [Scheduled tasks](/automation/cron-jobs)
- [Scheduled tasks: troubleshooting](/automation/cron-jobs#troubleshooting)
## Node paired tool fails
## Node paired, tool fails
If a node is paired but tools fail, isolate foreground, permission, and approval state.
@@ -486,9 +471,9 @@ Common signatures:
Related:
- [Exec approvals](/tools/exec-approvals)
- [Node troubleshooting](/nodes/troubleshooting)
- [Nodes](/nodes/index)
- [Exec approvals](/tools/exec-approvals)
## Browser tool fails
@@ -509,95 +494,104 @@ Look for:
- CDP profile reachability.
- Local Chrome availability for `existing-session` / `user` profiles.
Common signatures:
- `unknown command "browser"` or `unknown command 'browser'` → the bundled browser plugin is excluded by `plugins.allow`.
- browser tool missing / unavailable while `browser.enabled=true` → `plugins.allow` excludes `browser`, so the plugin never loaded.
- `Failed to start Chrome CDP on port` → browser process failed to launch.
- `browser.executablePath not found` → configured path is invalid.
- `browser.cdpUrl must be http(s) or ws(s)` → the configured CDP URL uses an unsupported scheme such as `file:` or `ftp:`.
- `browser.cdpUrl has invalid port` → the configured CDP URL has a bad or out-of-range port.
- `Could not find DevToolsActivePort for chrome` → Chrome MCP existing-session could not attach to the selected browser data dir yet. Open the browser inspect page, enable remote debugging, keep the browser open, approve the first attach prompt, then retry. If signed-in state is not required, prefer the managed `openclaw` profile.
- `No Chrome tabs found for profile="user"` → the Chrome MCP attach profile has no open local Chrome tabs.
- `Remote CDP for profile "<name>" is not reachable` → the configured remote CDP endpoint is not reachable from the gateway host.
- `Browser attachOnly is enabled ... not reachable` or `Browser attachOnly is enabled and CDP websocket ... is not reachable` → attach-only profile has no reachable target, or the HTTP endpoint answered but the CDP WebSocket still could not be opened.
- `Playwright is not available in this gateway build; '<feature>' is unsupported.` → the current gateway install lacks the bundled browser plugin's `playwright-core` runtime dependency; run `openclaw doctor --fix`, then restart the gateway. ARIA snapshots and basic page screenshots can still work, but navigation, AI snapshots, CSS-selector element screenshots, and PDF export stay unavailable.
- `fullPage is not supported for element screenshots` → screenshot request mixed `--full-page` with `--ref` or `--element`.
- `element screenshots are not supported for existing-session profiles; use ref from snapshot.` → Chrome MCP / `existing-session` screenshot calls must use page capture or a snapshot `--ref`, not CSS `--element`.
- `existing-session file uploads do not support element selectors; use ref/inputRef.` → Chrome MCP upload hooks need snapshot refs, not CSS selectors.
- `existing-session file uploads currently support one file at a time.` → send one upload per call on Chrome MCP profiles.
- `existing-session dialog handling does not support timeoutMs.` → dialog hooks on Chrome MCP profiles do not support timeout overrides.
- `existing-session type does not support timeoutMs overrides.` → omit `timeoutMs` for `act:type` on `profile="user"` / Chrome MCP existing-session profiles, or use a managed/CDP browser profile when a custom timeout is required.
- `existing-session evaluate does not support timeoutMs overrides.` → omit `timeoutMs` for `act:evaluate` on `profile="user"` / Chrome MCP existing-session profiles, or use a managed/CDP browser profile when a custom timeout is required.
- `response body is not supported for existing-session profiles yet.` → `responsebody` still requires a managed browser or raw CDP profile.
- stale viewport / dark-mode / locale / offline overrides on attach-only or remote CDP profiles → run `openclaw browser stop --browser-profile <name>` to close the active control session and release Playwright/CDP emulation state without restarting the whole gateway.
<AccordionGroup>
<Accordion title="Plugin / executable signatures">
- `unknown command "browser"` or `unknown command 'browser'` → the bundled browser plugin is excluded by `plugins.allow`.
- browser tool missing / unavailable while `browser.enabled=true` → `plugins.allow` excludes `browser`, so the plugin never loaded.
- `Failed to start Chrome CDP on port` → browser process failed to launch.
- `browser.executablePath not found` → configured path is invalid.
- `browser.cdpUrl must be http(s) or ws(s)` → the configured CDP URL uses an unsupported scheme such as `file:` or `ftp:`.
- `browser.cdpUrl has invalid port` → the configured CDP URL has a bad or out-of-range port.
- `Playwright is not available in this gateway build; '<feature>' is unsupported.` → the current gateway install lacks the bundled browser plugin's `playwright-core` runtime dependency; run `openclaw doctor --fix`, then restart the gateway. ARIA snapshots and basic page screenshots can still work, but navigation, AI snapshots, CSS-selector element screenshots, and PDF export stay unavailable.
</Accordion>
<Accordion title="Chrome MCP / existing-session signatures">
- `Could not find DevToolsActivePort for chrome` → Chrome MCP existing-session could not attach to the selected browser data dir yet. Open the browser inspect page, enable remote debugging, keep the browser open, approve the first attach prompt, then retry. If signed-in state is not required, prefer the managed `openclaw` profile.
- `No Chrome tabs found for profile="user"` → the Chrome MCP attach profile has no open local Chrome tabs.
- `Remote CDP for profile "<name>" is not reachable` → the configured remote CDP endpoint is not reachable from the gateway host.
- `Browser attachOnly is enabled ... not reachable` or `Browser attachOnly is enabled and CDP websocket ... is not reachable` → attach-only profile has no reachable target, or the HTTP endpoint answered but the CDP WebSocket still could not be opened.
</Accordion>
<Accordion title="Element / screenshot / upload signatures">
- `fullPage is not supported for element screenshots` → screenshot request mixed `--full-page` with `--ref` or `--element`.
- `element screenshots are not supported for existing-session profiles; use ref from snapshot.` Chrome MCP / `existing-session` screenshot calls must use page capture or a snapshot `--ref`, not CSS `--element`.
- `existing-session file uploads do not support element selectors; use ref/inputRef.` Chrome MCP upload hooks need snapshot refs, not CSS selectors.
- `existing-session file uploads currently support one file at a time.` → send one upload per call on Chrome MCP profiles.
- `existing-session dialog handling does not support timeoutMs.` → dialog hooks on Chrome MCP profiles do not support timeout overrides.
- `existing-session type does not support timeoutMs overrides.` → omit `timeoutMs` for `act:type` on `profile="user"` / Chrome MCP existing-session profiles, or use a managed/CDP browser profile when a custom timeout is required.
- `existing-session evaluate does not support timeoutMs overrides.` → omit `timeoutMs` for `act:evaluate` on `profile="user"` / Chrome MCP existing-session profiles, or use a managed/CDP browser profile when a custom timeout is required.
- `response body is not supported for existing-session profiles yet.` → `responsebody` still requires a managed browser or raw CDP profile.
- stale viewport / dark-mode / locale / offline overrides on attach-only or remote CDP profiles → run `openclaw browser stop --browser-profile <name>` to close the active control session and release Playwright/CDP emulation state without restarting the whole gateway.
</Accordion>
</AccordionGroup>
Related:
- [Browser troubleshooting](/tools/browser-linux-troubleshooting)
- [Browser (OpenClaw-managed)](/tools/browser)
- [Browser troubleshooting](/tools/browser-linux-troubleshooting)
## If you upgraded and something suddenly broke
Most post-upgrade breakage is config drift or stricter defaults now being enforced.
### 1) Auth and URL override behavior changed
<AccordionGroup>
<Accordion title="1. Auth and URL override behavior changed">
```bash
openclaw gateway status
openclaw config get gateway.mode
openclaw config get gateway.remote.url
openclaw config get gateway.auth.mode
```
```bash
openclaw gateway status
openclaw config get gateway.mode
openclaw config get gateway.remote.url
openclaw config get gateway.auth.mode
```
What to check:
What to check:
- If `gateway.mode=remote`, CLI calls may be targeting remote while your local service is fine.
- Explicit `--url` calls do not fall back to stored credentials.
- If `gateway.mode=remote`, CLI calls may be targeting remote while your local service is fine.
- Explicit `--url` calls do not fall back to stored credentials.
Common signatures:
Common signatures:
- `gateway connect failed:` → wrong URL target.
- `unauthorized` → endpoint reachable but wrong auth.
- `gateway connect failed:` → wrong URL target.
- `unauthorized` → endpoint reachable but wrong auth.
</Accordion>
<Accordion title="2. Bind and auth guardrails are stricter">
```bash
openclaw config get gateway.bind
openclaw config get gateway.auth.mode
openclaw config get gateway.auth.token
openclaw gateway status
openclaw logs --follow
```
### 2) Bind and auth guardrails are stricter
What to check:
```bash
openclaw config get gateway.bind
openclaw config get gateway.auth.mode
openclaw config get gateway.auth.token
openclaw gateway status
openclaw logs --follow
```
- Non-loopback binds (`lan`, `tailnet`, `custom`) need a valid gateway auth path: shared token/password auth, or a correctly configured non-loopback `trusted-proxy` deployment.
- Old keys like `gateway.token` do not replace `gateway.auth.token`.
What to check:
Common signatures:
- Non-loopback binds (`lan`, `tailnet`, `custom`) need a valid gateway auth path: shared token/password auth, or a correctly configured non-loopback `trusted-proxy` deployment.
- Old keys like `gateway.token` do not replace `gateway.auth.token`.
- `refusing to bind gateway ... without auth` → non-loopback bind without a valid gateway auth path.
- `Connectivity probe: failed` while runtime is running → gateway alive but inaccessible with current auth/url.
Common signatures:
</Accordion>
<Accordion title="3. Pairing and device identity state changed">
```bash
openclaw devices list
openclaw pairing list --channel <channel> [--account <id>]
openclaw logs --follow
openclaw doctor
```
- `refusing to bind gateway ... without auth` → non-loopback bind without a valid gateway auth path.
- `Connectivity probe: failed` while runtime is running → gateway alive but inaccessible with current auth/url.
What to check:
### 3) Pairing and device identity state changed
- Pending device approvals for dashboard/nodes.
- Pending DM pairing approvals after policy or identity changes.
```bash
openclaw devices list
openclaw pairing list --channel <channel> [--account <id>]
openclaw logs --follow
openclaw doctor
```
Common signatures:
What to check:
- `device identity required` → device auth not satisfied.
- `pairing required` → sender/device must be approved.
- Pending device approvals for dashboard/nodes.
- Pending DM pairing approvals after policy or identity changes.
Common signatures:
- `device identity required` → device auth not satisfied.
- `pairing required` → sender/device must be approved.
</Accordion>
</AccordionGroup>
If the service config and runtime still disagree after checks, reinstall service metadata from the same profile/state directory:
@@ -608,12 +602,12 @@ openclaw gateway restart
Related:
- [Gateway-owned pairing](/gateway/pairing)
- [Authentication](/gateway/authentication)
- [Background exec and process tool](/gateway/background-process)
- [Gateway-owned pairing](/gateway/pairing)
## Related
- [Gateway runbook](/gateway)
- [Doctor](/gateway/doctor)
- [FAQ](/help/faq)
- [Gateway runbook](/gateway)

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

@@ -597,7 +597,7 @@ and troubleshooting see the main [FAQ](/help/faq).
`openai-codex/gpt-5.5` for Codex OAuth through the default PI runner. Use
`openai/gpt-5.5` for direct OpenAI API-key access. GPT-5.5 can also use
subscription/OAuth via `openai-codex/gpt-5.5` or native Codex app-server
runs with `openai/gpt-5.5` and `embeddedHarness.runtime: "codex"`.
runs with `openai/gpt-5.5` and `agentRuntime.id: "codex"`.
See [Model providers](/concepts/model-providers) and [Onboarding (CLI)](/start/wizard).
</Accordion>
@@ -607,7 +607,7 @@ and troubleshooting see the main [FAQ](/help/faq).
- `openai/gpt-5.5` = current direct OpenAI API-key route in PI
- `openai-codex/gpt-5.5` = Codex OAuth route in PI
- `openai/gpt-5.5` + `embeddedHarness.runtime: "codex"` = native Codex app-server route
- `openai/gpt-5.5` + `agentRuntime.id: "codex"` = native Codex app-server route
- `openai-codex:...` = auth profile id, not a model ref
If you want the direct OpenAI Platform billing/limit path, set
@@ -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

@@ -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

@@ -292,6 +292,9 @@ by default, plus git-checkout installs under the same prefix flow.
- Refreshes a loaded gateway service best-effort (`openclaw gateway install --force`, then restart)
- Runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort)
</Step>
<Step title="Handle failures">
`iwr ... | iex` and scriptblock installs report a terminating error without closing the current PowerShell session. Direct `powershell -File` / `pwsh -File` installs still exit non-zero for automation.
</Step>
</Steps>
### Examples (install.ps1)

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,41 @@ 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`.
## Alternative: manual npm, pnpm, or bun
@@ -44,6 +73,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

@@ -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

@@ -9,8 +9,7 @@ title: "Plugin internals"
sidebarTitle: "Internals"
---
This is the **deep architecture reference** for the OpenClaw plugin system. For
practical guides, start with one of the focused pages below.
This is the **deep architecture reference** for the OpenClaw plugin system. For practical guides, start with one of the focused pages below.
<CardGroup cols={2}>
<Card title="Install and use plugins" icon="plug" href="/tools/plugin">
@@ -32,8 +31,7 @@ practical guides, start with one of the focused pages below.
## Public capability model
Capabilities are the public **native plugin** model inside OpenClaw. Every
native OpenClaw plugin registers against one or more capability types:
Capabilities are the public **native plugin** model inside OpenClaw. Every native OpenClaw plugin registers against one or more capability types:
| Capability | Registration method | Example plugins |
| ---------------------- | ------------------------------------------------ | ------------------------------------ |
@@ -51,15 +49,13 @@ native OpenClaw plugin registers against one or more capability types:
| Channel / messaging | `api.registerChannel(...)` | `msteams`, `matrix` |
| Gateway discovery | `api.registerGatewayDiscoveryService(...)` | `bonjour` |
A plugin that registers zero capabilities but provides hooks, tools, discovery
services, or background services is a **legacy hook-only** plugin. That pattern
is still fully supported.
<Note>
A plugin that registers zero capabilities but provides hooks, tools, discovery services, or background services is a **legacy hook-only** plugin. That pattern is still fully supported.
</Note>
### External compatibility stance
The capability model is landed in core and used by bundled/native plugins
today, but external plugin compatibility still needs a tighter bar than "it is
exported, therefore it is frozen."
The capability model is landed in core and used by bundled/native plugins today, but external plugin compatibility still needs a tighter bar than "it is exported, therefore it is frozen."
| Plugin situation | Guidance |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
@@ -67,33 +63,32 @@ exported, therefore it is frozen."
| New bundled/native plugins | Prefer explicit capability registration over vendor-specific reach-ins or new hook-only designs. |
| External plugins adopting capability registration | Allowed, but treat capability-specific helper surfaces as evolving unless docs mark them stable. |
Capability registration is the intended direction. Legacy hooks remain the
safest no-breakage path for external plugins during the transition. Exported
helper subpaths are not all equal — prefer narrow documented contracts over
incidental helper exports.
Capability registration is the intended direction. Legacy hooks remain the safest no-breakage path for external plugins during the transition. Exported helper subpaths are not all equal — prefer narrow documented contracts over incidental helper exports.
### Plugin shapes
OpenClaw classifies every loaded plugin into a shape based on its actual
registration behavior (not just static metadata):
OpenClaw classifies every loaded plugin into a shape based on its actual registration behavior (not just static metadata):
- **plain-capability**: registers exactly one capability type (for example a
provider-only plugin like `mistral`).
- **hybrid-capability**: registers multiple capability types (for example
`openai` owns text inference, speech, media understanding, and image
generation).
- **hook-only**: registers only hooks (typed or custom), no capabilities,
tools, commands, or services.
- **non-capability**: registers tools, commands, services, or routes but no
capabilities.
<AccordionGroup>
<Accordion title="plain-capability">
Registers exactly one capability type (for example a provider-only plugin like `mistral`).
</Accordion>
<Accordion title="hybrid-capability">
Registers multiple capability types (for example `openai` owns text inference, speech, media understanding, and image generation).
</Accordion>
<Accordion title="hook-only">
Registers only hooks (typed or custom), no capabilities, tools, commands, or services.
</Accordion>
<Accordion title="non-capability">
Registers tools, commands, services, or routes but no capabilities.
</Accordion>
</AccordionGroup>
Use `openclaw plugins inspect <id>` to see a plugin's shape and capability
breakdown. See [CLI reference](/cli/plugins#inspect) for details.
Use `openclaw plugins inspect <id>` to see a plugin's shape and capability breakdown. See [CLI reference](/cli/plugins#inspect) for details.
### Legacy hooks
The `before_agent_start` hook remains supported as a compatibility path for
hook-only plugins. Legacy real-world plugins still depend on it.
The `before_agent_start` hook remains supported as a compatibility path for hook-only plugins. Legacy real-world plugins still depend on it.
Direction:
@@ -105,8 +100,7 @@ Direction:
### Compatibility signals
When you run `openclaw doctor` or `openclaw plugins inspect <id>`, you may see
one of these labels:
When you run `openclaw doctor` or `openclaw plugins inspect <id>`, you may see one of these labels:
| Signal | Meaning |
| -------------------------- | ------------------------------------------------------------ |
@@ -115,98 +109,71 @@ one of these labels:
| **legacy warning** | Plugin uses `before_agent_start`, which is deprecated |
| **hard error** | Config is invalid or plugin failed to load |
Neither `hook-only` nor `before_agent_start` will break your plugin today:
`hook-only` is advisory, and `before_agent_start` only triggers a warning. These
signals also appear in `openclaw status --all` and `openclaw plugins doctor`.
Neither `hook-only` nor `before_agent_start` will break your plugin today: `hook-only` is advisory, and `before_agent_start` only triggers a warning. These signals also appear in `openclaw status --all` and `openclaw plugins doctor`.
## Architecture overview
OpenClaw's plugin system has four layers:
1. **Manifest + discovery**
OpenClaw finds candidate plugins from configured paths, workspace roots,
global plugin roots, and bundled plugins. Discovery reads native
`openclaw.plugin.json` manifests plus supported bundle manifests first.
2. **Enablement + validation**
Core decides whether a discovered plugin is enabled, disabled, blocked, or
selected for an exclusive slot such as memory.
3. **Runtime loading**
Native OpenClaw plugins are loaded in-process via jiti and register
capabilities into a central registry. Compatible bundles are normalized into
registry records without importing runtime code.
4. **Surface consumption**
The rest of OpenClaw reads the registry to expose tools, channels, provider
setup, hooks, HTTP routes, CLI commands, and services.
<Steps>
<Step title="Manifest + discovery">
OpenClaw finds candidate plugins from configured paths, workspace roots, global plugin roots, and bundled plugins. Discovery reads native `openclaw.plugin.json` manifests plus supported bundle manifests first.
</Step>
<Step title="Enablement + validation">
Core decides whether a discovered plugin is enabled, disabled, blocked, or selected for an exclusive slot such as memory.
</Step>
<Step title="Runtime loading">
Native OpenClaw plugins are loaded in-process via jiti and register capabilities into a central registry. Compatible bundles are normalized into registry records without importing runtime code.
</Step>
<Step title="Surface consumption">
The rest of OpenClaw reads the registry to expose tools, channels, provider setup, hooks, HTTP routes, CLI commands, and services.
</Step>
</Steps>
For plugin CLI specifically, root command discovery is split in two phases:
- parse-time metadata comes from `registerCli(..., { descriptors: [...] })`
- the real plugin CLI module can stay lazy and register on first invocation
That keeps plugin-owned CLI code inside the plugin while still letting OpenClaw
reserve root command names before parsing.
That keeps plugin-owned CLI code inside the plugin while still letting OpenClaw reserve root command names before parsing.
The important design boundary:
- manifest/config validation should work from **manifest/schema metadata**
without executing plugin code
- native capability discovery may load trusted plugin entry code to build a
non-activating registry snapshot
- native runtime behavior comes from the plugin module's `register(api)` path
with `api.registrationMode === "full"`
- manifest/config validation should work from **manifest/schema metadata** without executing plugin code
- native capability discovery may load trusted plugin entry code to build a non-activating registry snapshot
- native runtime behavior comes from the plugin module's `register(api)` path with `api.registrationMode === "full"`
That split lets OpenClaw validate config, explain missing/disabled plugins, and
build UI/schema hints before the full runtime is active.
That split lets OpenClaw validate config, explain missing/disabled plugins, and build UI/schema hints before the full runtime is active.
### Activation planning
Activation planning is part of the control plane. Callers can ask which plugins
are relevant to a concrete command, provider, channel, route, agent harness, or
capability before loading broader runtime registries.
Activation planning is part of the control plane. Callers can ask which plugins are relevant to a concrete command, provider, channel, route, agent harness, or capability before loading broader runtime registries.
The planner keeps current manifest behavior compatible:
- `activation.*` fields are explicit planner hints
- `providers`, `channels`, `commandAliases`, `setup.providers`,
`contracts.tools`, and hooks remain manifest ownership fallback
- `providers`, `channels`, `commandAliases`, `setup.providers`, `contracts.tools`, and hooks remain manifest ownership fallback
- the ids-only planner API stays available for existing callers
- the plan API reports reason labels so diagnostics can distinguish explicit
hints from ownership fallback
- the plan API reports reason labels so diagnostics can distinguish explicit hints from ownership fallback
Do not treat `activation` as a lifecycle hook or a replacement for
`register(...)`. It is metadata used to narrow loading. Prefer ownership fields
when they already describe the relationship; use `activation` only for extra
planner hints.
<Warning>
Do not treat `activation` as a lifecycle hook or a replacement for `register(...)`. It is metadata used to narrow loading. Prefer ownership fields when they already describe the relationship; use `activation` only for extra planner hints.
</Warning>
### Channel plugins and the shared message tool
Channel plugins do not need to register a separate send/edit/react tool for
normal chat actions. OpenClaw keeps one shared `message` tool in core, and
channel plugins own the channel-specific discovery and execution behind it.
Channel plugins do not need to register a separate send/edit/react tool for normal chat actions. OpenClaw keeps one shared `message` tool in core, and channel plugins own the channel-specific discovery and execution behind it.
The current boundary is:
- core owns the shared `message` tool host, prompt wiring, session/thread
bookkeeping, and execution dispatch
- channel plugins own scoped action discovery, capability discovery, and any
channel-specific schema fragments
- channel plugins own provider-specific session conversation grammar, such as
how conversation ids encode thread ids or inherit from parent conversations
- core owns the shared `message` tool host, prompt wiring, session/thread bookkeeping, and execution dispatch
- channel plugins own scoped action discovery, capability discovery, and any channel-specific schema fragments
- channel plugins own provider-specific session conversation grammar, such as how conversation ids encode thread ids or inherit from parent conversations
- channel plugins execute the final action through their action adapter
For channel plugins, the SDK surface is
`ChannelMessageActionAdapter.describeMessageTool(...)`. That unified discovery
call lets a plugin return its visible actions, capabilities, and schema
contributions together so those pieces do not drift apart.
For channel plugins, the SDK surface is `ChannelMessageActionAdapter.describeMessageTool(...)`. That unified discovery call lets a plugin return its visible actions, capabilities, and schema contributions together so those pieces do not drift apart.
When a channel-specific message-tool param carries a media source such as a
local path or remote media URL, the plugin should also return
`mediaSourceParams` from `describeMessageTool(...)`. Core uses that explicit
list to apply sandbox path normalization and outbound media-access hints
without hardcoding plugin-owned param names.
Prefer action-scoped maps there, not one channel-wide flat list, so a
profile-only media param does not get normalized on unrelated actions like
`send`.
When a channel-specific message-tool param carries a media source such as a local path or remote media URL, the plugin should also return `mediaSourceParams` from `describeMessageTool(...)`. Core uses that explicit list to apply sandbox path normalization and outbound media-access hints without hardcoding plugin-owned param names. Prefer action-scoped maps there, not one channel-wide flat list, so a profile-only media param does not get normalized on unrelated actions like `send`.
Core passes runtime scope into that discovery step. Important fields include:
@@ -219,107 +186,92 @@ Core passes runtime scope into that discovery step. Important fields include:
- `agentId`
- trusted inbound `requesterSenderId`
That matters for context-sensitive plugins. A channel can hide or expose
message actions based on the active account, current room/thread/message, or
trusted requester identity without hardcoding channel-specific branches in the
core `message` tool.
That matters for context-sensitive plugins. A channel can hide or expose message actions based on the active account, current room/thread/message, or trusted requester identity without hardcoding channel-specific branches in the core `message` tool.
This is why embedded-runner routing changes are still plugin work: the runner is
responsible for forwarding the current chat/session identity into the plugin
discovery boundary so the shared `message` tool exposes the right channel-owned
surface for the current turn.
This is why embedded-runner routing changes are still plugin work: the runner is responsible for forwarding the current chat/session identity into the plugin discovery boundary so the shared `message` tool exposes the right channel-owned surface for the current turn.
For channel-owned execution helpers, bundled plugins should keep the execution
runtime inside their own extension modules. Core no longer owns the Discord,
Slack, Telegram, or WhatsApp message-action runtimes under `src/agents/tools`.
We do not publish separate `plugin-sdk/*-action-runtime` subpaths, and bundled
plugins should import their own local runtime code directly from their
extension-owned modules.
For channel-owned execution helpers, bundled plugins should keep the execution runtime inside their own extension modules. Core no longer owns the Discord, Slack, Telegram, or WhatsApp message-action runtimes under `src/agents/tools`. We do not publish separate `plugin-sdk/*-action-runtime` subpaths, and bundled plugins should import their own local runtime code directly from their extension-owned modules.
The same boundary applies to provider-named SDK seams in general: core should
not import channel-specific convenience barrels for Slack, Discord, Signal,
WhatsApp, or similar extensions. If core needs a behavior, either consume the
bundled plugin's own `api.ts` / `runtime-api.ts` barrel or promote the need
into a narrow generic capability in the shared SDK.
The same boundary applies to provider-named SDK seams in general: core should not import channel-specific convenience barrels for Slack, Discord, Signal, WhatsApp, or similar extensions. If core needs a behavior, either consume the bundled plugin's own `api.ts` / `runtime-api.ts` barrel or promote the need into a narrow generic capability in the shared SDK.
For polls specifically, there are two execution paths:
- `outbound.sendPoll` is the shared baseline for channels that fit the common
poll model
- `actions.handleAction("poll")` is the preferred path for channel-specific
poll semantics or extra poll parameters
- `outbound.sendPoll` is the shared baseline for channels that fit the common poll model
- `actions.handleAction("poll")` is the preferred path for channel-specific poll semantics or extra poll parameters
Core now defers shared poll parsing until after plugin poll dispatch declines
the action, so plugin-owned poll handlers can accept channel-specific poll
fields without being blocked by the generic poll parser first.
Core now defers shared poll parsing until after plugin poll dispatch declines the action, so plugin-owned poll handlers can accept channel-specific poll fields without being blocked by the generic poll parser first.
See [Plugin architecture internals](/plugins/architecture-internals) for the full startup sequence.
## Capability ownership model
OpenClaw treats a native plugin as the ownership boundary for a **company** or a
**feature**, not as a grab bag of unrelated integrations.
OpenClaw treats a native plugin as the ownership boundary for a **company** or a **feature**, not as a grab bag of unrelated integrations.
That means:
- a company plugin should usually own all of that company's OpenClaw-facing
surfaces
- a company plugin should usually own all of that company's OpenClaw-facing surfaces
- a feature plugin should usually own the full feature surface it introduces
- channels should consume shared core capabilities instead of re-implementing
provider behavior ad hoc
- channels should consume shared core capabilities instead of re-implementing provider behavior ad hoc
<Accordion title="Example ownership patterns across bundled plugins">
- **Vendor multi-capability**: `openai` owns text inference, speech, realtime
voice, media understanding, and image generation. `google` owns text
inference plus media understanding, image generation, and web search.
`qwen` owns text inference plus media understanding and video generation.
- **Vendor single-capability**: `elevenlabs` and `microsoft` own speech;
`firecrawl` owns web-fetch; `minimax` / `mistral` / `moonshot` / `zai` own
media-understanding backends.
- **Feature plugin**: `voice-call` owns call transport, tools, CLI, routes,
and Twilio media-stream bridging, but consumes shared speech, realtime
transcription, and realtime voice capabilities instead of importing vendor
plugins directly.
</Accordion>
<AccordionGroup>
<Accordion title="Vendor multi-capability">
`openai` owns text inference, speech, realtime voice, media understanding, and image generation. `google` owns text inference plus media understanding, image generation, and web search. `qwen` owns text inference plus media understanding and video generation.
</Accordion>
<Accordion title="Vendor single-capability">
`elevenlabs` and `microsoft` own speech; `firecrawl` owns web-fetch; `minimax` / `mistral` / `moonshot` / `zai` own media-understanding backends.
</Accordion>
<Accordion title="Feature plugin">
`voice-call` owns call transport, tools, CLI, routes, and Twilio media-stream bridging, but consumes shared speech, realtime transcription, and realtime voice capabilities instead of importing vendor plugins directly.
</Accordion>
</AccordionGroup>
The intended end state is:
- OpenAI lives in one plugin even if it spans text models, speech, images, and
future video
- OpenAI lives in one plugin even if it spans text models, speech, images, and future video
- another vendor can do the same for its own surface area
- channels do not care which vendor plugin owns the provider; they consume the
shared capability contract exposed by core
- channels do not care which vendor plugin owns the provider; they consume the shared capability contract exposed by core
This is the key distinction:
- **plugin** = ownership boundary
- **capability** = core contract that multiple plugins can implement or consume
So if OpenClaw adds a new domain such as video, the first question is not
"which provider should hardcode video handling?" The first question is "what is
the core video capability contract?" Once that contract exists, vendor plugins
can register against it and channel/feature plugins can consume it.
So if OpenClaw adds a new domain such as video, the first question is not "which provider should hardcode video handling?" The first question is "what is the core video capability contract?" Once that contract exists, vendor plugins can register against it and channel/feature plugins can consume it.
If the capability does not exist yet, the right move is usually:
1. define the missing capability in core
2. expose it through the plugin API/runtime in a typed way
3. wire channels/features against that capability
4. let vendor plugins register implementations
<Steps>
<Step title="Define the capability">
Define the missing capability in core.
</Step>
<Step title="Expose through the SDK">
Expose it through the plugin API/runtime in a typed way.
</Step>
<Step title="Wire consumers">
Wire channels/features against that capability.
</Step>
<Step title="Vendor implementations">
Let vendor plugins register implementations.
</Step>
</Steps>
This keeps ownership explicit while avoiding core behavior that depends on a
single vendor or a one-off plugin-specific code path.
This keeps ownership explicit while avoiding core behavior that depends on a single vendor or a one-off plugin-specific code path.
### Capability layering
Use this mental model when deciding where code belongs:
- **core capability layer**: shared orchestration, policy, fallback, config
merge rules, delivery semantics, and typed contracts
- **vendor plugin layer**: vendor-specific APIs, auth, model catalogs, speech
synthesis, image generation, future video backends, usage endpoints
- **channel/feature plugin layer**: Slack/Discord/voice-call/etc. integration
that consumes core capabilities and presents them on a surface
<Tabs>
<Tab title="Core capability layer">
Shared orchestration, policy, fallback, config merge rules, delivery semantics, and typed contracts.
</Tab>
<Tab title="Vendor plugin layer">
Vendor-specific APIs, auth, model catalogs, speech synthesis, image generation, future video backends, usage endpoints.
</Tab>
<Tab title="Channel/feature plugin layer">
Slack/Discord/voice-call/etc. integration that consumes core capabilities and presents them on a surface.
</Tab>
</Tabs>
For example, TTS follows this shape:
@@ -331,10 +283,7 @@ That same pattern should be preferred for future capabilities.
### Multi-capability company plugin example
A company plugin should feel cohesive from the outside. If OpenClaw has shared
contracts for models, speech, realtime transcription, realtime voice, media
understanding, image generation, video generation, web fetch, and web search,
a vendor can own all of its surfaces in one place:
A company plugin should feel cohesive from the outside. If OpenClaw has shared contracts for models, speech, realtime transcription, realtime voice, media understanding, image generation, video generation, web fetch, and web search, a vendor can own all of its surfaces in one place:
```ts
import type { OpenClawPluginDefinition } from "openclaw/plugin-sdk/plugin-entry";
@@ -393,117 +342,101 @@ What matters is not the exact helper names. The shape matters:
- one plugin owns the vendor surface
- core still owns the capability contracts
- channels and feature plugins consume `api.runtime.*` helpers, not vendor code
- contract tests can assert that the plugin registered the capabilities it
claims to own
- contract tests can assert that the plugin registered the capabilities it claims to own
### Capability example: video understanding
OpenClaw already treats image/audio/video understanding as one shared
capability. The same ownership model applies there:
OpenClaw already treats image/audio/video understanding as one shared capability. The same ownership model applies there:
1. core defines the media-understanding contract
2. vendor plugins register `describeImage`, `transcribeAudio`, and
`describeVideo` as applicable
3. channels and feature plugins consume the shared core behavior instead of
wiring directly to vendor code
<Steps>
<Step title="Core defines the contract">
Core defines the media-understanding contract.
</Step>
<Step title="Vendor plugins register">
Vendor plugins register `describeImage`, `transcribeAudio`, and `describeVideo` as applicable.
</Step>
<Step title="Consumers use the shared behavior">
Channels and feature plugins consume the shared core behavior instead of wiring directly to vendor code.
</Step>
</Steps>
That avoids baking one provider's video assumptions into core. The plugin owns
the vendor surface; core owns the capability contract and fallback behavior.
That avoids baking one provider's video assumptions into core. The plugin owns the vendor surface; core owns the capability contract and fallback behavior.
Video generation already uses that same sequence: core owns the typed
capability contract and runtime helper, and vendor plugins register
`api.registerVideoGenerationProvider(...)` implementations against it.
Video generation already uses that same sequence: core owns the typed capability contract and runtime helper, and vendor plugins register `api.registerVideoGenerationProvider(...)` implementations against it.
Need a concrete rollout checklist? See
[Capability Cookbook](/tools/capability-cookbook).
Need a concrete rollout checklist? See [Capability Cookbook](/tools/capability-cookbook).
## Contracts and enforcement
The plugin API surface is intentionally typed and centralized in
`OpenClawPluginApi`. That contract defines the supported registration points and
the runtime helpers a plugin may rely on.
The plugin API surface is intentionally typed and centralized in `OpenClawPluginApi`. That contract defines the supported registration points and the runtime helpers a plugin may rely on.
Why this matters:
- plugin authors get one stable internal standard
- core can reject duplicate ownership such as two plugins registering the same
provider id
- core can reject duplicate ownership such as two plugins registering the same provider id
- startup can surface actionable diagnostics for malformed registration
- contract tests can enforce bundled-plugin ownership and prevent silent drift
There are two layers of enforcement:
1. **runtime registration enforcement**
The plugin registry validates registrations as plugins load. Examples:
duplicate provider ids, duplicate speech provider ids, and malformed
registrations produce plugin diagnostics instead of undefined behavior.
2. **contract tests**
Bundled plugins are captured in contract registries during test runs so
OpenClaw can assert ownership explicitly. Today this is used for model
providers, speech providers, web search providers, and bundled registration
ownership.
<AccordionGroup>
<Accordion title="Runtime registration enforcement">
The plugin registry validates registrations as plugins load. Examples: duplicate provider ids, duplicate speech provider ids, and malformed registrations produce plugin diagnostics instead of undefined behavior.
</Accordion>
<Accordion title="Contract tests">
Bundled plugins are captured in contract registries during test runs so OpenClaw can assert ownership explicitly. Today this is used for model providers, speech providers, web search providers, and bundled registration ownership.
</Accordion>
</AccordionGroup>
The practical effect is that OpenClaw knows, up front, which plugin owns which
surface. That lets core and channels compose seamlessly because ownership is
declared, typed, and testable rather than implicit.
The practical effect is that OpenClaw knows, up front, which plugin owns which surface. That lets core and channels compose seamlessly because ownership is declared, typed, and testable rather than implicit.
### What belongs in a contract
Good plugin contracts are:
<Tabs>
<Tab title="Good contracts">
- typed
- small
- capability-specific
- owned by core
- reusable by multiple plugins
- consumable by channels/features without vendor knowledge
</Tab>
<Tab title="Bad contracts">
- vendor-specific policy hidden in core
- one-off plugin escape hatches that bypass the registry
- channel code reaching straight into a vendor implementation
- ad hoc runtime objects that are not part of `OpenClawPluginApi` or `api.runtime`
</Tab>
</Tabs>
- typed
- small
- capability-specific
- owned by core
- reusable by multiple plugins
- consumable by channels/features without vendor knowledge
Bad plugin contracts are:
- vendor-specific policy hidden in core
- one-off plugin escape hatches that bypass the registry
- channel code reaching straight into a vendor implementation
- ad hoc runtime objects that are not part of `OpenClawPluginApi` or
`api.runtime`
When in doubt, raise the abstraction level: define the capability first, then
let plugins plug into it.
When in doubt, raise the abstraction level: define the capability first, then let plugins plug into it.
## Execution model
Native OpenClaw plugins run **in-process** with the Gateway. They are not
sandboxed. A loaded native plugin has the same process-level trust boundary as
core code.
Native OpenClaw plugins run **in-process** with the Gateway. They are not sandboxed. A loaded native plugin has the same process-level trust boundary as core code.
<Warning>
Implications:
- a native plugin can register tools, network handlers, hooks, and services
- a native plugin bug can crash or destabilize the gateway
- a malicious native plugin is equivalent to arbitrary code execution inside
the OpenClaw process
- a malicious native plugin is equivalent to arbitrary code execution inside the OpenClaw process
</Warning>
Compatible bundles are safer by default because OpenClaw currently treats them
as metadata/content packs. In current releases, that mostly means bundled
skills.
Compatible bundles are safer by default because OpenClaw currently treats them as metadata/content packs. In current releases, that mostly means bundled skills.
Use allowlists and explicit install/load paths for non-bundled plugins. Treat
workspace plugins as development-time code, not production defaults.
Use allowlists and explicit install/load paths for non-bundled plugins. Treat workspace plugins as development-time code, not production defaults.
For bundled workspace package names, keep the plugin id anchored in the npm
name: `@openclaw/<id>` by default, or an approved typed suffix such as
`-provider`, `-plugin`, `-speech`, `-sandbox`, or `-media-understanding` when
the package intentionally exposes a narrower plugin role.
For bundled workspace package names, keep the plugin id anchored in the npm name: `@openclaw/<id>` by default, or an approved typed suffix such as `-provider`, `-plugin`, `-speech`, `-sandbox`, or `-media-understanding` when the package intentionally exposes a narrower plugin role.
Important trust note:
<Note>
**Trust note:**
- `plugins.allow` trusts **plugin ids**, not source provenance.
- A workspace plugin with the same id as a bundled plugin intentionally shadows
the bundled copy when that workspace plugin is enabled/allowlisted.
- A workspace plugin with the same id as a bundled plugin intentionally shadows the bundled copy when that workspace plugin is enabled/allowlisted.
- This is normal and useful for local development, patch testing, and hotfixes.
- Bundled-plugin trust is resolved from the source snapshot — the manifest and
code on disk at load time — rather than from install metadata. A corrupted
or substituted install record cannot silently widen a bundled plugin's trust
surface beyond what the actual source claims.
- Bundled-plugin trust is resolved from the source snapshot — the manifest and code on disk at load time — rather than from install metadata. A corrupted or substituted install record cannot silently widen a bundled plugin's trust surface beyond what the actual source claims.
</Note>
## Export boundary
@@ -516,22 +449,14 @@ Keep capability registration public. Trim non-contract helper exports:
- vendor-specific convenience helpers
- setup/onboarding helpers that are implementation details
Some bundled-plugin helper subpaths still remain in the generated SDK export
map for compatibility and bundled-plugin maintenance. Current examples include
`plugin-sdk/feishu`, `plugin-sdk/feishu-setup`, `plugin-sdk/zalo`,
`plugin-sdk/zalo-setup`, and several `plugin-sdk/matrix*` seams. Treat those as
reserved implementation-detail exports, not as the recommended SDK pattern for
new third-party plugins.
Some bundled-plugin helper subpaths still remain in the generated SDK export map for compatibility and bundled-plugin maintenance. Current examples include `plugin-sdk/feishu`, `plugin-sdk/feishu-setup`, `plugin-sdk/zalo`, `plugin-sdk/zalo-setup`, and several `plugin-sdk/matrix*` seams. Treat those as reserved implementation-detail exports, not as the recommended SDK pattern for new third-party plugins.
## Internals and reference
For the load pipeline, registry model, provider runtime hooks, Gateway HTTP
routes, message tool schemas, channel target resolution, provider catalogs,
context engine plugins, and the guide to adding a new capability, see
[Plugin architecture internals](/plugins/architecture-internals).
For the load pipeline, registry model, provider runtime hooks, Gateway HTTP routes, message tool schemas, channel target resolution, provider catalogs, context engine plugins, and the guide to adding a new capability, see [Plugin architecture internals](/plugins/architecture-internals).
## Related
- [Building plugins](/plugins/building-plugins)
- [Plugin SDK setup](/plugins/sdk-setup)
- [Plugin manifest](/plugins/manifest)
- [Plugin SDK setup](/plugins/sdk-setup)

View File

@@ -26,7 +26,7 @@ The bundled `codex` plugin contributes several separate capabilities:
| Capability | How you use it | What it does |
| --------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------- |
| Native embedded runtime | `embeddedHarness.runtime: "codex"` | Runs OpenClaw embedded agent turns through Codex app-server. |
| Native embedded runtime | `agentRuntime.id: "codex"` | Runs OpenClaw embedded agent turns through Codex app-server. |
| Native chat-control commands | `/codex bind`, `/codex resume`, `/codex steer`, ... | Binds and controls Codex app-server threads from a messaging conversation. |
| Codex app-server provider/catalog | `codex` internals, surfaced through the harness | Lets the runtime discover and validate app-server models. |
| Codex media-understanding path | `codex/*` image-model compatibility paths | Runs bounded Codex app-server turns for supported image understanding models. |
@@ -69,7 +69,7 @@ and [Plugin guard behavior](/tools/plugin).
The harness is off by default. New configs should keep OpenAI model refs
canonical as `openai/gpt-*` and explicitly force
`embeddedHarness.runtime: "codex"` or `OPENCLAW_AGENT_RUNTIME=codex` when they
`agentRuntime.id: "codex"` or `OPENCLAW_AGENT_RUNTIME=codex` when they
want native app-server execution. Legacy `codex/*` model refs still auto-select
the harness for compatibility, but runtime-backed legacy provider prefixes are
not shown as normal model/provider choices.
@@ -87,14 +87,14 @@ Use this table before changing config:
| ------------------------------------------- | -------------------------- | -------------------------------------- | --------------------------- | ------------------------------ |
| OpenAI API through normal OpenClaw runner | `openai/gpt-*` | omitted or `runtime: "pi"` | OpenAI provider | `Runtime: OpenClaw Pi Default` |
| Codex OAuth/subscription through PI | `openai-codex/gpt-*` | omitted or `runtime: "pi"` | OpenAI Codex OAuth provider | `Runtime: OpenClaw Pi Default` |
| Native Codex app-server embedded turns | `openai/gpt-*` | `embeddedHarness.runtime: "codex"` | `codex` plugin | `Runtime: OpenAI Codex` |
| Mixed providers with conservative auto mode | provider-specific refs | `runtime: "auto", fallback: "pi"` | Optional plugin runtimes | Depends on selected runtime |
| Native Codex app-server embedded turns | `openai/gpt-*` | `agentRuntime.id: "codex"` | `codex` plugin | `Runtime: OpenAI Codex` |
| Mixed providers with conservative auto mode | provider-specific refs | `agentRuntime.id: "auto"` | Optional plugin runtimes | Depends on selected runtime |
| Explicit Codex ACP adapter session | ACP prompt/model dependent | `sessions_spawn` with `runtime: "acp"` | healthy `acpx` backend | ACP task/session status |
The important split is provider versus runtime:
- `openai-codex/*` answers "which provider/auth route should PI use?"
- `embeddedHarness.runtime: "codex"` answers "which loop should execute this
- `agentRuntime.id: "codex"` answers "which loop should execute this
embedded turn?"
- `/codex ...` answers "which native Codex conversation should this chat bind
or control?"
@@ -106,11 +106,11 @@ OpenAI-family routes are prefix-specific. Use `openai-codex/*` when you want
Codex OAuth through PI; use `openai/*` when you want direct OpenAI API access or
when you are forcing the native Codex app-server harness:
| Model ref | Runtime path | Use when |
| ----------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| `openai/gpt-5.4` | OpenAI provider through OpenClaw/PI plumbing | You want current direct OpenAI Platform API access with `OPENAI_API_KEY`. |
| `openai-codex/gpt-5.5` | OpenAI Codex OAuth through OpenClaw/PI | You want ChatGPT/Codex subscription auth with the default PI runner. |
| `openai/gpt-5.5` + `embeddedHarness.runtime: "codex"` | Codex app-server harness | You want native Codex app-server execution for the embedded agent turn. |
| Model ref | Runtime path | Use when |
| --------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| `openai/gpt-5.4` | OpenAI provider through OpenClaw/PI plumbing | You want current direct OpenAI Platform API access with `OPENAI_API_KEY`. |
| `openai-codex/gpt-5.5` | OpenAI Codex OAuth through OpenClaw/PI | You want ChatGPT/Codex subscription auth with the default PI runner. |
| `openai/gpt-5.5` + `agentRuntime.id: "codex"` | Codex app-server harness | You want native Codex app-server execution for the embedded agent turn. |
GPT-5.5 is currently subscription/OAuth-only in OpenClaw. Use
`openai-codex/gpt-5.5` for PI OAuth, or `openai/gpt-5.5` with the Codex
@@ -123,7 +123,7 @@ refs and records the runtime policy separately, while fallback-only legacy refs
are left unchanged because runtime is configured for the whole agent container.
New PI Codex OAuth configs should use `openai-codex/gpt-*`; new native
app-server harness configs should use `openai/gpt-*` plus
`embeddedHarness.runtime: "codex"`.
`agentRuntime.id: "codex"`.
`agents.defaults.imageModel` follows the same prefix split. Use
`openai-codex/gpt-*` when image understanding should run through the OpenAI
@@ -152,14 +152,14 @@ means:
- **No change is required** if you intended ChatGPT/Codex OAuth through PI.
- Change the model to `openai/<model>` and set
`embeddedHarness.runtime: "codex"` if you intended native app-server
`agentRuntime.id: "codex"` if you intended native app-server
execution.
- Existing sessions still need `/new` or `/reset` after a runtime change,
because session runtime pins are sticky.
Harness selection is not a live session control. When an embedded turn runs,
OpenClaw records the selected harness id on that session and keeps using it for
later turns in the same session id. Change `embeddedHarness` config or
later turns in the same session id. Change `agentRuntime` config or
`OPENCLAW_AGENT_RUNTIME` when you want future sessions to use another harness;
use `/new` or `/reset` to start a fresh session before switching an existing
conversation between PI and Codex. This avoids replaying one transcript through
@@ -205,8 +205,8 @@ Use `openai/gpt-5.5`, enable the bundled plugin, and force the `codex` harness:
agents: {
defaults: {
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
},
},
},
@@ -230,11 +230,11 @@ If your config uses `plugins.allow`, include `codex` there too:
Legacy configs that set `agents.defaults.model` or an agent model to
`codex/<model>` still auto-enable the bundled `codex` plugin. New configs should
prefer `openai/<model>` plus the explicit `embeddedHarness` entry above.
prefer `openai/<model>` plus the explicit `agentRuntime` entry above.
## Add Codex alongside other models
Do not set `runtime: "codex"` globally if the same agent should freely switch
Do not set `agentRuntime.id: "codex"` globally if the same agent should freely switch
between Codex and non-Codex provider models. A forced runtime applies to every
embedded turn for that agent or session. If you select an Anthropic model while
that runtime is forced, OpenClaw still tries the Codex harness and fails closed
@@ -242,8 +242,8 @@ instead of silently routing that turn through PI.
Use one of these shapes instead:
- Put Codex on a dedicated agent with `embeddedHarness.runtime: "codex"`.
- Keep the default agent on `runtime: "auto"` and PI fallback for normal mixed
- Put Codex on a dedicated agent with `agentRuntime.id: "codex"`.
- Keep the default agent on `agentRuntime.id: "auto"` and PI fallback for normal mixed
provider usage.
- Use legacy `codex/*` refs only for compatibility. New configs should prefer
`openai/*` plus an explicit Codex runtime policy.
@@ -262,8 +262,8 @@ adds a separate Codex agent:
},
agents: {
defaults: {
embeddedHarness: {
runtime: "auto",
agentRuntime: {
id: "auto",
fallback: "pi",
},
},
@@ -277,8 +277,8 @@ adds a separate Codex agent:
id: "codex",
name: "Codex",
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
},
},
],
@@ -302,7 +302,7 @@ Agents should route user requests by intent, not by the word "Codex" alone:
| "Bind this chat to Codex" | `/codex bind` |
| "Resume Codex thread `<id>` here" | `/codex resume <id>` |
| "Show Codex threads" | `/codex threads` |
| "Use Codex as the runtime for this agent" | config change to `embeddedHarness.runtime` |
| "Use Codex as the runtime for this agent" | config change to `agentRuntime.id` |
| "Use my ChatGPT/Codex subscription with normal OpenClaw" | `openai-codex/*` model refs |
| "Run Codex through ACP/acpx" | ACP `sessions_spawn({ runtime: "acp", ... })` |
| "Start Claude Code/Gemini/OpenCode/Cursor in a thread" | ACP/acpx, not `/codex` and not native sub-agents |
@@ -323,8 +323,8 @@ uses Codex. Explicit plugin runtimes default to no PI fallback, so
agents: {
defaults: {
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
fallback: "none",
},
},
@@ -352,8 +352,8 @@ auto-selection:
{
agents: {
defaults: {
embeddedHarness: {
runtime: "auto",
agentRuntime: {
id: "auto",
fallback: "pi",
},
},
@@ -367,8 +367,8 @@ auto-selection:
id: "codex",
name: "Codex",
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
fallback: "none",
},
},
@@ -565,8 +565,8 @@ Codex-only harness validation:
agents: {
defaults: {
model: "openai/gpt-5.5",
embeddedHarness: {
runtime: "codex",
agentRuntime: {
id: "codex",
},
},
},
@@ -772,15 +772,15 @@ understanding continue to use the matching provider/model settings such as
**Codex does not appear as a normal `/model` provider:** that is expected for
new configs. Select an `openai/gpt-*` model with
`embeddedHarness.runtime: "codex"` (or a legacy `codex/*` ref), enable
`agentRuntime.id: "codex"` (or a legacy `codex/*` ref), enable
`plugins.entries.codex.enabled`, and check whether `plugins.allow` excludes
`codex`.
**OpenClaw uses PI instead of Codex:** `runtime: "auto"` can still use PI as the
**OpenClaw uses PI instead of Codex:** `agentRuntime.id: "auto"` can still use PI as the
compatibility backend when no Codex harness claims the run. Set
`embeddedHarness.runtime: "codex"` to force Codex selection while testing. A
`agentRuntime.id: "codex"` to force Codex selection while testing. A
forced Codex runtime now fails instead of falling back to PI unless you
explicitly set `embeddedHarness.fallback: "pi"`. Once Codex app-server is
explicitly set `agentRuntime.fallback: "pi"`. Once Codex app-server is
selected, its failures surface directly without extra fallback config.
**The app-server is rejected:** upgrade Codex so the app-server handshake
@@ -795,9 +795,9 @@ or disable discovery.
and that the remote app-server speaks the same Codex app-server protocol version.
**A non-Codex model uses PI:** that is expected unless you forced
`embeddedHarness.runtime: "codex"` for that agent or selected a legacy
`agentRuntime.id: "codex"` for that agent or selected a legacy
`codex/*` ref. Plain `openai/gpt-*` and other provider refs stay on their normal
provider path in `auto` mode. If you force `runtime: "codex"`, every embedded
provider path in `auto` mode. If you force `agentRuntime.id: "codex"`, every embedded
turn for that agent must be a Codex-supported OpenAI model.
## Related

View File

@@ -71,7 +71,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 +82,27 @@ 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/plugin-sdk/channel-runtime`,
`openclaw/plugin-sdk/command-auth` status builders, and the
`ClawdbotConfig` type alias
- bundled plugin allowlist and enablement behavior
- legacy provider/channel env-var manifest metadata
- activation hints that are being replaced by manifest contribution ownership
- `embeddedHarness` and `agent-harness` naming aliases while public naming moves
toward `agentRuntime`
- `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`
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

@@ -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
@@ -524,6 +530,12 @@ Non-bundled plugins that declare `channels[]` should also declare matching
cold-path config schema, setup, and Control UI surfaces cannot know the
channel-owned option shape until plugin runtime executes.
`channelConfigs.<channel-id>.commands.nativeCommandsAutoEnabled` and
`nativeSkillsAutoEnabled` can declare static `auto` defaults for command config
checks that run before channel runtime loads. Bundled channels can also publish
the same defaults through `package.json#openclaw.channel.commands` alongside
their other package-owned channel catalog metadata.
```json
{
"channelConfigs": {
@@ -543,6 +555,10 @@ channel-owned option shape until plugin runtime executes.
},
"label": "Matrix",
"description": "Matrix homeserver connection",
"commands": {
"nativeCommandsAutoEnabled": true,
"nativeSkillsAutoEnabled": true
},
"preferOver": ["matrix-legacy"]
}
}
@@ -557,6 +573,7 @@ Each channel entry can include:
| `uiHints` | `Record<string, object>` | Optional UI labels/placeholders/sensitive hints for that channel config section. |
| `label` | `string` | Channel label merged into picker and inspect surfaces when runtime metadata is not ready. |
| `description` | `string` | Short channel description for inspect and catalog surfaces. |
| `commands` | `object` | Static native command and native skill auto-defaults for pre-runtime config checks. |
| `preferOver` | `string[]` | Legacy or lower-priority plugin ids this channel should outrank in selection surfaces. |
### Replacing another channel plugin
@@ -792,6 +809,7 @@ Important examples:
| `openclaw.setupEntry` | Lightweight setup-only entrypoint used during onboarding, deferred channel startup, and read-only channel status/SecretRef discovery. Must stay inside the plugin package directory. |
| `openclaw.runtimeSetupEntry` | Declares the built JavaScript setup entrypoint for installed packages. Must stay inside the plugin package directory. |
| `openclaw.channel` | Cheap channel catalog metadata like labels, docs paths, aliases, and selection copy. |
| `openclaw.channel.commands` | Static native command and native skill auto-default metadata used by config, audit, and command-list surfaces before channel runtime loads. |
| `openclaw.channel.configuredState` | Lightweight configured-state checker metadata that can answer "does env-only setup already exist?" without loading the full channel runtime. |
| `openclaw.channel.persistedAuthState` | Lightweight persisted-auth checker metadata that can answer "is anything already signed in?" without loading the full channel runtime. |
| `openclaw.install.npmSpec` / `openclaw.install.localPath` | Install/update hints for bundled and externally published plugins. |

View File

@@ -142,7 +142,7 @@ OpenClaw. The harness then claims that provider in `supports(...)`.
The bundled Codex plugin follows this pattern:
- preferred user model refs: `openai/gpt-5.5` plus
`embeddedHarness.runtime: "codex"`
`agentRuntime.id: "codex"`
- compatibility refs: legacy `codex/gpt-*` refs remain accepted, but new
configs should not use them as normal provider/model refs
- harness id: `codex`
@@ -153,7 +153,7 @@ The bundled Codex plugin follows this pattern:
The Codex plugin is additive. Plain `openai/gpt-*` refs continue to use the
normal OpenClaw provider path unless you force the Codex harness with
`embeddedHarness.runtime: "codex"`. Older `codex/gpt-*` refs still select the
`agentRuntime.id: "codex"`. Older `codex/gpt-*` refs still select the
Codex provider and harness for compatibility.
For operator setup, model prefix examples, and Codex-only configs, see
@@ -194,14 +194,14 @@ intentional silent replies such as `NO_REPLY` unclassified.
The bundled `codex` harness is the native Codex mode for embedded OpenClaw
agent turns. Enable the bundled `codex` plugin first, and include `codex` in
`plugins.allow` if your config uses a restrictive allowlist. Native app-server
configs should use `openai/gpt-*` with `embeddedHarness.runtime: "codex"`.
configs should use `openai/gpt-*` with `agentRuntime.id: "codex"`.
Use `openai-codex/*` for Codex OAuth through PI instead. Legacy `codex/*`
model refs remain compatibility aliases for the native harness.
When this mode runs, Codex owns the native thread id, resume behavior,
compaction, and app-server execution. OpenClaw still owns the chat channel,
visible transcript mirror, tool policy, approvals, media delivery, and session
selection. Use `embeddedHarness.runtime: "codex"` without a `fallback` override
selection. Use `agentRuntime.id: "codex"` without a `fallback` override
when you need to prove that only the Codex app-server path can claim the run.
Explicit plugin runtimes already fail closed by default. Set `fallback: "pi"`
only when you intentionally want PI to handle missing harness selection. Codex
@@ -209,8 +209,8 @@ app-server failures already fail directly instead of retrying through PI.
## Disable PI fallback
By default, OpenClaw runs embedded agents with `agents.defaults.embeddedHarness`
set to `{ runtime: "auto", fallback: "pi" }`. In `auto` mode, registered plugin
By default, OpenClaw runs embedded agents with `agents.defaults.agentRuntime`
set to `{ id: "auto", fallback: "pi" }`. In `auto` mode, registered plugin
harnesses can claim a provider/model pair. If none match, OpenClaw falls back
to PI.
@@ -228,8 +228,8 @@ For Codex-only embedded runs:
"agents": {
"defaults": {
"model": "openai/gpt-5.5",
"embeddedHarness": {
"runtime": "codex"
"agentRuntime": {
"id": "codex"
}
}
}
@@ -244,8 +244,8 @@ the fallback:
{
"agents": {
"defaults": {
"embeddedHarness": {
"runtime": "auto",
"agentRuntime": {
"id": "auto",
"fallback": "none"
}
}
@@ -259,8 +259,8 @@ Per-agent overrides use the same shape:
{
"agents": {
"defaults": {
"embeddedHarness": {
"runtime": "auto",
"agentRuntime": {
"id": "auto",
"fallback": "pi"
}
},
@@ -268,8 +268,8 @@ Per-agent overrides use the same shape:
{
"id": "codex-only",
"model": "openai/gpt-5.5",
"embeddedHarness": {
"runtime": "codex",
"agentRuntime": {
"id": "codex",
"fallback": "none"
}
}

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

@@ -97,6 +97,25 @@ Anthropic's current public docs:
Setup and runtime details for the Claude CLI backend are in [CLI Backends](/gateway/cli-backends).
</Note>
### Config example
Prefer the canonical Anthropic model ref plus a CLI runtime override:
```json5
{
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-7" },
agentRuntime: { id: "claude-cli" },
},
},
}
```
Legacy `claude-cli/claude-opus-4-7` model refs still work for
compatibility, but new config should keep provider/model selection as
`anthropic/*` and put the execution backend in `agentRuntime.id`.
<Tip>
If you want the clearest billing path, use an Anthropic API key instead. OpenClaw also supports subscription-style options from [OpenAI Codex](/providers/openai), [Qwen Cloud](/providers/qwen), [MiniMax](/providers/minimax), and [Z.AI / GLM](/providers/glm).
</Tip>

View File

@@ -13,7 +13,7 @@ Gemini Grounding.
- Provider: `google`
- Auth: `GEMINI_API_KEY` or `GOOGLE_API_KEY`
- API: Google Gemini API
- Runtime option: `agents.defaults.embeddedHarness.runtime: "google-gemini-cli"`
- Runtime option: `agents.defaults.agentRuntime.id: "google-gemini-cli"`
reuses Gemini CLI OAuth while keeping model refs canonical as `google/*`.
## Getting started

View File

@@ -17,7 +17,7 @@ embedded agent loop:
- **API key** — direct OpenAI Platform access with usage-based billing (`openai/*` models)
- **Codex subscription through PI** — ChatGPT/Codex sign-in with subscription access (`openai-codex/*` models)
- **Codex app-server harness** — native Codex app-server execution (`openai/*` models plus `agents.defaults.embeddedHarness.runtime: "codex"`)
- **Codex app-server harness** — native Codex app-server execution (`openai/*` models plus `agents.defaults.agentRuntime.id: "codex"`)
OpenAI explicitly supports subscription OAuth usage in external tools and workflows like OpenClaw.
@@ -27,13 +27,13 @@ changing config.
## Quick choice
| Goal | Use | Notes |
| --------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Direct API-key billing | `openai/gpt-5.5` | Set `OPENAI_API_KEY` or run OpenAI API-key onboarding. |
| GPT-5.5 with ChatGPT/Codex subscription auth | `openai-codex/gpt-5.5` | Default PI route for Codex OAuth. Best first choice for subscription setups. |
| GPT-5.5 with native Codex app-server behavior | `openai/gpt-5.5` plus `embeddedHarness.runtime: "codex"` | Forces the Codex app-server harness for that model ref. |
| Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. |
| Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. |
| Goal | Use | Notes |
| --------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------- |
| Direct API-key billing | `openai/gpt-5.5` | Set `OPENAI_API_KEY` or run OpenAI API-key onboarding. |
| GPT-5.5 with ChatGPT/Codex subscription auth | `openai-codex/gpt-5.5` | Default PI route for Codex OAuth. Best first choice for subscription setups. |
| GPT-5.5 with native Codex app-server behavior | `openai/gpt-5.5` plus `agentRuntime.id: "codex"` | Forces the Codex app-server harness for that model ref. |
| Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. |
| Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. |
## Naming map
@@ -44,7 +44,7 @@ The names are similar but not interchangeable:
| `openai` | Provider prefix | Direct OpenAI Platform API route. |
| `openai-codex` | Provider prefix | OpenAI Codex OAuth/subscription route through the normal OpenClaw PI runner. |
| `codex` plugin | Plugin | Bundled OpenClaw plugin that provides native Codex app-server runtime and `/codex` chat controls. |
| `embeddedHarness.runtime: codex` | Agent runtime | Force the native Codex app-server harness for embedded turns. |
| `agentRuntime.id: codex` | Agent runtime | Force the native Codex app-server harness for embedded turns. |
| `/codex ...` | Chat command set | Bind/control Codex app-server threads from a conversation. |
| `runtime: "acp", agentId: "codex"` | ACP session route | Explicit fallback path that runs Codex through ACP/acpx. |
@@ -57,7 +57,7 @@ combination so you can confirm it is intentional; it does not rewrite it.
GPT-5.5 is available through both direct OpenAI Platform API-key access and
subscription/OAuth routes. Use `openai/gpt-5.5` for direct `OPENAI_API_KEY`
traffic, `openai-codex/gpt-5.5` for Codex OAuth through PI, or
`openai/gpt-5.5` with `embeddedHarness.runtime: "codex"` for the native Codex
`openai/gpt-5.5` with `agentRuntime.id: "codex"` for the native Codex
app-server harness.
</Note>
@@ -65,7 +65,7 @@ app-server harness.
Enabling the OpenAI plugin, or selecting an `openai-codex/*` model, does not
enable the bundled Codex app-server plugin. OpenClaw enables that plugin only
when you explicitly select the native Codex harness with
`embeddedHarness.runtime: "codex"` or use a legacy `codex/*` model ref.
`agentRuntime.id: "codex"` or use a legacy `codex/*` model ref.
If the bundled `codex` plugin is enabled but `openai-codex/*` still resolves
through PI, `openclaw doctor` warns and leaves the route unchanged.
</Note>
@@ -76,7 +76,7 @@ through PI, `openclaw doctor` warns and leaves the route unchanged.
| ------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ |
| Chat / Responses | `openai/<model>` model provider | Yes |
| Codex subscription models | `openai-codex/<model>` with `openai-codex` OAuth | Yes |
| Codex app-server harness | `openai/<model>` with `embeddedHarness.runtime: codex` | Yes |
| Codex app-server harness | `openai/<model>` with `agentRuntime.id: codex` | Yes |
| Server-side web search | Native OpenAI Responses tool | Yes, when web search is enabled and no provider pinned |
| Images | `image_generate` | Yes |
| Videos | `video_generate` | Yes |
@@ -120,15 +120,15 @@ Choose your preferred auth method and follow the setup steps.
| Model ref | Runtime config | Route | Auth |
| ---------------------- | -------------------------- | --------------------------- | ---------------- |
| `openai/gpt-5.5` | omitted / `runtime: "pi"` | Direct OpenAI Platform API | `OPENAI_API_KEY` |
| `openai/gpt-5.4-mini` | omitted / `runtime: "pi"` | Direct OpenAI Platform API | `OPENAI_API_KEY` |
| `openai/gpt-5.5` | `runtime: "codex"` | Codex app-server harness | Codex app-server |
| `openai/gpt-5.5` | omitted / `agentRuntime.id: "pi"` | Direct OpenAI Platform API | `OPENAI_API_KEY` |
| `openai/gpt-5.4-mini` | omitted / `agentRuntime.id: "pi"` | Direct OpenAI Platform API | `OPENAI_API_KEY` |
| `openai/gpt-5.5` | `agentRuntime.id: "codex"` | Codex app-server harness | Codex app-server |
<Note>
`openai/*` is the direct OpenAI API-key route unless you explicitly force
the Codex app-server harness. Use `openai-codex/*` for Codex OAuth through
the default PI runner, or use `openai/gpt-5.5` with
`embeddedHarness.runtime: "codex"` for native Codex app-server execution.
`agentRuntime.id: "codex"` for native Codex app-server execution.
</Note>
### Config example
@@ -185,7 +185,7 @@ Choose your preferred auth method and follow the setup steps.
|-----------|----------------|-------|------|
| `openai-codex/gpt-5.5` | omitted / `runtime: "pi"` | ChatGPT/Codex OAuth through PI | Codex sign-in |
| `openai-codex/gpt-5.5` | `runtime: "auto"` | Still PI unless a plugin explicitly claims `openai-codex` | Codex sign-in |
| `openai/gpt-5.5` | `embeddedHarness.runtime: "codex"` | Codex app-server harness | Codex app-server auth |
| `openai/gpt-5.5` | `agentRuntime.id: "codex"` | Codex app-server harness | Codex app-server auth |
<Note>
Keep using the `openai-codex` provider id for auth/profile commands. The
@@ -211,7 +211,7 @@ Choose your preferred auth method and follow the setup steps.
The default PI harness appears as `Runtime: OpenClaw Pi Default`. When the
bundled Codex app-server harness is selected, `/status` shows
`Runtime: OpenAI Codex`. Existing sessions keep their recorded harness id, so use
`/new` or `/reset` after changing `embeddedHarness` if you want `/status` to
`/new` or `/reset` after changing `agentRuntime` if you want `/status` to
reflect a new PI/Codex choice.
### Doctor warning
@@ -220,7 +220,7 @@ Choose your preferred auth method and follow the setup steps.
`openai-codex/*` route is selected, `openclaw doctor` warns that the model
still resolves through PI. Keep the config unchanged when that is the
intended subscription-auth route. Switch to `openai/<model>` plus
`embeddedHarness.runtime: "codex"` only when you want native Codex
`agentRuntime.id: "codex"` only when you want native Codex
app-server execution.
### Context window cap
@@ -380,7 +380,7 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov
OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family runs across providers. It applies by model id, so `openai-codex/gpt-5.5`, `openai/gpt-5.5`, `openrouter/openai/gpt-5.5`, `opencode/gpt-5.5`, and other compatible GPT-5 refs receive the same overlay. Older GPT-4.x models do not.
The bundled native Codex harness uses the same GPT-5 behavior and heartbeat overlay through Codex app-server developer instructions, so `openai/gpt-5.x` sessions forced through `embeddedHarness.runtime: "codex"` keep the same follow-through and proactive heartbeat guidance even though Codex owns the rest of the harness prompt.
The bundled native Codex harness uses the same GPT-5 behavior and heartbeat overlay through Codex app-server developer instructions, so `openai/gpt-5.x` sessions forced through `agentRuntime.id: "codex"` keep the same follow-through and proactive heartbeat guidance even though Codex owns the rest of the harness prompt.
The GPT-5 contribution adds a tagged behavior contract for persona persistence, execution safety, tool discipline, output shape, completion checks, and verification. Channel-specific reply and silent-message behavior stays in the shared OpenClaw system prompt and outbound delivery policy. The GPT-5 guidance is always enabled for matching models. The friendly interaction-style layer is separate and configurable.
@@ -766,7 +766,7 @@ the Server-side compaction accordion below.
- Injects `context_management: [{ type: "compaction", compact_threshold: ... }]`
- Default `compact_threshold`: 70% of `contextWindow` (or `80000` when unavailable)
This applies to the built-in Pi harness path and to OpenAI provider hooks used by embedded runs. The native Codex app-server harness manages its own context through Codex and is configured separately with `agents.defaults.embeddedHarness.runtime`.
This applies to the built-in Pi harness path and to OpenAI provider hooks used by embedded runs. The native Codex app-server harness manages its own context through Codex and is configured separately with `agents.defaults.agentRuntime.id`.
<Tabs>
<Tab title="Enable explicitly">

View File

@@ -1,6 +1,7 @@
---
summary: "All configuration knobs for memory search, embedding providers, QMD, hybrid search, and multimodal indexing"
title: "Memory configuration reference"
sidebarTitle: "Memory config"
read_when:
- You want to configure memory search providers or embedding models
- You want to set up the QMD backend
@@ -8,28 +9,38 @@ read_when:
- You want to enable multimodal memory indexing
---
This page lists every configuration knob for OpenClaw memory search. For
conceptual overviews, see:
This page lists every configuration knob for OpenClaw memory search. For conceptual overviews, see:
- [Memory Overview](/concepts/memory) -- how memory works
- [Builtin Engine](/concepts/memory-builtin) -- default SQLite backend
- [QMD Engine](/concepts/memory-qmd) -- local-first sidecar
- [Memory Search](/concepts/memory-search) -- search pipeline and tuning
- [Active Memory](/concepts/active-memory) -- enabling the memory sub-agent for interactive sessions
<CardGroup cols={2}>
<Card title="Memory overview" href="/concepts/memory">
How memory works.
</Card>
<Card title="Builtin engine" href="/concepts/memory-builtin">
Default SQLite backend.
</Card>
<Card title="QMD engine" href="/concepts/memory-qmd">
Local-first sidecar.
</Card>
<Card title="Memory search" href="/concepts/memory-search">
Search pipeline and tuning.
</Card>
<Card title="Active memory" href="/concepts/active-memory">
Memory sub-agent for interactive sessions.
</Card>
</CardGroup>
All memory search settings live under `agents.defaults.memorySearch` in
`openclaw.json` unless noted otherwise.
All memory search settings live under `agents.defaults.memorySearch` in `openclaw.json` unless noted otherwise.
If you are looking for the **active memory** feature toggle and sub-agent config,
that lives under `plugins.entries.active-memory` instead of `memorySearch`.
<Note>
If you are looking for the **active memory** feature toggle and sub-agent config, that lives under `plugins.entries.active-memory` instead of `memorySearch`.
Active memory uses a two-gate model:
1. the plugin must be enabled and target the current agent id
2. the request must be an eligible interactive persistent chat session
See [Active Memory](/concepts/active-memory) for the activation model,
plugin-owned config, transcript persistence, and safe rollout pattern.
See [Active Memory](/concepts/active-memory) for the activation model, plugin-owned config, transcript persistence, and safe rollout pattern.
</Note>
---
@@ -46,20 +57,35 @@ plugin-owned config, transcript persistence, and safe rollout pattern.
When `provider` is not set, OpenClaw selects the first available:
1. `local` -- if `memorySearch.local.modelPath` is configured and the file exists.
2. `github-copilot` -- if a GitHub Copilot token can be resolved (env var or auth profile).
3. `openai` -- if an OpenAI key can be resolved.
4. `gemini` -- if a Gemini key can be resolved.
5. `voyage` -- if a Voyage key can be resolved.
6. `mistral` -- if a Mistral key can be resolved.
7. `bedrock` -- if the AWS SDK credential chain resolves (instance role, access keys, profile, SSO, web identity, or shared config).
<Steps>
<Step title="local">
Selected if `memorySearch.local.modelPath` is configured and the file exists.
</Step>
<Step title="github-copilot">
Selected if a GitHub Copilot token can be resolved (env var or auth profile).
</Step>
<Step title="openai">
Selected if an OpenAI key can be resolved.
</Step>
<Step title="gemini">
Selected if a Gemini key can be resolved.
</Step>
<Step title="voyage">
Selected if a Voyage key can be resolved.
</Step>
<Step title="mistral">
Selected if a Mistral key can be resolved.
</Step>
<Step title="bedrock">
Selected if the AWS SDK credential chain resolves (instance role, access keys, profile, SSO, web identity, or shared config).
</Step>
</Steps>
`ollama` is supported but not auto-detected (set it explicitly).
### API key resolution
Remote embeddings require an API key. Bedrock uses the AWS SDK default
credential chain instead (instance roles, SSO, access keys).
Remote embeddings require an API key. Bedrock uses the AWS SDK default credential chain instead (instance roles, SSO, access keys).
| Provider | Env var | Config key |
| -------------- | -------------------------------------------------- | --------------------------------- |
@@ -71,8 +97,9 @@ credential chain instead (instance roles, SSO, access keys).
| OpenAI | `OPENAI_API_KEY` | `models.providers.openai.apiKey` |
| Voyage | `VOYAGE_API_KEY` | `models.providers.voyage.apiKey` |
Codex OAuth covers chat/completions only and does not satisfy embedding
requests.
<Note>
Codex OAuth covers chat/completions only and does not satisfy embedding requests.
</Note>
---
@@ -80,11 +107,15 @@ requests.
For custom OpenAI-compatible endpoints or overriding provider defaults:
| Key | Type | Description |
| ---------------- | -------- | -------------------------------------------------- |
| `remote.baseUrl` | `string` | Custom API base URL |
| `remote.apiKey` | `string` | Override API key |
| `remote.headers` | `object` | Extra HTTP headers (merged with provider defaults) |
<ParamField path="remote.baseUrl" type="string">
Custom API base URL.
</ParamField>
<ParamField path="remote.apiKey" type="string">
Override API key.
</ParamField>
<ParamField path="remote.headers" type="object">
Extra HTTP headers (merged with provider defaults).
</ParamField>
```json5
{
@@ -105,130 +136,113 @@ For custom OpenAI-compatible endpoints or overriding provider defaults:
---
## Gemini-specific config
## Provider-specific config
| Key | Type | Default | Description |
| ---------------------- | -------- | ---------------------- | ------------------------------------------ |
| `model` | `string` | `gemini-embedding-001` | Also supports `gemini-embedding-2-preview` |
| `outputDimensionality` | `number` | `3072` | For Embedding 2: 768, 1536, or 3072 |
<AccordionGroup>
<Accordion title="Gemini">
| Key | Type | Default | Description |
| ---------------------- | -------- | ---------------------- | ------------------------------------------ |
| `model` | `string` | `gemini-embedding-001` | Also supports `gemini-embedding-2-preview` |
| `outputDimensionality` | `number` | `3072` | For Embedding 2: 768, 1536, or 3072 |
<Warning>
Changing model or `outputDimensionality` triggers an automatic full reindex.
</Warning>
<Warning>
Changing model or `outputDimensionality` triggers an automatic full reindex.
</Warning>
---
</Accordion>
<Accordion title="Bedrock">
Bedrock uses the AWS SDK default credential chain — no API keys needed. If OpenClaw runs on EC2 with a Bedrock-enabled instance role, just set the provider and model:
## Bedrock embedding config
Bedrock uses the AWS SDK default credential chain -- no API keys needed.
If OpenClaw runs on EC2 with a Bedrock-enabled instance role, just set the
provider and model:
```json5
{
agents: {
defaults: {
memorySearch: {
provider: "bedrock",
model: "amazon.titan-embed-text-v2:0",
```json5
{
agents: {
defaults: {
memorySearch: {
provider: "bedrock",
model: "amazon.titan-embed-text-v2:0",
},
},
},
},
},
}
```
}
```
| Key | Type | Default | Description |
| ---------------------- | -------- | ------------------------------ | ------------------------------- |
| `model` | `string` | `amazon.titan-embed-text-v2:0` | Any Bedrock embedding model ID |
| `outputDimensionality` | `number` | model default | For Titan V2: 256, 512, or 1024 |
| Key | Type | Default | Description |
| ---------------------- | -------- | ------------------------------ | ------------------------------- |
| `model` | `string` | `amazon.titan-embed-text-v2:0` | Any Bedrock embedding model ID |
| `outputDimensionality` | `number` | model default | For Titan V2: 256, 512, or 1024 |
### Supported models
**Supported models** (with family detection and dimension defaults):
The following models are supported (with family detection and dimension
defaults):
| Model ID | Provider | Default Dims | Configurable Dims |
| ------------------------------------------ | ---------- | ------------ | -------------------- |
| `amazon.titan-embed-text-v2:0` | Amazon | 1024 | 256, 512, 1024 |
| `amazon.titan-embed-text-v1` | Amazon | 1536 | -- |
| `amazon.titan-embed-g1-text-02` | Amazon | 1536 | -- |
| `amazon.titan-embed-image-v1` | Amazon | 1024 | -- |
| `amazon.nova-2-multimodal-embeddings-v1:0` | Amazon | 1024 | 256, 384, 1024, 3072 |
| `cohere.embed-english-v3` | Cohere | 1024 | -- |
| `cohere.embed-multilingual-v3` | Cohere | 1024 | -- |
| `cohere.embed-v4:0` | Cohere | 1536 | 256-1536 |
| `twelvelabs.marengo-embed-3-0-v1:0` | TwelveLabs | 512 | -- |
| `twelvelabs.marengo-embed-2-7-v1:0` | TwelveLabs | 1024 | -- |
| Model ID | Provider | Default Dims | Configurable Dims |
| ------------------------------------------ | ---------- | ------------ | -------------------- |
| `amazon.titan-embed-text-v2:0` | Amazon | 1024 | 256, 512, 1024 |
| `amazon.titan-embed-text-v1` | Amazon | 1536 | -- |
| `amazon.titan-embed-g1-text-02` | Amazon | 1536 | -- |
| `amazon.titan-embed-image-v1` | Amazon | 1024 | -- |
| `amazon.nova-2-multimodal-embeddings-v1:0` | Amazon | 1024 | 256, 384, 1024, 3072 |
| `cohere.embed-english-v3` | Cohere | 1024 | -- |
| `cohere.embed-multilingual-v3` | Cohere | 1024 | -- |
| `cohere.embed-v4:0` | Cohere | 1536 | 256-1536 |
| `twelvelabs.marengo-embed-3-0-v1:0` | TwelveLabs | 512 | -- |
| `twelvelabs.marengo-embed-2-7-v1:0` | TwelveLabs | 1024 | -- |
Throughput-suffixed variants (e.g., `amazon.titan-embed-text-v1:2:8k`) inherit the base model's configuration.
Throughput-suffixed variants (e.g., `amazon.titan-embed-text-v1:2:8k`) inherit
the base model's configuration.
**Authentication:** Bedrock auth uses the standard AWS SDK credential resolution order:
### Authentication
1. Environment variables (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`)
2. SSO token cache
3. Web identity token credentials
4. Shared credentials and config files
5. ECS or EC2 metadata credentials
Bedrock auth uses the standard AWS SDK credential resolution order:
Region is resolved from `AWS_REGION`, `AWS_DEFAULT_REGION`, the `amazon-bedrock` provider `baseUrl`, or defaults to `us-east-1`.
1. Environment variables (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`)
2. SSO token cache
3. Web identity token credentials
4. Shared credentials and config files
5. ECS or EC2 metadata credentials
**IAM permissions:** the IAM role or user needs:
Region is resolved from `AWS_REGION`, `AWS_DEFAULT_REGION`, the
`amazon-bedrock` provider `baseUrl`, or defaults to `us-east-1`.
```json
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "*"
}
```
### IAM permissions
For least-privilege, scope `InvokeModel` to the specific model:
The IAM role or user needs:
```
arn:aws:bedrock:*::foundation-model/amazon.titan-embed-text-v2:0
```
```json
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "*"
}
```
</Accordion>
<Accordion title="Local (GGUF + node-llama-cpp)">
| Key | Type | Default | Description |
| --------------------- | ------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `local.modelPath` | `string` | auto-downloaded | Path to GGUF model file |
| `local.modelCacheDir` | `string` | node-llama-cpp default | Cache dir for downloaded models |
| `local.contextSize` | `number \| "auto"` | `4096` | Context window size for the embedding context. 4096 covers typical chunks (128512 tokens) while bounding non-weight VRAM. Lower to 10242048 on constrained hosts. `"auto"` uses the model's trained maximum — not recommended for 8B+ models (Qwen3-Embedding-8B: 40 960 tokens → ~32 GB VRAM vs ~8.8 GB at 4096). |
For least-privilege, scope `InvokeModel` to the specific model:
Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded). Requires native build: `pnpm approve-builds` then `pnpm rebuild node-llama-cpp`.
```
arn:aws:bedrock:*::foundation-model/amazon.titan-embed-text-v2:0
```
Use the standalone CLI to verify the same provider path the Gateway uses:
---
```bash
openclaw memory status --deep --agent main
openclaw memory index --force --agent main
```
## Local embedding config
If `provider` is `auto`, `local` is selected only when `local.modelPath` points to an existing local file. `hf:` and HTTP(S) model references can still be used explicitly with `provider: "local"`, but they do not make `auto` select local before the model is available on disk.
| Key | Type | Default | Description |
| --------------------- | ------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `local.modelPath` | `string` | auto-downloaded | Path to GGUF model file |
| `local.modelCacheDir` | `string` | node-llama-cpp default | Cache dir for downloaded models |
| `local.contextSize` | `number \| "auto"` | `4096` | Context window size for the embedding context. 4096 covers typical chunks (128512 tokens) while bounding non-weight VRAM. Lower to 10242048 on constrained hosts. `"auto"` uses the model's trained maximum — not recommended for 8B+ models (Qwen3-Embedding-8B: 40 960 tokens → ~32 GB VRAM vs ~8.8 GB at 4096). |
Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded).
Requires native build: `pnpm approve-builds` then `pnpm rebuild node-llama-cpp`.
Use the standalone CLI to verify the same provider path the Gateway uses:
```bash
openclaw memory status --deep --agent main
openclaw memory index --force --agent main
```
If `provider` is `auto`, `local` is selected only when `local.modelPath` points
to an existing local file. `hf:` and HTTP(S) model references can still be used
explicitly with `provider: "local"`, but they do not make `auto` select local
before the model is available on disk.
</Accordion>
</AccordionGroup>
### Inline embedding timeout
| Key | Type | Default | Description |
| ----------------------------------- | -------- | ---------------- | ------------------------------------------------------------------------ |
| `sync.embeddingBatchTimeoutSeconds` | `number` | provider default | Override the timeout for inline embedding batches during memory indexing |
<ParamField path="sync.embeddingBatchTimeoutSeconds" type="number">
Override the timeout for inline embedding batches during memory indexing.
Unset uses the provider default: 600 seconds for local/self-hosted providers
such as `local`, `ollama`, and `lmstudio`, and 120 seconds for hosted providers.
Increase this when local CPU-bound embedding batches are healthy but slow.
Unset uses the provider default: 600 seconds for local/self-hosted providers such as `local`, `ollama`, and `lmstudio`, and 120 seconds for hosted providers. Increase this when local CPU-bound embedding batches are healthy but slow.
</ParamField>
---
@@ -243,21 +257,23 @@ All under `memorySearch.query.hybrid`:
| `textWeight` | `number` | `0.3` | Weight for BM25 scores (0-1) |
| `candidateMultiplier` | `number` | `4` | Candidate pool size multiplier |
### MMR (diversity)
<Tabs>
<Tab title="MMR (diversity)">
| Key | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------------ |
| `mmr.enabled` | `boolean` | `false` | Enable MMR re-ranking |
| `mmr.lambda` | `number` | `0.7` | 0 = max diversity, 1 = max relevance |
</Tab>
<Tab title="Temporal decay (recency)">
| Key | Type | Default | Description |
| ---------------------------- | --------- | ------- | ------------------------- |
| `temporalDecay.enabled` | `boolean` | `false` | Enable recency boost |
| `temporalDecay.halfLifeDays` | `number` | `30` | Score halves every N days |
| Key | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------------ |
| `mmr.enabled` | `boolean` | `false` | Enable MMR re-ranking |
| `mmr.lambda` | `number` | `0.7` | 0 = max diversity, 1 = max relevance |
Evergreen files (`MEMORY.md`, non-dated files in `memory/`) are never decayed.
### Temporal decay (recency)
| Key | Type | Default | Description |
| ---------------------------- | --------- | ------- | ------------------------- |
| `temporalDecay.enabled` | `boolean` | `false` | Enable recency boost |
| `temporalDecay.halfLifeDays` | `number` | `30` | Score halves every N days |
Evergreen files (`MEMORY.md`, non-dated files in `memory/`) are never decayed.
</Tab>
</Tabs>
### Full example
@@ -300,19 +316,9 @@ Evergreen files (`MEMORY.md`, non-dated files in `memory/`) are never decayed.
}
```
Paths can be absolute or workspace-relative. Directories are scanned
recursively for `.md` files. Symlink handling depends on the active backend:
the builtin engine ignores symlinks, while QMD follows the underlying QMD
scanner behavior.
Paths can be absolute or workspace-relative. Directories are scanned recursively for `.md` files. Symlink handling depends on the active backend: the builtin engine ignores symlinks, while QMD follows the underlying QMD scanner behavior.
For agent-scoped cross-agent transcript search, use
`agents.list[].memorySearch.qmd.extraCollections` instead of `memory.qmd.paths`.
Those extra collections follow the same `{ path, name, pattern? }` shape, but
they are merged per agent and can preserve explicit shared names when the path
points outside the current workspace.
If the same resolved path appears in both `memory.qmd.paths` and
`memorySearch.qmd.extraCollections`, QMD keeps the first entry and skips the
duplicate.
For agent-scoped cross-agent transcript search, use `agents.list[].memorySearch.qmd.extraCollections` instead of `memory.qmd.paths`. Those extra collections follow the same `{ path, name, pattern? }` shape, but they are merged per agent and can preserve explicit shared names when the path points outside the current workspace. If the same resolved path appears in both `memory.qmd.paths` and `memorySearch.qmd.extraCollections`, QMD keeps the first entry and skips the duplicate.
---
@@ -326,11 +332,11 @@ Index images and audio alongside Markdown using Gemini Embedding 2:
| `multimodal.modalities` | `string[]` | -- | `["image"]`, `["audio"]`, or `["all"]` |
| `multimodal.maxFileBytes` | `number` | `10000000` | Max file size for indexing |
Only applies to files in `extraPaths`. Default memory roots stay Markdown-only.
Requires `gemini-embedding-2-preview`. `fallback` must be `"none"`.
<Note>
Only applies to files in `extraPaths`. Default memory roots stay Markdown-only. Requires `gemini-embedding-2-preview`. `fallback` must be `"none"`.
</Note>
Supported formats: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.heic`, `.heif`
(images); `.mp3`, `.wav`, `.ogg`, `.opus`, `.m4a`, `.aac`, `.flac` (audio).
Supported formats: `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif`, `.heic`, `.heif` (images); `.mp3`, `.wav`, `.ogg`, `.opus`, `.m4a`, `.aac`, `.flac` (audio).
---
@@ -355,12 +361,9 @@ Prevents re-embedding unchanged text during reindex or transcript updates.
| `remote.batch.pollIntervalMs` | `number` | -- | Poll interval |
| `remote.batch.timeoutMinutes` | `number` | -- | Batch timeout |
Available for `openai`, `gemini`, and `voyage`. OpenAI batch is typically
fastest and cheapest for large backfills.
Available for `openai`, `gemini`, and `voyage`. OpenAI batch is typically fastest and cheapest for large backfills.
This is separate from `sync.embeddingBatchTimeoutSeconds`, which controls inline
embedding calls used by local/self-hosted providers and hosted providers when
provider batch APIs are not active.
This is separate from `sync.embeddingBatchTimeoutSeconds`, which controls inline embedding calls used by local/self-hosted providers and hosted providers when provider batch APIs are not active.
---
@@ -375,9 +378,9 @@ Index session transcripts and surface them via `memory_search`:
| `sync.sessions.deltaBytes` | `number` | `100000` | Byte threshold for reindex |
| `sync.sessions.deltaMessages` | `number` | `50` | Message threshold for reindex |
Session indexing is opt-in and runs asynchronously. Results can be slightly
stale. Session logs live on disk, so treat filesystem access as the trust
boundary.
<Warning>
Session indexing is opt-in and runs asynchronously. Results can be slightly stale. Session logs live on disk, so treat filesystem access as the trust boundary.
</Warning>
---
@@ -388,8 +391,7 @@ boundary.
| `store.vector.enabled` | `boolean` | `true` | Use sqlite-vec for vector queries |
| `store.vector.extensionPath` | `string` | bundled | Override sqlite-vec path |
When sqlite-vec is unavailable, OpenClaw falls back to in-process cosine
similarity automatically.
When sqlite-vec is unavailable, OpenClaw falls back to in-process cosine similarity automatically.
---
@@ -404,8 +406,7 @@ similarity automatically.
## QMD backend config
Set `memory.backend = "qmd"` to enable. All QMD settings live under
`memory.qmd`:
Set `memory.backend = "qmd"` to enable. All QMD settings live under `memory.qmd`:
| Key | Type | Default | Description |
| ------------------------ | --------- | -------- | -------------------------------------------- |
@@ -417,70 +418,65 @@ Set `memory.backend = "qmd"` to enable. All QMD settings live under
| `sessions.retentionDays` | `number` | -- | Transcript retention |
| `sessions.exportDir` | `string` | -- | Export directory |
OpenClaw prefers the current QMD collection and MCP query shapes, but keeps
older QMD releases working by falling back to legacy `--mask` collection flags
and older MCP tool names when needed.
OpenClaw prefers the current QMD collection and MCP query shapes, but keeps older QMD releases working by falling back to legacy `--mask` collection flags and older MCP tool names when needed.
QMD model overrides stay on the QMD side, not OpenClaw config. If you need to
override QMD's models globally, set environment variables such as
`QMD_EMBED_MODEL`, `QMD_RERANK_MODEL`, and `QMD_GENERATE_MODEL` in the gateway
runtime environment.
<Note>
QMD model overrides stay on the QMD side, not OpenClaw config. If you need to override QMD's models globally, set environment variables such as `QMD_EMBED_MODEL`, `QMD_RERANK_MODEL`, and `QMD_GENERATE_MODEL` in the gateway runtime environment.
</Note>
### Update schedule
<AccordionGroup>
<Accordion title="Update schedule">
| Key | Type | Default | Description |
| ------------------------- | --------- | ------- | ------------------------------------- |
| `update.interval` | `string` | `5m` | Refresh interval |
| `update.debounceMs` | `number` | `15000` | Debounce file changes |
| `update.onBoot` | `boolean` | `true` | Refresh on startup |
| `update.waitForBootSync` | `boolean` | `false` | Block startup until refresh completes |
| `update.embedInterval` | `string` | -- | Separate embed cadence |
| `update.commandTimeoutMs` | `number` | -- | Timeout for QMD commands |
| `update.updateTimeoutMs` | `number` | -- | Timeout for QMD update operations |
| `update.embedTimeoutMs` | `number` | -- | Timeout for QMD embed operations |
</Accordion>
<Accordion title="Limits">
| Key | Type | Default | Description |
| ------------------------- | -------- | ------- | -------------------------- |
| `limits.maxResults` | `number` | `6` | Max search results |
| `limits.maxSnippetChars` | `number` | -- | Clamp snippet length |
| `limits.maxInjectedChars` | `number` | -- | Clamp total injected chars |
| `limits.timeoutMs` | `number` | `4000` | Search timeout |
</Accordion>
<Accordion title="Scope">
Controls which sessions can receive QMD search results. Same schema as [`session.sendPolicy`](/gateway/config-agents#session):
| Key | Type | Default | Description |
| ------------------------- | --------- | ------- | ------------------------------------- |
| `update.interval` | `string` | `5m` | Refresh interval |
| `update.debounceMs` | `number` | `15000` | Debounce file changes |
| `update.onBoot` | `boolean` | `true` | Refresh on startup |
| `update.waitForBootSync` | `boolean` | `false` | Block startup until refresh completes |
| `update.embedInterval` | `string` | -- | Separate embed cadence |
| `update.commandTimeoutMs` | `number` | -- | Timeout for QMD commands |
| `update.updateTimeoutMs` | `number` | -- | Timeout for QMD update operations |
| `update.embedTimeoutMs` | `number` | -- | Timeout for QMD embed operations |
### Limits
| Key | Type | Default | Description |
| ------------------------- | -------- | ------- | -------------------------- |
| `limits.maxResults` | `number` | `6` | Max search results |
| `limits.maxSnippetChars` | `number` | -- | Clamp snippet length |
| `limits.maxInjectedChars` | `number` | -- | Clamp total injected chars |
| `limits.timeoutMs` | `number` | `4000` | Search timeout |
### Scope
Controls which sessions can receive QMD search results. Same schema as
[`session.sendPolicy`](/gateway/config-agents#session):
```json5
{
memory: {
qmd: {
scope: {
default: "deny",
rules: [{ action: "allow", match: { chatType: "direct" } }],
```json5
{
memory: {
qmd: {
scope: {
default: "deny",
rules: [{ action: "allow", match: { chatType: "direct" } }],
},
},
},
},
},
}
```
}
```
The shipped default allows direct and channel sessions, while still denying
groups.
The shipped default allows direct and channel sessions, while still denying groups.
Default is DM-only. `match.keyPrefix` matches the normalized session key;
`match.rawKeyPrefix` matches the raw key including `agent:<id>:`.
Default is DM-only. `match.keyPrefix` matches the normalized session key; `match.rawKeyPrefix` matches the raw key including `agent:<id>:`.
### Citations
</Accordion>
<Accordion title="Citations">
`memory.citations` applies to all backends:
`memory.citations` applies to all backends:
| Value | Behavior |
| ---------------- | --------------------------------------------------- |
| `auto` (default) | Include `Source: <path#line>` footer in snippets |
| `on` | Always include footer |
| `off` | Omit footer (path still passed to agent internally) |
| Value | Behavior |
| ---------------- | --------------------------------------------------- |
| `auto` (default) | Include `Source: <path#line>` footer in snippets |
| `on` | Always include footer |
| `off` | Omit footer (path still passed to agent internally) |
</Accordion>
</AccordionGroup>
### Full QMD example
@@ -507,11 +503,9 @@ Default is DM-only. `match.keyPrefix` matches the normalized session key;
## Dreaming
Dreaming is configured under `plugins.entries.memory-core.config.dreaming`,
not under `agents.defaults.memorySearch`.
Dreaming is configured under `plugins.entries.memory-core.config.dreaming`, not under `agents.defaults.memorySearch`.
Dreaming runs as one scheduled sweep and uses internal light/deep/REM phases as
an implementation detail.
Dreaming runs as one scheduled sweep and uses internal light/deep/REM phases as an implementation detail.
For conceptual behavior and slash commands, see [Dreaming](/concepts/dreaming).
@@ -541,14 +535,14 @@ For conceptual behavior and slash commands, see [Dreaming](/concepts/dreaming).
}
```
Notes:
<Note>
- Dreaming writes machine state to `memory/.dreams/`.
- Dreaming writes human-readable narrative output to `DREAMS.md` (or existing `dreams.md`).
- The light/deep/REM phase policy and thresholds are internal behavior, not user-facing config.
</Note>
## Related
- [Configuration reference](/gateway/configuration-reference)
- [Memory overview](/concepts/memory)
- [Memory search](/concepts/memory-search)
- [Configuration reference](/gateway/configuration-reference)

View File

@@ -11,12 +11,13 @@ title: "Tests"
- `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:focused`: inner-loop changed test run. It only 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 instead of expanding to the full changed-test fallback.
- `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 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.
- 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`.

View File

@@ -20,7 +20,7 @@ Codex has two OpenClaw routes:
| Route | Config/command | Setup page |
| -------------------------- | ------------------------------------------------------ | --------------------------------------- |
| Native Codex app-server | `/codex ...`, `embeddedHarness.runtime: "codex"` | [Codex harness](/plugins/codex-harness) |
| Native Codex app-server | `/codex ...`, `agentRuntime.id: "codex"` | [Codex harness](/plugins/codex-harness) |
| Explicit Codex ACP adapter | `/acp spawn codex`, `runtime: "acp", agentId: "codex"` | This page |
Prefer the native route unless you explicitly need ACP/acpx behavior.

View File

@@ -20,7 +20,7 @@ Each ACP session spawn is tracked as a [background task](/automation/tasks).
<Note>
**ACP is the external-harness path, not the default Codex path.** The
native Codex app-server plugin owns `/codex ...` controls and the
`embeddedHarness.runtime: "codex"` embedded runtime; ACP owns
`agentRuntime.id: "codex"` embedded runtime; ACP owns
`/acp ...` controls and `sessions_spawn({ runtime: "acp" })` sessions.
If you want Codex or Claude Code to connect as an external MCP client
@@ -172,7 +172,7 @@ Quick `/acp` flow from chat:
</Accordion>
<Accordion title="Model / provider / runtime selection cheat sheet">
- `openai-codex/*` — PI Codex OAuth/subscription route.
- `openai/*` plus `embeddedHarness.runtime: "codex"` — native Codex app-server embedded runtime.
- `openai/*` plus `agentRuntime.id: "codex"` — native Codex app-server embedded runtime.
- `/codex ...` — native Codex conversation control.
- `/acp ...` or `runtime: "acp"` — explicit ACP/acpx control.
</Accordion>

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

@@ -212,7 +212,7 @@ OpenClaw scans for plugins in this order (first match wins):
runtime
- OpenAI-family Codex routes keep separate plugin boundaries:
`openai-codex/*` belongs to the OpenAI plugin, while the bundled Codex
app-server plugin is selected by `embeddedHarness.runtime: "codex"` or legacy
app-server plugin is selected by `agentRuntime.id: "codex"` or legacy
`codex/*` model refs
## Troubleshooting runtime hooks
@@ -337,8 +337,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

@@ -403,8 +403,41 @@ Precedence order for automatic replies, `/tts audio`, `/tts status`, and the
1. `messages.tts`
2. active `agents.list[].tts`
3. local `/tts` preferences for this host
4. inline `[[tts:...]]` directives when [model overrides](#model-driven-directives) are enabled
3. channel override, when the channel supports `channels.<channel>.tts`
4. account override, when the channel passes `channels.<channel>.accounts.<id>.tts`
5. local `/tts` preferences for this host
6. inline `[[tts:...]]` directives when [model overrides](#model-driven-directives) are enabled
Channel and account overrides use the same shape as `messages.tts` and
deep-merge over the earlier layers, so shared provider credentials can stay in
`messages.tts` while a channel or bot account changes only voice, model, persona,
or auto mode:
```json5
{
messages: {
tts: {
provider: "openai",
providers: {
openai: { apiKey: "${OPENAI_API_KEY}", model: "gpt-4o-mini-tts" },
},
},
},
channels: {
feishu: {
accounts: {
english: {
tts: {
providers: {
openai: { voice: "shimmer" },
},
},
},
},
},
},
}
```
## Personas

View File

@@ -4,6 +4,7 @@ read_when:
- You want to operate the Gateway from a browser
- You want Tailnet access without SSH tunnels
title: "Control UI"
sidebarTitle: "Control UI"
---
The Control UI is a small **Vite + Lit** single-page app served by the Gateway:
@@ -28,80 +29,53 @@ Auth is supplied during the WebSocket handshake via:
- Tailscale Serve identity headers when `gateway.auth.allowTailscale: true`
- trusted-proxy identity headers when `gateway.auth.mode: "trusted-proxy"`
The dashboard settings panel keeps a token for the current browser tab session
and selected gateway URL; passwords are not persisted. Onboarding usually
generates a gateway token for shared-secret auth on first connect, but password
auth works too when `gateway.auth.mode` is `"password"`.
The dashboard settings panel keeps a token for the current browser tab session and selected gateway URL; passwords are not persisted. Onboarding usually generates a gateway token for shared-secret auth on first connect, but password auth works too when `gateway.auth.mode` is `"password"`.
## 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"
**To approve the device:**
<Steps>
<Step title="List pending requests">
```bash
openclaw devices list
```
</Step>
<Step title="Approve by request ID">
```bash
openclaw devices approve <requestId>
```
</Step>
</Steps>
```bash
# List pending requests
openclaw devices list
If the browser retries pairing with changed auth details (role/scopes/public key), the previous pending request is superseded and a new `requestId` is created. Re-run `openclaw devices list` before approval.
# Approve by request ID
openclaw devices approve <requestId>
```
If the browser is already paired and you change it from read access to write/admin access, this is treated as an approval upgrade, not a silent reconnect. OpenClaw keeps the old approval active, blocks the broader reconnect, and asks you to approve the new scope set explicitly.
If the browser retries pairing with changed auth details (role/scopes/public
key), the previous pending request is superseded and a new `requestId` is
created. Re-run `openclaw devices list` before approval.
Once approved, the device is remembered and won't require re-approval unless you revoke it with `openclaw devices revoke --device <id> --role <role>`. See [Devices CLI](/cli/devices) for token rotation and revocation.
If the browser is already paired and you change it from read access to
write/admin access, this is treated as an approval upgrade, not a silent
reconnect. OpenClaw keeps the old approval active, blocks the broader reconnect,
and asks you to approve the new scope set explicitly.
Once approved, the device is remembered and won't require re-approval unless
you revoke it with `openclaw devices revoke --device <id> --role <role>`. See
[Devices CLI](/cli/devices) for token rotation and revocation.
**Notes:**
- 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.
- Each browser profile generates a unique device ID, so switching browsers or
clearing browser data will require re-pairing.
<Note>
- Direct local loopback browser connections (`127.0.0.1` / `localhost`) are auto-approved.
- 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>
## Personal identity (browser-local)
The Control UI supports a per-browser personal identity (display name and
avatar) attached to outgoing messages for attribution in shared sessions. It
lives in browser storage, is scoped to the current browser profile, and is not
synced to other devices or persisted server-side beyond the normal transcript
authorship metadata on messages you actually send. Clearing site data or
switching browsers resets it to empty.
The Control UI supports a per-browser personal identity (display name and avatar) attached to outgoing messages for attribution in shared sessions. It lives in browser storage, is scoped to the current browser profile, and is not synced to other devices or persisted server-side beyond the normal transcript authorship metadata on messages you actually send. Clearing site data or switching browsers resets it to empty.
The same browser-local pattern applies to the assistant avatar override.
Uploaded assistant avatars overlay the gateway-resolved identity on the local
browser only and never round-trip through `config.patch`. The shared
`ui.assistant.avatar` config field is still available for non-UI clients
writing the field directly (such as scripted gateways or custom dashboards).
The same browser-local pattern applies to the assistant avatar override. Uploaded assistant avatars overlay the gateway-resolved identity on the local browser only and never round-trip through `config.patch`. The shared `ui.assistant.avatar` config field is still available for non-UI clients writing the field directly (such as scripted gateways or custom dashboards).
## Runtime config endpoint
The Control UI fetches its runtime settings from
`/__openclaw/control-ui-config.json`. That endpoint is gated by the same
gateway auth as the rest of the HTTP surface: unauthenticated browsers cannot
fetch it, and a successful fetch requires either an already valid gateway
token/password, Tailscale Serve identity, or a trusted-proxy identity.
The Control UI fetches its runtime settings from `/__openclaw/control-ui-config.json`. That endpoint is gated by the same gateway auth as the rest of the HTTP surface: unauthenticated browsers cannot fetch it, and a successful fetch requires either an already valid gateway token/password, Tailscale Serve identity, or a trusted-proxy identity.
## Language support
The Control UI can localize itself on first load based on your browser locale.
To override it later, open **Overview -> Gateway Access -> Language**. The
locale picker lives in the Gateway Access card, not under Appearance.
The Control UI can localize itself on first load based on your browser locale. To override it later, open **Overview -> Gateway Access -> Language**. The locale picker lives in the Gateway Access card, not under Appearance.
- Supported locales: `en`, `zh-CN`, `zh-TW`, `pt-BR`, `de`, `es`, `ja-JP`, `ko`, `fr`, `tr`, `uk`, `id`, `pl`, `th`
- Non-English translations are lazy-loaded in the browser.
@@ -110,95 +84,87 @@ locale picker lives in the Gateway Access card, not under Appearance.
## What it can do (today)
- Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`, `chat.inject`)
- Talk to OpenAI Realtime directly from the browser via WebRTC. The Gateway
mints a short-lived Realtime client secret with `talk.realtime.session`; the
browser sends microphone audio directly to OpenAI and relays
`openclaw_agent_consult` tool calls back through `chat.send` for the larger
configured OpenClaw model.
- Stream tool calls + live tool output cards in Chat (agent events)
- Channels: built-in plus bundled/external plugin channels status, QR login, and per-channel config (`channels.status`, `web.login.*`, `config.patch`)
- Instances: presence list + refresh (`system-presence`)
- Sessions: list + per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`)
- Dreams: dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`)
- Cron jobs: list/add/edit/run/enable/disable + run history (`cron.*`)
- Skills: status, enable/disable, install, API key updates (`skills.*`)
- Nodes: list + caps (`node.list`)
- Exec approvals: edit gateway or node allowlists + ask policy for `exec host=gateway/node` (`exec.approvals.*`)
- Config: view/edit `~/.openclaw/openclaw.json` (`config.get`, `config.set`)
- Config: apply + restart with validation (`config.apply`) and wake the last active session
- Config writes include a base-hash guard to prevent clobbering concurrent edits
- Config writes (`config.set`/`config.apply`/`config.patch`) also preflight active SecretRef resolution for refs in the submitted config payload; unresolved active submitted refs are rejected before write
- Config schema + form rendering (`config.schema` / `config.schema.lookup`,
including field `title` / `description`, matched UI hints, immediate child
summaries, docs metadata on nested object/wildcard/array/composition nodes,
plus plugin + channel schemas when available); Raw JSON editor is
available only when the snapshot has a safe raw round-trip
- If a snapshot cannot safely round-trip raw text, Control UI forces Form mode and disables Raw mode for that snapshot
- Raw JSON editor "Reset to saved" preserves the raw-authored shape (formatting, comments, `$include` layout) instead of re-rendering a flattened snapshot, so external edits survive a reset when the snapshot can safely round-trip
- Structured SecretRef object values are rendered read-only in form text inputs to prevent accidental object-to-string corruption
- Debug: status/health/models snapshots + event log + manual RPC calls (`status`, `health`, `models.list`)
- Logs: live tail of gateway file logs with filter/export (`logs.tail`)
- Update: run a package/git update + restart (`update.run`) with a restart report
Cron jobs panel notes:
- For isolated jobs, delivery defaults to announce summary. You can switch to none if you want internal-only runs.
- Channel/target fields appear when announce is selected.
- Webhook mode uses `delivery.mode = "webhook"` with `delivery.to` set to a valid HTTP(S) webhook URL.
- For main-session jobs, webhook and none delivery modes are available.
- Advanced edit controls include delete-after-run, clear agent override, cron exact/stagger options,
agent model/thinking overrides, and best-effort delivery toggles.
- Form validation is inline with field-level errors; invalid values disable the save button until fixed.
- Set `cron.webhookToken` to send a dedicated bearer token, if omitted the webhook is sent without an auth header.
- Deprecated fallback: stored legacy jobs with `notify: true` can still use `cron.webhook` until migrated.
<AccordionGroup>
<Accordion title="Chat and Talk">
- Chat with the model via Gateway WS (`chat.history`, `chat.send`, `chat.abort`, `chat.inject`).
- Talk to OpenAI Realtime directly from the browser via WebRTC. The Gateway mints a short-lived Realtime client secret with `talk.realtime.session`; the browser sends microphone audio directly to OpenAI and relays `openclaw_agent_consult` tool calls back through `chat.send` for the larger configured OpenClaw model.
- Stream tool calls + live tool output cards in Chat (agent events).
</Accordion>
<Accordion title="Channels, instances, sessions, dreams">
- Channels: built-in plus bundled/external plugin channels status, QR login, and per-channel config (`channels.status`, `web.login.*`, `config.patch`).
- Instances: presence list + refresh (`system-presence`).
- Sessions: list + per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`).
- Dreams: dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`).
</Accordion>
<Accordion title="Cron, skills, nodes, exec approvals">
- Cron jobs: list/add/edit/run/enable/disable + run history (`cron.*`).
- Skills: status, enable/disable, install, API key updates (`skills.*`).
- Nodes: list + caps (`node.list`).
- Exec approvals: edit gateway or node allowlists + ask policy for `exec host=gateway/node` (`exec.approvals.*`).
</Accordion>
<Accordion title="Config">
- View/edit `~/.openclaw/openclaw.json` (`config.get`, `config.set`).
- Apply + restart with validation (`config.apply`) and wake the last active session.
- Writes include a base-hash guard to prevent clobbering concurrent edits.
- Writes (`config.set`/`config.apply`/`config.patch`) preflight active SecretRef resolution for refs in the submitted config payload; unresolved active submitted refs are rejected before write.
- Schema + form rendering (`config.schema` / `config.schema.lookup`, including field `title` / `description`, matched UI hints, immediate child summaries, docs metadata on nested object/wildcard/array/composition nodes, plus plugin + channel schemas when available); Raw JSON editor is available only when the snapshot has a safe raw round-trip.
- If a snapshot cannot safely round-trip raw text, Control UI forces Form mode and disables Raw mode for that snapshot.
- Raw JSON editor "Reset to saved" preserves the raw-authored shape (formatting, comments, `$include` layout) instead of re-rendering a flattened snapshot, so external edits survive a reset when the snapshot can safely round-trip.
- Structured SecretRef object values are rendered read-only in form text inputs to prevent accidental object-to-string corruption.
</Accordion>
<Accordion title="Debug, logs, update">
- Debug: status/health/models snapshots + event log + manual RPC calls (`status`, `health`, `models.list`).
- Logs: live tail of gateway file logs with filter/export (`logs.tail`).
- Update: run a package/git update + restart (`update.run`) with a restart report.
</Accordion>
<Accordion title="Cron jobs panel notes">
- For isolated jobs, delivery defaults to announce summary. You can switch to none if you want internal-only runs.
- Channel/target fields appear when announce is selected.
- Webhook mode uses `delivery.mode = "webhook"` with `delivery.to` set to a valid HTTP(S) webhook URL.
- For main-session jobs, webhook and none delivery modes are available.
- Advanced edit controls include delete-after-run, clear agent override, cron exact/stagger options, agent model/thinking overrides, and best-effort delivery toggles.
- Form validation is inline with field-level errors; invalid values disable the save button until fixed.
- Set `cron.webhookToken` to send a dedicated bearer token, if omitted the webhook is sent without an auth header.
- Deprecated fallback: stored legacy jobs with `notify: true` can still use `cron.webhook` until migrated.
</Accordion>
</AccordionGroup>
## Chat behavior
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events.
- 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.
- `chat.history` also strips display-only inline directive tags from visible assistant text (for example `[[reply_to_*]]` and `[[audio_as_voice]]`), 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), and leaked ASCII/full-width model control tokens, and omits assistant entries whose whole visible text is only the exact silent token `NO_REPLY` / `no_reply`.
- During an active send and the final history refresh, the chat view keeps local
optimistic user/assistant messages visible if `chat.history` briefly returns
an older snapshot; the canonical transcript replaces those local messages once
the Gateway history catches up.
- `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery).
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
- When fresh Gateway session usage reports show high context pressure, the chat
composer area shows a context notice and, at recommended compaction levels, a
compact button that runs the normal session compaction path. Stale token
snapshots are hidden until the Gateway reports fresh usage again.
- Talk mode uses a registered realtime voice provider that supports browser
WebRTC sessions. Configure OpenAI with `talk.provider: "openai"` plus
`talk.providers.openai.apiKey`, or reuse the Voice Call realtime provider
config. The browser never receives the standard OpenAI API key; it receives
only the ephemeral Realtime client secret. Google Live realtime voice is
supported for backend Voice Call and Google Meet bridges, but not this browser
WebRTC path yet. The Realtime session prompt is assembled by the Gateway;
`talk.realtime.session` does not accept caller-provided instruction overrides.
- In the Chat composer, the Talk control is the waves button next to the
microphone dictation button. When Talk starts, the composer status row shows
`Connecting Talk...`, then `Talk live` while audio is connected, or
`Asking OpenClaw...` while a realtime tool call is consulting the configured
larger model through `chat.send`.
- Stop:
- Click **Stop** (calls `chat.abort`)
- While a run is active, normal follow-ups queue. Click **Steer** on a queued message to inject that follow-up into the running turn.
- Type `/stop` (or standalone abort phrases like `stop`, `stop action`, `stop run`, `stop openclaw`, `please stop`) to abort out-of-band
- `chat.abort` supports `{ sessionKey }` (no `runId`) to abort all active runs for that session
- Abort partial retention:
- When a run is aborted, partial assistant text can still be shown in the UI
- Gateway persists aborted partial assistant text into transcript history when buffered output exists
- Persisted entries include abort metadata so transcript consumers can tell abort partials from normal completion output
<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.
- 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.
- `chat.history` also strips display-only inline directive tags from visible assistant text (for example `[[reply_to_*]]` and `[[audio_as_voice]]`), 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), and leaked ASCII/full-width model control tokens, and omits assistant entries whose whole visible text is only the exact silent token `NO_REPLY` / `no_reply`.
- During an active send and the final history refresh, the chat view keeps local optimistic user/assistant messages visible if `chat.history` briefly returns an older snapshot; the canonical transcript replaces those local messages once the Gateway history catches up.
- `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery).
- The chat header model and thinking pickers patch the active session immediately through `sessions.patch`; they are persistent session overrides, not one-turn-only send options.
- When fresh Gateway session usage reports show high context pressure, the chat composer area shows a context notice and, at recommended compaction levels, a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again.
</Accordion>
<Accordion title="Talk mode (browser WebRTC)">
Talk mode uses a registered realtime voice provider that supports browser WebRTC sessions. Configure OpenAI with `talk.provider: "openai"` plus `talk.providers.openai.apiKey`, or reuse the Voice Call realtime provider config. The browser never receives the standard OpenAI API key; it receives only the ephemeral Realtime client secret. Google Live realtime voice is supported for backend Voice Call and Google Meet bridges, but not this browser WebRTC path yet. The Realtime session prompt is assembled by the Gateway; `talk.realtime.session` does not accept caller-provided instruction overrides.
In the Chat composer, the Talk control is the waves button next to the microphone dictation button. When Talk starts, the composer status row shows `Connecting Talk...`, then `Talk live` while audio is connected, or `Asking OpenClaw...` while a realtime tool call is consulting the configured larger model through `chat.send`.
</Accordion>
<Accordion title="Stop and abort">
- Click **Stop** (calls `chat.abort`).
- While a run is active, normal follow-ups queue. Click **Steer** on a queued message to inject that follow-up into the running turn.
- Type `/stop` (or standalone abort phrases like `stop`, `stop action`, `stop run`, `stop openclaw`, `please stop`) to abort out-of-band.
- `chat.abort` supports `{ sessionKey }` (no `runId`) to abort all active runs for that session.
</Accordion>
<Accordion title="Abort partial retention">
- When a run is aborted, partial assistant text can still be shown in the UI.
- Gateway persists aborted partial assistant text into transcript history when buffered output exists.
- Persisted entries include abort metadata so transcript consumers can tell abort partials from normal completion output.
</Accordion>
</AccordionGroup>
## PWA install and web push
The Control UI ships a `manifest.webmanifest` and a service worker, so
modern browsers can install it as a standalone PWA. Web Push lets the
Gateway wake the installed PWA with notifications even when the tab or
browser window is not open.
The Control UI ships a `manifest.webmanifest` and a service worker, so modern browsers can install it as a standalone PWA. Web Push lets the Gateway wake the installed PWA with notifications even when the tab or browser window is not open.
| Surface | What it does |
| ----------------------------------------------------- | ------------------------------------------------------------------ |
@@ -207,37 +173,38 @@ browser window is not open.
| `push/vapid-keys.json` (under the OpenClaw state dir) | Auto-generated VAPID keypair used to sign Web Push payloads. |
| `push/web-push-subscriptions.json` | Persisted browser subscription endpoints. |
Override the VAPID keypair through env vars on the Gateway process when
you want to pin keys (for multi-host deployments, secrets rotation, or
tests):
Override the VAPID keypair through env vars on the Gateway process when you want to pin keys (for multi-host deployments, secrets rotation, or tests):
- `OPENCLAW_VAPID_PUBLIC_KEY`
- `OPENCLAW_VAPID_PRIVATE_KEY`
- `OPENCLAW_VAPID_SUBJECT` (defaults to `mailto:openclaw@localhost`)
The Control UI uses these scope-gated Gateway methods to register and
test browser subscriptions:
The Control UI uses these scope-gated Gateway methods to register and test browser subscriptions:
- `push.web.vapidPublicKey` — fetches the active VAPID public key.
- `push.web.subscribe` — registers an `endpoint` plus `keys.p256dh`/`keys.auth`.
- `push.web.unsubscribe` — removes a registered endpoint.
- `push.web.test` — sends a test notification to the caller's subscription.
Web Push is independent of the iOS APNS relay path
(see [Configuration](/gateway/configuration) for relay-backed push) and
the existing `push.test` method, which target native mobile pairing.
<Note>
Web Push is independent of the iOS APNS relay path (see [Configuration](/gateway/configuration) for relay-backed push) and the existing `push.test` method, which target native mobile pairing.
</Note>
## Hosted embeds
Assistant messages can render hosted web content inline with the `[embed ...]`
shortcode. The iframe sandbox policy is controlled by
`gateway.controlUi.embedSandbox`:
Assistant messages can render hosted web content inline with the `[embed ...]` shortcode. The iframe sandbox policy is controlled by `gateway.controlUi.embedSandbox`:
- `strict`: disables script execution inside hosted embeds
- `scripts`: allows interactive embeds while keeping origin isolation; this is
the default and is usually enough for self-contained browser games/widgets
- `trusted`: adds `allow-same-origin` on top of `allow-scripts` for same-site
documents that intentionally need stronger privileges
<Tabs>
<Tab title="strict">
Disables script execution inside hosted embeds.
</Tab>
<Tab title="scripts (default)">
Allows interactive embeds while keeping origin isolation; this is the default and is usually enough for self-contained browser games/widgets.
</Tab>
<Tab title="trusted">
Adds `allow-same-origin` on top of `allow-scripts` for same-site documents that intentionally need stronger privileges.
</Tab>
</Tabs>
Example:
@@ -251,61 +218,52 @@ Example:
}
```
Use `trusted` only when the embedded document genuinely needs same-origin
behavior. For most agent-generated games and interactive canvases, `scripts` is
the safer choice.
<Warning>
Use `trusted` only when the embedded document genuinely needs same-origin behavior. For most agent-generated games and interactive canvases, `scripts` is the safer choice.
</Warning>
Absolute external `http(s)` embed URLs stay blocked by default. If you
intentionally want `[embed url="https://..."]` to load third-party pages, set
`gateway.controlUi.allowExternalEmbedUrls: true`.
Absolute external `http(s)` embed URLs stay blocked by default. If you intentionally want `[embed url="https://..."]` to load third-party pages, set `gateway.controlUi.allowExternalEmbedUrls: true`.
## Tailnet access (recommended)
### Integrated Tailscale Serve (preferred)
<Tabs>
<Tab title="Integrated Tailscale Serve (preferred)">
Keep the Gateway on loopback and let Tailscale Serve proxy it with HTTPS:
Keep the Gateway on loopback and let Tailscale Serve proxy it with HTTPS:
```bash
openclaw gateway --tailscale serve
```
```bash
openclaw gateway --tailscale serve
```
Open:
Open:
- `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
- `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. 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"`.
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 Tailscales `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"`.
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.
Tokenless Serve auth assumes the gateway host is trusted. If untrusted local
code may run on that host, require token/password auth.
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.
### Bind to tailnet + token
<Warning>
Tokenless Serve auth assumes the gateway host is trusted. If untrusted local code may run on that host, require token/password auth.
</Warning>
```bash
openclaw gateway --bind tailnet --token "$(openssl rand -hex 32)"
```
</Tab>
<Tab title="Bind to tailnet + token">
```bash
openclaw gateway --bind tailnet --token "$(openssl rand -hex 32)"
```
Then open:
Then open:
- `http://<tailscale-ip>:18789/` (or your configured `gateway.controlUi.basePath`)
- `http://<tailscale-ip>:18789/` (or your configured `gateway.controlUi.basePath`)
Paste the matching shared secret into the UI settings (sent as
`connect.params.auth.token` or `connect.params.auth.password`).
Paste the matching shared secret into the UI settings (sent as `connect.params.auth.token` or `connect.params.auth.password`).
</Tab>
</Tabs>
## Insecure HTTP
If you open the dashboard over plain HTTP (`http://<lan-ip>` or `http://<tailscale-ip>`),
the browser runs in a **non-secure context** and blocks WebCrypto. By default,
OpenClaw **blocks** Control UI connections without device identity.
If you open the dashboard over plain HTTP (`http://<lan-ip>` or `http://<tailscale-ip>`), the browser runs in a **non-secure context** and blocks WebCrypto. By default, OpenClaw **blocks** Control UI connections without device identity.
Documented exceptions:
@@ -318,47 +276,47 @@ Documented exceptions:
- `https://<magicdns>/` (Serve)
- `http://127.0.0.1:18789/` (on the gateway host)
**Insecure-auth toggle behavior:**
<AccordionGroup>
<Accordion title="Insecure-auth toggle behavior">
```json5
{
gateway: {
controlUi: { allowInsecureAuth: true },
bind: "tailnet",
auth: { mode: "token", token: "replace-me" },
},
}
```
```json5
{
gateway: {
controlUi: { allowInsecureAuth: true },
bind: "tailnet",
auth: { mode: "token", token: "replace-me" },
},
}
```
`allowInsecureAuth` is a local compatibility toggle only:
`allowInsecureAuth` is a local compatibility toggle only:
- It allows localhost Control UI sessions to proceed without device identity in non-secure HTTP contexts.
- It does not bypass pairing checks.
- It does not relax remote (non-localhost) device identity requirements.
- It allows localhost Control UI sessions to proceed without device identity in
non-secure HTTP contexts.
- It does not bypass pairing checks.
- It does not relax remote (non-localhost) device identity requirements.
</Accordion>
<Accordion title="Break-glass only">
```json5
{
gateway: {
controlUi: { dangerouslyDisableDeviceAuth: true },
bind: "tailnet",
auth: { mode: "token", token: "replace-me" },
},
}
```
**Break-glass only:**
<Warning>
`dangerouslyDisableDeviceAuth` disables Control UI device identity checks and is a severe security downgrade. Revert quickly after emergency use.
</Warning>
```json5
{
gateway: {
controlUi: { dangerouslyDisableDeviceAuth: true },
bind: "tailnet",
auth: { mode: "token", token: "replace-me" },
},
}
```
`dangerouslyDisableDeviceAuth` disables Control UI device identity checks and is a
severe security downgrade. Revert quickly after emergency use.
Trusted-proxy note:
- successful trusted-proxy auth can admit **operator** Control UI sessions without
device identity
- this does **not** extend to node-role Control UI sessions
- same-host loopback reverse proxies still do not satisfy trusted-proxy auth; see
[Trusted proxy auth](/gateway/trusted-proxy-auth)
</Accordion>
<Accordion title="Trusted-proxy note">
- Successful trusted-proxy auth can admit **operator** Control UI sessions without device identity.
- This does **not** extend to node-role Control UI sessions.
- Same-host loopback reverse proxies still do not satisfy trusted-proxy auth; see [Trusted proxy auth](/gateway/trusted-proxy-auth).
</Accordion>
</AccordionGroup>
See [Tailscale](/gateway/tailscale) for HTTPS setup guidance.
@@ -409,42 +367,42 @@ Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`).
## Debugging/testing: dev server + remote Gateway
The Control UI is static files; the WebSocket target is configurable and can be
different from the HTTP origin. This is handy when you want the Vite dev server
locally but the Gateway runs elsewhere.
The Control UI is static files; the WebSocket target is configurable and can be different from the HTTP origin. This is handy when you want the Vite dev server locally but the Gateway runs elsewhere.
1. Start the UI dev server: `pnpm ui:dev`
2. Open a URL like:
<Steps>
<Step title="Start the UI dev server">
```bash
pnpm ui:dev
```
</Step>
<Step title="Open with gatewayUrl">
```text
http://localhost:5173/?gatewayUrl=ws://<gateway-host>:18789
```
```text
http://localhost:5173/?gatewayUrl=ws://<gateway-host>:18789
```
Optional one-time auth (if needed):
Optional one-time auth (if needed):
```text
http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-token>
```
```text
http://localhost:5173/?gatewayUrl=wss://<gateway-host>:18789#token=<gateway-token>
```
</Step>
</Steps>
Notes:
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
- `token` should be passed via the URL fragment (`#token=...`) whenever possible. Fragments are not sent to the server, which avoids request-log and Referer leakage. Legacy `?token=` query params are still imported once for compatibility, but only as a fallback, and are stripped immediately after bootstrap.
- `password` is kept in memory only.
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials.
Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.
- Use `wss://` when the Gateway is behind TLS (Tailscale Serve, HTTPS proxy, etc.).
- `gatewayUrl` is only accepted in a top-level window (not embedded) to prevent clickjacking.
- Non-loopback Control UI deployments must set `gateway.controlUi.allowedOrigins`
explicitly (full origins). This includes remote dev setups.
- Gateway startup may seed local origins such as `http://localhost:<port>` and
`http://127.0.0.1:<port>` from the effective runtime bind and port, but remote
browser origins still need explicit entries.
- Do not use `gateway.controlUi.allowedOrigins: ["*"]` except for tightly controlled
local testing. It means allow any browser origin, not “match whatever host I am
using.”
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables
Host-header origin fallback mode, but it is a dangerous security mode.
<AccordionGroup>
<Accordion title="Notes">
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
- `token` should be passed via the URL fragment (`#token=...`) whenever possible. Fragments are not sent to the server, which avoids request-log and Referer leakage. Legacy `?token=` query params are still imported once for compatibility, but only as a fallback, and are stripped immediately after bootstrap.
- `password` is kept in memory only.
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials. Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.
- Use `wss://` when the Gateway is behind TLS (Tailscale Serve, HTTPS proxy, etc.).
- `gatewayUrl` is only accepted in a top-level window (not embedded) to prevent clickjacking.
- Non-loopback Control UI deployments must set `gateway.controlUi.allowedOrigins` explicitly (full origins). This includes remote dev setups.
- Gateway startup may seed local origins such as `http://localhost:<port>` and `http://127.0.0.1:<port>` from the effective runtime bind and port, but remote browser origins still need explicit entries.
- Do not use `gateway.controlUi.allowedOrigins: ["*"]` except for tightly controlled local testing. It means allow any browser origin, not "match whatever host I am using."
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables Host-header origin fallback mode, but it is a dangerous security mode.
</Accordion>
</AccordionGroup>
Example:
@@ -463,6 +421,6 @@ Remote access setup details: [Remote access](/gateway/remote).
## Related
- [Dashboard](/web/dashboard) — gateway dashboard
- [WebChat](/web/webchat) — browser-based chat interface
- [TUI](/web/tui) — terminal user interface
- [Health Checks](/gateway/health) — gateway health monitoring
- [TUI](/web/tui) — terminal user interface
- [WebChat](/web/webchat) — browser-based chat interface

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

@@ -122,7 +122,7 @@ describe("anthropic cli migration", () => {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
embeddedHarness: { runtime: "claude-cli" },
agentRuntime: { id: "claude-cli" },
models: {
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-sonnet-4-6": {},
@@ -153,7 +153,7 @@ describe("anthropic cli migration", () => {
expect(result.configPatch).toEqual({
agents: {
defaults: {
embeddedHarness: { runtime: "claude-cli" },
agentRuntime: { id: "claude-cli" },
models: {
"openai/gpt-5.2": {},
"anthropic/claude-opus-4-7": {},
@@ -184,7 +184,7 @@ describe("anthropic cli migration", () => {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-7" },
embeddedHarness: { runtime: "claude-cli" },
agentRuntime: { id: "claude-cli" },
models: {
"anthropic/claude-opus-4-7": {},
"anthropic/claude-sonnet-4-6": {},
@@ -325,7 +325,7 @@ describe("anthropic cli migration", () => {
primary: "anthropic/claude-opus-4-7",
fallbacks: ["anthropic/claude-opus-4-6", "openai/gpt-5.2"],
},
embeddedHarness: { runtime: "claude-cli" },
agentRuntime: { id: "claude-cli" },
models: {
"anthropic/claude-opus-4-7": { alias: "Opus" },
"anthropic/claude-opus-4-6": { alias: "Opus" },

View File

@@ -12,9 +12,9 @@ import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS } from "./cli-
type AgentDefaultsModel = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>["model"];
type AgentDefaultsModels = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>["models"];
type AgentDefaultsEmbeddedHarness = NonNullable<
type AgentDefaultsRuntimePolicy = NonNullable<
NonNullable<OpenClawConfig["agents"]>["defaults"]
>["embeddedHarness"];
>["agentRuntime"];
type ClaudeCliCredential = NonNullable<ReturnType<typeof readClaudeCliCredentialsForSetup>>;
function toAnthropicModelRef(raw: string): string | null {
@@ -125,16 +125,14 @@ function seedClaudeCliAllowlist(
return next;
}
function selectClaudeCliRuntime(
embeddedHarness: AgentDefaultsEmbeddedHarness | undefined,
): AgentDefaultsEmbeddedHarness {
const currentRuntime = embeddedHarness?.runtime?.trim();
function selectClaudeCliRuntime(agentRuntime: AgentDefaultsRuntimePolicy | undefined) {
const currentRuntime = agentRuntime?.id?.trim();
if (currentRuntime && currentRuntime !== "auto") {
return embeddedHarness;
return agentRuntime;
}
return {
...embeddedHarness,
runtime: CLAUDE_CLI_BACKEND_ID,
...agentRuntime,
id: CLAUDE_CLI_BACKEND_ID,
};
}
@@ -198,7 +196,7 @@ export function buildAnthropicCliMigrationResult(
agents: {
defaults: {
...(rewrittenModel.changed ? { model: rewrittenModel.value } : {}),
embeddedHarness: selectClaudeCliRuntime(defaults?.embeddedHarness),
agentRuntime: selectClaudeCliRuntime(defaults?.agentRuntime),
models: nextModels,
},
},

View File

@@ -140,7 +140,7 @@ function isAnthropicCacheRetentionTarget(
}
function usesClaudeCliModelSelection(config: OpenClawConfig): boolean {
if (config.agents?.defaults?.embeddedHarness?.runtime === CLAUDE_CLI_BACKEND_ID) {
if (config.agents?.defaults?.agentRuntime?.id === CLAUDE_CLI_BACKEND_ID) {
return true;
}
const primary = resolveModelPrimaryValue(

View File

@@ -176,7 +176,7 @@ describe("anthropic provider replay hooks", () => {
},
agents: {
defaults: {
embeddedHarness: { runtime: "claude-cli" },
agentRuntime: { id: "claude-cli" },
model: { primary: "anthropic/claude-opus-4-7" },
models: {
"anthropic/claude-opus-4-7": {},

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

@@ -5,6 +5,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 +13,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 +85,7 @@ const startAdvertiser = (
): ReturnType<StartGatewayBonjourAdvertiser> =>
startGatewayBonjourAdvertiser(opts, {
logger,
registerUncaughtExceptionHandler: (handler) => registerUncaughtExceptionHandler(handler),
registerUnhandledRejectionHandler: (handler) => registerUnhandledRejectionHandler(handler),
});
@@ -103,6 +112,7 @@ describe("gateway bonjour advertiser", () => {
createService.mockClear();
getResponder.mockReset();
shutdown.mockClear();
registerUncaughtExceptionHandler.mockClear();
registerUnhandledRejectionHandler.mockClear();
logger.info.mockClear();
logger.warn.mockClear();
@@ -220,7 +230,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 +247,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 +263,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 +279,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 +301,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 +320,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();
});

View File

@@ -1,6 +1,6 @@
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,6 +50,7 @@ type CiaoModule = {
type BonjourCycle = {
responder: BonjourResponder;
services: Array<{ label: string; svc: BonjourService }>;
cleanupUncaughtException?: () => void;
cleanupUnhandledRejection?: () => void;
};
@@ -59,10 +60,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;
};
@@ -175,19 +178,22 @@ export async function startGatewayBonjourAdvertiser(
};
const { getResponder, Protocol } = await loadCiaoModule();
const restoreConsoleLog = installCiaoConsoleNoiseFilter();
let requestCiaoRecovery: ((classification: CiaoProcessErrorClassification) => void) | undefined;
const handleCiaoUnhandledRejection = (reason: unknown): boolean => {
const classification = classifyCiaoUnhandledRejection(reason);
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;
};
@@ -255,10 +261,14 @@ export async function startGatewayBonjourAdvertiser(
const cleanupUnhandledRejection =
services.length > 0 && deps.registerUnhandledRejectionHandler
? deps.registerUnhandledRejectionHandler(handleCiaoUnhandledRejection)
? deps.registerUnhandledRejectionHandler(handleCiaoProcessError)
: undefined;
const cleanupUncaughtException =
services.length > 0 && deps.registerUncaughtExceptionHandler
? deps.registerUncaughtExceptionHandler(handleCiaoProcessError)
: undefined;
return { responder, services, cleanupUnhandledRejection };
return { responder, services, cleanupUncaughtException, cleanupUnhandledRejection };
}
async function stopCycle(cycle: BonjourCycle | null, opts?: { shutdownResponder?: boolean }) {
@@ -279,6 +289,7 @@ export async function startGatewayBonjourAdvertiser(
} catch {
/* ignore */
} finally {
cycle.cleanupUncaughtException?.();
cycle.cleanupUnhandledRejection?.();
}
}
@@ -388,6 +399,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(() => {

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,
@@ -44,6 +61,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,15 +2,16 @@ 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 {
export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClassification | null {
const formatted = formatBonjourError(reason);
const message = formatted.toUpperCase();
if (CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
@@ -19,9 +20,14 @@ export function classifyCiaoUnhandledRejection(
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

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

View File

@@ -1140,6 +1140,28 @@ describe("diagnostics-otel service", () => {
traceFlags: "01",
},
});
emitDiagnosticEvent({
type: "harness.run.completed",
runId: "run-1",
sessionKey: "session-key",
sessionId: "session-1",
provider: "codex",
model: "gpt-5.4",
channel: "qa",
harnessId: "codex",
pluginId: "codex-plugin",
outcome: "completed",
durationMs: 90,
resultClassification: "reasoning-only",
yieldDetected: true,
itemLifecycle: { startedCount: 3, completedCount: 2, activeCount: 1 },
trace: {
traceId: TRACE_ID,
spanId: GRANDCHILD_SPAN_ID,
parentSpanId: CHILD_SPAN_ID,
traceFlags: "01",
},
});
emitDiagnosticEvent({
type: "tool.execution.error",
runId: "run-1",
@@ -1160,7 +1182,12 @@ describe("diagnostics-otel service", () => {
const spanNames = telemetryState.tracer.startSpan.mock.calls.map((call) => call[0]);
expect(spanNames).toEqual(
expect.arrayContaining(["openclaw.run", "openclaw.model.call", "openclaw.tool.execution"]),
expect.arrayContaining([
"openclaw.run",
"openclaw.model.call",
"openclaw.harness.run",
"openclaw.tool.execution",
]),
);
const runCall = telemetryState.tracer.startSpan.mock.calls.find(
@@ -1207,6 +1234,36 @@ describe("diagnostics-otel service", () => {
});
expect(modelCall?.[2]).toBeUndefined();
const harnessCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.harness.run",
);
expect(harnessCall?.[1]).toMatchObject({
attributes: {
"openclaw.harness.id": "codex",
"openclaw.harness.plugin": "codex-plugin",
"openclaw.outcome": "completed",
"openclaw.provider": "codex",
"openclaw.model": "gpt-5.4",
"openclaw.channel": "qa",
"openclaw.harness.result_classification": "reasoning-only",
"openclaw.harness.yield_detected": true,
"openclaw.harness.items.started": 3,
"openclaw.harness.items.completed": 2,
"openclaw.harness.items.active": 1,
},
startTime: expect.any(Number),
});
expect(harnessCall?.[1]).toEqual({
attributes: expect.not.objectContaining({
"openclaw.runId": expect.anything(),
"openclaw.sessionId": expect.anything(),
"openclaw.sessionKey": expect.anything(),
"openclaw.traceId": expect.anything(),
}),
startTime: expect.any(Number),
});
expect(harnessCall?.[2]).toBeUndefined();
const toolCall = telemetryState.tracer.startSpan.mock.calls.find(
(call) => call[0] === "openclaw.tool.execution",
);
@@ -1244,6 +1301,25 @@ describe("diagnostics-otel service", () => {
"openclaw.runId": expect.anything(),
}),
);
expect(
telemetryState.histograms.get("openclaw.harness.duration_ms")?.record,
).toHaveBeenCalledWith(
90,
expect.objectContaining({
"openclaw.harness.id": "codex",
"openclaw.harness.plugin": "codex-plugin",
"openclaw.outcome": "completed",
}),
);
expect(
telemetryState.histograms.get("openclaw.harness.duration_ms")?.record,
).toHaveBeenCalledWith(
90,
expect.not.objectContaining({
"openclaw.runId": expect.anything(),
"openclaw.sessionKey": expect.anything(),
}),
);
expect(
telemetryState.histograms.get("openclaw.tool.execution.duration_ms")?.record,
).toHaveBeenCalledWith(

View File

@@ -81,6 +81,10 @@ type ModelCallLifecycleDiagnosticEvent = Extract<
DiagnosticEventPayload,
{ type: "model.call.completed" | "model.call.error" }
>;
type HarnessRunLifecycleDiagnosticEvent = Extract<
DiagnosticEventPayload,
{ type: "harness.run.completed" | "harness.run.error" }
>;
type TelemetryExporterDiagnosticEvent = Extract<
DiagnosticEventPayload,
{ type: "telemetry.exporter" }
@@ -720,6 +724,10 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
unit: "ms",
description: "Agent run duration",
});
const harnessDurationHistogram = meter.createHistogram("openclaw.harness.duration_ms", {
unit: "ms",
description: "Agent harness lifecycle duration",
});
const contextHistogram = meter.createHistogram("openclaw.context.tokens", {
unit: "1",
description: "Context window size and usage",
@@ -1426,6 +1434,82 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
span.end(evt.ts);
};
const harnessRunMetricAttrs = (evt: HarnessRunLifecycleDiagnosticEvent) => ({
"openclaw.harness.id": lowCardinalityAttr(evt.harnessId, "unknown"),
"openclaw.harness.plugin": lowCardinalityAttr(evt.pluginId),
"openclaw.outcome": evt.type === "harness.run.error" ? "error" : evt.outcome,
"openclaw.provider": lowCardinalityAttr(evt.provider, "unknown"),
"openclaw.model": lowCardinalityAttr(evt.model, "unknown"),
...(evt.channel ? { "openclaw.channel": lowCardinalityAttr(evt.channel) } : {}),
});
const recordHarnessRunCompleted = (
evt: Extract<DiagnosticEventPayload, { type: "harness.run.completed" }>,
metadata: DiagnosticEventMetadata,
) => {
harnessDurationHistogram.record(evt.durationMs, harnessRunMetricAttrs(evt));
if (!tracesEnabled) {
return;
}
const spanAttrs: Record<string, string | number | boolean> = {
...harnessRunMetricAttrs(evt),
};
if (evt.resultClassification) {
spanAttrs["openclaw.harness.result_classification"] = lowCardinalityAttr(
evt.resultClassification,
);
}
if (typeof evt.yieldDetected === "boolean") {
spanAttrs["openclaw.harness.yield_detected"] = evt.yieldDetected;
}
if (evt.itemLifecycle) {
spanAttrs["openclaw.harness.items.started"] = evt.itemLifecycle.startedCount;
spanAttrs["openclaw.harness.items.completed"] = evt.itemLifecycle.completedCount;
spanAttrs["openclaw.harness.items.active"] = evt.itemLifecycle.activeCount;
}
const span = spanWithDuration("openclaw.harness.run", spanAttrs, evt.durationMs, {
parentContext: contextForTrustedDiagnosticSpanParent(evt, metadata),
endTimeMs: evt.ts,
});
if (evt.outcome === "error") {
span.setStatus({
code: SpanStatusCode.ERROR,
message: "error",
});
}
span.end(evt.ts);
};
const recordHarnessRunError = (
evt: Extract<DiagnosticEventPayload, { type: "harness.run.error" }>,
metadata: DiagnosticEventMetadata,
) => {
const errorType = lowCardinalityAttr(evt.errorCategory, "other");
const attrs = {
...harnessRunMetricAttrs(evt),
"openclaw.harness.phase": evt.phase,
"openclaw.errorCategory": errorType,
};
harnessDurationHistogram.record(evt.durationMs, attrs);
if (!tracesEnabled) {
return;
}
const spanAttrs: Record<string, string | number | boolean> = {
...attrs,
"error.type": errorType,
...(evt.cleanupFailed ? { "openclaw.harness.cleanup_failed": true } : {}),
};
const span = spanWithDuration("openclaw.harness.run", spanAttrs, evt.durationMs, {
parentContext: contextForTrustedDiagnosticSpanParent(evt, metadata),
endTimeMs: evt.ts,
});
span.setStatus({
code: SpanStatusCode.ERROR,
message: errorType,
});
span.end(evt.ts);
};
const recordContextAssembled = (
evt: Extract<DiagnosticEventPayload, { type: "context.assembled" }>,
metadata: DiagnosticEventMetadata,
@@ -1746,6 +1830,12 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
case "run.completed":
recordRunCompleted(evt, metadata);
return;
case "harness.run.completed":
recordHarnessRunCompleted(evt, metadata);
return;
case "harness.run.error":
recordHarnessRunError(evt, metadata);
return;
case "context.assembled":
recordContextAssembled(evt, metadata);
return;
@@ -1781,6 +1871,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService {
return;
case "tool.execution.started":
case "run.started":
case "harness.run.started":
case "model.call.started":
case "payload.large":
return;

View File

@@ -0,0 +1 @@
export * from "openclaw/plugin-sdk/diagnostics-prometheus";

View File

@@ -0,0 +1,20 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createDiagnosticsPrometheusExporter } from "./src/service.js";
const exporter = createDiagnosticsPrometheusExporter();
export default definePluginEntry({
id: "diagnostics-prometheus",
name: "Diagnostics Prometheus",
description: "Expose OpenClaw diagnostics metrics in Prometheus text format",
register(api) {
api.registerService(exporter.service);
api.registerHttpRoute({
path: "/api/diagnostics/prometheus",
auth: "gateway",
match: "exact",
gatewayRuntimeScopeSurface: "trusted-operator",
handler: exporter.handler,
});
},
});

View File

@@ -0,0 +1,8 @@
{
"id": "diagnostics-prometheus",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,24 @@
{
"name": "@openclaw/diagnostics-prometheus",
"version": "2026.4.25",
"description": "OpenClaw diagnostics Prometheus exporter",
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"compat": {
"pluginApi": ">=2026.4.25"
},
"build": {
"openclawVersion": "2026.4.25"
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -0,0 +1,169 @@
import { describe, expect, it, vi } from "vitest";
import type { DiagnosticEventMetadata, DiagnosticEventPayload } from "../api.js";
import { createDiagnosticsPrometheusExporter, __test__ } from "./service.js";
const trusted: DiagnosticEventMetadata = Object.freeze({ trusted: true });
const untrusted: DiagnosticEventMetadata = Object.freeze({ trusted: false });
function baseEvent(): Pick<DiagnosticEventPayload, "seq" | "ts"> {
return { seq: 1, ts: 1700000000000 };
}
describe("diagnostics-prometheus service", () => {
it("records trusted run metrics without raw diagnostic identifiers", () => {
const store = __test__.createPrometheusMetricStore();
__test__.recordDiagnosticEvent(
store,
{
...baseEvent(),
type: "run.completed",
runId: "run-should-not-export",
sessionKey: "session-should-not-export",
provider: "openai",
model: "gpt-5.4",
channel: "discord",
trigger: "message",
durationMs: 1500,
outcome: "completed",
},
trusted,
);
const rendered = __test__.renderPrometheusMetrics(store);
expect(rendered).toContain("# TYPE openclaw_run_completed_total counter");
expect(rendered).toContain(
'openclaw_run_completed_total{channel="discord",model="gpt-5.4",outcome="completed",provider="openai",trigger="message"} 1',
);
expect(rendered).toContain(
'openclaw_run_duration_seconds_sum{channel="discord",model="gpt-5.4",outcome="completed",provider="openai",trigger="message"} 1.5',
);
expect(rendered).not.toContain("run-should-not-export");
expect(rendered).not.toContain("session-should-not-export");
});
it("drops untrusted plugin-emitted diagnostic events", () => {
const store = __test__.createPrometheusMetricStore();
__test__.recordDiagnosticEvent(
store,
{
...baseEvent(),
type: "model.call.completed",
runId: "run-1",
callId: "call-1",
provider: "openai",
model: "gpt-5.4",
durationMs: 10,
},
untrusted,
);
expect(__test__.renderPrometheusMetrics(store)).toBe("");
});
it("redacts and bounds label values", () => {
const store = __test__.createPrometheusMetricStore();
__test__.recordDiagnosticEvent(
store,
{
...baseEvent(),
type: "tool.execution.error",
toolName: "shell\nbad",
durationMs: 25,
errorCategory: "Bearer sk-secret-token-value",
},
trusted,
);
const rendered = __test__.renderPrometheusMetrics(store);
expect(rendered).toContain(
'openclaw_tool_execution_total{error_category="other",outcome="error",params_kind="unknown",tool="tool"} 1',
);
expect(rendered).not.toContain("Bearer");
expect(rendered).not.toContain("sk-secret");
});
it("caps metric series growth and reports dropped series", () => {
const store = __test__.createPrometheusMetricStore();
for (let index = 0; index < 2100; index += 1) {
__test__.recordDiagnosticEvent(
store,
{
...baseEvent(),
type: "model.call.completed",
runId: `run-${index}`,
callId: `call-${index}`,
provider: "openai",
model: `model.${index}`,
durationMs: 10,
},
trusted,
);
}
const rendered = __test__.renderPrometheusMetrics(store);
expect(rendered).toContain("# TYPE openclaw_prometheus_series_dropped_total counter");
expect(rendered).toContain("openclaw_prometheus_series_dropped_total ");
});
it("subscribes to internal diagnostics and renders scrape text", () => {
const listeners: Array<
(event: DiagnosticEventPayload, metadata: DiagnosticEventMetadata) => void
> = [];
const emitted: unknown[] = [];
const exporter = createDiagnosticsPrometheusExporter();
const unsubscribe = vi.fn();
exporter.service.start({
config: {} as never,
stateDir: "/tmp/openclaw-prometheus-test",
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
internalDiagnostics: {
emit: (event) => emitted.push(event),
onEvent: (listener) => {
listeners.push(listener);
return unsubscribe;
},
},
});
listeners[0]?.(
{
...baseEvent(),
type: "model.usage",
provider: "openai",
model: "gpt-5.4",
usage: { input: 12, output: 3, total: 15 },
},
trusted,
);
expect(emitted).toContainEqual(
expect.objectContaining({
type: "telemetry.exporter",
exporter: "diagnostics-prometheus",
signal: "metrics",
status: "started",
}),
);
expect(exporter.render()).toContain(
'openclaw_model_tokens_total{agent="unknown",channel="unknown",model="gpt-5.4",provider="openai",token_type="input"} 12',
);
exporter.service.stop?.();
expect(unsubscribe).toHaveBeenCalledOnce();
expect(exporter.render()).toBe("");
});
});

View File

@@ -0,0 +1,684 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type {
DiagnosticEventMetadata,
DiagnosticEventPayload,
OpenClawPluginHttpRouteHandler,
OpenClawPluginService,
} from "../api.js";
import { redactSensitiveText } from "../api.js";
type LabelSet = Record<string, string>;
type CounterSample = {
help: string;
labels: LabelSet;
value: number;
};
type HistogramSample = {
buckets: number[];
counts: number[];
count: number;
help: string;
labels: LabelSet;
sum: number;
};
type GaugeSample = {
help: string;
labels: LabelSet;
value: number;
};
type MetricSnapshot = {
counters: Map<string, CounterSample>;
gauges: Map<string, GaugeSample>;
histograms: Map<string, HistogramSample>;
};
type PrometheusMetricStore = ReturnType<typeof createPrometheusMetricStore>;
const DURATION_BUCKETS_SECONDS = [
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600,
];
const TOKEN_BUCKETS = [1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576];
const BYTE_BUCKETS = [
1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456, 1073741824,
4294967296, 17179869184,
];
const LOW_CARDINALITY_VALUE_RE = /^[A-Za-z0-9_.:-]{1,120}$/u;
const MAX_PROMETHEUS_SERIES = 2048;
const DROPPED_SERIES_COUNTER_NAME = "openclaw_prometheus_series_dropped_total";
function lowCardinalityLabel(value: string | undefined, fallback = "unknown"): string {
if (!value) {
return fallback;
}
const redacted = redactSensitiveText(value.trim());
return LOW_CARDINALITY_VALUE_RE.test(redacted) ? redacted : fallback;
}
function numericValue(value: number | undefined): number | undefined {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
}
function seconds(ms: number | undefined): number | undefined {
const value = numericValue(ms);
return value === undefined ? undefined : value / 1000;
}
function sortedLabels(labels: LabelSet): [string, string][] {
return Object.entries(labels).toSorted(([left], [right]) => left.localeCompare(right));
}
function metricKey(name: string, labels: LabelSet): string {
return `${name}|${JSON.stringify(sortedLabels(labels))}`;
}
function escapeHelp(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
}
function escapeLabelValue(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
}
function formatLabels(labels: LabelSet): string {
const entries = sortedLabels(labels);
if (entries.length === 0) {
return "";
}
return `{${entries.map(([key, value]) => `${key}="${escapeLabelValue(value)}"`).join(",")}}`;
}
function formatPrometheusNumber(value: number): string {
if (!Number.isFinite(value)) {
return "0";
}
return Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(12)));
}
function createPrometheusMetricStore() {
const counters = new Map<string, CounterSample>();
const gauges = new Map<string, GaugeSample>();
const histograms = new Map<string, HistogramSample>();
let droppedSeries = 0;
const canCreateSeries = <T>(map: Map<string, T>, key: string, metricName: string): boolean => {
if (map.has(key)) {
return true;
}
if (metricName === DROPPED_SERIES_COUNTER_NAME) {
return true;
}
if (counters.size + gauges.size + histograms.size < MAX_PROMETHEUS_SERIES) {
return true;
}
droppedSeries += 1;
return false;
};
const counter = (name: string, help: string, labels: LabelSet, amount = 1) => {
if (!Number.isFinite(amount) || amount <= 0) {
return;
}
const key = metricKey(name, labels);
if (!canCreateSeries(counters, key, name)) {
return;
}
const existing = counters.get(key);
if (existing) {
existing.value += amount;
return;
}
counters.set(key, { help, labels, value: amount });
};
const gauge = (name: string, help: string, labels: LabelSet, value: number | undefined) => {
if (value === undefined || !Number.isFinite(value)) {
return;
}
const key = metricKey(name, labels);
if (!canCreateSeries(gauges, key, name)) {
return;
}
gauges.set(key, { help, labels, value });
};
const histogram = (
name: string,
help: string,
labels: LabelSet,
value: number | undefined,
buckets = DURATION_BUCKETS_SECONDS,
) => {
if (value === undefined || !Number.isFinite(value) || value < 0) {
return;
}
const key = metricKey(name, labels);
if (!canCreateSeries(histograms, key, name)) {
return;
}
let sample = histograms.get(key);
if (!sample) {
sample = {
buckets,
counts: buckets.map(() => 0),
count: 0,
help,
labels,
sum: 0,
};
histograms.set(key, sample);
}
sample.count += 1;
sample.sum += value;
for (let index = 0; index < sample.buckets.length; index += 1) {
const bucket = sample.buckets[index];
if (bucket !== undefined && value <= bucket) {
sample.counts[index] = (sample.counts[index] ?? 0) + 1;
}
}
};
const snapshot = (): MetricSnapshot => {
const counterSnapshot = new Map(counters);
if (droppedSeries > 0) {
counterSnapshot.set(metricKey(DROPPED_SERIES_COUNTER_NAME, {}), {
help: "Prometheus metric series dropped because the exporter series cap was reached.",
labels: {},
value: droppedSeries,
});
}
return {
counters: counterSnapshot,
gauges: new Map(gauges),
histograms: new Map(histograms),
};
};
const reset = () => {
counters.clear();
gauges.clear();
histograms.clear();
droppedSeries = 0;
};
return { counter, gauge, histogram, reset, snapshot };
}
function safeErrorMessage(err: unknown): string {
const message = err instanceof Error ? (err.message ?? err.name) : String(err);
return redactSensitiveText(message)
.replaceAll("\u0000", " ")
.replace(/[\r\n\t\u2028\u2029]/gu, " ")
.slice(0, 500);
}
function renderPrometheusMetrics(store: PrometheusMetricStore): string {
const snapshot = store.snapshot();
const lines: string[] = [];
const emitted = new Set<string>();
const emitHeader = (name: string, type: "counter" | "gauge" | "histogram", help: string) => {
if (emitted.has(name)) {
return;
}
emitted.add(name);
lines.push(`# HELP ${name} ${escapeHelp(help)}`);
lines.push(`# TYPE ${name} ${type}`);
};
const counterEntries = [...snapshot.counters.entries()].toSorted(([left], [right]) =>
left.localeCompare(right),
);
for (const [key, sample] of counterEntries) {
const name = key.split("|", 1)[0] ?? "";
emitHeader(name, "counter", sample.help);
lines.push(`${name}${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.value)}`);
}
const gaugeEntries = [...snapshot.gauges.entries()].toSorted(([left], [right]) =>
left.localeCompare(right),
);
for (const [key, sample] of gaugeEntries) {
const name = key.split("|", 1)[0] ?? "";
emitHeader(name, "gauge", sample.help);
lines.push(`${name}${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.value)}`);
}
const histogramEntries = [...snapshot.histograms.entries()].toSorted(([left], [right]) =>
left.localeCompare(right),
);
for (const [key, sample] of histogramEntries) {
const name = key.split("|", 1)[0] ?? "";
emitHeader(name, "histogram", sample.help);
for (let index = 0; index < sample.buckets.length; index += 1) {
const bucket = sample.buckets[index];
if (bucket === undefined) {
continue;
}
lines.push(
`${name}_bucket${formatLabels({ ...sample.labels, le: String(bucket) })} ${formatPrometheusNumber(sample.counts[index] ?? 0)}`,
);
}
lines.push(
`${name}_bucket${formatLabels({ ...sample.labels, le: "+Inf" })} ${formatPrometheusNumber(sample.count)}`,
);
lines.push(`${name}_sum${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.sum)}`);
lines.push(
`${name}_count${formatLabels(sample.labels)} ${formatPrometheusNumber(sample.count)}`,
);
}
lines.push("");
return lines.join("\n");
}
function runLabels(evt: {
channel?: string;
model?: string;
outcome?: string;
provider?: string;
trigger?: string;
}): LabelSet {
return {
channel: lowCardinalityLabel(evt.channel),
model: lowCardinalityLabel(evt.model),
outcome: lowCardinalityLabel(evt.outcome, "unknown"),
provider: lowCardinalityLabel(evt.provider),
trigger: lowCardinalityLabel(evt.trigger),
};
}
function modelCallLabels(evt: {
api?: string;
errorCategory?: string;
model?: string;
provider?: string;
transport?: string;
type: string;
}): LabelSet {
return {
api: lowCardinalityLabel(evt.api),
error_category:
evt.type === "model.call.error" ? lowCardinalityLabel(evt.errorCategory, "other") : "none",
model: lowCardinalityLabel(evt.model),
outcome: evt.type === "model.call.error" ? "error" : "completed",
provider: lowCardinalityLabel(evt.provider),
transport: lowCardinalityLabel(evt.transport),
};
}
function toolExecutionLabels(evt: {
errorCategory?: string;
paramsSummary?: { kind: string };
toolName: string;
type: string;
}): LabelSet {
return {
error_category:
evt.type === "tool.execution.error"
? lowCardinalityLabel(evt.errorCategory, "other")
: "none",
outcome: evt.type === "tool.execution.error" ? "error" : "completed",
params_kind: lowCardinalityLabel(evt.paramsSummary?.kind),
tool: lowCardinalityLabel(evt.toolName, "tool"),
};
}
function harnessLabels(evt: {
channel?: string;
errorCategory?: string;
harnessId: string;
model?: string;
outcome?: string;
phase?: string;
pluginId?: string;
provider?: string;
type: string;
}): LabelSet {
return {
channel: lowCardinalityLabel(evt.channel),
error_category:
evt.type === "harness.run.error" ? lowCardinalityLabel(evt.errorCategory, "other") : "none",
harness: lowCardinalityLabel(evt.harnessId),
model: lowCardinalityLabel(evt.model),
outcome: evt.type === "harness.run.error" ? "error" : lowCardinalityLabel(evt.outcome),
phase: evt.type === "harness.run.error" ? lowCardinalityLabel(evt.phase) : "none",
plugin: lowCardinalityLabel(evt.pluginId),
provider: lowCardinalityLabel(evt.provider),
};
}
function recordModelUsage(
store: PrometheusMetricStore,
evt: Extract<DiagnosticEventPayload, { type: "model.usage" }>,
) {
const labels = {
agent: lowCardinalityLabel(evt.agentId),
channel: lowCardinalityLabel(evt.channel),
model: lowCardinalityLabel(evt.model),
provider: lowCardinalityLabel(evt.provider),
};
const usage = evt.usage;
const recordTokens = (tokenType: string, value: number | undefined) => {
const amount = numericValue(value);
if (amount === undefined || amount === 0) {
return;
}
store.counter(
"openclaw_model_tokens_total",
"Model tokens reported by diagnostic usage events.",
{
...labels,
token_type: tokenType,
},
amount,
);
if (tokenType === "input" || tokenType === "output") {
store.histogram(
"openclaw_gen_ai_client_token_usage",
"GenAI token usage distribution for input and output tokens.",
{
model: labels.model,
provider: labels.provider,
token_type: tokenType,
},
amount,
TOKEN_BUCKETS,
);
}
};
recordTokens("input", usage.input);
recordTokens("output", usage.output);
recordTokens("cache_read", usage.cacheRead);
recordTokens("cache_write", usage.cacheWrite);
recordTokens("prompt", usage.promptTokens);
recordTokens("total", usage.total);
store.counter(
"openclaw_model_cost_usd_total",
"Estimated model cost in USD reported by diagnostic usage events.",
labels,
numericValue(evt.costUsd) ?? 0,
);
store.histogram(
"openclaw_model_usage_duration_seconds",
"Model usage event duration in seconds.",
labels,
seconds(evt.durationMs),
);
}
function recordDiagnosticEvent(
store: PrometheusMetricStore,
evt: DiagnosticEventPayload,
metadata: DiagnosticEventMetadata,
): void {
if (!metadata.trusted) {
return;
}
switch (evt.type) {
case "model.usage":
recordModelUsage(store, evt);
return;
case "run.completed":
store.histogram(
"openclaw_run_duration_seconds",
"Agent run duration in seconds.",
runLabels(evt),
seconds(evt.durationMs),
);
store.counter(
"openclaw_run_completed_total",
"Agent runs completed by outcome.",
runLabels(evt),
);
return;
case "model.call.completed":
case "model.call.error":
store.histogram(
"openclaw_model_call_duration_seconds",
"Provider model call duration in seconds.",
modelCallLabels(evt),
seconds(evt.durationMs),
);
store.counter(
"openclaw_model_call_total",
"Provider model calls completed by outcome.",
modelCallLabels(evt),
);
return;
case "tool.execution.completed":
case "tool.execution.error":
store.histogram(
"openclaw_tool_execution_duration_seconds",
"Tool execution duration in seconds.",
toolExecutionLabels(evt),
seconds(evt.durationMs),
);
store.counter(
"openclaw_tool_execution_total",
"Tool executions completed by outcome.",
toolExecutionLabels(evt),
);
return;
case "harness.run.completed":
case "harness.run.error":
store.histogram(
"openclaw_harness_run_duration_seconds",
"Agent harness run duration in seconds.",
harnessLabels(evt),
seconds(evt.durationMs),
);
store.counter(
"openclaw_harness_run_total",
"Agent harness runs completed by outcome.",
harnessLabels(evt),
);
return;
case "message.processed":
store.counter("openclaw_message_processed_total", "Inbound messages processed by outcome.", {
channel: lowCardinalityLabel(evt.channel),
outcome: evt.outcome,
reason: lowCardinalityLabel(evt.reason, "none"),
});
store.histogram(
"openclaw_message_processed_duration_seconds",
"Inbound message processing duration in seconds.",
{
channel: lowCardinalityLabel(evt.channel),
outcome: evt.outcome,
reason: lowCardinalityLabel(evt.reason, "none"),
},
seconds(evt.durationMs),
);
return;
case "message.delivery.completed":
case "message.delivery.error":
store.counter(
"openclaw_message_delivery_total",
"Outbound message delivery attempts by outcome.",
{
channel: lowCardinalityLabel(evt.channel),
delivery_kind: evt.deliveryKind,
error_category:
evt.type === "message.delivery.error"
? lowCardinalityLabel(evt.errorCategory, "other")
: "none",
outcome: evt.type === "message.delivery.error" ? "error" : "completed",
},
);
store.histogram(
"openclaw_message_delivery_duration_seconds",
"Outbound message delivery duration in seconds.",
{
channel: lowCardinalityLabel(evt.channel),
delivery_kind: evt.deliveryKind,
error_category:
evt.type === "message.delivery.error"
? lowCardinalityLabel(evt.errorCategory, "other")
: "none",
outcome: evt.type === "message.delivery.error" ? "error" : "completed",
},
seconds(evt.durationMs),
);
return;
case "queue.lane.enqueue":
case "queue.lane.dequeue":
store.gauge(
"openclaw_queue_lane_size",
"Current diagnostic queue lane size.",
{
lane: lowCardinalityLabel(evt.lane),
},
numericValue(evt.queueSize),
);
if (evt.type === "queue.lane.dequeue") {
store.histogram(
"openclaw_queue_lane_wait_seconds",
"Queue lane wait time in seconds.",
{ lane: lowCardinalityLabel(evt.lane) },
seconds(evt.waitMs),
);
}
return;
case "session.state":
store.counter("openclaw_session_state_total", "Session state observations.", {
reason: lowCardinalityLabel(evt.reason, "none"),
state: evt.state,
});
if (evt.queueDepth !== undefined) {
store.gauge(
"openclaw_session_queue_depth",
"Latest observed session queue depth.",
{
state: evt.state,
},
numericValue(evt.queueDepth),
);
}
return;
case "diagnostic.memory.sample":
store.gauge(
"openclaw_memory_bytes",
"Latest process memory usage by memory kind.",
{ kind: "rss" },
evt.memory.rssBytes,
);
store.gauge(
"openclaw_memory_bytes",
"Latest process memory usage by memory kind.",
{ kind: "heap_total" },
evt.memory.heapTotalBytes,
);
store.gauge(
"openclaw_memory_bytes",
"Latest process memory usage by memory kind.",
{ kind: "heap_used" },
evt.memory.heapUsedBytes,
);
store.histogram(
"openclaw_memory_rss_bytes",
"RSS memory sample distribution in bytes.",
{},
numericValue(evt.memory.rssBytes),
BYTE_BUCKETS,
);
return;
case "diagnostic.memory.pressure":
store.counter(
"openclaw_memory_pressure_total",
"Memory pressure events by level and reason.",
{
level: evt.level,
reason: evt.reason,
},
);
return;
case "telemetry.exporter":
store.counter("openclaw_telemetry_exporter_total", "Telemetry exporter lifecycle events.", {
exporter: lowCardinalityLabel(evt.exporter),
reason: lowCardinalityLabel(evt.reason, "none"),
signal: evt.signal,
status: evt.status,
});
return;
default:
return;
}
}
function createMetricsHandler(store: PrometheusMetricStore): OpenClawPluginHttpRouteHandler {
return (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== "GET" && req.method !== "HEAD") {
res.statusCode = 405;
res.setHeader("Allow", "GET, HEAD");
res.end("Method Not Allowed");
return true;
}
const body = renderPrometheusMetrics(store);
res.statusCode = 200;
res.setHeader("Cache-Control", "no-store");
res.setHeader("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
if (req.method === "HEAD") {
res.end();
return true;
}
res.end(body);
return true;
};
}
export function createDiagnosticsPrometheusExporter() {
const store = createPrometheusMetricStore();
let unsubscribe: (() => void) | undefined;
const service = {
id: "diagnostics-prometheus",
start(ctx) {
const subscribe = ctx.internalDiagnostics?.onEvent;
if (!subscribe) {
ctx.logger.error("diagnostics-prometheus: internal diagnostics capability unavailable");
return;
}
unsubscribe = subscribe((event, metadata) => {
try {
recordDiagnosticEvent(store, event, metadata);
} catch (err) {
ctx.logger.error(
`diagnostics-prometheus: event handler failed (${event.type}): ${safeErrorMessage(err)}`,
);
}
});
ctx.internalDiagnostics?.emit({
type: "telemetry.exporter",
exporter: "diagnostics-prometheus",
signal: "metrics",
status: "started",
reason: "configured",
});
},
stop() {
unsubscribe?.();
unsubscribe = undefined;
store.reset();
},
} satisfies OpenClawPluginService;
return {
handler: createMetricsHandler(store),
render: () => renderPrometheusMetrics(store),
service,
};
}
export const __test__ = {
createPrometheusMetricStore,
recordDiagnosticEvent,
renderPrometheusMetrics,
};

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}

View File

@@ -40,6 +40,10 @@
"blurb": "very well supported right now.",
"systemImage": "bubble.left.and.bubble.right",
"markdownCapable": true,
"commands": {
"nativeCommandsAutoEnabled": true,
"nativeSkillsAutoEnabled": true
},
"configuredState": {
"specifier": "./configured-state",
"exportName": "hasDiscordConfiguredState"

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