mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-19 12:52:06 +08:00
Compare commits
193 Commits
fix/codeql
...
codex-prov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f267de142 | ||
|
|
585784643e | ||
|
|
b979f2964c | ||
|
|
e633f43c53 | ||
|
|
4bfa7d17a3 | ||
|
|
d7da3d470e | ||
|
|
40e5d9adc7 | ||
|
|
1b99f8aedb | ||
|
|
eb769ee4ec | ||
|
|
7c6c0a8d54 | ||
|
|
1ed8c41f33 | ||
|
|
6cc74595e3 | ||
|
|
1377baee1a | ||
|
|
ce04866019 | ||
|
|
57c1c7d886 | ||
|
|
48d83b7566 | ||
|
|
5a89330c33 | ||
|
|
e67093f333 | ||
|
|
d613c8e29b | ||
|
|
2784710f4d | ||
|
|
ee2ab9a644 | ||
|
|
54f4c45e5d | ||
|
|
6ff7a30b9f | ||
|
|
cd89adf0ac | ||
|
|
e54f5c4068 | ||
|
|
50c427efc8 | ||
|
|
62a5963d24 | ||
|
|
194818960c | ||
|
|
fd35ba2cad | ||
|
|
db0864ad41 | ||
|
|
d5eae0d959 | ||
|
|
bf2c992a86 | ||
|
|
e69c2853b2 | ||
|
|
e4e69c5bc6 | ||
|
|
2b29594611 | ||
|
|
d54d2d6b9b | ||
|
|
78c7292c95 | ||
|
|
c5c40b22af | ||
|
|
036b422fc6 | ||
|
|
cbf9c60f1d | ||
|
|
be8a3617d9 | ||
|
|
142577d9b2 | ||
|
|
eca9f46824 | ||
|
|
33b6962273 | ||
|
|
257e767e5b | ||
|
|
639cd50261 | ||
|
|
a57d681db9 | ||
|
|
6e3eeb526f | ||
|
|
503a3aa125 | ||
|
|
9f4b155c47 | ||
|
|
0e58654dba | ||
|
|
d531760898 | ||
|
|
af8648e00e | ||
|
|
58a31b12f7 | ||
|
|
f0ea901a0d | ||
|
|
5d3168c343 | ||
|
|
d1502c2ba1 | ||
|
|
eb5bb67e04 | ||
|
|
113794f277 | ||
|
|
96988914ff | ||
|
|
dfaa9ee87e | ||
|
|
4cc2ffce09 | ||
|
|
ef7ad8229a | ||
|
|
cbcc1227d3 | ||
|
|
e74c079b22 | ||
|
|
afe1abc297 | ||
|
|
a7382ec563 | ||
|
|
724e92505a | ||
|
|
15ea0e1f83 | ||
|
|
f9146cabfc | ||
|
|
edc3504c77 | ||
|
|
8c35e45c00 | ||
|
|
fbd6b3ce3c | ||
|
|
71b79f49ad | ||
|
|
73e2151107 | ||
|
|
ad5c00b8e0 | ||
|
|
d1a5ea2024 | ||
|
|
4cba24a4c3 | ||
|
|
1a8f765147 | ||
|
|
b7340ec6a9 | ||
|
|
3ea20d1413 | ||
|
|
9c8245b178 | ||
|
|
27aedcfd56 | ||
|
|
6a67f65568 | ||
|
|
46b9044c3f | ||
|
|
9b93b7df62 | ||
|
|
427e485f76 | ||
|
|
6893e8f5f4 | ||
|
|
5f2273e81e | ||
|
|
dc9ce2a1bf | ||
|
|
1252da325f | ||
|
|
ae45eebef1 | ||
|
|
b8aef04ccd | ||
|
|
4428661779 | ||
|
|
f1eef47839 | ||
|
|
c953e98c59 | ||
|
|
89f368e2f9 | ||
|
|
e827778129 | ||
|
|
911172e1e6 | ||
|
|
f1e28370c4 | ||
|
|
96ac51d23d | ||
|
|
ac0fa474f8 | ||
|
|
008e4ca81f | ||
|
|
bcc9fc4cf5 | ||
|
|
cc2044633c | ||
|
|
f801fe7d27 | ||
|
|
9975de89d1 | ||
|
|
f7c837b374 | ||
|
|
0594fa3c4d | ||
|
|
80219ed1b3 | ||
|
|
86328585fa | ||
|
|
f9c8a5107c | ||
|
|
8559a84e4e | ||
|
|
12e4841d96 | ||
|
|
0ba28c0911 | ||
|
|
3eff589ac0 | ||
|
|
dfd5940c34 | ||
|
|
b277eac656 | ||
|
|
9ed11d6c49 | ||
|
|
44da034516 | ||
|
|
d251932fcf | ||
|
|
948c32dd33 | ||
|
|
acd3d2b197 | ||
|
|
76dc66f5fa | ||
|
|
ad27e0069d | ||
|
|
911fcb47f1 | ||
|
|
c9e7bfd1fc | ||
|
|
29741f696a | ||
|
|
38e61e0046 | ||
|
|
540c70d166 | ||
|
|
42f87c07e9 | ||
|
|
26a647d4bb | ||
|
|
0f27f2b351 | ||
|
|
469bd5f51e | ||
|
|
4a195b37d5 | ||
|
|
8749f1deb4 | ||
|
|
35171f4e47 | ||
|
|
82a529aaaf | ||
|
|
9e4a0e7f3c | ||
|
|
e40094a9ef | ||
|
|
4edf22f63f | ||
|
|
ed1ac2fc44 | ||
|
|
0ca9c4dcb0 | ||
|
|
e74f2e1501 | ||
|
|
2d68fda31f | ||
|
|
34bd66d929 | ||
|
|
2e7635f4f9 | ||
|
|
6d4f65c9d4 | ||
|
|
6336ed4166 | ||
|
|
b58223510c | ||
|
|
844d2bd515 | ||
|
|
21082d2ede | ||
|
|
96d90091c4 | ||
|
|
2c8c79de5c | ||
|
|
f4e6322649 | ||
|
|
924e132d96 | ||
|
|
7b943667a0 | ||
|
|
ee8f41f56e | ||
|
|
7fef13abbc | ||
|
|
b3ac316e0b | ||
|
|
862b39976d | ||
|
|
48ba3a4198 | ||
|
|
f5f4477bae | ||
|
|
28e4cd81a9 | ||
|
|
64630e1c39 | ||
|
|
8abbae0101 | ||
|
|
bb389a37d0 | ||
|
|
a91baa16de | ||
|
|
969a3757b9 | ||
|
|
cf834e2a21 | ||
|
|
2261918c8c | ||
|
|
6df120fb39 | ||
|
|
d0d93d0fde | ||
|
|
8748ae3bb7 | ||
|
|
18a638ceae | ||
|
|
a8b4be0b48 | ||
|
|
1c77515396 | ||
|
|
1b41513b3b | ||
|
|
015e39e3cf | ||
|
|
c3833f7729 | ||
|
|
ed5276f9b9 | ||
|
|
7a85c1a822 | ||
|
|
1231f21679 | ||
|
|
f5812aa64d | ||
|
|
0cf30b6a65 | ||
|
|
de5b173546 | ||
|
|
d955bf0ff8 | ||
|
|
1a193b2d96 | ||
|
|
f8a677bcfd | ||
|
|
0ddbae171d | ||
|
|
c149de7750 | ||
|
|
07877d71cd | ||
|
|
97ae1c7c2e |
@@ -1,18 +0,0 @@
|
||||
name: openclaw-codeql-javascript-typescript-extensions
|
||||
|
||||
paths:
|
||||
- extensions
|
||||
|
||||
paths-ignore:
|
||||
- apps
|
||||
- dist
|
||||
- docs
|
||||
- "**/node_modules"
|
||||
- "**/coverage"
|
||||
- "**/*.generated.ts"
|
||||
- "**/*.bundle.js"
|
||||
- "**/*-runtime.js"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/*.e2e.test.ts"
|
||||
- "**/*.e2e.test.tsx"
|
||||
@@ -1,7 +1,8 @@
|
||||
name: openclaw-codeql-javascript-typescript-core
|
||||
name: openclaw-codeql-javascript-typescript
|
||||
|
||||
paths:
|
||||
- src
|
||||
- extensions
|
||||
- ui/src
|
||||
- skills
|
||||
|
||||
37
.github/workflows/codeql.yml
vendored
37
.github/workflows/codeql.yml
vendored
@@ -19,14 +19,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.job_name }})
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- job_name: javascript-typescript-core
|
||||
language: javascript-typescript
|
||||
- language: javascript-typescript
|
||||
runs_on: blacksmith-32vcpu-ubuntu-2404
|
||||
needs_node: true
|
||||
needs_python: false
|
||||
@@ -34,21 +33,8 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: javascript-typescript-core
|
||||
config_file: ./.github/codeql/codeql-javascript-typescript-core.yml
|
||||
- job_name: javascript-typescript-extensions
|
||||
language: javascript-typescript
|
||||
runs_on: blacksmith-32vcpu-ubuntu-2404
|
||||
needs_node: true
|
||||
needs_python: false
|
||||
needs_java: false
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: javascript-typescript-extensions
|
||||
config_file: ./.github/codeql/codeql-javascript-typescript-extensions.yml
|
||||
- job_name: actions
|
||||
language: actions
|
||||
config_file: ./.github/codeql/codeql-javascript-typescript.yml
|
||||
- language: actions
|
||||
runs_on: blacksmith-16vcpu-ubuntu-2404
|
||||
needs_node: false
|
||||
needs_python: false
|
||||
@@ -56,10 +42,8 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: actions
|
||||
config_file: ""
|
||||
- job_name: python
|
||||
language: python
|
||||
- language: python
|
||||
runs_on: blacksmith-16vcpu-ubuntu-2404
|
||||
needs_node: false
|
||||
needs_python: true
|
||||
@@ -67,10 +51,8 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: python
|
||||
config_file: ""
|
||||
- job_name: java-kotlin
|
||||
language: java-kotlin
|
||||
- language: java-kotlin
|
||||
runs_on: blacksmith-16vcpu-ubuntu-2404
|
||||
needs_node: false
|
||||
needs_python: false
|
||||
@@ -78,10 +60,8 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: true
|
||||
needs_autobuild: false
|
||||
analyze_category: java-kotlin
|
||||
config_file: ""
|
||||
- job_name: swift
|
||||
language: swift
|
||||
- language: swift
|
||||
runs_on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
|
||||
needs_node: false
|
||||
needs_python: false
|
||||
@@ -89,7 +69,6 @@ jobs:
|
||||
needs_swift_tools: true
|
||||
needs_manual_build: true
|
||||
needs_autobuild: false
|
||||
analyze_category: swift
|
||||
config_file: ""
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -156,4 +135,4 @@ jobs:
|
||||
- name: Analyze
|
||||
uses: github/codeql-action/analyze@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
|
||||
with:
|
||||
category: "/language:${{ matrix.analyze_category }}"
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
3
.github/workflows/docs-agent.yml
vendored
3
.github/workflows/docs-agent.yml
vendored
@@ -197,7 +197,8 @@ jobs:
|
||||
|
||||
- name: Restore Node 24 path
|
||||
if: steps.gate.outputs.run_agent == 'true'
|
||||
run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
|
||||
run:
|
||||
| # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
|
||||
set -euo pipefail
|
||||
export PATH="${NODE_BIN}:${PATH}"
|
||||
echo "${NODE_BIN}" >> "$GITHUB_PATH"
|
||||
|
||||
16
.github/workflows/install-smoke.yml
vendored
16
.github/workflows/install-smoke.yml
vendored
@@ -114,7 +114,21 @@ jobs:
|
||||
|
||||
- name: Run root Dockerfile CLI smoke
|
||||
run: |
|
||||
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
|
||||
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
|
||||
which openclaw &&
|
||||
openclaw --version &&
|
||||
node -e "
|
||||
const fs = require(\"node:fs\");
|
||||
const path = require(\"node:path\");
|
||||
const pkg = require(\"/app/package.json\");
|
||||
for (const [dep, rel] of Object.entries(pkg.pnpm?.patchedDependencies ?? {})) {
|
||||
const absolute = path.join(\"/app\", rel);
|
||||
if (!fs.existsSync(absolute)) {
|
||||
throw new Error(`missing patch for ${dep}: ${rel}`);
|
||||
}
|
||||
}
|
||||
"
|
||||
'
|
||||
|
||||
- name: Run agents delete shared workspace Docker CLI smoke
|
||||
env:
|
||||
|
||||
3
.github/workflows/test-performance-agent.yml
vendored
3
.github/workflows/test-performance-agent.yml
vendored
@@ -181,7 +181,8 @@ jobs:
|
||||
|
||||
- name: Restore Node 24 path
|
||||
if: steps.gate.outputs.run_agent == 'true' && steps.patch.outputs.has_changes == 'true'
|
||||
run: | # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
|
||||
run:
|
||||
| # zizmor: ignore[github-env] NODE_BIN is set by the trusted local setup-node-env action in this same job
|
||||
set -euo pipefail
|
||||
export PATH="${NODE_BIN}:${PATH}"
|
||||
echo "${NODE_BIN}" >> "$GITHUB_PATH"
|
||||
|
||||
11
AGENTS.md
11
AGENTS.md
@@ -86,6 +86,13 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
- 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`.
|
||||
- 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
|
||||
`git status`, `git diff --check`, and diff/stat sanity check; rerun targeted or
|
||||
full checks only if conflict resolution, upstream overlap, generated drift,
|
||||
dependency/config changes, or touched-file content changes make the prior
|
||||
result stale.
|
||||
- Landing on `main`: verify touched surface near landing. Default feasible bar: `pnpm check` + `pnpm test`.
|
||||
- Hard build gate: `pnpm build` before push if build output, packaging, lazy/module boundaries, or published surfaces can change.
|
||||
- Do not land related failing format/lint/type/build/tests. If unrelated on latest `origin/main`, say so with scoped proof.
|
||||
@@ -132,7 +139,9 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only. It formats staged files; still run gates.
|
||||
- Commits: conventional-ish, concise, grouped.
|
||||
- No manual stash/autostash unless explicit. No branch/worktree changes unless requested.
|
||||
- `main`: no merge commits; rebase on latest `origin/main` before push.
|
||||
- `main`: no merge commits; rebase on latest `origin/main` before push. Do not
|
||||
keep chasing `main` with repeated full gates after one green run plus a clean
|
||||
rebase sanity pass.
|
||||
- User says `commit`: your changes only. `commit all`: all changes in grouped chunks. `push`: may `git pull --rebase` first.
|
||||
- Do not delete/rename unexpected files; ask if blocking, else ignore.
|
||||
- Bulk PR close/reopen >5: ask with count/scope.
|
||||
|
||||
138
CHANGELOG.md
138
CHANGELOG.md
@@ -8,10 +8,13 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### 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.
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -29,6 +32,11 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/startup: normalize startup and provider plugin enablement through registry aliases so boot paths do not need the legacy manifest alias scan. Thanks @vincentkoc.
|
||||
- Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc.
|
||||
- Plugins/registry: keep installed plugin index records focused on install/state/load paths and resolve plugin capabilities from manifests scoped to indexed plugins. Thanks @shakkernerd.
|
||||
- Plugins/registry: route cold manifest and capability lookups through the installed plugin index so setup, channels, config, secrets, doctor, and provider metadata paths avoid broad plugin-root scans before runtime execution. Thanks @shakkernerd.
|
||||
- CLI/models: speed up `models list --all --provider <id>` for static manifest-backed providers by loading catalog rows through the installed plugin index instead of broad manifest scans or runtime suppression hooks. Thanks @shakkernerd.
|
||||
- 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.
|
||||
- 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/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.
|
||||
@@ -38,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Diagnostics/OTEL: export existing tool-loop diagnostics as `openclaw.tool.loop` counters and spans without loop messages, session identifiers, params, or tool output. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: export diagnostic memory samples and pressure as bounded memory histograms, counters, and pressure spans to help spot leak regressions without session or payload data. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add the GenAI `gen_ai.client.token.usage` histogram for input/output model usage while keeping session identifiers and aggregate cache counters out of the semantic metric. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add a bounded `openclaw.agent` label to OpenClaw token metrics so per-agent Grafana dashboards can group usage without exporting session identifiers. Thanks @oc-factus.
|
||||
- Plugins/install: consolidate managed plugin install metadata into the state-managed plugin index at `plugins/installs.json`, replacing the temporary `plugins/installed-index.json` path and removing `plugins.installs` as an authored config surface. Thanks @vincentkoc and @shakkernerd.
|
||||
- Diagnostics/OTEL: add the GenAI `gen_ai.client.operation.duration` histogram for model-call latency in seconds with bounded provider/model/API and error attributes. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: add GenAI usage token attributes to model-usage spans, including cache read/write input token counts without session identifiers or prompt/response content. Thanks @vincentkoc.
|
||||
@@ -60,32 +69,137 @@ Docs: https://docs.openclaw.ai
|
||||
- Codex harness: require Codex app-server `0.125.0` or newer and cover native MCP `PreToolUse`, `PostToolUse`, and `PermissionRequest` payloads through the OpenClaw hook relay.
|
||||
- Agents/Codex: teach prompts and `agents_list` to surface native Codex app-server availability so agents prefer `/codex ...` over Codex ACP unless ACP/acpx is explicit. Thanks @vincentkoc.
|
||||
- ACPX/Droid: add Factory Droid to the live ACP bind Docker matrix, including `.factory` settings staging, `FACTORY_API_KEY` forwarding, and the single-agent `test:docker:live-acp-bind:droid` recipe.
|
||||
- TTS/personas: add provider-aware TTS personas with deterministic provider binding merges, `/tts persona` controls, gateway/CLI persona state, Google Gemini `audio-profile-v1` prompt wrapping, and OpenAI instruction mapping. (#70748) Thanks @barronlroth.
|
||||
- Voice Wake: add trigger-based routing so macOS voice wake phrases can select a configured agent or session target, with Gateway routing APIs and node update events. (#30354) Thanks @longbiaochen.
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg.
|
||||
- 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.
|
||||
- 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.
|
||||
- Security/audit: read channel exposure and plugin allowlist ownership from read-only plugin index metadata so cold audits do not depend on loaded channel runtime. Thanks @shakkernerd.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Gateway/Linux: include fnm `aliases/default/bin` in generated service PATHs and let doctor accept either modern fnm aliases or the legacy `current/bin` symlink, avoiding false PATH repair prompts. Fixes #68169. Thanks @richard-scott.
|
||||
- Installer/Linux: run apt installs with noninteractive dpkg and needrestart settings so fresh Ubuntu 24.04 `curl | bash` installs do not hang while installing Node.js, Git, or build tools. Fixes #41146. Thanks @iht76, @alexcarv318, @cs3gallery, @firofame, and @cgdusek.
|
||||
- Providers/Bedrock: defer the AWS SDK import until Bedrock discovery actually runs so plugin registration and setup stay lightweight on cold start. Fixes #71690. Thanks @jarvis-ai-gregmoser.
|
||||
- Installer/macOS: stop immediately when Homebrew `node@24` installation fails and avoid printing PATH advice for missing Homebrew Node installs. Fixes #70411. Thanks @1fanwang.
|
||||
- WhatsApp: remove ack reactions after a visible reply when `messages.removeAckAfterReply` is enabled, matching other reaction-capable channels. Fixes #26183. Thanks @MrUnforsaken.
|
||||
- Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo.
|
||||
- Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- ACP: keep `/acp` management commands, plus local `/status` and `/unfocus`, on the Gateway path inside ACP-bound threads so they are not consumed as ACP prompt text. Fixes #66298. Thanks @kindomLee.
|
||||
- ACPX: stop probing ACP agents during normal Gateway startup; the embedded backend now registers without spawning Codex/ACP child processes unless `OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=1` is explicitly set.
|
||||
- CLI/image edit: accept `--size`, `--aspect-ratio`, and `--resolution` on `openclaw infer image edit` and report all supported edit flags from `capability inspect image.edit`. Thanks @Pinghuachiu.
|
||||
- ACP: wait for the configured runtime backend to become healthy before startup identity reconciliation, avoiding transient acpx warnings during Gateway boot. Fixes #40566.
|
||||
- Channels/ACP bindings: time out configured binding readiness checks instead of letting Discord preflight hang forever when an ACP target never settles. Fixes #68776.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- macOS/remote SSH: keep discovered gateway hosts in `gateway.remote.sshTarget` while pinning SSH transport URLs to the local loopback tunnel, so browser automation does not regress into blocked non-loopback `ws://` endpoints. Fixes #67336.
|
||||
- Gateway/proxy: bootstrap env proxy dispatching from direct Gateway startup so provider and plugin network requests honor `HTTPS_PROXY`/`HTTP_PROXY` before the first embedded agent attempt runs. (#71833) Thanks @mjamiv.
|
||||
- Plugins/runtime deps: verify clean npm installs actually place requested bundled runtime packages in the managed install root, reporting exact missing specs instead of a false successful repair. (#71883) Thanks @Solvely-Colin.
|
||||
- Plugins/discovery: ignore stale `plugins.load.paths` aliases that point back at packaged bundled plugin directories and have doctor remove them, keeping bundled plugins on the runtime-deps staging path. Thanks @codex.
|
||||
- Models/LM Studio: preserve `@iq*` quant suffixes in model refs and provider matching so `/model lmstudio/...@iq3_xxs` keeps the exact LM Studio variant. Fixes #71474. (#71486) Thanks @Bartok9, @XinwuC, and @Sanjays2402.
|
||||
- Matrix/cron: preserve the live Matrix delivery target when creating implicit announce reminder jobs so mixed-case room IDs are not reconstructed from lowercased session keys. Fixes #71798.
|
||||
- Feishu: accept Schema 2.0 card action callbacks that report `context.open_chat_id` instead of legacy `context.chat_id`, so button callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.
|
||||
@@ -94,11 +208,17 @@ Docs: https://docs.openclaw.ai
|
||||
- Browser automation: keep stable tab ids and labels attached when Chromium replaces the raw target after form submissions or other action-triggered navigations, and return the replacement `targetId` from `/act` when the match is provable. Fixes #46137.
|
||||
- QQ Bot: make `qqbot_remind` schedule, list, and remove Gateway cron jobs directly for owner-authorized senders instead of returning `cronParams` and relying on a follow-up generic `cron` tool call. Fixes #70865. (#70937) Thanks @GaosCode.
|
||||
- Agents/ACP: hide `sessions_spawn` ACP runtime options unless an ACP backend is loaded, and make `/acp doctor` call out `plugins.allow` blocking bundled `acpx`. Thanks @vincentkoc.
|
||||
- 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/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.
|
||||
- Plugins/install: reject native plugin archives that do not include a valid `openclaw.plugin.json`, preventing manifestless archives from writing install records that later show missing-manifest diagnostics. Thanks @shakkernerd.
|
||||
- Plugins/uninstall: remove tracked managed plugin install directories even when the persisted install path differs from the default id-derived target, while still refusing deletes outside the managed extensions root. Thanks @shakkernerd.
|
||||
- Plugins/update: restore previous plugin index records if core update or channel setup hits a concurrent config write conflict after plugin metadata changes. Thanks @shakkernerd.
|
||||
- Plugins/onboarding: defer channel/provider plugin install records until the owning config write commits, keeping setup failures from advancing the plugin index ahead of `openclaw.json`. Thanks @shakkernerd.
|
||||
- Plugins/config: route configure and agent setup writes with pending plugin install records through the plugin index commit helper so provider onboarding metadata is not stripped by plain config writes. Thanks @shakkernerd.
|
||||
@@ -109,9 +229,11 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- Image understanding: resolve configured image models such as local LM Studio vision entries before reporting `Unknown model` when the discovery registry has not registered that provider. Fixes #66486. Thanks @zhanggpcsu.
|
||||
- QQ Bot: ignore self-echoed bot messages using the outbound ref-index marker, preventing mirrored replies from re-entering the agent loop while still allowing users to quote bot replies. Fixes #71912. Thanks @wangyc6003.
|
||||
- Sessions: separate reset freshness from session-store `updatedAt`, so heartbeat, cron, exec, and gateway bookkeeping no longer prevent configured daily/idle resets from rolling long-running channel sessions. Fixes #68315, #63732, #63820, and #69083. Thanks @maxatv, @longhairedsi, @bradfreels, and @akessel56.
|
||||
- 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.
|
||||
@@ -150,6 +272,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Plugins/install: pass onboarding plugin config into plugin index writes so local plugin installs outside default discovery roots keep their install records. Thanks @shakkernerd.
|
||||
- Plugins/install: migrate shipped `plugins.installs` config records into the plugin index while stripping them from runtime config and future writes. Thanks @shakkernerd.
|
||||
- Plugins/install: durably remove shipped `plugins.installs` from `openclaw.json` after its records are copied into the plugin index, while rolling back the index write if config cleanup fails. Thanks @shakkernerd.
|
||||
- Plugins/install: keep migrated plugin install records in the plugin index even when the plugin manifest is missing or invalid, so update, uninstall, inspect, and audit can still recover broken installs. Thanks @shakkernerd.
|
||||
- Plugins/security: keep plugin audit JSON check ids stable while reporting plugin index install-record findings with updated wording. Thanks @shakkernerd.
|
||||
- CLI/config: reject direct `plugins.installs` edits with guidance to use `openclaw plugins install`, `openclaw plugins update`, or `openclaw plugins uninstall` instead. Thanks @shakkernerd.
|
||||
@@ -242,6 +365,13 @@ Docs: https://docs.openclaw.ai
|
||||
- Codex: consume unauthorized bound conversation inbound claims before they can fall through to other claim handlers or enqueue Codex turns. (#71702) Thanks @vincentkoc.
|
||||
- Codex media understanding: require approval-checked app-server image turns while explicitly declining tool, file, permission, and elicitation approval requests for the bounded image worker. (#71703) Thanks @vincentkoc.
|
||||
- Agents/Claude CLI: allow large live `stream-json` JSONL lines up to the existing per-turn raw limit, preventing large Telegram, WebChat, MCP, and image turns from aborting on the old stdout buffer cap. Fixes #71793, #71080, and #70766. (#71897) Thanks @chacher86, @shivamgrover21, and @tpjordan.
|
||||
- Agents/Claude CLI: unwrap nested Claude result envelopes in CLI JSON output so delegated agent responses surface as final text instead of raw result JSON. (#66819) Thanks @mraleko.
|
||||
- Agents/Claude CLI: apply the configured 1M context window override to eligible Claude CLI Opus and Sonnet models when `context1m` is enabled. (#70863) Thanks @bidadh.
|
||||
- Models/status: report fresh Claude CLI native auth instead of stale stored `anthropic:claude-cli` profile expiry when local credentials are current. Fixes #71256. (#71332) Thanks @matthiasjanke and @neeravmakwana.
|
||||
- CLI backends: compact OpenClaw transcripts after over-budget CLI turns and reseed fresh CLI sessions from the compacted transcript instead of stale external resume state. Fixes #68329. (#71916) Thanks @obviyus.
|
||||
- Telegram: keep default tool progress messages visible when answer preview streaming is disabled. (#71825) Thanks @VACInc.
|
||||
- Configure/models: clear deselected model fallbacks when updating the model picker allowlist, including provider-scoped setup flows. (#71596) Thanks @rubencu.
|
||||
- Agents/streaming: strip namespaced `<antml:thinking>` reasoning tags from streamed assistant replies before user-visible text is emitted. (#69288) Thanks @xialonglee.
|
||||
|
||||
## 2026.4.24
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ RUN chown node:node /app
|
||||
COPY --from=runtime-assets --chown=node:node /app/dist ./dist
|
||||
COPY --from=runtime-assets --chown=node:node /app/node_modules ./node_modules
|
||||
COPY --from=runtime-assets --chown=node:node /app/package.json .
|
||||
COPY --from=runtime-assets --chown=node:node /app/patches ./patches
|
||||
COPY --from=runtime-assets --chown=node:node /app/openclaw.mjs .
|
||||
COPY --from=runtime-assets --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
|
||||
COPY --from=runtime-assets --chown=node:node /app/skills ./skills
|
||||
|
||||
@@ -35,11 +35,18 @@ public struct WakeWordGateMatch: Sendable, Equatable {
|
||||
public let triggerEndTime: TimeInterval
|
||||
public let postGap: TimeInterval
|
||||
public let command: String
|
||||
public let trigger: String?
|
||||
|
||||
public init(triggerEndTime: TimeInterval, postGap: TimeInterval, command: String) {
|
||||
public init(
|
||||
triggerEndTime: TimeInterval,
|
||||
postGap: TimeInterval,
|
||||
command: String,
|
||||
trigger: String? = nil)
|
||||
{
|
||||
self.triggerEndTime = triggerEndTime
|
||||
self.postGap = postGap
|
||||
self.command = command
|
||||
self.trigger = trigger
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +60,17 @@ public enum WakeWordGate {
|
||||
}
|
||||
|
||||
private struct TriggerTokens {
|
||||
let source: String
|
||||
let tokens: [String]
|
||||
}
|
||||
|
||||
private struct MatchCandidate {
|
||||
let index: Int
|
||||
let endIndex: Int
|
||||
let tokenCount: Int
|
||||
let triggerEnd: TimeInterval
|
||||
let gap: TimeInterval
|
||||
let trigger: String
|
||||
}
|
||||
|
||||
public static func match(
|
||||
@@ -87,9 +98,19 @@ public enum WakeWordGate {
|
||||
let gap = nextToken.start - triggerEnd
|
||||
if gap < config.minPostTriggerGap { continue }
|
||||
|
||||
if let best, i <= best.index { continue }
|
||||
let endIndex = i + count - 1
|
||||
if let best {
|
||||
if endIndex < best.endIndex { continue }
|
||||
if endIndex == best.endIndex, count <= best.tokenCount { continue }
|
||||
}
|
||||
|
||||
best = MatchCandidate(index: i, triggerEnd: triggerEnd, gap: gap)
|
||||
best = MatchCandidate(
|
||||
index: i,
|
||||
endIndex: endIndex,
|
||||
tokenCount: count,
|
||||
triggerEnd: triggerEnd,
|
||||
gap: gap,
|
||||
trigger: trigger.source)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +118,11 @@ public enum WakeWordGate {
|
||||
let command = commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd)
|
||||
.trimmingCharacters(in: Self.whitespaceAndPunctuation)
|
||||
guard command.count >= config.minCommandLength else { return nil }
|
||||
return WakeWordGateMatch(triggerEndTime: best.triggerEnd, postGap: best.gap, command: command)
|
||||
return WakeWordGateMatch(
|
||||
triggerEndTime: best.triggerEnd,
|
||||
postGap: best.gap,
|
||||
command: command,
|
||||
trigger: best.trigger)
|
||||
}
|
||||
|
||||
public static func commandText(
|
||||
@@ -145,7 +170,7 @@ public enum WakeWordGate {
|
||||
.map { normalizeToken(String($0)) }
|
||||
.filter { !$0.isEmpty }
|
||||
if tokens.isEmpty { continue }
|
||||
output.append(TriggerTokens(tokens: tokens))
|
||||
output.append(TriggerTokens(source: tokens.joined(separator: " "), tokens: tokens))
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -47,6 +47,21 @@ import Testing
|
||||
#expect(match?.command == "do it")
|
||||
}
|
||||
|
||||
@Test func matchPrefersMostSpecificTriggerWhenOverlapping() {
|
||||
let transcript = "hey clawd do it"
|
||||
let segments = makeSegments(
|
||||
transcript: transcript,
|
||||
words: [
|
||||
("hey", 0.0, 0.1),
|
||||
("clawd", 0.2, 0.1),
|
||||
("do", 0.8, 0.1),
|
||||
("it", 1.0, 0.1),
|
||||
])
|
||||
let config = WakeWordGateConfig(triggers: ["clawd", "hey clawd"], minPostTriggerGap: 0.3)
|
||||
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
|
||||
#expect(match?.trigger == "hey clawd")
|
||||
}
|
||||
|
||||
@Test func commandTextHandlesForeignRangeIndices() {
|
||||
let transcript = "hey clawd do thing"
|
||||
let other = "do thing"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Observation
|
||||
import OpenClawKit
|
||||
import ServiceManagement
|
||||
import SwiftUI
|
||||
|
||||
@@ -366,7 +367,8 @@ final class AppState {
|
||||
if resolvedConnectionMode == .remote,
|
||||
configRemoteTransport != .direct,
|
||||
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
let host = AppState.remoteHost(from: configRemoteUrl)
|
||||
let host = AppState.remoteHost(from: configRemoteUrl),
|
||||
!LoopbackHost.isLoopbackHost(host)
|
||||
{
|
||||
self.remoteTarget = "\(NSUserName())@\(host)"
|
||||
} else {
|
||||
@@ -435,6 +437,30 @@ final class AppState {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
private static func sshTunnelGatewayUrl(existingUrl: String?, expectedRemoteHost: String?) -> String {
|
||||
let fallback = "ws://127.0.0.1:18789"
|
||||
let trimmed = existingUrl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !trimmed.isEmpty,
|
||||
let url = URL(string: trimmed),
|
||||
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!host.isEmpty
|
||||
else {
|
||||
return fallback
|
||||
}
|
||||
|
||||
let preservePort: Bool = if LoopbackHost.isLoopbackHost(host) {
|
||||
true
|
||||
} else if let expectedRemoteHost {
|
||||
OpenClawConfigFile.canonicalHostForComparison(host) ==
|
||||
OpenClawConfigFile.canonicalHostForComparison(expectedRemoteHost)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
guard preservePort else { return fallback }
|
||||
|
||||
return "ws://127.0.0.1:\(url.port ?? 18789)"
|
||||
}
|
||||
|
||||
private static func updateGatewayString(
|
||||
_ dictionary: inout [String: Any],
|
||||
key: String,
|
||||
@@ -491,17 +517,14 @@ final class AppState {
|
||||
case .ssh:
|
||||
changed = Self.updateGatewayString(&remote, key: "transport", value: nil) || changed
|
||||
|
||||
if let host = draft.remoteHost {
|
||||
let existingUrl = (remote["url"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let parsedExisting = existingUrl.isEmpty ? nil : URL(string: existingUrl)
|
||||
let scheme = parsedExisting?.scheme?.isEmpty == false ? parsedExisting?.scheme : "ws"
|
||||
let port = parsedExisting?.port ?? 18789
|
||||
let desiredUrl = "\(scheme ?? "ws")://\(host):\(port)"
|
||||
changed = Self.updateGatewayString(&remote, key: "url", value: desiredUrl) || changed
|
||||
}
|
||||
|
||||
let sanitizedTarget = Self.sanitizeSSHTarget(draft.remoteTarget)
|
||||
let expectedRemoteHost = CommandResolver.parseSSHTarget(sanitizedTarget)?.host ?? draft.remoteHost
|
||||
let existingUrl = (remote["url"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let desiredUrl = Self.sshTunnelGatewayUrl(
|
||||
existingUrl: existingUrl,
|
||||
expectedRemoteHost: expectedRemoteHost)
|
||||
changed = Self.updateGatewayString(&remote, key: "url", value: desiredUrl) || changed
|
||||
changed = Self.updateGatewayString(&remote, key: "sshTarget", value: sanitizedTarget) || changed
|
||||
changed = Self.updateGatewayString(&remote, key: "sshIdentity", value: draft.remoteIdentity) || changed
|
||||
}
|
||||
@@ -569,7 +592,8 @@ final class AppState {
|
||||
let targetMode = desiredMode ?? self.connectionMode
|
||||
if targetMode == .remote,
|
||||
remoteTransport != .direct,
|
||||
let host = AppState.remoteHost(from: remoteUrl)
|
||||
let host = AppState.remoteHost(from: remoteUrl),
|
||||
!LoopbackHost.isLoopbackHost(host)
|
||||
{
|
||||
self.updateRemoteTarget(host: host)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ struct GatewayAgentInvocation {
|
||||
var channel: GatewayAgentChannel = .last
|
||||
var timeoutSeconds: Int?
|
||||
var idempotencyKey: String = UUID().uuidString
|
||||
var voiceWakeTrigger: String?
|
||||
}
|
||||
|
||||
/// Single, shared Gateway websocket connection for the whole app.
|
||||
@@ -499,6 +500,10 @@ extension GatewayConnection {
|
||||
if let timeout = invocation.timeoutSeconds {
|
||||
params["timeout"] = AnyCodable(timeout)
|
||||
}
|
||||
if let trigger = invocation.voiceWakeTrigger {
|
||||
params["voiceWakeTrigger"] = AnyCodable(
|
||||
trigger.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
}
|
||||
|
||||
do {
|
||||
try await self.requestVoid(method: .agent, params: params)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import Foundation
|
||||
import OpenClawDiscovery
|
||||
import OpenClawKit
|
||||
|
||||
@MainActor
|
||||
enum GatewayDiscoverySelectionSupport {
|
||||
private static let defaultSshTunnelGatewayUrl = "ws://127.0.0.1:18789"
|
||||
|
||||
static func applyRemoteSelection(
|
||||
gateway: GatewayDiscoveryModel.DiscoveredGateway,
|
||||
state: AppState)
|
||||
@@ -13,18 +17,40 @@ enum GatewayDiscoverySelectionSupport {
|
||||
state.remoteTransport = preferredTransport
|
||||
}
|
||||
|
||||
state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
|
||||
if preferredTransport == .direct {
|
||||
state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
|
||||
} else {
|
||||
state.remoteUrl = self.sshTunnelGatewayUrl(current: state.remoteUrl)
|
||||
}
|
||||
state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
|
||||
|
||||
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
|
||||
OpenClawConfigFile.setRemoteGatewayUrl(
|
||||
host: endpoint.host,
|
||||
port: endpoint.port)
|
||||
if preferredTransport == .direct {
|
||||
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
|
||||
OpenClawConfigFile.setRemoteGatewayUrl(
|
||||
host: endpoint.host,
|
||||
port: endpoint.port)
|
||||
} else {
|
||||
OpenClawConfigFile.clearRemoteGatewayUrl()
|
||||
}
|
||||
} else {
|
||||
OpenClawConfigFile.clearRemoteGatewayUrl()
|
||||
OpenClawConfigFile.setRemoteGatewayUrlString(state.remoteUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private static func sshTunnelGatewayUrl(current: String) -> String {
|
||||
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty,
|
||||
let url = URL(string: trimmed),
|
||||
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!host.isEmpty,
|
||||
LoopbackHost.isLoopbackHost(host)
|
||||
else {
|
||||
return self.defaultSshTunnelGatewayUrl
|
||||
}
|
||||
|
||||
return "ws://127.0.0.1:\(url.port ?? 18789)"
|
||||
}
|
||||
|
||||
static func preferredTransport(
|
||||
for gateway: GatewayDiscoveryModel.DiscoveredGateway,
|
||||
current: AppState.RemoteTransport) -> AppState.RemoteTransport
|
||||
|
||||
@@ -135,6 +135,10 @@ struct OpenClawOSLogHandler: AppLogLevelBackedHandler {
|
||||
self.osLogger = os.Logger(subsystem: subsystem, category: category)
|
||||
}
|
||||
|
||||
func log(event: LogEvent) {
|
||||
self.writeLog(level: event.level, message: event.message, metadata: event.metadata)
|
||||
}
|
||||
|
||||
func log(
|
||||
level: Logger.Level,
|
||||
message: Logger.Message,
|
||||
@@ -143,6 +147,14 @@ struct OpenClawOSLogHandler: AppLogLevelBackedHandler {
|
||||
file: String,
|
||||
function: String,
|
||||
line: UInt)
|
||||
{
|
||||
self.writeLog(level: level, message: message, metadata: metadata)
|
||||
}
|
||||
|
||||
private func writeLog(
|
||||
level: Logger.Level,
|
||||
message: Logger.Message,
|
||||
metadata: Logger.Metadata?)
|
||||
{
|
||||
let merged = Self.mergeMetadata(self.metadata, metadata)
|
||||
let rendered = Self.renderMessage(message, metadata: merged)
|
||||
@@ -186,6 +198,17 @@ struct OpenClawFileLogHandler: AppLogLevelBackedHandler {
|
||||
let label: String
|
||||
var metadata: Logger.Metadata = [:]
|
||||
|
||||
func log(event: LogEvent) {
|
||||
self.writeLog(
|
||||
level: event.level,
|
||||
message: event.message,
|
||||
metadata: event.metadata,
|
||||
source: event.source,
|
||||
file: event.file,
|
||||
function: event.function,
|
||||
line: event.line)
|
||||
}
|
||||
|
||||
func log(
|
||||
level: Logger.Level,
|
||||
message: Logger.Message,
|
||||
@@ -194,6 +217,25 @@ struct OpenClawFileLogHandler: AppLogLevelBackedHandler {
|
||||
file: String,
|
||||
function: String,
|
||||
line: UInt)
|
||||
{
|
||||
self.writeLog(
|
||||
level: level,
|
||||
message: message,
|
||||
metadata: metadata,
|
||||
source: source,
|
||||
file: file,
|
||||
function: function,
|
||||
line: line)
|
||||
}
|
||||
|
||||
private func writeLog(
|
||||
level: Logger.Level,
|
||||
message: Logger.Message,
|
||||
metadata: Logger.Metadata?,
|
||||
source: String,
|
||||
file: String,
|
||||
function: String,
|
||||
line: UInt)
|
||||
{
|
||||
guard AppLogSettings.fileLoggingEnabled() else { return }
|
||||
let (subsystem, category) = OpenClawLogging.parseLabel(self.label)
|
||||
|
||||
@@ -54,8 +54,15 @@ actor MacNodeBrowserProxy {
|
||||
|
||||
func request(paramsJSON: String?) async throws -> String {
|
||||
let params = try Self.decodeRequestParams(from: paramsJSON)
|
||||
let request = try Self.makeRequest(params: params, endpoint: self.endpointProvider())
|
||||
let (data, response) = try await self.performRequest(request)
|
||||
let endpoint = self.endpointProvider()
|
||||
let request = try Self.makeRequest(params: params, endpoint: endpoint)
|
||||
let data: Data
|
||||
let response: URLResponse
|
||||
do {
|
||||
(data, response) = try await self.performRequest(request)
|
||||
} catch {
|
||||
throw Self.unavailableError(endpoint: endpoint, cause: error)
|
||||
}
|
||||
let http = try Self.requireHTTPResponse(response)
|
||||
guard (200..<300).contains(http.statusCode) else {
|
||||
throw NSError(domain: "MacNodeBrowserProxy", code: http.statusCode, userInfo: [
|
||||
@@ -165,6 +172,19 @@ actor MacNodeBrowserProxy {
|
||||
return http
|
||||
}
|
||||
|
||||
private static func unavailableError(endpoint: Endpoint, cause: Error) -> NSError {
|
||||
let url = endpoint.baseURL.absoluteString
|
||||
let message = """
|
||||
UNAVAILABLE: macOS app node could not reach the local browser control service at \(url). \
|
||||
In remote mode, browser control is owned by the CLI node-host; start `openclaw node start` \
|
||||
on this Mac and target that browser node. Underlying error: \(cause.localizedDescription)
|
||||
"""
|
||||
return NSError(domain: "MacNodeBrowserProxy", code: 9, userInfo: [
|
||||
NSLocalizedDescriptionKey: message,
|
||||
NSUnderlyingErrorKey: cause,
|
||||
])
|
||||
}
|
||||
|
||||
private static func httpErrorMessage(statusCode: Int, data: Data) -> String {
|
||||
if let object = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) as? [String: Any],
|
||||
let error = object["error"] as? String,
|
||||
|
||||
@@ -116,27 +116,40 @@ final class MacNodeModeCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private func currentCaps() -> [String] {
|
||||
nonisolated static func resolvedCaps(
|
||||
browserControlEnabled: Bool,
|
||||
cameraEnabled: Bool,
|
||||
locationMode: OpenClawLocationMode,
|
||||
connectionMode: AppState.ConnectionMode) -> [String]
|
||||
{
|
||||
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
|
||||
if OpenClawConfigFile.browserControlEnabled() {
|
||||
if browserControlEnabled, connectionMode == .local {
|
||||
caps.append(OpenClawCapability.browser.rawValue)
|
||||
}
|
||||
if UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false {
|
||||
if cameraEnabled {
|
||||
caps.append(OpenClawCapability.camera.rawValue)
|
||||
}
|
||||
let rawLocationMode = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
|
||||
if OpenClawLocationMode(rawValue: rawLocationMode) != .off {
|
||||
if locationMode != .off {
|
||||
caps.append(OpenClawCapability.location.rawValue)
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
private func currentCaps() -> [String] {
|
||||
let rawLocationMode = UserDefaults.standard.string(forKey: locationModeKey) ?? "off"
|
||||
return Self.resolvedCaps(
|
||||
browserControlEnabled: OpenClawConfigFile.browserControlEnabled(),
|
||||
cameraEnabled: UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false,
|
||||
locationMode: OpenClawLocationMode(rawValue: rawLocationMode) ?? .off,
|
||||
connectionMode: AppStateStore.shared.connectionMode)
|
||||
}
|
||||
|
||||
private func currentPermissions() async -> [String: Bool] {
|
||||
let statuses = await PermissionManager.status()
|
||||
return Dictionary(uniqueKeysWithValues: statuses.map { ($0.key.rawValue, $0.value) })
|
||||
}
|
||||
|
||||
private func currentCommands(caps: [String]) -> [String] {
|
||||
nonisolated static func resolvedCommands(caps: [String]) -> [String] {
|
||||
var commands: [String] = [
|
||||
OpenClawCanvasCommand.present.rawValue,
|
||||
OpenClawCanvasCommand.hide.rawValue,
|
||||
@@ -171,6 +184,10 @@ final class MacNodeModeCoordinator {
|
||||
return commands
|
||||
}
|
||||
|
||||
private func currentCommands(caps: [String]) -> [String] {
|
||||
Self.resolvedCommands(caps: caps)
|
||||
}
|
||||
|
||||
private func buildSessionBox(url: URL) -> WebSocketSessionBox? {
|
||||
guard url.scheme?.lowercased() == "wss" else { return nil }
|
||||
let host = url.host ?? "gateway"
|
||||
|
||||
@@ -192,20 +192,17 @@ enum OpenClawConfigFile {
|
||||
}
|
||||
|
||||
static func remoteGatewayPort(matchingHost sshHost: String) -> Int? {
|
||||
let trimmedSshHost = sshHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedSshHost.isEmpty,
|
||||
guard let normalizedSshHost = canonicalHostForComparison(sshHost),
|
||||
let url = self.remoteGatewayUrl(),
|
||||
let port = url.port,
|
||||
port > 0,
|
||||
let urlHost = url.host?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!urlHost.isEmpty
|
||||
let urlHost = url.host,
|
||||
let normalizedUrlHost = canonicalHostForComparison(urlHost)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let sshKey = Self.hostKey(trimmedSshHost)
|
||||
let urlKey = Self.hostKey(urlHost)
|
||||
guard !sshKey.isEmpty, !urlKey.isEmpty, sshKey == urlKey else { return nil }
|
||||
guard normalizedSshHost == normalizedUrlHost else { return nil }
|
||||
return port
|
||||
}
|
||||
|
||||
@@ -223,6 +220,16 @@ enum OpenClawConfigFile {
|
||||
}
|
||||
}
|
||||
|
||||
static func setRemoteGatewayUrlString(_ value: String) {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
self.updateGatewayDict { gateway in
|
||||
var remote = gateway["remote"] as? [String: Any] ?? [:]
|
||||
remote["url"] = trimmed
|
||||
gateway["remote"] = remote
|
||||
}
|
||||
}
|
||||
|
||||
static func clearRemoteGatewayUrl() {
|
||||
self.updateGatewayDict { gateway in
|
||||
guard var remote = gateway["remote"] as? [String: Any] else { return }
|
||||
@@ -249,15 +256,17 @@ enum OpenClawConfigFile {
|
||||
return url
|
||||
}
|
||||
|
||||
static func hostKey(_ host: String) -> String {
|
||||
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard !trimmed.isEmpty else { return "" }
|
||||
if trimmed.contains(":") { return trimmed }
|
||||
let digits = CharacterSet(charactersIn: "0123456789.")
|
||||
if trimmed.rangeOfCharacter(from: digits.inverted) == nil {
|
||||
return trimmed
|
||||
static func canonicalHostForComparison(_ raw: String?) -> String? {
|
||||
guard var host = raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
|
||||
!host.isEmpty
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return trimmed.split(separator: ".").first.map(String.init) ?? trimmed
|
||||
host = host.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
|
||||
while host.hasSuffix(".") {
|
||||
host.removeLast()
|
||||
}
|
||||
return host.isEmpty ? nil : host
|
||||
}
|
||||
|
||||
private static func parseConfigData(_ data: Data) -> [String: Any]? {
|
||||
|
||||
@@ -150,9 +150,11 @@ final class RemotePortTunnel {
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let sshKey = OpenClawConfigFile.hostKey(sshHost)
|
||||
let urlKey = OpenClawConfigFile.hostKey(host)
|
||||
guard !sshKey.isEmpty, !urlKey.isEmpty else { return nil }
|
||||
guard let sshKey = OpenClawConfigFile.canonicalHostForComparison(sshHost),
|
||||
let urlKey = OpenClawConfigFile.canonicalHostForComparison(host)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
guard sshKey == urlKey else {
|
||||
Self.logger.debug(
|
||||
"remote url host mismatch sshHost=\(sshHost, privacy: .public) urlHost=\(host, privacy: .public)")
|
||||
|
||||
@@ -17,6 +17,7 @@ final class VoiceSessionCoordinator {
|
||||
var isFinal: Bool
|
||||
var sendChime: VoiceWakeChime
|
||||
var autoSendDelay: TimeInterval?
|
||||
var voiceWakeTrigger: String?
|
||||
}
|
||||
|
||||
private let logger = Logger(subsystem: "ai.openclaw", category: "voicewake.coordinator")
|
||||
@@ -28,7 +29,8 @@ final class VoiceSessionCoordinator {
|
||||
source: Source,
|
||||
text: String,
|
||||
attributed: NSAttributedString? = nil,
|
||||
forwardEnabled: Bool = false) -> UUID
|
||||
forwardEnabled: Bool = false,
|
||||
voiceWakeTrigger: String? = nil) -> UUID
|
||||
{
|
||||
let token = UUID()
|
||||
self.logger.info("coordinator start token=\(token.uuidString) source=\(source.rawValue) len=\(text.count)")
|
||||
@@ -40,7 +42,8 @@ final class VoiceSessionCoordinator {
|
||||
attributed: attributedText,
|
||||
isFinal: false,
|
||||
sendChime: .none,
|
||||
autoSendDelay: nil)
|
||||
autoSendDelay: nil,
|
||||
voiceWakeTrigger: voiceWakeTrigger)
|
||||
self.session = session
|
||||
VoiceWakeOverlayController.shared.startSession(
|
||||
token: token,
|
||||
@@ -63,7 +66,8 @@ final class VoiceSessionCoordinator {
|
||||
token: UUID,
|
||||
text: String,
|
||||
sendChime: VoiceWakeChime,
|
||||
autoSendAfter: TimeInterval?)
|
||||
autoSendAfter: TimeInterval?,
|
||||
voiceWakeTrigger: String? = nil)
|
||||
{
|
||||
guard let session, session.token == token else { return }
|
||||
self.logger
|
||||
@@ -73,6 +77,9 @@ final class VoiceSessionCoordinator {
|
||||
self.session?.isFinal = true
|
||||
self.session?.sendChime = sendChime
|
||||
self.session?.autoSendDelay = autoSendAfter
|
||||
if let voiceWakeTrigger {
|
||||
self.session?.voiceWakeTrigger = voiceWakeTrigger
|
||||
}
|
||||
|
||||
let attributed = VoiceWakeOverlayController.shared.makeAttributed(from: text)
|
||||
VoiceWakeOverlayController.shared.presentFinal(
|
||||
@@ -86,15 +93,20 @@ final class VoiceSessionCoordinator {
|
||||
func sendNow(token: UUID, reason: String = "explicit") {
|
||||
guard let session, session.token == token else { return }
|
||||
let text = session.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let voiceWakeTrigger = session.voiceWakeTrigger
|
||||
let sendChime = session.sendChime
|
||||
guard !text.isEmpty else {
|
||||
self.logger.info("coordinator sendNow \(reason) empty -> dismiss")
|
||||
VoiceWakeOverlayController.shared.dismiss(token: token, reason: .empty, outcome: .empty)
|
||||
self.clearSession()
|
||||
return
|
||||
}
|
||||
VoiceWakeOverlayController.shared.beginSendUI(token: token, sendChime: session.sendChime)
|
||||
VoiceWakeOverlayController.shared.beginSendUI(token: token, sendChime: sendChime)
|
||||
Task.detached {
|
||||
_ = await VoiceWakeForwarder.forward(transcript: text)
|
||||
_ = await VoiceWakeForwarder.forward(
|
||||
transcript: text,
|
||||
options: .init(
|
||||
voiceWakeTrigger: voiceWakeTrigger))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ enum VoiceWakeForwarder {
|
||||
var deliver: Bool = true
|
||||
var to: String?
|
||||
var channel: GatewayAgentChannel = .webchat
|
||||
var voiceWakeTrigger: String?
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -53,7 +54,8 @@ enum VoiceWakeForwarder {
|
||||
thinking: options.thinking,
|
||||
deliver: deliver,
|
||||
to: options.to,
|
||||
channel: options.channel))
|
||||
channel: options.channel,
|
||||
voiceWakeTrigger: options.voiceWakeTrigger))
|
||||
|
||||
if result.ok {
|
||||
self.logger.info("voice wake forward ok")
|
||||
|
||||
@@ -41,7 +41,11 @@ enum VoiceWakeRecognitionDebugSupport {
|
||||
minCommandLength: config.minCommandLength,
|
||||
trimWake: trimWake)
|
||||
else { return nil }
|
||||
return WakeWordGateMatch(triggerEndTime: 0, postGap: 0, command: command)
|
||||
return WakeWordGateMatch(
|
||||
triggerEndTime: 0,
|
||||
postGap: 0,
|
||||
command: command,
|
||||
trigger: VoiceWakeTextUtils.matchedTriggerWord(transcript: transcript, triggers: triggers))
|
||||
}
|
||||
|
||||
static func transcriptSummary(
|
||||
|
||||
@@ -37,6 +37,7 @@ actor VoiceWakeRuntime {
|
||||
private var listeningState: ListeningState = .idle
|
||||
private var overlayToken: UUID?
|
||||
private var activeTriggerEndTime: TimeInterval?
|
||||
private var activeTriggerWord: String?
|
||||
private var scheduledRestartTask: Task<Void, Never>?
|
||||
private var lastLoggedText: String?
|
||||
private var lastLoggedAt: Date?
|
||||
@@ -256,6 +257,7 @@ actor VoiceWakeRuntime {
|
||||
self.currentConfig = nil
|
||||
self.listeningState = .idle
|
||||
self.activeTriggerEndTime = nil
|
||||
self.activeTriggerWord = nil
|
||||
self.logger.debug("voicewake runtime stopped")
|
||||
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "stopped")
|
||||
|
||||
@@ -366,7 +368,11 @@ actor VoiceWakeRuntime {
|
||||
} else {
|
||||
self.logger.info("voicewake runtime detected len=\(match.command.count)")
|
||||
}
|
||||
await self.beginCapture(command: match.command, triggerEndTime: match.triggerEndTime, config: config)
|
||||
await self.beginCapture(
|
||||
command: match.command,
|
||||
triggerEndTime: match.triggerEndTime,
|
||||
triggerWord: match.trigger,
|
||||
config: config)
|
||||
} else if !transcript.isEmpty, update.error == nil {
|
||||
if self.isTriggerOnly(transcript: transcript, triggers: config.triggers) {
|
||||
self.preDetectTask?.cancel()
|
||||
@@ -494,13 +500,33 @@ actor VoiceWakeRuntime {
|
||||
return
|
||||
}
|
||||
self.logger.info("voicewake runtime detected (trigger-only pause)")
|
||||
await self.beginCapture(command: "", triggerEndTime: nil, config: config)
|
||||
let matchedTrigger = self.matchedTriggerWord(transcript: lastText, triggers: triggers)
|
||||
await self.beginCapture(
|
||||
command: "",
|
||||
triggerEndTime: nil,
|
||||
triggerWord: matchedTrigger,
|
||||
config: config)
|
||||
}
|
||||
|
||||
private func isTriggerOnly(transcript: String, triggers: [String]) -> Bool {
|
||||
Self.isTriggerOnlyText(transcript: transcript, triggers: triggers)
|
||||
}
|
||||
|
||||
private func matchedTriggerWord(transcript: String, triggers: [String]) -> String? {
|
||||
Self.matchedTriggerWordText(transcript: transcript, triggers: triggers)
|
||||
}
|
||||
|
||||
private static func isTriggerOnlyText(transcript: String, triggers: [String]) -> Bool {
|
||||
guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return false }
|
||||
guard VoiceWakeTextUtils.startsWithTrigger(transcript: transcript, triggers: triggers) else { return false }
|
||||
return Self.trimmedAfterTrigger(transcript, triggers: triggers).isEmpty
|
||||
guard
|
||||
VoiceWakeTextUtils.startsWithTrigger(transcript: transcript, triggers: triggers)
|
||||
|| VoiceWakeTextUtils.hasOnlyFillerBeforeTrigger(transcript: transcript, triggers: triggers)
|
||||
else { return false }
|
||||
return self.trimmedAfterTrigger(transcript, triggers: triggers).isEmpty
|
||||
}
|
||||
|
||||
private static func matchedTriggerWordText(transcript: String, triggers: [String]) -> String? {
|
||||
VoiceWakeTextUtils.matchedTriggerWord(transcript: transcript, triggers: triggers)
|
||||
}
|
||||
|
||||
private func preDetectSilenceCheck(
|
||||
@@ -527,10 +553,16 @@ actor VoiceWakeRuntime {
|
||||
await self.beginCapture(
|
||||
command: match.command,
|
||||
triggerEndTime: match.triggerEndTime,
|
||||
triggerWord: match.trigger,
|
||||
config: config)
|
||||
}
|
||||
|
||||
private func beginCapture(command: String, triggerEndTime: TimeInterval?, config: RuntimeConfig) async {
|
||||
private func beginCapture(
|
||||
command: String,
|
||||
triggerEndTime: TimeInterval?,
|
||||
triggerWord: String?,
|
||||
config: RuntimeConfig) async
|
||||
{
|
||||
// When "Trigger Talk Mode" is enabled, skip the capture/overlay flow entirely
|
||||
// and activate Talk Mode immediately. Talk Mode handles its own STT pipeline.
|
||||
// Pause the wake listener to avoid two audio pipelines competing on the mic
|
||||
@@ -545,7 +577,6 @@ actor VoiceWakeRuntime {
|
||||
await AppStateStore.shared.setTalkEnabled(true)
|
||||
return
|
||||
}
|
||||
|
||||
self.listeningState = .voiceWake
|
||||
self.isCapturing = true
|
||||
DiagnosticsFileLog.shared.log(category: "voicewake.runtime", event: "beginCapture")
|
||||
@@ -557,6 +588,7 @@ actor VoiceWakeRuntime {
|
||||
self.heardBeyondTrigger = !command.isEmpty
|
||||
self.triggerChimePlayed = false
|
||||
self.activeTriggerEndTime = triggerEndTime
|
||||
self.activeTriggerWord = triggerWord
|
||||
self.preDetectTask?.cancel()
|
||||
self.preDetectTask = nil
|
||||
self.triggerOnlyTask?.cancel()
|
||||
@@ -577,7 +609,8 @@ actor VoiceWakeRuntime {
|
||||
source: .wakeWord,
|
||||
text: snapshot,
|
||||
attributed: attributed,
|
||||
forwardEnabled: true)
|
||||
forwardEnabled: true,
|
||||
voiceWakeTrigger: triggerWord)
|
||||
}
|
||||
|
||||
// Keep the "ears" boosted for the capture window so the status icon animates while recording.
|
||||
@@ -632,7 +665,9 @@ actor VoiceWakeRuntime {
|
||||
self.lastHeard = nil
|
||||
self.heardBeyondTrigger = false
|
||||
self.triggerChimePlayed = false
|
||||
let triggerWord = self.activeTriggerWord
|
||||
self.activeTriggerEndTime = nil
|
||||
self.activeTriggerWord = nil
|
||||
self.lastTranscript = nil
|
||||
self.lastTranscriptAt = nil
|
||||
self.preDetectTask?.cancel()
|
||||
@@ -653,14 +688,17 @@ actor VoiceWakeRuntime {
|
||||
token: token,
|
||||
text: finalTranscript,
|
||||
sendChime: sendChime,
|
||||
autoSendAfter: delay)
|
||||
autoSendAfter: delay,
|
||||
voiceWakeTrigger: triggerWord)
|
||||
}
|
||||
} else if !finalTranscript.isEmpty {
|
||||
if sendChime != .none {
|
||||
await MainActor.run { VoiceWakeChimePlayer.play(sendChime, reason: "voicewake.send") }
|
||||
}
|
||||
Task.detached {
|
||||
await VoiceWakeForwarder.forward(transcript: finalTranscript)
|
||||
await VoiceWakeForwarder.forward(
|
||||
transcript: finalTranscript,
|
||||
options: .init(voiceWakeTrigger: triggerWord))
|
||||
}
|
||||
}
|
||||
self.overlayToken = nil
|
||||
@@ -784,6 +822,14 @@ actor VoiceWakeRuntime {
|
||||
!self.trimmedAfterTrigger(text, triggers: triggers).isEmpty
|
||||
}
|
||||
|
||||
static func _testIsTriggerOnly(_ text: String, triggers: [String]) -> Bool {
|
||||
self.isTriggerOnlyText(transcript: text, triggers: triggers)
|
||||
}
|
||||
|
||||
static func _testMatchedTriggerWord(_ text: String, triggers: [String]) -> String? {
|
||||
self.matchedTriggerWordText(transcript: text, triggers: triggers)
|
||||
}
|
||||
|
||||
static func _testAttributedColor(isFinal: Bool) -> NSColor {
|
||||
VoiceOverlayTextFormatting.makeAttributed(committed: "sample", volatile: "", isFinal: isFinal)
|
||||
.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? .clear
|
||||
|
||||
@@ -4,6 +4,11 @@ import SwabbleKit
|
||||
enum VoiceWakeTextUtils {
|
||||
private static let whitespaceAndPunctuation = CharacterSet.whitespacesAndNewlines
|
||||
.union(.punctuationCharacters)
|
||||
.union(.symbols)
|
||||
private static let wakePrefixFillers: Set<String> = [
|
||||
"a", "ah", "eh", "er", "erm", "hey", "hmm", "huh", "mhm", "mm", "oh", "uh", "um",
|
||||
"yo", "呃", "嗯", "啊", "诶", "欸",
|
||||
]
|
||||
typealias TrimWake = (String, [String]) -> String
|
||||
|
||||
static func normalizeToken(_ token: String) -> String {
|
||||
@@ -12,6 +17,104 @@ enum VoiceWakeTextUtils {
|
||||
.lowercased()
|
||||
}
|
||||
|
||||
private static func normalizedTriggerTokens(_ trigger: String) -> [String] {
|
||||
trigger
|
||||
.split(whereSeparator: { $0.isWhitespace })
|
||||
.map { self.normalizeToken(String($0)) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
private static func isASCIIWordScalar(_ scalar: UnicodeScalar) -> Bool {
|
||||
scalar.isASCII && CharacterSet.alphanumerics.contains(scalar)
|
||||
}
|
||||
|
||||
private static func requiresASCIIWordBoundaries(_ value: String) -> Bool {
|
||||
value.unicodeScalars.contains(where: self.isASCIIWordScalar)
|
||||
}
|
||||
|
||||
private static func hasASCIIWordBoundaries(
|
||||
transcript: String,
|
||||
range: Range<String.Index>,
|
||||
trigger: String) -> Bool
|
||||
{
|
||||
guard self.requiresASCIIWordBoundaries(trigger) else { return true }
|
||||
|
||||
if range.lowerBound > transcript.startIndex {
|
||||
let beforeIndex = transcript.index(before: range.lowerBound)
|
||||
let beforeScalars = transcript[beforeIndex].unicodeScalars
|
||||
if beforeScalars.contains(where: self.isASCIIWordScalar) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if range.upperBound < transcript.endIndex {
|
||||
let afterScalars = transcript[range.upperBound].unicodeScalars
|
||||
if afterScalars.contains(where: self.isASCIIWordScalar) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private static func bestRawTriggerMatch(
|
||||
transcript: String,
|
||||
triggers: [String]) -> (range: Range<String.Index>, normalizedTrigger: String)?
|
||||
{
|
||||
var bestMatch: (range: Range<String.Index>, normalizedTrigger: String, tokenCount: Int)?
|
||||
|
||||
for trigger in triggers {
|
||||
let normalizedTokens = self.normalizedTriggerTokens(trigger)
|
||||
guard !normalizedTokens.isEmpty else { continue }
|
||||
let rawTrigger = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation)
|
||||
let tokenCount = normalizedTokens.count
|
||||
guard !rawTrigger.isEmpty else { continue }
|
||||
|
||||
var searchStart = transcript.startIndex
|
||||
while searchStart < transcript.endIndex,
|
||||
let range = transcript.range(
|
||||
of: rawTrigger,
|
||||
options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive],
|
||||
range: searchStart..<transcript.endIndex)
|
||||
{
|
||||
defer {
|
||||
searchStart = transcript.index(after: range.lowerBound)
|
||||
}
|
||||
guard self.hasASCIIWordBoundaries(
|
||||
transcript: transcript,
|
||||
range: range,
|
||||
trigger: rawTrigger)
|
||||
else { continue }
|
||||
|
||||
if let bestMatch {
|
||||
if range.lowerBound > bestMatch.range.lowerBound { continue }
|
||||
if range.lowerBound == bestMatch.range.lowerBound,
|
||||
tokenCount <= bestMatch.tokenCount
|
||||
{
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
bestMatch = (range, normalizedTokens.joined(separator: " "), tokenCount)
|
||||
break
|
||||
}
|
||||
|
||||
if let bestMatch,
|
||||
bestMatch.range.lowerBound == transcript.startIndex,
|
||||
bestMatch.tokenCount >= tokenCount
|
||||
{
|
||||
// Earlier matches take precedence, so once we match from the
|
||||
// start there is no need to scan later triggers with fewer
|
||||
// tokens at the same offset.
|
||||
if bestMatch.tokenCount > tokenCount {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch.map { (range: $0.range, normalizedTrigger: $0.normalizedTrigger) }
|
||||
}
|
||||
|
||||
static func startsWithTrigger(transcript: String, triggers: [String]) -> Bool {
|
||||
let tokens = transcript
|
||||
.split(whereSeparator: { $0.isWhitespace })
|
||||
@@ -19,10 +122,7 @@ enum VoiceWakeTextUtils {
|
||||
.filter { !$0.isEmpty }
|
||||
guard !tokens.isEmpty else { return false }
|
||||
for trigger in triggers {
|
||||
let triggerTokens = trigger
|
||||
.split(whereSeparator: { $0.isWhitespace })
|
||||
.map { self.normalizeToken(String($0)) }
|
||||
.filter { !$0.isEmpty }
|
||||
let triggerTokens = self.normalizedTriggerTokens(trigger)
|
||||
guard !triggerTokens.isEmpty, tokens.count >= triggerTokens.count else { continue }
|
||||
if zip(triggerTokens, tokens.prefix(triggerTokens.count)).allSatisfy({ $0 == $1 }) {
|
||||
return true
|
||||
@@ -40,9 +140,55 @@ enum VoiceWakeTextUtils {
|
||||
guard !transcript.isEmpty else { return nil }
|
||||
guard !self.normalizeToken(transcript).isEmpty else { return nil }
|
||||
guard WakeWordGate.matchesTextOnly(text: transcript, triggers: triggers) else { return nil }
|
||||
guard self.startsWithTrigger(transcript: transcript, triggers: triggers) else { return nil }
|
||||
guard
|
||||
self.startsWithTrigger(transcript: transcript, triggers: triggers)
|
||||
|| self.hasOnlyFillerBeforeTrigger(transcript: transcript, triggers: triggers)
|
||||
else { return nil }
|
||||
let trimmed = trimWake(transcript, triggers)
|
||||
guard trimmed.count >= minCommandLength else { return nil }
|
||||
return trimmed
|
||||
}
|
||||
|
||||
static func hasOnlyFillerBeforeTrigger(transcript: String, triggers: [String]) -> Bool {
|
||||
guard let match = self.bestRawTriggerMatch(transcript: transcript, triggers: triggers) else { return false }
|
||||
let prefixTokens = transcript[..<match.range.lowerBound]
|
||||
.split(whereSeparator: {
|
||||
$0.isWhitespace || self.whitespaceAndPunctuation.contains($0.unicodeScalars.first!)
|
||||
})
|
||||
.map { self.normalizeToken(String($0)) }
|
||||
.filter { !$0.isEmpty }
|
||||
return prefixTokens.allSatisfy { self.wakePrefixFillers.contains($0) }
|
||||
}
|
||||
|
||||
static func matchedTriggerWord(transcript: String, triggers: [String]) -> String? {
|
||||
if let rawMatch = self.bestRawTriggerMatch(transcript: transcript, triggers: triggers) {
|
||||
return rawMatch.normalizedTrigger
|
||||
}
|
||||
|
||||
let transcriptTokens = transcript
|
||||
.split(whereSeparator: { $0.isWhitespace })
|
||||
.map { self.normalizeToken(String($0)) }
|
||||
.filter { !$0.isEmpty }
|
||||
guard !transcriptTokens.isEmpty else { return nil }
|
||||
|
||||
var bestStartIndex = Int.max
|
||||
var bestTokenCount = -1
|
||||
var bestTokens: [String]?
|
||||
|
||||
for trigger in triggers {
|
||||
let triggerTokens = self.normalizedTriggerTokens(trigger)
|
||||
guard !triggerTokens.isEmpty, transcriptTokens.count >= triggerTokens.count else { continue }
|
||||
for index in 0...(transcriptTokens.count - triggerTokens.count) {
|
||||
let candidate = transcriptTokens[index..<(index + triggerTokens.count)]
|
||||
guard zip(triggerTokens, candidate).allSatisfy({ $0 == $1 }) else { continue }
|
||||
if index < bestStartIndex || (index == bestStartIndex && triggerTokens.count > bestTokenCount) {
|
||||
bestStartIndex = index
|
||||
bestTokenCount = triggerTokens.count
|
||||
bestTokens = triggerTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestTokens?.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,11 +595,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let besteffortdeliver: Bool?
|
||||
public let lane: String?
|
||||
public let cleanupbundlemcponrunend: Bool?
|
||||
public let modelrun: Bool?
|
||||
public let promptmode: AnyCodable?
|
||||
public let extrasystemprompt: String?
|
||||
public let bootstrapcontextmode: AnyCodable?
|
||||
public let bootstrapcontextrunkind: AnyCodable?
|
||||
public let internalevents: [[String: AnyCodable]]?
|
||||
public let inputprovenance: [String: AnyCodable]?
|
||||
public let voicewaketrigger: String?
|
||||
public let idempotencykey: String
|
||||
public let label: String?
|
||||
|
||||
@@ -627,11 +630,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
besteffortdeliver: Bool?,
|
||||
lane: String?,
|
||||
cleanupbundlemcponrunend: Bool?,
|
||||
modelrun: Bool?,
|
||||
promptmode: AnyCodable?,
|
||||
extrasystemprompt: String?,
|
||||
bootstrapcontextmode: AnyCodable?,
|
||||
bootstrapcontextrunkind: AnyCodable?,
|
||||
internalevents: [[String: AnyCodable]]?,
|
||||
inputprovenance: [String: AnyCodable]?,
|
||||
voicewaketrigger: String?,
|
||||
idempotencykey: String,
|
||||
label: String?)
|
||||
{
|
||||
@@ -658,11 +664,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.besteffortdeliver = besteffortdeliver
|
||||
self.lane = lane
|
||||
self.cleanupbundlemcponrunend = cleanupbundlemcponrunend
|
||||
self.modelrun = modelrun
|
||||
self.promptmode = promptmode
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
self.bootstrapcontextmode = bootstrapcontextmode
|
||||
self.bootstrapcontextrunkind = bootstrapcontextrunkind
|
||||
self.internalevents = internalevents
|
||||
self.inputprovenance = inputprovenance
|
||||
self.voicewaketrigger = voicewaketrigger
|
||||
self.idempotencykey = idempotencykey
|
||||
self.label = label
|
||||
}
|
||||
@@ -691,11 +700,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
case besteffortdeliver = "bestEffortDeliver"
|
||||
case lane
|
||||
case cleanupbundlemcponrunend = "cleanupBundleMcpOnRunEnd"
|
||||
case modelrun = "modelRun"
|
||||
case promptmode = "promptMode"
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
case bootstrapcontextmode = "bootstrapContextMode"
|
||||
case bootstrapcontextrunkind = "bootstrapContextRunKind"
|
||||
case internalevents = "internalEvents"
|
||||
case inputprovenance = "inputProvenance"
|
||||
case voicewaketrigger = "voiceWakeTrigger"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
case label
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@@ -36,6 +37,130 @@ struct AppStateRemoteConfigTests {
|
||||
#expect((remote["token"] as? String) == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func updatedRemoteGatewayConfigPinsLoopbackUrlForSshTransport() {
|
||||
let remote = AppState._testUpdatedRemoteGatewayConfig(
|
||||
current: ["url": "ws://gateway.example:18789"],
|
||||
draft: .init(
|
||||
transport: .ssh,
|
||||
remoteUrl: "",
|
||||
remoteHost: "gateway.example",
|
||||
remoteTarget: "alice@gateway.example",
|
||||
remoteIdentity: "",
|
||||
remoteToken: "",
|
||||
remoteTokenDirty: false))
|
||||
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
|
||||
#expect((remote["transport"] as? String) == nil)
|
||||
#expect(remote["sshTarget"] as? String == "alice@gateway.example")
|
||||
}
|
||||
|
||||
@Test
|
||||
func updatedRemoteGatewayConfigPreservesCustomLoopbackTunnelPort() {
|
||||
let remote = AppState._testUpdatedRemoteGatewayConfig(
|
||||
current: ["url": "ws://localhost.:29876"],
|
||||
draft: .init(
|
||||
transport: .ssh,
|
||||
remoteUrl: "",
|
||||
remoteHost: "gateway.example",
|
||||
remoteTarget: "alice@gateway.example",
|
||||
remoteIdentity: "",
|
||||
remoteToken: "",
|
||||
remoteTokenDirty: false))
|
||||
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:29876")
|
||||
}
|
||||
|
||||
@Test
|
||||
func updatedRemoteGatewayConfigPreservesCustomPortWhenExistingHostMatchesSshTarget() {
|
||||
let remote = AppState._testUpdatedRemoteGatewayConfig(
|
||||
current: ["url": "ws://gateway.example:19999"],
|
||||
draft: .init(
|
||||
transport: .ssh,
|
||||
remoteUrl: "",
|
||||
remoteHost: nil,
|
||||
remoteTarget: "alice@gateway.example",
|
||||
remoteIdentity: "",
|
||||
remoteToken: "",
|
||||
remoteTokenDirty: false))
|
||||
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:19999")
|
||||
}
|
||||
|
||||
@Test
|
||||
func updatedRemoteGatewayConfigDropsCustomPortWhenExistingHostDoesNotMatchSshTarget() {
|
||||
let remote = AppState._testUpdatedRemoteGatewayConfig(
|
||||
current: ["url": "ws://other-host.example:19999"],
|
||||
draft: .init(
|
||||
transport: .ssh,
|
||||
remoteUrl: "",
|
||||
remoteHost: "gateway.example",
|
||||
remoteTarget: "alice@gateway.example",
|
||||
remoteIdentity: "",
|
||||
remoteToken: "",
|
||||
remoteTokenDirty: false))
|
||||
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
|
||||
}
|
||||
|
||||
@Test
|
||||
func updatedRemoteGatewayConfigDoesNotPreservePortForHostnamePrefixCollision() {
|
||||
let remote = AppState._testUpdatedRemoteGatewayConfig(
|
||||
current: ["url": "ws://example.attacker.tld:19999"],
|
||||
draft: .init(
|
||||
transport: .ssh,
|
||||
remoteUrl: "",
|
||||
remoteHost: nil,
|
||||
remoteTarget: "alice@example.com",
|
||||
remoteIdentity: "",
|
||||
remoteToken: "",
|
||||
remoteTokenDirty: false))
|
||||
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
|
||||
}
|
||||
|
||||
@Test
|
||||
func appStateInitDoesNotInferLoopbackHostIntoRemoteTarget() async {
|
||||
let configPath = TestIsolation.tempConfigPath()
|
||||
await TestIsolation.withIsolatedState(
|
||||
env: ["OPENCLAW_CONFIG_PATH": configPath],
|
||||
defaults: [remoteTargetKey: nil])
|
||||
{
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"mode": "remote",
|
||||
"remote": [
|
||||
"url": "ws://127.0.0.1:19999",
|
||||
],
|
||||
],
|
||||
])
|
||||
|
||||
let state = AppState(preview: true)
|
||||
#expect(state.remoteTarget == "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func appStateInitPreservesExistingRemoteTargetWhenRemoteUrlIsLoopback() async {
|
||||
let configPath = TestIsolation.tempConfigPath()
|
||||
await TestIsolation.withIsolatedState(
|
||||
env: ["OPENCLAW_CONFIG_PATH": configPath],
|
||||
defaults: [remoteTargetKey: "alice@gateway.example"])
|
||||
{
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"mode": "remote",
|
||||
"remote": [
|
||||
"url": "ws://127.0.0.1:19999",
|
||||
],
|
||||
],
|
||||
])
|
||||
|
||||
let state = AppState(preview: true)
|
||||
#expect(state.remoteTarget == "alice@gateway.example")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func syncedGatewayRootPreservesObjectTokenAcrossModeAndTransportChangesWhenUntouched() {
|
||||
let initialRoot: [String: Any] = [
|
||||
|
||||
@@ -6,6 +6,10 @@ import Testing
|
||||
|
||||
private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
var state: URLSessionTask.State = .running
|
||||
var autoRespond = false
|
||||
private(set) var sentMessages: [URLSessionWebSocketTask.Message] = []
|
||||
private var sentChallenge = false
|
||||
private var respondedRequestIds = Set<String>()
|
||||
|
||||
func resume() {}
|
||||
|
||||
@@ -13,41 +17,90 @@ private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
self.state = .canceling
|
||||
}
|
||||
|
||||
func send(_: URLSessionWebSocketTask.Message) async throws {}
|
||||
func send(_ message: URLSessionWebSocketTask.Message) async throws {
|
||||
self.sentMessages.append(message)
|
||||
}
|
||||
|
||||
func receive() async throws -> URLSessionWebSocketTask.Message {
|
||||
if self.autoRespond {
|
||||
if !self.sentChallenge {
|
||||
self.sentChallenge = true
|
||||
return .string("""
|
||||
{"type":"event","event":"connect.challenge","payload":{"nonce":"test-nonce"}}
|
||||
""")
|
||||
}
|
||||
if let request = self.latestUnrespondedRequest() {
|
||||
self.respondedRequestIds.insert(request.id)
|
||||
if request.method == "connect" {
|
||||
return .string("""
|
||||
{"type":"res","id":"\(request.id)","ok":true,"payload":{"type":"hello","protocol":3,"server":{},"features":{},"snapshot":{"presence":[],"health":{},"stateVersion":{"presence":0,"health":0},"uptimeMs":0},"policy":{}}}
|
||||
""")
|
||||
}
|
||||
return .string("""
|
||||
{"type":"res","id":"\(request.id)","ok":true,"payload":{}}
|
||||
""")
|
||||
}
|
||||
}
|
||||
throw URLError(.cannotConnectToHost)
|
||||
}
|
||||
|
||||
func receive(completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void) {
|
||||
completionHandler(.failure(URLError(.cannotConnectToHost)))
|
||||
}
|
||||
}
|
||||
|
||||
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
|
||||
func makeWebSocketTask(url _: URL) -> WebSocketTaskBox {
|
||||
WebSocketTaskBox(task: FakeWebSocketTask())
|
||||
private func latestUnrespondedRequest() -> (id: String, method: String)? {
|
||||
for message in self.sentMessages.reversed() {
|
||||
let data: Data?
|
||||
switch message {
|
||||
case .string(let text):
|
||||
data = Data(text.utf8)
|
||||
case .data(let raw):
|
||||
data = raw
|
||||
@unknown default:
|
||||
data = nil
|
||||
}
|
||||
guard let data,
|
||||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let id = json["id"] as? String,
|
||||
let method = json["method"] as? String,
|
||||
!self.respondedRequestIds.contains(id)
|
||||
else {
|
||||
continue
|
||||
}
|
||||
return (id, method)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTestGatewayConnection() -> GatewayConnection {
|
||||
GatewayConnection(
|
||||
private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable {
|
||||
let task = FakeWebSocketTask()
|
||||
|
||||
func makeWebSocketTask(url _: URL) -> WebSocketTaskBox {
|
||||
WebSocketTaskBox(task: self.task)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSession) {
|
||||
let session = FakeWebSocketSession()
|
||||
let connection = GatewayConnection(
|
||||
configProvider: {
|
||||
(url: URL(string: "ws://127.0.0.1:1")!, token: nil, password: nil)
|
||||
},
|
||||
sessionBox: WebSocketSessionBox(session: FakeWebSocketSession()))
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
return (connection, session)
|
||||
}
|
||||
|
||||
@Suite(.serialized) struct GatewayConnectionControlTests {
|
||||
@Test func `status fails when process missing`() async {
|
||||
let connection = makeTestGatewayConnection()
|
||||
let (connection, _) = makeTestGatewayConnection()
|
||||
let result = await connection.status()
|
||||
#expect(result.ok == false)
|
||||
#expect(result.error != nil)
|
||||
}
|
||||
|
||||
@Test func `reject empty message`() async {
|
||||
let connection = makeTestGatewayConnection()
|
||||
let (connection, _) = makeTestGatewayConnection()
|
||||
let result = await connection.sendAgent(
|
||||
message: "",
|
||||
thinking: nil,
|
||||
@@ -56,4 +109,38 @@ private func makeTestGatewayConnection() -> GatewayConnection {
|
||||
to: nil)
|
||||
#expect(result.ok == false)
|
||||
}
|
||||
|
||||
@Test func `send agent keeps empty voice wake trigger field`() async throws {
|
||||
let (connection, session) = makeTestGatewayConnection()
|
||||
session.task.autoRespond = true
|
||||
_ = await connection.sendAgent(GatewayAgentInvocation(
|
||||
message: "test",
|
||||
sessionKey: "main",
|
||||
thinking: nil,
|
||||
deliver: false,
|
||||
to: nil,
|
||||
channel: .last,
|
||||
timeoutSeconds: nil,
|
||||
idempotencyKey: "idem-1",
|
||||
voiceWakeTrigger: " "))
|
||||
|
||||
guard let lastMessage = session.task.sentMessages.last else {
|
||||
Issue.record("expected websocket send payload")
|
||||
return
|
||||
}
|
||||
let payloadData: Data
|
||||
switch lastMessage {
|
||||
case .string(let text):
|
||||
payloadData = Data(text.utf8)
|
||||
case .data(let data):
|
||||
payloadData = data
|
||||
@unknown default:
|
||||
Issue.record("unexpected websocket message type")
|
||||
return
|
||||
}
|
||||
|
||||
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
|
||||
let params = json?["params"] as? [String: Any]
|
||||
#expect(params?["voiceWakeTrigger"] as? String == "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,35 @@ struct GatewayDiscoverySelectionSupportTests {
|
||||
state: state)
|
||||
|
||||
#expect(state.remoteTransport == .ssh)
|
||||
#expect(state.remoteUrl == "ws://127.0.0.1:18789")
|
||||
#expect(CommandResolver.parseSSHTarget(state.remoteTarget)?.host == "nearby-gateway.local")
|
||||
|
||||
let configRoot = OpenClawConfigFile.loadDict()
|
||||
let remote = ((configRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:18789")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `selecting nearby lan gateway preserves existing ssh tunnel port`() async {
|
||||
let configPath = TestIsolation.tempConfigPath()
|
||||
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": configPath]) {
|
||||
let state = AppState(preview: true)
|
||||
state.remoteTransport = .ssh
|
||||
state.remoteUrl = "ws://localhost:29876"
|
||||
|
||||
GatewayDiscoverySelectionSupport.applyRemoteSelection(
|
||||
gateway: self.makeGateway(
|
||||
serviceHost: "nearby-gateway.local",
|
||||
servicePort: 19999,
|
||||
stableID: "bonjour|nearby-gateway-custom"),
|
||||
state: state)
|
||||
|
||||
#expect(state.remoteTransport == .ssh)
|
||||
#expect(state.remoteUrl == "ws://127.0.0.1:29876")
|
||||
|
||||
let configRoot = OpenClawConfigFile.loadDict()
|
||||
let remote = ((configRoot["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
|
||||
#expect(remote["url"] as? String == "ws://127.0.0.1:29876")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +83,28 @@ struct MacNodeBrowserProxyTests {
|
||||
let arr = try #require(parsed["arr"] as? [Any])
|
||||
#expect(arr.count == 2)
|
||||
}
|
||||
|
||||
@Test func requestReportsActionableUnavailableWhenControlServiceIsMissing() async throws {
|
||||
let proxy = MacNodeBrowserProxy(
|
||||
endpointProvider: {
|
||||
MacNodeBrowserProxy.Endpoint(
|
||||
baseURL: URL(string: "http://127.0.0.1:18791")!,
|
||||
token: nil,
|
||||
password: nil)
|
||||
},
|
||||
performRequest: { _ in
|
||||
throw URLError(.cannotConnectToHost)
|
||||
})
|
||||
|
||||
do {
|
||||
_ = try await proxy.request(paramsJSON: #"{"method":"GET","path":"/"}"#)
|
||||
Issue.record("request should fail when browser control is unreachable")
|
||||
} catch {
|
||||
let message = error.localizedDescription
|
||||
#expect(message.contains("UNAVAILABLE: macOS app node could not reach the local browser control service"))
|
||||
#expect(message.contains("http://127.0.0.1:18791"))
|
||||
#expect(message.contains("browser control is owned by the CLI node-host"))
|
||||
#expect(message.contains("openclaw node start"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct MacNodeModeCoordinatorTests {
|
||||
@Test func remoteModeDoesNotAdvertiseBrowserProxy() {
|
||||
let caps = MacNodeModeCoordinator.resolvedCaps(
|
||||
browserControlEnabled: true,
|
||||
cameraEnabled: false,
|
||||
locationMode: .off,
|
||||
connectionMode: .remote)
|
||||
let commands = MacNodeModeCoordinator.resolvedCommands(caps: caps)
|
||||
|
||||
#expect(!caps.contains(OpenClawCapability.browser.rawValue))
|
||||
#expect(!commands.contains(OpenClawBrowserCommand.proxy.rawValue))
|
||||
#expect(commands.contains(OpenClawCanvasCommand.present.rawValue))
|
||||
#expect(commands.contains(OpenClawSystemCommand.notify.rawValue))
|
||||
}
|
||||
|
||||
@Test func localModeAdvertisesBrowserProxyWhenEnabled() {
|
||||
let caps = MacNodeModeCoordinator.resolvedCaps(
|
||||
browserControlEnabled: true,
|
||||
cameraEnabled: false,
|
||||
locationMode: .off,
|
||||
connectionMode: .local)
|
||||
let commands = MacNodeModeCoordinator.resolvedCommands(caps: caps)
|
||||
|
||||
#expect(caps.contains(OpenClawCapability.browser.rawValue))
|
||||
#expect(commands.contains(OpenClawBrowserCommand.proxy.rawValue))
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,30 @@ struct OpenClawConfigFileTests {
|
||||
])
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort() == 19999)
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway.ts.net") == 19999)
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway") == 19999)
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "GATEWAY.ts.net.") == 19999)
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway") == nil)
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "other.ts.net") == nil)
|
||||
#expect(OpenClawConfigFile.remoteGatewayPort(matchingHost: "gateway.attacker.tld") == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func `set remote gateway url string replaces scheme`() async {
|
||||
let override = self.makeConfigOverridePath()
|
||||
|
||||
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"url": "wss://old-host:111",
|
||||
],
|
||||
],
|
||||
])
|
||||
OpenClawConfigFile.setRemoteGatewayUrlString("ws://127.0.0.1:18789")
|
||||
let root = OpenClawConfigFile.loadDict()
|
||||
let url = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any])?["url"] as? String
|
||||
#expect(url == "ws://127.0.0.1:18789")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,80 @@ struct VoiceWakeRuntimeTests {
|
||||
#expect(VoiceWakeRuntime._testHasContentAfterTrigger(text, triggers: triggers))
|
||||
}
|
||||
|
||||
@Test func `trigger only allows filler before trigger`() {
|
||||
let triggers = ["openclaw"]
|
||||
let text = "uh openclaw"
|
||||
#expect(VoiceWakeRuntime._testIsTriggerOnly(text, triggers: triggers))
|
||||
}
|
||||
|
||||
@Test func `trigger only rejects trailing wake word mentions in ordinary speech`() {
|
||||
let triggers = ["openclaw"]
|
||||
let text = "tell me about openclaw"
|
||||
#expect(!VoiceWakeRuntime._testIsTriggerOnly(text, triggers: triggers))
|
||||
}
|
||||
|
||||
@Test func `matched trigger finds trigger not at transcript start`() {
|
||||
let triggers = ["openclaw"]
|
||||
let text = "uh openclaw"
|
||||
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "openclaw")
|
||||
}
|
||||
|
||||
@Test func `matched trigger rejects larger word suffix matches`() {
|
||||
let triggers = ["computer"]
|
||||
let text = "uh computers"
|
||||
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == nil)
|
||||
}
|
||||
|
||||
@Test func `matched trigger prefers most specific overlapping phrase`() {
|
||||
let triggers = ["openclaw", "hey openclaw"]
|
||||
let text = "hey openclaw"
|
||||
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "hey openclaw")
|
||||
}
|
||||
|
||||
@Test func `matched trigger handles width insensitive forms without whitespace tokens`() {
|
||||
let triggers = ["openclaw"]
|
||||
let text = "OpenClaw"
|
||||
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "openclaw")
|
||||
}
|
||||
|
||||
@Test func `matched trigger handles chinese forms without whitespace tokens`() {
|
||||
let triggers = ["小爪"]
|
||||
let text = "嘿小爪"
|
||||
#expect(VoiceWakeRuntime._testMatchedTriggerWord(text, triggers: triggers) == "小爪")
|
||||
}
|
||||
|
||||
@Test func `text only fallback populates matched trigger`() {
|
||||
let transcript = "hey openclaw do thing"
|
||||
let config = WakeWordGateConfig(triggers: ["openclaw"], minCommandLength: 1)
|
||||
let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch(
|
||||
transcript: transcript,
|
||||
triggers: ["openclaw"],
|
||||
config: config,
|
||||
trimWake: VoiceWakeRuntime._testTrimmedAfterTrigger)
|
||||
#expect(match?.trigger == "openclaw")
|
||||
}
|
||||
|
||||
@Test func `text only fallback keeps the first trigger phrase when later words match another trigger`() {
|
||||
let transcript = "openclaw tell me about computer vision"
|
||||
let config = WakeWordGateConfig(triggers: ["openclaw", "computer"], minCommandLength: 1)
|
||||
let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch(
|
||||
transcript: transcript,
|
||||
triggers: ["openclaw", "computer"],
|
||||
config: config,
|
||||
trimWake: VoiceWakeRuntime._testTrimmedAfterTrigger)
|
||||
#expect(match?.trigger == "openclaw")
|
||||
}
|
||||
|
||||
@Test func `text only fallback rejects filler prefixed larger word suffix matches`() {
|
||||
let transcript = "uh computers"
|
||||
let config = WakeWordGateConfig(triggers: ["computer"], minCommandLength: 1)
|
||||
let match = VoiceWakeRecognitionDebugSupport.textOnlyFallbackMatch(
|
||||
transcript: transcript,
|
||||
triggers: ["computer"],
|
||||
config: config,
|
||||
trimWake: VoiceWakeRuntime._testTrimmedAfterTrigger)
|
||||
#expect(match == nil)
|
||||
}
|
||||
@Test func `trims after chinese trigger keeps post speech`() {
|
||||
let triggers = ["小爪", "openclaw"]
|
||||
let text = "嘿 小爪 帮我打开设置"
|
||||
|
||||
@@ -595,11 +595,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
public let besteffortdeliver: Bool?
|
||||
public let lane: String?
|
||||
public let cleanupbundlemcponrunend: Bool?
|
||||
public let modelrun: Bool?
|
||||
public let promptmode: AnyCodable?
|
||||
public let extrasystemprompt: String?
|
||||
public let bootstrapcontextmode: AnyCodable?
|
||||
public let bootstrapcontextrunkind: AnyCodable?
|
||||
public let internalevents: [[String: AnyCodable]]?
|
||||
public let inputprovenance: [String: AnyCodable]?
|
||||
public let voicewaketrigger: String?
|
||||
public let idempotencykey: String
|
||||
public let label: String?
|
||||
|
||||
@@ -627,11 +630,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
besteffortdeliver: Bool?,
|
||||
lane: String?,
|
||||
cleanupbundlemcponrunend: Bool?,
|
||||
modelrun: Bool?,
|
||||
promptmode: AnyCodable?,
|
||||
extrasystemprompt: String?,
|
||||
bootstrapcontextmode: AnyCodable?,
|
||||
bootstrapcontextrunkind: AnyCodable?,
|
||||
internalevents: [[String: AnyCodable]]?,
|
||||
inputprovenance: [String: AnyCodable]?,
|
||||
voicewaketrigger: String?,
|
||||
idempotencykey: String,
|
||||
label: String?)
|
||||
{
|
||||
@@ -658,11 +664,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
self.besteffortdeliver = besteffortdeliver
|
||||
self.lane = lane
|
||||
self.cleanupbundlemcponrunend = cleanupbundlemcponrunend
|
||||
self.modelrun = modelrun
|
||||
self.promptmode = promptmode
|
||||
self.extrasystemprompt = extrasystemprompt
|
||||
self.bootstrapcontextmode = bootstrapcontextmode
|
||||
self.bootstrapcontextrunkind = bootstrapcontextrunkind
|
||||
self.internalevents = internalevents
|
||||
self.inputprovenance = inputprovenance
|
||||
self.voicewaketrigger = voicewaketrigger
|
||||
self.idempotencykey = idempotencykey
|
||||
self.label = label
|
||||
}
|
||||
@@ -691,11 +700,14 @@ public struct AgentParams: Codable, Sendable {
|
||||
case besteffortdeliver = "bestEffortDeliver"
|
||||
case lane
|
||||
case cleanupbundlemcponrunend = "cleanupBundleMcpOnRunEnd"
|
||||
case modelrun = "modelRun"
|
||||
case promptmode = "promptMode"
|
||||
case extrasystemprompt = "extraSystemPrompt"
|
||||
case bootstrapcontextmode = "bootstrapContextMode"
|
||||
case bootstrapcontextrunkind = "bootstrapContextRunKind"
|
||||
case internalevents = "internalEvents"
|
||||
case inputprovenance = "inputProvenance"
|
||||
case voicewaketrigger = "voiceWakeTrigger"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
case label
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ services:
|
||||
TERM: xterm-256color
|
||||
OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN:-}
|
||||
OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: ${OPENCLAW_ALLOW_INSECURE_PRIVATE_WS:-}
|
||||
# Docker bridge networks usually do not carry mDNS multicast reliably.
|
||||
# Set OPENCLAW_DISABLE_BONJOUR=0 only on host/macvlan/mDNS-capable networks.
|
||||
OPENCLAW_DISABLE_BONJOUR: ${OPENCLAW_DISABLE_BONJOUR:-1}
|
||||
CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-}
|
||||
CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-}
|
||||
CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
3efb041739877bd5387ffc87e0ddd11be43d80d38e7779407ce8091dcb797e5e config-baseline.json
|
||||
5c6e35c5846f654d717d4b20853649e0b45a746423834f539b2a2223abcd5226 config-baseline.core.json
|
||||
f1eefb91a486188915373b09199959f0f1a7cd01dc75ef923832741f72a12543 config-baseline.json
|
||||
9f0e386d5118cbca785a2e8e9c8b170d844faf1b7ef5e82e6b15d9e1c39f3796 config-baseline.core.json
|
||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||
a5479c182ec987bb21e814b8a4e7b3bda7190ae5c2b35fd5ca403dfa48afa115 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
690c1cd4c0c2c3d31577958120e14ac0bf555af529e03aa5e7965b1d04659c49 plugin-sdk-api-baseline.json
|
||||
a0e6ba472ddd3acea34c0a8fda8cbb7d1172b1671a671d5fef5a9f42d749ce0d plugin-sdk-api-baseline.jsonl
|
||||
947221d62a0eb0b66250fba2b011ca28a11cb1058bc542b9c155d55479f15935 plugin-sdk-api-baseline.json
|
||||
0d750f785adbe4d90f209842ed9297476669dd62f7be81fa41e06b6736cc2aaf plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -5,29 +5,37 @@ read_when:
|
||||
- Wiring external triggers (webhooks, Gmail) into OpenClaw
|
||||
- Deciding between heartbeat and cron for scheduled tasks
|
||||
title: "Scheduled tasks"
|
||||
sidebarTitle: "Scheduled tasks"
|
||||
---
|
||||
|
||||
Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Add a one-shot reminder
|
||||
openclaw cron add \
|
||||
--name "Reminder" \
|
||||
--at "2026-02-01T16:00:00Z" \
|
||||
--session main \
|
||||
--system-event "Reminder: check the cron docs draft" \
|
||||
--wake now \
|
||||
--delete-after-run
|
||||
|
||||
# Check your jobs
|
||||
openclaw cron list
|
||||
openclaw cron show <job-id>
|
||||
|
||||
# See run history
|
||||
openclaw cron runs --id <job-id>
|
||||
```
|
||||
<Steps>
|
||||
<Step title="Add a one-shot reminder">
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Reminder" \
|
||||
--at "2026-02-01T16:00:00Z" \
|
||||
--session main \
|
||||
--system-event "Reminder: check the cron docs draft" \
|
||||
--wake now \
|
||||
--delete-after-run
|
||||
```
|
||||
</Step>
|
||||
<Step title="Check your jobs">
|
||||
```bash
|
||||
openclaw cron list
|
||||
openclaw cron show <job-id>
|
||||
```
|
||||
</Step>
|
||||
<Step title="See run history">
|
||||
```bash
|
||||
openclaw cron runs --id <job-id>
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How cron works
|
||||
|
||||
@@ -38,18 +46,13 @@ openclaw cron runs --id <job-id>
|
||||
- All cron executions create [background task](/automation/tasks) records.
|
||||
- One-shot jobs (`--at`) auto-delete after success by default.
|
||||
- Isolated cron runs best-effort close tracked browser tabs/processes for their `cron:<jobId>` session when the run completes, so detached browser automation does not leave orphaned processes behind.
|
||||
- Isolated cron runs also guard against stale acknowledgement replies. If the
|
||||
first result is just an interim status update (`on it`, `pulling everything
|
||||
together`, and similar hints) and no descendant subagent run is still
|
||||
responsible for the final answer, OpenClaw re-prompts once for the actual
|
||||
result before delivery.
|
||||
- Isolated cron runs also guard against stale acknowledgement replies. If the first result is just an interim status update (`on it`, `pulling everything together`, and similar hints) and no descendant subagent run is still responsible for the final answer, OpenClaw re-prompts once for the actual result before delivery.
|
||||
|
||||
<a id="maintenance"></a>
|
||||
|
||||
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`.
|
||||
<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`.
|
||||
</Note>
|
||||
|
||||
## Schedule types
|
||||
|
||||
@@ -84,35 +87,46 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
|
||||
| Current session | `current` | Bound at creation time | Context-aware recurring work |
|
||||
| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history |
|
||||
|
||||
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). Those system events do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Main session vs isolated vs custom">
|
||||
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). Those system events do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
|
||||
</Accordion>
|
||||
<Accordion title="What 'fresh session' means for isolated jobs">
|
||||
For isolated jobs, "fresh session" means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
|
||||
</Accordion>
|
||||
<Accordion title="Runtime cleanup">
|
||||
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.
|
||||
|
||||
For isolated jobs, “fresh session” means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
|
||||
Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
|
||||
|
||||
For isolated jobs, runtime teardown now includes best-effort browser cleanup for that cron session. Cleanup failures are ignored so the actual cron result still wins.
|
||||
</Accordion>
|
||||
<Accordion title="Subagent and Discord delivery">
|
||||
When isolated cron runs orchestrate subagents, delivery also prefers the final descendant output over stale parent interim text. If descendants are still running, OpenClaw suppresses that partial parent update instead of announcing it.
|
||||
|
||||
Isolated cron runs also dispose any bundled MCP runtime instances created for the job through the shared runtime-cleanup path. This matches how main-session and custom-session MCP clients are torn down, so isolated cron jobs do not leak stdio child processes or long-lived MCP connections across runs.
|
||||
For text-only Discord announce targets, OpenClaw sends the canonical final assistant text once instead of replaying both streamed/intermediate text payloads and the final answer. Media and structured Discord payloads are still delivered as separate payloads so attachments and components are not dropped.
|
||||
|
||||
When isolated cron runs orchestrate subagents, delivery also prefers the final
|
||||
descendant output over stale parent interim text. If descendants are still
|
||||
running, OpenClaw suppresses that partial parent update instead of announcing it.
|
||||
|
||||
For text-only Discord announce targets, OpenClaw sends the canonical final
|
||||
assistant text once instead of replaying both streamed/intermediate text payloads
|
||||
and the final answer. Media and structured Discord payloads are still delivered
|
||||
as separate payloads so attachments and components are not dropped.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Payload options for isolated jobs
|
||||
|
||||
- `--message`: prompt text (required for isolated)
|
||||
- `--model` / `--thinking`: model and thinking level overrides
|
||||
- `--light-context`: skip workspace bootstrap file injection
|
||||
- `--tools exec,read`: restrict which tools the job can use
|
||||
<ParamField path="--message" type="string" required>
|
||||
Prompt text (required for isolated).
|
||||
</ParamField>
|
||||
<ParamField path="--model" type="string">
|
||||
Model override; uses the selected allowed model for the job.
|
||||
</ParamField>
|
||||
<ParamField path="--thinking" type="string">
|
||||
Thinking level override.
|
||||
</ParamField>
|
||||
<ParamField path="--light-context" type="boolean">
|
||||
Skip workspace bootstrap file injection.
|
||||
</ParamField>
|
||||
<ParamField path="--tools" type="string">
|
||||
Restrict which tools the job can use, for example `--tools exec,read`.
|
||||
</ParamField>
|
||||
|
||||
`--model` uses the selected allowed model for that job. If the requested model
|
||||
is not allowed, cron logs a warning and falls back to the job's agent/default
|
||||
model selection instead. Configured fallback chains still apply, but a plain
|
||||
model override with no explicit per-job fallback list no longer appends the
|
||||
agent primary as a hidden extra retry target.
|
||||
`--model` uses the selected allowed model for that job. If the requested model is not allowed, cron logs a warning and falls back to the job's agent/default model selection instead. Configured fallback chains still apply, but a plain model override with no explicit per-job fallback list no longer appends the agent primary as a hidden extra retry target.
|
||||
|
||||
Model-selection precedence for isolated jobs is:
|
||||
|
||||
@@ -121,16 +135,9 @@ Model-selection precedence for isolated jobs is:
|
||||
3. User-selected stored cron session model override
|
||||
4. Agent/default model selection
|
||||
|
||||
Fast mode follows the resolved live selection too. If the selected model config
|
||||
has `params.fastMode`, isolated cron uses that by default. A stored session
|
||||
`fastMode` override still wins over config in either direction.
|
||||
Fast mode follows the resolved live selection too. If the selected model config has `params.fastMode`, isolated cron uses that by default. A stored session `fastMode` override still wins over config in either direction.
|
||||
|
||||
If an isolated run hits a live model-switch handoff, cron retries with the
|
||||
switched provider/model and persists that live selection for the active run
|
||||
before retrying. When the switch also carries a new auth profile, cron persists
|
||||
that auth profile override for the active run too. Retries are bounded: after
|
||||
the initial attempt plus 2 switch retries, cron aborts instead of looping
|
||||
forever.
|
||||
If an isolated run hits a live model-switch handoff, cron retries with the switched provider/model and persists that live selection for the active run before retrying. When the switch also carries a new auth profile, cron persists that auth profile override for the active run too. Retries are bounded: after the initial attempt plus 2 switch retries, cron aborts instead of looping forever.
|
||||
|
||||
## Delivery and output
|
||||
|
||||
@@ -142,16 +149,9 @@ forever.
|
||||
|
||||
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
|
||||
|
||||
For isolated jobs, chat delivery is shared. If a chat route is available, the
|
||||
agent can use the `message` tool even when the job uses `--no-deliver`. If the
|
||||
agent sends to the configured/current target, OpenClaw skips the fallback
|
||||
announce. Otherwise `announce`, `webhook`, and `none` only control what the
|
||||
runner does with the final reply after the agent turn.
|
||||
For isolated jobs, chat delivery is shared. If a chat route is available, the agent can use the `message` tool even when the job uses `--no-deliver`. If the agent sends to the configured/current target, OpenClaw skips the fallback announce. Otherwise `announce`, `webhook`, and `none` only control what the runner does with the final reply after the agent turn.
|
||||
|
||||
When an agent creates an isolated reminder from an active chat, OpenClaw stores
|
||||
the preserved live delivery target for the fallback announce route. Internal
|
||||
session keys may be lowercase; provider delivery targets are not reconstructed
|
||||
from those keys when current chat context is available.
|
||||
When an agent creates an isolated reminder from an active chat, OpenClaw stores the preserved live delivery target for the fallback announce route. Internal session keys may be lowercase; provider delivery targets are not reconstructed from those keys when current chat context is available.
|
||||
|
||||
Failure notifications follow a separate destination path:
|
||||
|
||||
@@ -162,44 +162,44 @@ Failure notifications follow a separate destination path:
|
||||
|
||||
## CLI examples
|
||||
|
||||
One-shot reminder (main session):
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Calendar check" \
|
||||
--at "20m" \
|
||||
--session main \
|
||||
--system-event "Next heartbeat: check calendar." \
|
||||
--wake now
|
||||
```
|
||||
|
||||
Recurring isolated job with delivery:
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Morning brief" \
|
||||
--cron "0 7 * * *" \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Summarize overnight updates." \
|
||||
--announce \
|
||||
--channel slack \
|
||||
--to "channel:C1234567890"
|
||||
```
|
||||
|
||||
Isolated job with model and thinking override:
|
||||
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Deep analysis" \
|
||||
--cron "0 6 * * 1" \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Weekly deep analysis of project progress." \
|
||||
--model "opus" \
|
||||
--thinking high \
|
||||
--announce
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="One-shot reminder">
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Calendar check" \
|
||||
--at "20m" \
|
||||
--session main \
|
||||
--system-event "Next heartbeat: check calendar." \
|
||||
--wake now
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Recurring isolated job">
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Morning brief" \
|
||||
--cron "0 7 * * *" \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Summarize overnight updates." \
|
||||
--announce \
|
||||
--channel slack \
|
||||
--to "channel:C1234567890"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Model and thinking override">
|
||||
```bash
|
||||
openclaw cron add \
|
||||
--name "Deep analysis" \
|
||||
--cron "0 6 * * 1" \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Weekly deep analysis of project progress." \
|
||||
--model "opus" \
|
||||
--thinking high \
|
||||
--announce
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Webhooks
|
||||
|
||||
@@ -224,52 +224,61 @@ Every request must include the hook token via header:
|
||||
|
||||
Query-string tokens are rejected.
|
||||
|
||||
### POST /hooks/wake
|
||||
<AccordionGroup>
|
||||
<Accordion title="POST /hooks/wake">
|
||||
Enqueue a system event for the main session:
|
||||
|
||||
Enqueue a system event for the main session:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:18789/hooks/wake \
|
||||
-H 'Authorization: Bearer SECRET' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"text":"New email received","mode":"now"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:18789/hooks/wake \
|
||||
-H 'Authorization: Bearer SECRET' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"text":"New email received","mode":"now"}'
|
||||
```
|
||||
<ParamField path="text" type="string" required>
|
||||
Event description.
|
||||
</ParamField>
|
||||
<ParamField path="mode" type="string" default="now">
|
||||
`now` or `next-heartbeat`.
|
||||
</ParamField>
|
||||
|
||||
- `text` (required): event description
|
||||
- `mode` (optional): `now` (default) or `next-heartbeat`
|
||||
</Accordion>
|
||||
<Accordion title="POST /hooks/agent">
|
||||
Run an isolated agent turn:
|
||||
|
||||
### POST /hooks/agent
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:18789/hooks/agent \
|
||||
-H 'Authorization: Bearer SECRET' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.4"}'
|
||||
```
|
||||
|
||||
Run an isolated agent turn:
|
||||
Fields: `message` (required), `name`, `agentId`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `thinking`, `timeoutSeconds`.
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:18789/hooks/agent \
|
||||
-H 'Authorization: Bearer SECRET' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.4"}'
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Mapped hooks (POST /hooks/<name>)">
|
||||
Custom hook names are resolved via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Fields: `message` (required), `name`, `agentId`, `wakeMode`, `deliver`, `channel`, `to`, `model`, `thinking`, `timeoutSeconds`.
|
||||
<Warning>
|
||||
Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
||||
|
||||
### Mapped hooks (POST /hooks/\<name\>)
|
||||
|
||||
Custom hook names are resolved via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms.
|
||||
|
||||
### Security
|
||||
|
||||
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
||||
- Use a dedicated hook token; do not reuse gateway auth tokens.
|
||||
- Keep `hooks.path` on a dedicated subpath; `/` is rejected.
|
||||
- Set `hooks.allowedAgentIds` to limit explicit `agentId` routing.
|
||||
- Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions.
|
||||
- If you enable `hooks.allowRequestSessionKey`, also set `hooks.allowedSessionKeyPrefixes` to constrain allowed session key shapes.
|
||||
- Hook payloads are wrapped with safety boundaries by default.
|
||||
</Warning>
|
||||
|
||||
## Gmail PubSub integration
|
||||
|
||||
Wire Gmail inbox triggers to OpenClaw via Google PubSub.
|
||||
|
||||
**Prerequisites**: `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint.
|
||||
<Note>
|
||||
**Prerequisites:** `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint.
|
||||
</Note>
|
||||
|
||||
### Wizard setup (recommended)
|
||||
|
||||
@@ -285,31 +294,34 @@ When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts `
|
||||
|
||||
### Manual one-time setup
|
||||
|
||||
1. Select the GCP project that owns the OAuth client used by `gog`:
|
||||
<Steps>
|
||||
<Step title="Select the GCP project">
|
||||
Select the GCP project that owns the OAuth client used by `gog`:
|
||||
|
||||
```bash
|
||||
gcloud auth login
|
||||
gcloud config set project <project-id>
|
||||
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
|
||||
```
|
||||
```bash
|
||||
gcloud auth login
|
||||
gcloud config set project <project-id>
|
||||
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
|
||||
```
|
||||
|
||||
2. Create topic and grant Gmail push access:
|
||||
|
||||
```bash
|
||||
gcloud pubsub topics create gog-gmail-watch
|
||||
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
|
||||
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
|
||||
--role=roles/pubsub.publisher
|
||||
```
|
||||
|
||||
3. Start the watch:
|
||||
|
||||
```bash
|
||||
gog gmail watch start \
|
||||
--account openclaw@gmail.com \
|
||||
--label INBOX \
|
||||
--topic projects/<project-id>/topics/gog-gmail-watch
|
||||
```
|
||||
</Step>
|
||||
<Step title="Create topic and grant Gmail push access">
|
||||
```bash
|
||||
gcloud pubsub topics create gog-gmail-watch
|
||||
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
|
||||
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
|
||||
--role=roles/pubsub.publisher
|
||||
```
|
||||
</Step>
|
||||
<Step title="Start the watch">
|
||||
```bash
|
||||
gog gmail watch start \
|
||||
--account openclaw@gmail.com \
|
||||
--label INBOX \
|
||||
--topic projects/<project-id>/topics/gog-gmail-watch
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Gmail model override
|
||||
|
||||
@@ -353,16 +365,14 @@ openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --mes
|
||||
openclaw cron edit <jobId> --clear-agent
|
||||
```
|
||||
|
||||
<Note>
|
||||
Model override note:
|
||||
|
||||
- `openclaw cron add|edit --model ...` changes the job's selected model.
|
||||
- If the model is allowed, that exact provider/model reaches the isolated agent
|
||||
run.
|
||||
- If it is not allowed, cron warns and falls back to the job's agent/default
|
||||
model selection.
|
||||
- Configured fallback chains still apply, but a plain `--model` override with
|
||||
no explicit per-job fallback list no longer falls through to the agent
|
||||
primary as a silent extra retry target.
|
||||
- If the model is allowed, that exact provider/model reaches the isolated agent run.
|
||||
- If it is not allowed, cron warns and falls back to the job's agent/default model selection.
|
||||
- Configured fallback chains still apply, but a plain `--model` override with no explicit per-job fallback list no longer falls through to the agent primary as a silent extra retry target.
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -384,17 +394,21 @@ Model override note:
|
||||
}
|
||||
```
|
||||
|
||||
The runtime state sidecar is derived from `cron.store`: a `.json` store such as
|
||||
`~/clawd/cron/jobs.json` uses `~/clawd/cron/jobs-state.json`, while a store path
|
||||
without a `.json` suffix appends `-state.json`.
|
||||
The runtime state sidecar is derived from `cron.store`: a `.json` store such as `~/clawd/cron/jobs.json` uses `~/clawd/cron/jobs-state.json`, while a store path without a `.json` suffix appends `-state.json`.
|
||||
|
||||
Disable cron: `cron.enabled: false` or `OPENCLAW_SKIP_CRON=1`.
|
||||
|
||||
**One-shot retry**: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Retry behavior">
|
||||
**One-shot retry**: transient errors (rate limit, overload, network, server error) retry up to 3 times with exponential backoff. Permanent errors disable immediately.
|
||||
|
||||
**Recurring retry**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run.
|
||||
**Recurring retry**: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run.
|
||||
|
||||
**Maintenance**: `cron.sessionRetention` (default `24h`) prunes isolated run-session entries. `cron.runLog.maxBytes` / `cron.runLog.keepLines` auto-prune run-log files.
|
||||
</Accordion>
|
||||
<Accordion title="Maintenance">
|
||||
`cron.sessionRetention` (default `24h`) prunes isolated run-session entries. `cron.runLog.maxBytes` / `cron.runLog.keepLines` auto-prune run-log files.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -411,45 +425,32 @@ openclaw logs --follow
|
||||
openclaw doctor
|
||||
```
|
||||
|
||||
### Cron not firing
|
||||
|
||||
- Check `cron.enabled` and `OPENCLAW_SKIP_CRON` env var.
|
||||
- Confirm the Gateway is running continuously.
|
||||
- For `cron` schedules, verify timezone (`--tz`) vs the host timezone.
|
||||
- `reason: not-due` in run output means manual run was checked with `openclaw cron run <jobId> --due` and the job was not due yet.
|
||||
|
||||
### Cron fired but no delivery
|
||||
|
||||
- Delivery mode `none` means no runner fallback send is expected. The agent can
|
||||
still send directly with the `message` tool when a chat route is available.
|
||||
- Delivery target missing/invalid (`channel`/`to`) means outbound was skipped.
|
||||
- For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can
|
||||
fail because Matrix room IDs are case-sensitive. Edit the job to the exact
|
||||
`!room:server` or `room:!room:server` value from Matrix.
|
||||
- Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials.
|
||||
- If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`),
|
||||
OpenClaw suppresses direct outbound delivery and also suppresses the fallback
|
||||
queued summary path, so nothing is posted back to chat.
|
||||
- If the agent should message the user itself, check that the job has a usable
|
||||
route (`channel: "last"` with a previous chat, or an explicit channel/target).
|
||||
|
||||
### Cron or heartbeat appears to prevent `/new`-style rollover
|
||||
|
||||
- Daily and idle reset freshness is not based on `updatedAt`; see
|
||||
[Session management](/concepts/session#session-lifecycle).
|
||||
- Cron wakeups, heartbeat runs, exec notifications, and gateway bookkeeping may
|
||||
update the session row for routing/status, but they do not extend
|
||||
`sessionStartedAt` or `lastInteractionAt`.
|
||||
- For legacy rows created before those fields existed, OpenClaw can recover
|
||||
`sessionStartedAt` from the transcript JSONL session header when the file is
|
||||
still available. Legacy idle rows without `lastInteractionAt` use that
|
||||
recovered start time as their idle baseline.
|
||||
|
||||
### Timezone gotchas
|
||||
|
||||
- Cron without `--tz` uses the gateway host timezone.
|
||||
- `at` schedules without timezone are treated as UTC.
|
||||
- Heartbeat `activeHours` uses configured timezone resolution.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Cron not firing">
|
||||
- Check `cron.enabled` and `OPENCLAW_SKIP_CRON` env var.
|
||||
- Confirm the Gateway is running continuously.
|
||||
- For `cron` schedules, verify timezone (`--tz`) vs the host timezone.
|
||||
- `reason: not-due` in run output means manual run was checked with `openclaw cron run <jobId> --due` and the job was not due yet.
|
||||
</Accordion>
|
||||
<Accordion title="Cron fired but no delivery">
|
||||
- Delivery mode `none` means no runner fallback send is expected. The agent can still send directly with the `message` tool when a chat route is available.
|
||||
- Delivery target missing/invalid (`channel`/`to`) means outbound was skipped.
|
||||
- For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can fail because Matrix room IDs are case-sensitive. Edit the job to the exact `!room:server` or `room:!room:server` value from Matrix.
|
||||
- Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials.
|
||||
- If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`), OpenClaw suppresses direct outbound delivery and also suppresses the fallback queued summary path, so nothing is posted back to chat.
|
||||
- If the agent should message the user itself, check that the job has a usable route (`channel: "last"` with a previous chat, or an explicit channel/target).
|
||||
</Accordion>
|
||||
<Accordion title="Cron or heartbeat appears to prevent /new-style rollover">
|
||||
- Daily and idle reset freshness is not based on `updatedAt`; see [Session management](/concepts/session#session-lifecycle).
|
||||
- Cron wakeups, heartbeat runs, exec notifications, and gateway bookkeeping may update the session row for routing/status, but they do not extend `sessionStartedAt` or `lastInteractionAt`.
|
||||
- For legacy rows created before those fields existed, OpenClaw can recover `sessionStartedAt` from the transcript JSONL session header when the file is still available. Legacy idle rows without `lastInteractionAt` use that recovered start time as their idle baseline.
|
||||
</Accordion>
|
||||
<Accordion title="Timezone gotchas">
|
||||
- Cron without `--tz` uses the gateway host timezone.
|
||||
- `at` schedules without timezone are treated as UTC.
|
||||
- Heartbeat `activeHours` uses configured timezone resolution.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ read_when:
|
||||
- Debugging delivery failures for detached agent runs
|
||||
- Understanding how background runs relate to sessions, cron, and heartbeat
|
||||
title: "Background tasks"
|
||||
sidebarTitle: "Background tasks"
|
||||
---
|
||||
|
||||
> **Looking for scheduling?** See [Automation & Tasks](/automation) for choosing the right mechanism. This page covers **tracking** background work, not scheduling it.
|
||||
<Note>
|
||||
Looking for scheduling? See [Automation & Tasks](/automation) for choosing the right mechanism. This page covers **tracking** background work, not scheduling it.
|
||||
</Note>
|
||||
|
||||
Background tasks track work that runs **outside your main conversation session**:
|
||||
ACP runs, subagent spawns, isolated cron job executions, and CLI-initiated operations.
|
||||
Background tasks track work that runs **outside your main conversation session**: ACP runs, subagent spawns, isolated cron job executions, and CLI-initiated operations.
|
||||
|
||||
Tasks do **not** replace sessions, cron jobs, or heartbeats — they are the **activity ledger** that records what detached work happened, when, and whether it succeeded.
|
||||
|
||||
@@ -24,48 +26,63 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
|
||||
- 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.
|
||||
- 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.
|
||||
- Isolated cron delivery suppresses stale interim parent replies while descendant subagent work is still draining, and it prefers final descendant output when that arrives before delivery.
|
||||
- Completion notifications are delivered directly to a channel or queued for the next heartbeat.
|
||||
- `openclaw tasks list` shows all tasks; `openclaw tasks audit` surfaces issues.
|
||||
- Terminal records are kept for 7 days, then automatically pruned.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# List all tasks (newest first)
|
||||
openclaw tasks list
|
||||
<Tabs>
|
||||
<Tab title="List and filter">
|
||||
```bash
|
||||
# List all tasks (newest first)
|
||||
openclaw tasks list
|
||||
|
||||
# Filter by runtime or status
|
||||
openclaw tasks list --runtime acp
|
||||
openclaw tasks list --status running
|
||||
# Filter by runtime or status
|
||||
openclaw tasks list --runtime acp
|
||||
openclaw tasks list --status running
|
||||
```
|
||||
|
||||
# Show details for a specific task (by ID, run ID, or session key)
|
||||
openclaw tasks show <lookup>
|
||||
</Tab>
|
||||
<Tab title="Inspect">
|
||||
```bash
|
||||
# Show details for a specific task (by ID, run ID, or session key)
|
||||
openclaw tasks show <lookup>
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Cancel and notify">
|
||||
```bash
|
||||
# Cancel a running task (kills the child session)
|
||||
openclaw tasks cancel <lookup>
|
||||
|
||||
# Cancel a running task (kills the child session)
|
||||
openclaw tasks cancel <lookup>
|
||||
# Change notification policy for a task
|
||||
openclaw tasks notify <lookup> state_changes
|
||||
```
|
||||
|
||||
# Change notification policy for a task
|
||||
openclaw tasks notify <lookup> state_changes
|
||||
</Tab>
|
||||
<Tab title="Audit and maintenance">
|
||||
```bash
|
||||
# Run a health audit
|
||||
openclaw tasks audit
|
||||
|
||||
# Run a health audit
|
||||
openclaw tasks audit
|
||||
# Preview or apply maintenance
|
||||
openclaw tasks maintenance
|
||||
openclaw tasks maintenance --apply
|
||||
```
|
||||
|
||||
# Preview or apply maintenance
|
||||
openclaw tasks maintenance
|
||||
openclaw tasks maintenance --apply
|
||||
|
||||
# Inspect TaskFlow state
|
||||
openclaw tasks flow list
|
||||
openclaw tasks flow show <lookup>
|
||||
openclaw tasks flow cancel <lookup>
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Task flow">
|
||||
```bash
|
||||
# Inspect TaskFlow state
|
||||
openclaw tasks flow list
|
||||
openclaw tasks flow show <lookup>
|
||||
openclaw tasks flow cancel <lookup>
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## What creates a task
|
||||
|
||||
@@ -77,17 +94,22 @@ openclaw tasks flow cancel <lookup>
|
||||
| CLI operations | `cli` | `openclaw agent` commands that run through the gateway | `silent` |
|
||||
| Agent media jobs | `cli` | Session-backed `video_generate` runs | `silent` |
|
||||
|
||||
Main-session cron tasks use `silent` notify policy by default — they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Notify defaults for cron and media">
|
||||
Main-session cron tasks use `silent` notify policy by default — they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
|
||||
|
||||
Session-backed `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished video itself. If you opt into `tools.media.asyncCompletion.directSend`, async `music_generate` and `video_generate` completions try direct channel delivery first before falling back to the requester-session wake path.
|
||||
Session-backed `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished video itself. If you opt into `tools.media.asyncCompletion.directSend`, async `music_generate` and `video_generate` completions try direct channel delivery first before falling back to the requester-session wake path.
|
||||
|
||||
While a session-backed `video_generate` task is still active, the tool also acts as a guardrail: repeated `video_generate` calls in that same session return the active task status instead of starting a second concurrent generation. Use `action: "status"` when you want an explicit progress/status lookup from the agent side.
|
||||
|
||||
**What does not create tasks:**
|
||||
|
||||
- Heartbeat turns — main-session; see [Heartbeat](/gateway/heartbeat)
|
||||
- Normal interactive chat turns
|
||||
- Direct `/command` responses
|
||||
</Accordion>
|
||||
<Accordion title="Concurrent video_generate guardrail">
|
||||
While a session-backed `video_generate` task is still active, the tool also acts as a guardrail: repeated `video_generate` calls in that same session return the active task status instead of starting a second concurrent generation. Use `action: "status"` when you want an explicit progress/status lookup from the agent side.
|
||||
</Accordion>
|
||||
<Accordion title="What does not create tasks">
|
||||
- Heartbeat turns — main-session; see [Heartbeat](/gateway/heartbeat)
|
||||
- Normal interactive chat turns
|
||||
- Direct `/command` responses
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Task lifecycle
|
||||
|
||||
@@ -115,12 +137,14 @@ stateDiagram-v2
|
||||
|
||||
Transitions happen automatically — when the associated agent run ends, the task status updates to match.
|
||||
|
||||
Agent run completion is authoritative for active task records. A successful detached run finalizes as `succeeded`, ordinary run errors finalize as `failed`, and timeout or abort outcomes finalize as `timed_out`. If an operator already cancelled the task, or the runtime already recorded a stronger terminal state such as `failed`, `timed_out`, or `lost`, a later success signal does not downgrade that terminal status.
|
||||
|
||||
`lost` is runtime-aware:
|
||||
|
||||
- ACP tasks: backing ACP child session metadata disappeared.
|
||||
- Subagent tasks: backing child session disappeared from the target agent store.
|
||||
- Cron tasks: the cron runtime no longer tracks the job as active.
|
||||
- CLI tasks: isolated child-session tasks use the child session; chat-backed CLI tasks use the live run context instead, so lingering channel/group/direct session rows do not keep them alive.
|
||||
- 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
|
||||
|
||||
@@ -134,9 +158,7 @@ When a task reaches a terminal state, OpenClaw notifies you. There are two deliv
|
||||
Task completion triggers an immediate heartbeat wake so you see the result quickly — you do not have to wait for the next scheduled heartbeat tick.
|
||||
</Tip>
|
||||
|
||||
That means the usual workflow is push-based: start detached work once, then let
|
||||
the runtime wake or notify you on completion. Poll task state only when you
|
||||
need debugging, intervention, or an explicit audit.
|
||||
That means the usual workflow is push-based: start detached work once, then let the runtime wake or notify you on completion. Poll task state only when you need debugging, intervention, or an explicit audit.
|
||||
|
||||
### Notification policies
|
||||
|
||||
@@ -156,96 +178,93 @@ openclaw tasks notify <lookup> state_changes
|
||||
|
||||
## CLI reference
|
||||
|
||||
### `tasks list`
|
||||
<AccordionGroup>
|
||||
<Accordion title="tasks list">
|
||||
```bash
|
||||
openclaw tasks list [--runtime <acp|subagent|cron|cli>] [--status <status>] [--json]
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw tasks list [--runtime <acp|subagent|cron|cli>] [--status <status>] [--json]
|
||||
```
|
||||
Output columns: Task ID, Kind, Status, Delivery, Run ID, Child Session, Summary.
|
||||
|
||||
Output columns: Task ID, Kind, Status, Delivery, Run ID, Child Session, Summary.
|
||||
</Accordion>
|
||||
<Accordion title="tasks show">
|
||||
```bash
|
||||
openclaw tasks show <lookup>
|
||||
```
|
||||
|
||||
### `tasks show`
|
||||
The lookup token accepts a task ID, run ID, or session key. Shows the full record including timing, delivery state, error, and terminal summary.
|
||||
|
||||
```bash
|
||||
openclaw tasks show <lookup>
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="tasks cancel">
|
||||
```bash
|
||||
openclaw tasks cancel <lookup>
|
||||
```
|
||||
|
||||
The lookup token accepts a task ID, run ID, or session key. Shows the full record including timing, delivery state, error, and terminal summary.
|
||||
For ACP and subagent tasks, this kills the child session. For CLI-tracked tasks, cancellation is recorded in the task registry (there is no separate child runtime handle). Status transitions to `cancelled` and a delivery notification is sent when applicable.
|
||||
|
||||
### `tasks cancel`
|
||||
</Accordion>
|
||||
<Accordion title="tasks notify">
|
||||
```bash
|
||||
openclaw tasks notify <lookup> <done_only|state_changes|silent>
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="tasks audit">
|
||||
```bash
|
||||
openclaw tasks audit [--json]
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw tasks cancel <lookup>
|
||||
```
|
||||
Surfaces operational issues. Findings also appear in `openclaw status` when issues are detected.
|
||||
|
||||
For ACP and subagent tasks, this kills the child session. For CLI-tracked tasks, cancellation is recorded in the task registry (there is no separate child runtime handle). Status transitions to `cancelled` and a delivery notification is sent when applicable.
|
||||
| Finding | Severity | Trigger |
|
||||
| ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `stale_queued` | warn | Queued for more than 10 minutes |
|
||||
| `stale_running` | error | Running for more than 30 minutes |
|
||||
| `lost` | warn/error | Runtime-backed task ownership disappeared; retained lost tasks warn until `cleanupAfter`, then become errors |
|
||||
| `delivery_failed` | warn | Delivery failed and notify policy is not `silent` |
|
||||
| `missing_cleanup` | warn | Terminal task with no cleanup timestamp |
|
||||
| `inconsistent_timestamps` | warn | Timeline violation (for example ended before started) |
|
||||
|
||||
### `tasks notify`
|
||||
</Accordion>
|
||||
<Accordion title="tasks maintenance">
|
||||
```bash
|
||||
openclaw tasks maintenance [--json]
|
||||
openclaw tasks maintenance --apply [--json]
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw tasks notify <lookup> <done_only|state_changes|silent>
|
||||
```
|
||||
Use this to preview or apply reconciliation, cleanup stamping, and pruning for tasks and Task Flow state.
|
||||
|
||||
### `tasks audit`
|
||||
Reconciliation is runtime-aware:
|
||||
|
||||
```bash
|
||||
openclaw tasks audit [--json]
|
||||
```
|
||||
- ACP/subagent tasks check their backing child session.
|
||||
- Cron tasks check whether the cron runtime still owns the job.
|
||||
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
|
||||
|
||||
Surfaces operational issues. Findings also appear in `openclaw status` when issues are detected.
|
||||
Completion cleanup is also runtime-aware:
|
||||
|
||||
| Finding | Severity | Trigger |
|
||||
| ------------------------- | -------- | ----------------------------------------------------- |
|
||||
| `stale_queued` | warn | Queued for more than 10 minutes |
|
||||
| `stale_running` | error | Running for more than 30 minutes |
|
||||
| `lost` | error | Runtime-backed task ownership disappeared |
|
||||
| `delivery_failed` | warn | Delivery failed and notify policy is not `silent` |
|
||||
| `missing_cleanup` | warn | Terminal task with no cleanup timestamp |
|
||||
| `inconsistent_timestamps` | warn | Timeline violation (for example ended before started) |
|
||||
- Subagent completion best-effort closes tracked browser tabs/processes for the child session before announce cleanup continues.
|
||||
- Isolated cron completion best-effort closes tracked browser tabs/processes for the cron session before the run fully tears down.
|
||||
- Isolated cron delivery waits out descendant subagent follow-up when needed and suppresses stale parent acknowledgement text instead of announcing it.
|
||||
- Subagent completion delivery prefers the latest visible assistant text; if that is empty it falls back to sanitized latest tool/toolResult text, and timeout-only tool-call runs can collapse to a short partial-progress summary. Terminal failed runs announce failure status without replaying captured reply text.
|
||||
- Cleanup failures do not mask the real task outcome.
|
||||
|
||||
### `tasks maintenance`
|
||||
</Accordion>
|
||||
<Accordion title="tasks flow list | show | cancel">
|
||||
```bash
|
||||
openclaw tasks flow list [--status <status>] [--json]
|
||||
openclaw tasks flow show <lookup> [--json]
|
||||
openclaw tasks flow cancel <lookup>
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw tasks maintenance [--json]
|
||||
openclaw tasks maintenance --apply [--json]
|
||||
```
|
||||
Use these when the orchestrating Task Flow is the thing you care about rather than one individual background task record.
|
||||
|
||||
Use this to preview or apply reconciliation, cleanup stamping, and pruning for
|
||||
tasks and Task Flow state.
|
||||
|
||||
Reconciliation is runtime-aware:
|
||||
|
||||
- ACP/subagent tasks check their backing child session.
|
||||
- Cron tasks check whether the cron runtime still owns the job.
|
||||
- Chat-backed CLI tasks check the owning live run context, not just the chat session row.
|
||||
|
||||
Completion cleanup is also runtime-aware:
|
||||
|
||||
- Subagent completion best-effort closes tracked browser tabs/processes for the child session before announce cleanup continues.
|
||||
- Isolated cron completion best-effort closes tracked browser tabs/processes for the cron session before the run fully tears down.
|
||||
- Isolated cron delivery waits out descendant subagent follow-up when needed and
|
||||
suppresses stale parent acknowledgement text instead of announcing it.
|
||||
- Subagent completion delivery prefers the latest visible assistant text; if that is empty it falls back to sanitized latest tool/toolResult text, and timeout-only tool-call runs can collapse to a short partial-progress summary. Terminal failed runs announce failure status without replaying captured reply text.
|
||||
- Cleanup failures do not mask the real task outcome.
|
||||
|
||||
### `tasks flow list|show|cancel`
|
||||
|
||||
```bash
|
||||
openclaw tasks flow list [--status <status>] [--json]
|
||||
openclaw tasks flow show <lookup> [--json]
|
||||
openclaw tasks flow cancel <lookup>
|
||||
```
|
||||
|
||||
Use these when the orchestrating Task Flow is the thing you care about rather
|
||||
than one individual background task record.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Chat task board (`/tasks`)
|
||||
|
||||
Use `/tasks` in any chat session to see background tasks linked to that session. The board shows
|
||||
active and recently completed tasks with runtime, status, timing, and progress or error detail.
|
||||
Use `/tasks` in any chat session to see background tasks linked to that session. The board shows active and recently completed tasks with runtime, status, timing, and progress or error detail.
|
||||
|
||||
When the current session has no visible linked tasks, `/tasks` falls back to agent-local task counts
|
||||
so you still get an overview without leaking other-session details.
|
||||
When the current session has no visible linked tasks, `/tasks` falls back to agent-local task counts so you still get an overview without leaking other-session details.
|
||||
|
||||
For the full operator ledger, use the CLI: `openclaw tasks list`.
|
||||
|
||||
@@ -263,9 +282,7 @@ The summary reports:
|
||||
- **failures** — count of `failed` + `timed_out` + `lost`
|
||||
- **byRuntime** — breakdown by `acp`, `subagent`, `cron`, `cli`
|
||||
|
||||
Both `/status` and the `session_status` tool use a cleanup-aware task snapshot: active tasks are
|
||||
preferred, stale completed rows are hidden, and recent failures only surface when no active work
|
||||
remains. This keeps the status card focused on what matters right now.
|
||||
Both `/status` and the `session_status` tool use a cleanup-aware task snapshot: active tasks are preferred, stale completed rows are hidden, and recent failures only surface when no active work remains. This keeps the status card focused on what matters right now.
|
||||
|
||||
## Storage and maintenance
|
||||
|
||||
@@ -283,44 +300,55 @@ The registry loads into memory at gateway start and syncs writes to SQLite for d
|
||||
|
||||
A sweeper runs every **60 seconds** and handles three things:
|
||||
|
||||
1. **Reconciliation** — checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and chat-backed CLI tasks use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
|
||||
2. **Cleanup stamping** — sets a `cleanupAfter` timestamp on terminal tasks (endedAt + 7 days).
|
||||
3. **Pruning** — deletes records past their `cleanupAfter` date.
|
||||
<Steps>
|
||||
<Step title="Reconciliation">
|
||||
Checks whether active tasks still have authoritative runtime backing. ACP/subagent tasks use child-session state, cron tasks use active-job ownership, and chat-backed CLI tasks use the owning run context. If that backing state is gone for more than 5 minutes, the task is marked `lost`.
|
||||
</Step>
|
||||
<Step title="Cleanup stamping">
|
||||
Sets a `cleanupAfter` timestamp on terminal tasks (endedAt + 7 days). During retention, lost tasks still appear in audit as warnings; after `cleanupAfter` expires or when cleanup metadata is missing, they are errors.
|
||||
</Step>
|
||||
<Step title="Pruning">
|
||||
Deletes records past their `cleanupAfter` date.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
**Retention**: terminal task records are kept for **7 days**, then automatically pruned. No configuration needed.
|
||||
<Note>
|
||||
**Retention:** terminal task records are kept for **7 days**, then automatically pruned. No configuration needed.
|
||||
</Note>
|
||||
|
||||
## How tasks relate to other systems
|
||||
|
||||
### Tasks and Task Flow
|
||||
<AccordionGroup>
|
||||
<Accordion title="Tasks and Task Flow">
|
||||
[Task Flow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow.
|
||||
|
||||
[Task Flow](/automation/taskflow) is the flow orchestration layer above background tasks. A single flow may coordinate multiple tasks over its lifetime using managed or mirrored sync modes. Use `openclaw tasks` to inspect individual task records and `openclaw tasks flow` to inspect the orchestrating flow.
|
||||
See [Task Flow](/automation/taskflow) for details.
|
||||
|
||||
See [Task Flow](/automation/taskflow) for details.
|
||||
</Accordion>
|
||||
<Accordion title="Tasks and cron">
|
||||
A cron job **definition** lives in `~/.openclaw/cron/jobs.json`; runtime execution state lives beside it in `~/.openclaw/cron/jobs-state.json`. **Every** cron execution creates a task record — both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications.
|
||||
|
||||
### Tasks and cron
|
||||
See [Cron Jobs](/automation/cron-jobs).
|
||||
|
||||
A cron job **definition** lives in `~/.openclaw/cron/jobs.json`; runtime execution state lives beside it in `~/.openclaw/cron/jobs-state.json`. **Every** cron execution creates a task record — both main-session and isolated. Main-session cron tasks default to `silent` notify policy so they track without generating notifications.
|
||||
</Accordion>
|
||||
<Accordion title="Tasks and heartbeat">
|
||||
Heartbeat runs are main-session turns — they do not create task records. When a task completes, it can trigger a heartbeat wake so you see the result promptly.
|
||||
|
||||
See [Cron Jobs](/automation/cron-jobs).
|
||||
See [Heartbeat](/gateway/heartbeat).
|
||||
|
||||
### Tasks and heartbeat
|
||||
|
||||
Heartbeat runs are main-session turns — they do not create task records. When a task completes, it can trigger a heartbeat wake so you see the result promptly.
|
||||
|
||||
See [Heartbeat](/gateway/heartbeat).
|
||||
|
||||
### Tasks and sessions
|
||||
|
||||
A task may reference a `childSessionKey` (where work runs) and a `requesterSessionKey` (who started it). Sessions are conversation context; tasks are activity tracking on top of that.
|
||||
|
||||
### Tasks and agent runs
|
||||
|
||||
A task's `runId` links to the agent run doing the work. Agent lifecycle events (start, end, error) automatically update the task status — you do not need to manage the lifecycle manually.
|
||||
</Accordion>
|
||||
<Accordion title="Tasks and sessions">
|
||||
A task may reference a `childSessionKey` (where work runs) and a `requesterSessionKey` (who started it). Sessions are conversation context; tasks are activity tracking on top of that.
|
||||
</Accordion>
|
||||
<Accordion title="Tasks and agent runs">
|
||||
A task's `runId` links to the agent run doing the work. Agent lifecycle events (start, end, error) automatically update the task status — you do not need to manage the lifecycle manually.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Related
|
||||
|
||||
- [Automation & Tasks](/automation) — all automation mechanisms at a glance
|
||||
- [Task Flow](/automation/taskflow) — flow orchestration above tasks
|
||||
- [Scheduled Tasks](/automation/cron-jobs) — scheduling background work
|
||||
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
|
||||
- [CLI: Tasks](/cli/tasks) — CLI command reference
|
||||
- [Heartbeat](/gateway/heartbeat) — periodic main-session turns
|
||||
- [Scheduled Tasks](/automation/cron-jobs) — scheduling background work
|
||||
- [Task Flow](/automation/taskflow) — flow orchestration above tasks
|
||||
|
||||
@@ -21,6 +21,8 @@ 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.
|
||||
- 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.
|
||||
|
||||
@@ -263,6 +263,10 @@ Now create some channels on your Discord server and start chatting. Your agent c
|
||||
|
||||
- Gateway owns the Discord connection.
|
||||
- Reply routing is deterministic: Discord inbound replies back to Discord.
|
||||
- Discord guild/channel metadata is added to the model prompt as untrusted
|
||||
context, not as a user-visible reply prefix. If a model copies that envelope
|
||||
back, OpenClaw strips the copied metadata from outbound replies and from
|
||||
future replay context.
|
||||
- By default (`session.dmScope=main`), direct chats share the agent main session (`agent:main:main`).
|
||||
- Guild channels are isolated session keys (`agent:<agentId>:discord:channel:<channelId>`).
|
||||
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).
|
||||
|
||||
@@ -414,6 +414,15 @@ Full configuration: [Gateway configuration](/gateway/configuration)
|
||||
- ✅ Video/media
|
||||
- ✅ Stickers
|
||||
|
||||
Inbound Feishu/Lark audio messages are normalized as media placeholders instead
|
||||
of raw `file_key` JSON. When `tools.media.audio` is configured, OpenClaw
|
||||
downloads the voice-note resource and runs shared audio transcription before the
|
||||
agent turn, so the agent receives the spoken transcript. If Feishu includes
|
||||
transcript text directly in the audio payload, that text is used without another
|
||||
ASR call. Without an audio transcription provider, the agent still receives a
|
||||
`<media:audio>` placeholder plus the saved attachment, not the raw Feishu
|
||||
resource payload.
|
||||
|
||||
### Send
|
||||
|
||||
- ✅ Text
|
||||
|
||||
@@ -272,6 +272,7 @@ Notes:
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -209,6 +209,10 @@ Approval prompts generated by the bot itself (for example, "allow this action?"
|
||||
- **Bot replies "gone to Mars":** credentials not configured or Gateway not started.
|
||||
- **No inbound messages:** verify `appId` and `clientSecret` are correct, and the
|
||||
bot is enabled on the QQ Open Platform.
|
||||
- **Repeated self-replies:** OpenClaw records QQ outbound ref indexes as
|
||||
bot-authored and ignores inbound events whose current `msgIdx` matches that
|
||||
same bot account. This prevents platform echo loops while still allowing users
|
||||
to quote or reply to previous bot messages.
|
||||
- **Setup with `--token-file` still shows unconfigured:** `--token-file` only sets
|
||||
the AppSecret. You still need `appId` in config or `QQBOT_APP_ID`.
|
||||
- **Proactive messages not arriving:** QQ may intercept bot-initiated messages if
|
||||
|
||||
@@ -489,6 +489,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
- `first`
|
||||
- `all`
|
||||
|
||||
When reply threading is enabled and the original Telegram text or caption is available, OpenClaw includes a native Telegram quote excerpt automatically. Telegram caps native quote text at 1024 UTF-16 code units, so longer messages are quoted from the start and fall back to a plain reply if Telegram rejects the quote.
|
||||
|
||||
Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -151,6 +151,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
|
||||
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
|
||||
- Group sessions are isolated (`agent:<agentId>:whatsapp:group:<jid>`).
|
||||
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` / lowercase variants). Prefer host-level proxy config over channel-specific WhatsApp proxy settings.
|
||||
- When `messages.removeAckAfterReply` is enabled, OpenClaw clears the WhatsApp ack reaction after a visible reply is delivered.
|
||||
|
||||
## Plugin hooks and privacy
|
||||
|
||||
@@ -243,6 +244,7 @@ content and identifiers.
|
||||
|
||||
- explicit WhatsApp mentions of the bot identity
|
||||
- configured mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`)
|
||||
- inbound voice-note transcripts for authorized group messages
|
||||
- implicit reply-to-bot detection (reply sender matches bot identity)
|
||||
|
||||
Security note:
|
||||
@@ -295,6 +297,11 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
|
||||
- `<media:document>`
|
||||
- `<media:sticker>`
|
||||
|
||||
Authorized group voice notes are transcribed before mention gating when the
|
||||
body is only `<media:audio>`, so saying the bot mention in the voice note can
|
||||
trigger the reply. If the transcript still does not mention the bot, the
|
||||
transcript is kept in pending group history instead of the raw placeholder.
|
||||
|
||||
Location bodies use terse coordinate text. Location labels/comments and contact/vCard details are rendered as fenced untrusted metadata, not inline prompt text.
|
||||
|
||||
</Accordion>
|
||||
@@ -365,6 +372,7 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
|
||||
- reply payloads preserve `audioAsVoice`; TTS voice-note output for WhatsApp stays on this PTT path even when the provider returns MP3 or WebM
|
||||
- native Ogg/Opus audio is sent as `audio/ogg; codecs=opus` for voice-note compatibility
|
||||
- non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded with `ffmpeg` to 48 kHz mono Ogg/Opus before PTT delivery
|
||||
- `/tts latest` sends the latest assistant reply as one voice note and suppresses repeat sends for the same reply; `/tts chat on|off|default` controls auto-TTS for the current WhatsApp chat
|
||||
- animated GIF playback is supported via `gifPlayback: true` on video sends
|
||||
- captions are applied to the first media item when sending multi-media reply payloads, except PTT voice notes send the audio first and visible text separately because WhatsApp clients do not render voice-note captions consistently
|
||||
- media source can be HTTP(S), `file://`, or local paths
|
||||
|
||||
@@ -55,6 +55,7 @@ Detailed guidance: [Browser troubleshooting](/tools/browser#cdp-startup-failure-
|
||||
```bash
|
||||
openclaw browser status
|
||||
openclaw browser doctor
|
||||
openclaw browser doctor --deep
|
||||
openclaw browser start
|
||||
openclaw browser start --headless
|
||||
openclaw browser stop
|
||||
@@ -63,6 +64,8 @@ openclaw browser --browser-profile openclaw reset-profile
|
||||
|
||||
Notes:
|
||||
|
||||
- `doctor --deep` adds a live snapshot probe. It is useful when basic CDP
|
||||
readiness is green but you want proof that the current tab can be inspected.
|
||||
- For `attachOnly` and remote CDP profiles, `openclaw browser stop` closes the
|
||||
active control session and clears temporary emulation overrides even when
|
||||
OpenClaw did not launch the browser process itself.
|
||||
|
||||
@@ -324,6 +324,7 @@ Command options:
|
||||
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`.
|
||||
|
||||
@@ -159,6 +159,7 @@ openclaw infer image generate --prompt "cinematic product photo of headphones" -
|
||||
openclaw infer image generate --model openai/gpt-image-1.5 --output-format png --background transparent --prompt "simple red circle sticker on a transparent background" --json
|
||||
openclaw infer image generate --prompt "slow image backend" --timeout-ms 180000 --json
|
||||
openclaw infer image edit --file ./logo.png --model openai/gpt-image-1.5 --output-format png --background transparent --prompt "keep the logo, remove the background" --json
|
||||
openclaw infer image edit --file ./poster.png --prompt "make this a vertical story ad" --size 2160x3840 --aspect-ratio 9:16 --resolution 4K --json
|
||||
openclaw infer image describe --file ./photo.jpg --json
|
||||
openclaw infer image describe --file ./ui-screenshot.png --model openai/gpt-4.1-mini --json
|
||||
openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --json
|
||||
@@ -167,6 +168,8 @@ openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --j
|
||||
Notes:
|
||||
|
||||
- Use `image edit` when starting from existing input files.
|
||||
- Use `--size`, `--aspect-ratio`, or `--resolution` with `image edit` for
|
||||
providers/models that support geometry hints on reference-image edits.
|
||||
- Use `--output-format png --background transparent` with
|
||||
`--model openai/gpt-image-1.5` for transparent-background OpenAI PNG output;
|
||||
`--openai-background` remains available as an OpenAI-specific alias. Providers
|
||||
|
||||
@@ -47,9 +47,10 @@ Notes:
|
||||
- `models list` is read-only: it reads config, auth profiles, existing catalog
|
||||
state, and provider-owned catalog rows, but it does not rewrite
|
||||
`models.json`.
|
||||
- `models list --all` includes bundled provider-owned static catalog rows even
|
||||
when you have not authenticated with that provider yet. Those rows still show
|
||||
as unavailable until matching auth is configured.
|
||||
- `models list --all --provider <id>` can include provider-owned static catalog
|
||||
rows from plugin manifests or bundled provider catalog metadata even when you
|
||||
have not authenticated with that provider yet. Those rows still show as
|
||||
unavailable until matching auth is configured.
|
||||
- `models list` keeps native model metadata and runtime caps distinct. In table
|
||||
output, `Ctx` shows `contextTokens/contextWindow` when an effective runtime
|
||||
cap differs from the native context window; JSON rows include `contextTokens`
|
||||
@@ -153,6 +154,9 @@ provider you choose.
|
||||
|
||||
`models auth login` runs a provider plugin’s auth flow (OAuth/API key). Use
|
||||
`openclaw plugins list` to see which providers are installed.
|
||||
Use `openclaw models auth --agent <id> <subcommand>` to write auth results to a
|
||||
specific configured agent store. The parent `--agent` flag is honored by
|
||||
`add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`.
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ Manage the service:
|
||||
|
||||
```bash
|
||||
openclaw node status
|
||||
openclaw node start
|
||||
openclaw node stop
|
||||
openclaw node restart
|
||||
openclaw node uninstall
|
||||
|
||||
@@ -122,6 +122,9 @@ installs the bundled plugin directly. To install an npm package with the same
|
||||
name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
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.
|
||||
|
||||
@@ -244,6 +247,9 @@ 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
|
||||
|
||||
@@ -255,13 +261,10 @@ 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.
|
||||
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`.
|
||||
|
||||
By default, uninstall also removes the plugin install directory under the active
|
||||
state-dir plugin root. Use
|
||||
`--keep-files` to keep files on disk.
|
||||
|
||||
`--keep-config` is supported as a deprecated alias for `--keep-files`.
|
||||
|
||||
### Update
|
||||
|
||||
@@ -75,7 +75,7 @@ Cancels a running background task.
|
||||
openclaw tasks audit [--severity <warn|error>] [--code <name>] [--limit <n>] [--json]
|
||||
```
|
||||
|
||||
Surfaces stale, lost, delivery-failed, or otherwise inconsistent task and Task Flow records.
|
||||
Surfaces stale, lost, delivery-failed, or otherwise inconsistent task and Task Flow records. Lost tasks retained until `cleanupAfter` are warnings; expired or unstamped lost tasks are errors.
|
||||
|
||||
### `maintenance`
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ openclaw --update
|
||||
|
||||
## Options
|
||||
|
||||
- `--no-restart`: skip restarting the Gateway service after a successful update.
|
||||
- `--no-restart`: skip restarting the Gateway service after a successful update. Package-manager updates that do restart the Gateway verify the restarted service reports the expected updated version before the command succeeds.
|
||||
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
||||
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
|
||||
- `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting.
|
||||
|
||||
@@ -27,6 +27,24 @@ 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.
|
||||
|
||||
## Three things named Codex
|
||||
|
||||
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. |
|
||||
| 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. |
|
||||
|
||||
Those surfaces are intentionally independent. Enabling the `codex` plugin makes
|
||||
the native app-server features available; it does not rewrite
|
||||
`openai-codex/*` into `openai/*`, does not change existing sessions, and does
|
||||
not make ACP the Codex default. Selecting `openai-codex/*` means "use the Codex
|
||||
OAuth provider route" unless you separately force a runtime.
|
||||
|
||||
The common Codex setup uses the `openai` provider with the `codex` runtime:
|
||||
|
||||
```json5
|
||||
@@ -53,6 +71,19 @@ Codex only when the user explicitly asks for ACP/acpx or is testing the ACP
|
||||
adapter path. Claude Code, Gemini CLI, OpenCode, Cursor, and similar external
|
||||
harnesses still use ACP.
|
||||
|
||||
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"`.
|
||||
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
|
||||
ACP with `runtime: "acp"` and `agentId: "codex"`.
|
||||
5. If the request is for **Claude Code, Gemini CLI, OpenCode, Cursor, Droid, or
|
||||
another external harness**, use ACP/acpx, not the native sub-agent runtime.
|
||||
|
||||
| You mean... | Use... |
|
||||
| --------------------------------------- | -------------------------------------------- |
|
||||
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
|
||||
@@ -106,6 +137,18 @@ 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"`.
|
||||
|
||||
`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
|
||||
`openai-codex/*` as the explicit PI Codex OAuth route and avoids silently
|
||||
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
|
||||
Codex app-server execution.
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
When a runtime is not PI, it should document what OpenClaw surfaces it supports.
|
||||
|
||||
@@ -5,115 +5,114 @@ read_when:
|
||||
- You are switching between the legacy engine and a plugin engine
|
||||
- You are building a context engine plugin
|
||||
title: "Context engine"
|
||||
sidebarTitle: "Context engine"
|
||||
---
|
||||
|
||||
A **context engine** controls how OpenClaw builds model context for each run:
|
||||
which messages to include, how to summarize older history, and how to manage
|
||||
context across subagent boundaries.
|
||||
A **context engine** controls how OpenClaw builds model context for each run: which messages to include, how to summarize older history, and how to manage context across subagent boundaries.
|
||||
|
||||
OpenClaw ships with a built-in `legacy` engine and uses it by default — most
|
||||
users never need to change this. Install and select a plugin engine only when
|
||||
you want different assembly, compaction, or cross-session recall behavior.
|
||||
OpenClaw ships with a built-in `legacy` engine and uses it by default — most users never need to change this. Install and select a plugin engine only when you want different assembly, compaction, or cross-session recall behavior.
|
||||
|
||||
## Quick start
|
||||
|
||||
Check which engine is active:
|
||||
<Steps>
|
||||
<Step title="Check which engine is active">
|
||||
```bash
|
||||
openclaw doctor
|
||||
# or inspect config directly:
|
||||
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
|
||||
```
|
||||
</Step>
|
||||
<Step title="Install a plugin engine">
|
||||
Context engine plugins are installed like any other OpenClaw plugin.
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
# or inspect config directly:
|
||||
cat ~/.openclaw/openclaw.json | jq '.plugins.slots.contextEngine'
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="From npm">
|
||||
```bash
|
||||
openclaw plugins install @martian-engineering/lossless-claw
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="From a local path">
|
||||
```bash
|
||||
openclaw plugins install -l ./my-context-engine
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Installing a context engine plugin
|
||||
|
||||
Context engine plugins are installed like any other OpenClaw plugin. Install
|
||||
first, then select the engine in the slot:
|
||||
|
||||
```bash
|
||||
# Install from npm
|
||||
openclaw plugins install @martian-engineering/lossless-claw
|
||||
|
||||
# Or install from a local path (for development)
|
||||
openclaw plugins install -l ./my-context-engine
|
||||
```
|
||||
|
||||
Then enable the plugin and select it as the active engine in your config:
|
||||
|
||||
```json5
|
||||
// openclaw.json
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
contextEngine: "lossless-claw", // must match the plugin's registered engine id
|
||||
},
|
||||
entries: {
|
||||
"lossless-claw": {
|
||||
enabled: true,
|
||||
// Plugin-specific config goes here (see the plugin's docs)
|
||||
</Step>
|
||||
<Step title="Enable and select the engine">
|
||||
```json5
|
||||
// openclaw.json
|
||||
{
|
||||
plugins: {
|
||||
slots: {
|
||||
contextEngine: "lossless-claw", // must match the plugin's registered engine id
|
||||
},
|
||||
entries: {
|
||||
"lossless-claw": {
|
||||
enabled: true,
|
||||
// Plugin-specific config goes here (see the plugin's docs)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
Restart the gateway after installing and configuring.
|
||||
Restart the gateway after installing and configuring.
|
||||
|
||||
To switch back to the built-in engine, set `contextEngine` to `"legacy"` (or
|
||||
remove the key entirely — `"legacy"` is the default).
|
||||
</Step>
|
||||
<Step title="Switch back to legacy (optional)">
|
||||
Set `contextEngine` to `"legacy"` (or remove the key entirely — `"legacy"` is the default).
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How it works
|
||||
|
||||
Every time OpenClaw runs a model prompt, the context engine participates at
|
||||
four lifecycle points:
|
||||
Every time OpenClaw runs a model prompt, the context engine participates at four lifecycle points:
|
||||
|
||||
1. **Ingest** — called when a new message is added to the session. The engine
|
||||
can store or index the message in its own data store.
|
||||
2. **Assemble** — called before each model run. The engine returns an ordered
|
||||
set of messages (and an optional `systemPromptAddition`) that fit within
|
||||
the token budget.
|
||||
3. **Compact** — called when the context window is full, or when the user runs
|
||||
`/compact`. The engine summarizes older history to free space.
|
||||
4. **After turn** — called after a run completes. The engine can persist state,
|
||||
trigger background compaction, or update indexes.
|
||||
<AccordionGroup>
|
||||
<Accordion title="1. Ingest">
|
||||
Called when a new message is added to the session. The engine can store or index the message in its own data store.
|
||||
</Accordion>
|
||||
<Accordion title="2. Assemble">
|
||||
Called before each model run. The engine returns an ordered set of messages (and an optional `systemPromptAddition`) that fit within the token budget.
|
||||
</Accordion>
|
||||
<Accordion title="3. Compact">
|
||||
Called when the context window is full, or when the user runs `/compact`. The engine summarizes older history to free space.
|
||||
</Accordion>
|
||||
<Accordion title="4. After turn">
|
||||
Called after a run completes. The engine can persist state, trigger background compaction, or update indexes.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by
|
||||
projecting assembled context into Codex developer instructions and the current
|
||||
turn prompt. Codex still owns its native thread history and native compactor.
|
||||
For the bundled non-ACP Codex harness, OpenClaw applies the same lifecycle by projecting assembled context into Codex developer instructions and the current turn prompt. Codex still owns its native thread history and native compactor.
|
||||
|
||||
### Subagent lifecycle (optional)
|
||||
|
||||
OpenClaw calls two optional subagent lifecycle hooks:
|
||||
|
||||
- **prepareSubagentSpawn** — prepare shared context state before a child run
|
||||
starts. The hook receives parent/child session keys, `contextMode`
|
||||
(`isolated` or `fork`), available transcript ids/files, and optional TTL.
|
||||
If it returns a rollback handle, OpenClaw calls it when spawn fails after
|
||||
preparation succeeds.
|
||||
- **onSubagentEnded** — clean up when a subagent session completes or is swept.
|
||||
<ParamField path="prepareSubagentSpawn" type="method">
|
||||
Prepare shared context state before a child run starts. The hook receives parent/child session keys, `contextMode` (`isolated` or `fork`), available transcript ids/files, and optional TTL. If it returns a rollback handle, OpenClaw calls it when spawn fails after preparation succeeds.
|
||||
</ParamField>
|
||||
<ParamField path="onSubagentEnded" type="method">
|
||||
Clean up when a subagent session completes or is swept.
|
||||
</ParamField>
|
||||
|
||||
### System prompt addition
|
||||
|
||||
The `assemble` method can return a `systemPromptAddition` string. OpenClaw
|
||||
prepends this to the system prompt for the run. This lets engines inject
|
||||
dynamic recall guidance, retrieval instructions, or context-aware hints
|
||||
without requiring static workspace files.
|
||||
The `assemble` method can return a `systemPromptAddition` string. OpenClaw prepends this to the system prompt for the run. This lets engines inject dynamic recall guidance, retrieval instructions, or context-aware hints without requiring static workspace files.
|
||||
|
||||
## The legacy engine
|
||||
|
||||
The built-in `legacy` engine preserves OpenClaw's original behavior:
|
||||
|
||||
- **Ingest**: no-op (the session manager handles message persistence directly).
|
||||
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline
|
||||
in the runtime handles context assembly).
|
||||
- **Compact**: delegates to the built-in summarization compaction, which creates
|
||||
a single summary of older messages and keeps recent messages intact.
|
||||
- **Assemble**: pass-through (the existing sanitize → validate → limit pipeline in the runtime handles context assembly).
|
||||
- **Compact**: delegates to the built-in summarization compaction, which creates a single summary of older messages and keeps recent messages intact.
|
||||
- **After turn**: no-op.
|
||||
|
||||
The legacy engine does not register tools or provide a `systemPromptAddition`.
|
||||
|
||||
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this
|
||||
engine is used automatically.
|
||||
When no `plugins.slots.contextEngine` is set (or it's set to `"legacy"`), this engine is used automatically.
|
||||
|
||||
## Plugin engines
|
||||
|
||||
@@ -185,11 +184,15 @@ Required members:
|
||||
|
||||
`assemble` returns an `AssembleResult` with:
|
||||
|
||||
- `messages` — the ordered messages to send to the model.
|
||||
- `estimatedTokens` (required, `number`) — the engine's estimate of total
|
||||
tokens in the assembled context. OpenClaw uses this for compaction threshold
|
||||
decisions and diagnostic reporting.
|
||||
- `systemPromptAddition` (optional, `string`) — prepended to the system prompt.
|
||||
<ParamField path="messages" type="Message[]" required>
|
||||
The ordered messages to send to the model.
|
||||
</ParamField>
|
||||
<ParamField path="estimatedTokens" type="number" required>
|
||||
The engine's estimate of total tokens in the assembled context. OpenClaw uses this for compaction threshold decisions and diagnostic reporting.
|
||||
</ParamField>
|
||||
<ParamField path="systemPromptAddition" type="string">
|
||||
Prepended to the system prompt.
|
||||
</ParamField>
|
||||
|
||||
Optional members:
|
||||
|
||||
@@ -204,34 +207,33 @@ Optional members:
|
||||
|
||||
### ownsCompaction
|
||||
|
||||
`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays
|
||||
enabled for the run:
|
||||
`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays enabled for the run:
|
||||
|
||||
- `true` — the engine owns compaction behavior. OpenClaw disables Pi's built-in
|
||||
auto-compaction for that run, and the engine's `compact()` implementation is
|
||||
responsible for `/compact`, overflow recovery compaction, and any proactive
|
||||
compaction it wants to do in `afterTurn()`. OpenClaw may still run the
|
||||
pre-prompt overflow safeguard; when it predicts the full transcript will
|
||||
overflow, the recovery path calls the active engine's `compact()` before
|
||||
submitting another prompt.
|
||||
- `false` or unset — Pi's built-in auto-compaction may still run during prompt
|
||||
execution, but the active engine's `compact()` method is still called for
|
||||
`/compact` and overflow recovery.
|
||||
<AccordionGroup>
|
||||
<Accordion title="ownsCompaction: true">
|
||||
The engine owns compaction behavior. OpenClaw disables Pi's built-in auto-compaction for that run, and the engine's `compact()` implementation is responsible for `/compact`, overflow recovery compaction, and any proactive compaction it wants to do in `afterTurn()`. OpenClaw may still run the pre-prompt overflow safeguard; when it predicts the full transcript will overflow, the recovery path calls the active engine's `compact()` before submitting another prompt.
|
||||
</Accordion>
|
||||
<Accordion title="ownsCompaction: false or unset">
|
||||
Pi's built-in auto-compaction may still run during prompt execution, but the active engine's `compact()` method is still called for `/compact` and overflow recovery.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to
|
||||
the legacy engine's compaction path.
|
||||
<Warning>
|
||||
`ownsCompaction: false` does **not** mean OpenClaw automatically falls back to the legacy engine's compaction path.
|
||||
</Warning>
|
||||
|
||||
That means there are two valid plugin patterns:
|
||||
|
||||
- **Owning mode** — implement your own compaction algorithm and set
|
||||
`ownsCompaction: true`.
|
||||
- **Delegating mode** — set `ownsCompaction: false` and have `compact()` call
|
||||
`delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use
|
||||
OpenClaw's built-in compaction behavior.
|
||||
<Tabs>
|
||||
<Tab title="Owning mode">
|
||||
Implement your own compaction algorithm and set `ownsCompaction: true`.
|
||||
</Tab>
|
||||
<Tab title="Delegating mode">
|
||||
Set `ownsCompaction: false` and have `compact()` call `delegateCompactionToRuntime(...)` from `openclaw/plugin-sdk/core` to use OpenClaw's built-in compaction behavior.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
A no-op `compact()` is unsafe for an active non-owning engine because it
|
||||
disables the normal `/compact` and overflow-recovery compaction path for that
|
||||
engine slot.
|
||||
A no-op `compact()` is unsafe for an active non-owning engine because it disables the normal `/compact` and overflow-recovery compaction path for that engine slot.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
@@ -247,47 +249,35 @@ engine slot.
|
||||
}
|
||||
```
|
||||
|
||||
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>
|
||||
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>
|
||||
|
||||
## Relationship to compaction and memory
|
||||
|
||||
- **Compaction** is one responsibility of the context engine. The legacy engine
|
||||
delegates to OpenClaw's built-in summarization. Plugin engines can implement
|
||||
any compaction strategy (DAG summaries, vector retrieval, etc.).
|
||||
- **Memory plugins** (`plugins.slots.memory`) are separate from context engines.
|
||||
Memory plugins provide search/retrieval; context engines control what the
|
||||
model sees. They can work together — a context engine might use memory
|
||||
plugin data during assembly. Plugin engines that want the active memory
|
||||
prompt path should prefer `buildMemorySystemPromptAddition(...)` from
|
||||
`openclaw/plugin-sdk/core`, which converts the active memory prompt sections
|
||||
into a ready-to-prepend `systemPromptAddition`. If an engine needs lower-level
|
||||
control, it can still pull raw lines from
|
||||
`openclaw/plugin-sdk/memory-host-core` via
|
||||
`buildActiveMemoryPromptSection(...)`.
|
||||
- **Session pruning** (trimming old tool results in-memory) still runs
|
||||
regardless of which context engine is active.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Compaction">
|
||||
Compaction is one responsibility of the context engine. The legacy engine delegates to OpenClaw's built-in summarization. Plugin engines can implement any compaction strategy (DAG summaries, vector retrieval, etc.).
|
||||
</Accordion>
|
||||
<Accordion title="Memory plugins">
|
||||
Memory plugins (`plugins.slots.memory`) are separate from context engines. Memory plugins provide search/retrieval; context engines control what the model sees. They can work together — a context engine might use memory plugin data during assembly. Plugin engines that want the active memory prompt path should prefer `buildMemorySystemPromptAddition(...)` from `openclaw/plugin-sdk/core`, which converts the active memory prompt sections into a ready-to-prepend `systemPromptAddition`. If an engine needs lower-level control, it can still pull raw lines from `openclaw/plugin-sdk/memory-host-core` via `buildActiveMemoryPromptSection(...)`.
|
||||
</Accordion>
|
||||
<Accordion title="Session pruning">
|
||||
Trimming old tool results in-memory still runs regardless of which context engine is active.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Tips
|
||||
|
||||
- Use `openclaw doctor` to verify your engine is loading correctly.
|
||||
- If switching engines, existing sessions continue with their current history.
|
||||
The new engine takes over for future runs.
|
||||
- Engine errors are logged and surfaced in diagnostics. If a plugin engine
|
||||
fails to register or the selected engine id cannot be resolved, OpenClaw
|
||||
does not fall back automatically; runs fail until you fix the plugin or
|
||||
switch `plugins.slots.contextEngine` back to `"legacy"`.
|
||||
- For development, use `openclaw plugins install -l ./my-engine` to link a
|
||||
local plugin directory without copying.
|
||||
|
||||
See also: [Compaction](/concepts/compaction), [Context](/concepts/context),
|
||||
[Plugins](/tools/plugin), [Plugin manifest](/plugins/manifest).
|
||||
- If switching engines, existing sessions continue with their current history. The new engine takes over for future runs.
|
||||
- Engine errors are logged and surfaced in diagnostics. If a plugin engine fails to register or the selected engine id cannot be resolved, OpenClaw does not fall back automatically; runs fail until you fix the plugin or switch `plugins.slots.contextEngine` back to `"legacy"`.
|
||||
- For development, use `openclaw plugins install -l ./my-engine` to link a local plugin directory without copying.
|
||||
|
||||
## Related
|
||||
|
||||
- [Compaction](/concepts/compaction) — summarizing long conversations
|
||||
- [Context](/concepts/context) — how context is built for agent turns
|
||||
- [Plugin Architecture](/plugins/architecture) — registering context engine plugins
|
||||
- [Compaction](/concepts/compaction) — summarizing long conversations
|
||||
- [Plugin manifest](/plugins/manifest) — plugin manifest fields
|
||||
- [Plugins](/tools/plugin) — plugin overview
|
||||
|
||||
@@ -631,6 +631,11 @@ Notes:
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -214,6 +214,10 @@ stale. The prompt also notes the public docs mirror, community Discord, and Claw
|
||||
([https://clawhub.ai](https://clawhub.ai)) for skills discovery. It tells the model to
|
||||
consult docs first for OpenClaw behavior, commands, configuration, or architecture, and to
|
||||
run `openclaw status` itself when possible (asking the user only when it lacks access).
|
||||
For configuration specifically, it points agents to the `gateway` tool action
|
||||
`config.schema.lookup` for exact field-level docs and constraints, then to
|
||||
`docs/gateway/configuration.md` and `docs/gateway/configuration-reference.md`
|
||||
for broader guidance.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -1238,6 +1238,7 @@
|
||||
"tools/tokenjuice",
|
||||
"tools/loop-detection",
|
||||
"tools/trajectory",
|
||||
"tools/tts",
|
||||
"tools/video-generation",
|
||||
{
|
||||
"group": "Web browser",
|
||||
|
||||
@@ -152,6 +152,85 @@ To capture logs:
|
||||
|
||||
The log includes browser state transitions and result‑set changes.
|
||||
|
||||
## When to disable Bonjour
|
||||
|
||||
Disable Bonjour only when LAN multicast advertising is unavailable or harmful.
|
||||
The common case is a Gateway running behind Docker bridge networking, WSL, or a
|
||||
network policy that drops mDNS multicast. In those environments the Gateway is
|
||||
still reachable through its published URL, SSH, Tailnet, or wide-area DNS-SD,
|
||||
but LAN auto-discovery is not reliable.
|
||||
|
||||
Prefer the existing environment override when the problem is deployment-scoped:
|
||||
|
||||
```bash
|
||||
OPENCLAW_DISABLE_BONJOUR=1
|
||||
```
|
||||
|
||||
That disables LAN multicast advertising without changing plugin configuration.
|
||||
It is safe for Docker images, service files, launch scripts, and one-off
|
||||
debugging because the setting disappears when the environment does.
|
||||
|
||||
Use plugin configuration only when you intentionally want to turn off the
|
||||
bundled LAN discovery plugin for that OpenClaw config:
|
||||
|
||||
```bash
|
||||
openclaw plugins disable bonjour
|
||||
```
|
||||
|
||||
## Docker gotchas
|
||||
|
||||
Bundled Docker Compose sets `OPENCLAW_DISABLE_BONJOUR=1` for the Gateway service
|
||||
by default. Docker bridge networks usually do not forward mDNS multicast
|
||||
(`224.0.0.251:5353`) between the container and the LAN, so leaving Bonjour on can
|
||||
produce repeated ciao `probing` or `announcing` failures without making discovery
|
||||
work.
|
||||
|
||||
Important gotchas:
|
||||
|
||||
- Disabling Bonjour does not stop the Gateway. It only stops LAN multicast
|
||||
advertising.
|
||||
- Disabling Bonjour does not change `gateway.bind`; Docker still defaults to
|
||||
`OPENCLAW_GATEWAY_BIND=lan` so the published host port can work.
|
||||
- Disabling Bonjour does not disable wide-area DNS-SD. Use wide-area discovery
|
||||
or Tailnet when the Gateway and node are not on the same LAN.
|
||||
- Reusing the same `OPENCLAW_CONFIG_DIR` outside Docker does not inherit the
|
||||
Compose default unless the environment still sets `OPENCLAW_DISABLE_BONJOUR`.
|
||||
- Set `OPENCLAW_DISABLE_BONJOUR=0` only for host networking, macvlan, or another
|
||||
network where mDNS multicast is known to pass.
|
||||
|
||||
## Troubleshooting disabled Bonjour
|
||||
|
||||
If a node no longer auto-discovers the Gateway after Docker setup:
|
||||
|
||||
1. Confirm whether the Gateway is intentionally suppressing LAN advertising:
|
||||
|
||||
```bash
|
||||
docker compose config | grep OPENCLAW_DISABLE_BONJOUR
|
||||
```
|
||||
|
||||
2. Confirm the Gateway itself is reachable through the published port:
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:18789/healthz
|
||||
```
|
||||
|
||||
3. Use a direct target when Bonjour is disabled:
|
||||
- Control UI or local tools: `http://127.0.0.1:18789`
|
||||
- LAN clients: `http://<gateway-host>:18789`
|
||||
- Cross-network clients: Tailnet MagicDNS, Tailnet IP, SSH tunnel, or
|
||||
wide-area DNS-SD
|
||||
|
||||
4. If you deliberately enabled Bonjour in Docker with
|
||||
`OPENCLAW_DISABLE_BONJOUR=0`, test multicast from the host:
|
||||
|
||||
```bash
|
||||
dns-sd -B _openclaw-gw._tcp local.
|
||||
```
|
||||
|
||||
If browsing is empty or the Gateway logs show repeated ciao watchdog
|
||||
cancellations, restore `OPENCLAW_DISABLE_BONJOUR=1` and use a direct or
|
||||
Tailnet route.
|
||||
|
||||
## Common failure modes
|
||||
|
||||
- **Bonjour doesn’t cross networks**: use Tailnet or SSH.
|
||||
@@ -160,6 +239,9 @@ The log includes browser state transitions and result‑set changes.
|
||||
container bridges, WSL, or interface churn can leave the ciao advertiser in a
|
||||
non-announced state. OpenClaw retries a few times and then disables Bonjour
|
||||
for the current Gateway process instead of restarting the advertiser forever.
|
||||
- **Docker bridge networking**: bundled Docker Compose disables Bonjour by
|
||||
default with `OPENCLAW_DISABLE_BONJOUR=1`. Set it to `0` only for host,
|
||||
macvlan, or another mDNS-capable network.
|
||||
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
|
||||
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or
|
||||
punctuation), then restart the Gateway. The service instance name derives from
|
||||
@@ -178,6 +260,7 @@ sequences (e.g. spaces become `\032`).
|
||||
- `openclaw plugins disable bonjour` disables LAN multicast advertising by disabling the bundled plugin.
|
||||
- `openclaw plugins enable bonjour` restores the default LAN discovery plugin.
|
||||
- `OPENCLAW_DISABLE_BONJOUR=1` disables LAN multicast advertising without changing plugin config; accepted truthy values are `1`, `true`, `yes`, and `on` (legacy: `OPENCLAW_DISABLE_BONJOUR`).
|
||||
- Docker Compose sets `OPENCLAW_DISABLE_BONJOUR=1` by default for bridge networking; override with `OPENCLAW_DISABLE_BONJOUR=0` only when mDNS multicast is available.
|
||||
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
|
||||
- `OPENCLAW_SSH_PORT` overrides the SSH port when `sshPort` is advertised (legacy: `OPENCLAW_SSH_PORT`).
|
||||
- `OPENCLAW_TAILNET_DNS` publishes a MagicDNS hint in TXT when mDNS full mode is enabled (legacy: `OPENCLAW_TAILNET_DNS`).
|
||||
|
||||
@@ -364,13 +364,15 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
- `verboseDefault`: default verbose level for agents. Values: `"off"`, `"on"`, `"full"`. Default: `"off"`.
|
||||
- `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`.
|
||||
- `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for API-key access or `openai-codex/gpt-5.5` for Codex OAuth). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default.
|
||||
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific, for example `temperature`, `maxTokens`, `cacheRetention`, `context1m`, `responsesServerCompaction`, `responsesCompactThreshold`, `extra_body`/`extraBody`).
|
||||
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific, for example `temperature`, `maxTokens`, `cacheRetention`, `context1m`, `responsesServerCompaction`, `responsesCompactThreshold`, `chat_template_kwargs`, `extra_body`/`extraBody`).
|
||||
- Safe edits: use `openclaw config set agents.defaults.models '<json>' --strict-json --merge` to add entries. `config set` refuses replacements that would remove existing allowlist entries unless you pass `--replace`.
|
||||
- Provider-scoped configure/onboarding flows merge selected provider models into this map and preserve unrelated providers already configured.
|
||||
- For direct OpenAI Responses models, server-side compaction is enabled automatically. Use `params.responsesServerCompaction: false` to stop injecting `context_management`, or `params.responsesCompactThreshold` to override the threshold. See [OpenAI server-side compaction](/providers/openai#server-side-compaction-responses-api).
|
||||
- `params`: global default provider parameters applied to all models. Set at `agents.defaults.params` (e.g. `{ cacheRetention: "long" }`).
|
||||
- `params` merge precedence (config): `agents.defaults.params` (global base) is overridden by `agents.defaults.models["provider/model"].params` (per-model), then `agents.list[].params` (matching agent id) overrides by key. See [Prompt Caching](/reference/prompt-caching) for details.
|
||||
- `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.
|
||||
- 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.
|
||||
@@ -1255,7 +1257,7 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
|
||||
- Per-channel overrides: `channels.<channel>.ackReaction`, `channels.<channel>.accounts.<id>.ackReaction`.
|
||||
- Resolution order: account → channel → `messages.ackReaction` → identity fallback.
|
||||
- Scope: `group-mentions` (default), `group-all`, `direct`, `all`.
|
||||
- `removeAckAfterReply`: removes ack after reply on Slack, Discord, and Telegram.
|
||||
- `removeAckAfterReply`: removes ack after reply on reaction-capable channels such as Slack, Discord, Telegram, WhatsApp, and BlueBubbles.
|
||||
- `messages.statusReactions.enabled`: enables lifecycle status reactions on Slack, Discord, and Telegram.
|
||||
On Slack and Discord, unset keeps status reactions enabled when ack reactions are active.
|
||||
On Telegram, set it explicitly to `true` to enable lifecycle status reactions.
|
||||
|
||||
@@ -16,6 +16,11 @@ Code truth:
|
||||
- `config.schema.lookup` returns one path-scoped schema node for drill-down tooling
|
||||
- `pnpm config:docs:check` / `pnpm config:docs:gen` validate the config-doc baseline hash against the current schema surface
|
||||
|
||||
Agent lookup path: use the `gateway` tool action `config.schema.lookup` for
|
||||
exact field-level docs and constraints before edits. Use
|
||||
[Configuration](/gateway/configuration) for task-oriented guidance and this page
|
||||
for the broader field map, defaults, and links to subsystem references.
|
||||
|
||||
Dedicated deep references:
|
||||
|
||||
- [Memory configuration reference](/reference/memory-config) for `agents.defaults.memorySearch.*`, `memory.qmd.*`, `memory.citations`, and dreaming config under `plugins.entries.memory-core.config.dreaming`
|
||||
@@ -853,7 +858,7 @@ Notes:
|
||||
- Default log file: `/tmp/openclaw/openclaw-YYYY-MM-DD.log`.
|
||||
- Set `logging.file` for a stable path.
|
||||
- `consoleLevel` bumps to `debug` when `--verbose`.
|
||||
- `maxFileBytes`: maximum log file size in bytes before writes are suppressed (positive integer; default: `524288000` = 500 MB). Use external log rotation for production deployments.
|
||||
- `maxFileBytes`: maximum active log file size in bytes before rotation (positive integer; default: `104857600` = 100 MB). OpenClaw keeps up to five numbered archives beside the active file.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ If the file is missing, OpenClaw uses safe defaults. Common reasons to add a con
|
||||
|
||||
See the [full reference](/gateway/configuration-reference) for every available field.
|
||||
|
||||
Agents and automation should use `config.schema.lookup` for exact field-level
|
||||
docs before editing config. Use this page for task-oriented guidance and
|
||||
[Configuration reference](/gateway/configuration-reference) for the broader
|
||||
field map and defaults.
|
||||
|
||||
<Tip>
|
||||
**New to configuration?** Start with `openclaw onboard` for interactive setup, or check out the [Configuration Examples](/gateway/configuration-examples) guide for complete copy-paste configs.
|
||||
</Tip>
|
||||
@@ -575,6 +580,11 @@ For tooling that writes config over the gateway API, prefer this flow:
|
||||
- `config.apply` only when you intend to replace the entire config
|
||||
- `update.run` for explicit self-update plus restart
|
||||
|
||||
Agents should treat `config.schema.lookup` as the first stop for exact
|
||||
field-level docs and constraints. Use [Configuration reference](/gateway/configuration-reference)
|
||||
when they need the broader config map, defaults, or links to dedicated
|
||||
subsystem references.
|
||||
|
||||
<Note>
|
||||
Control-plane writes (`config.apply`, `config.patch`, `update.run`) are
|
||||
rate-limited to 3 requests per 60 seconds per `deviceId+clientIp`. Restart
|
||||
|
||||
@@ -86,6 +86,9 @@ Security notes:
|
||||
Disable/override:
|
||||
|
||||
- `OPENCLAW_DISABLE_BONJOUR=1` disables advertising.
|
||||
- Docker Compose defaults `OPENCLAW_DISABLE_BONJOUR=1` because bridge networks
|
||||
usually do not carry mDNS multicast reliably; use `0` only on host, macvlan,
|
||||
or another mDNS-capable network.
|
||||
- `gateway.bind` in `~/.openclaw/openclaw.json` controls the Gateway bind mode.
|
||||
- `OPENCLAW_SSH_PORT` overrides the SSH port advertised when `sshPort` is emitted.
|
||||
- `OPENCLAW_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS).
|
||||
|
||||
@@ -249,7 +249,7 @@ doctor prints platform-specific fix guidance. On macOS with a Homebrew Node, the
|
||||
fix is usually `brew postinstall ca-certificates`. With `--deep`, the probe runs
|
||||
even if the gateway is healthy.
|
||||
|
||||
### 2c) Codex OAuth provider overrides
|
||||
### 2e) Codex OAuth provider overrides
|
||||
|
||||
If you previously added legacy OpenAI transport settings under
|
||||
`models.providers.openai-codex`, they can shadow the built-in Codex OAuth
|
||||
@@ -259,6 +259,28 @@ the stale transport override and get the built-in routing/fallback behavior
|
||||
back. Custom proxies and header-only overrides are still supported and do not
|
||||
trigger this warning.
|
||||
|
||||
### 2f) Codex plugin route warnings
|
||||
|
||||
When the bundled Codex plugin is enabled, doctor also checks whether
|
||||
`openai-codex/*` primary model refs still resolve through the default PI runner.
|
||||
That combination is valid when you want Codex OAuth/subscription auth through
|
||||
PI, but it is easy to confuse with the native Codex app-server harness. Doctor
|
||||
warns and points to the explicit app-server shape:
|
||||
`openai/*` plus `embeddedHarness.runtime: "codex"` or
|
||||
`OPENCLAW_AGENT_RUNTIME=codex`.
|
||||
|
||||
Doctor does not repair this automatically because both routes are valid:
|
||||
|
||||
- `openai-codex/*` + PI means "use Codex OAuth/subscription auth through the
|
||||
normal OpenClaw runner."
|
||||
- `openai/*` + `runtime: "codex"` means "run the embedded turn through native
|
||||
Codex app-server."
|
||||
- `/codex ...` means "control or bind a native Codex conversation from chat."
|
||||
- `/acp ...` or `runtime: "acp"` means "use the external ACP/acpx adapter."
|
||||
|
||||
If the warning appears, choose the route you intended and edit config manually.
|
||||
Keep the warning as-is when PI Codex OAuth is intentional.
|
||||
|
||||
### 3) Legacy state migrations (disk layout)
|
||||
|
||||
Doctor can migrate older on-disk layouts into the current structure:
|
||||
@@ -562,6 +584,9 @@ Notes:
|
||||
- If token auth requires a token and the configured token SecretRef is unresolved, doctor blocks the install/repair path with actionable guidance.
|
||||
- If both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, doctor blocks install/repair until mode is set explicitly.
|
||||
- For Linux user-systemd units, doctor token drift checks now include both `Environment=` and `EnvironmentFile=` sources when comparing service auth metadata.
|
||||
- Doctor service repairs refuse to rewrite, stop, or restart a gateway service
|
||||
from an older OpenClaw binary when the config was last written by a newer
|
||||
version. See [Gateway troubleshooting](/gateway/troubleshooting#split-brain-installs-and-newer-config-guard).
|
||||
- You can always force a full rewrite via `openclaw gateway install --force`.
|
||||
|
||||
### 16) Gateway runtime + port diagnostics
|
||||
|
||||
@@ -251,6 +251,8 @@ openclaw gateway restart
|
||||
openclaw gateway stop
|
||||
```
|
||||
|
||||
Use `openclaw gateway restart` for restarts. Do not chain `openclaw gateway stop` and `openclaw gateway start`; on macOS, `gateway stop` intentionally disables the LaunchAgent before stopping it.
|
||||
|
||||
LaunchAgent labels are `ai.openclaw.gateway` (default) or `ai.openclaw.<profile>` (named profile). `openclaw doctor` audits and repairs service config drift.
|
||||
|
||||
</Tab>
|
||||
|
||||
@@ -19,6 +19,8 @@ OpenClaw has two log “surfaces”:
|
||||
|
||||
- Default rolling log file is under `/tmp/openclaw/` (one file per day): `openclaw-YYYY-MM-DD.log`
|
||||
- Date uses the gateway host's local timezone.
|
||||
- Active log files rotate at `logging.maxFileBytes` (default: 100 MB), keeping
|
||||
up to five numbered archives and continuing to write a fresh active file.
|
||||
- The log file path and level can be configured via `~/.openclaw/openclaw.json`:
|
||||
- `logging.file`
|
||||
- `logging.level`
|
||||
|
||||
@@ -147,7 +147,7 @@ When any subkey is enabled, model and tool spans get bounded, redacted
|
||||
|
||||
### Model usage
|
||||
|
||||
- `openclaw.tokens` (counter, attrs: `openclaw.token`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.tokens` (counter, attrs: `openclaw.token`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`, `openclaw.agent`)
|
||||
- `openclaw.cost.usd` (counter, attrs: `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.run.duration_ms` (histogram, attrs: `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.context.tokens` (histogram, attrs: `openclaw.context`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
|
||||
@@ -98,6 +98,8 @@ You can persist a remote target so CLI commands use it by default:
|
||||
```
|
||||
|
||||
When the gateway is loopback-only, keep the URL at `ws://127.0.0.1:18789` and open the SSH tunnel first.
|
||||
In the macOS app’s SSH tunnel transport, discovered gateway hostnames belong in
|
||||
`gateway.remote.sshTarget`; `gateway.remote.url` remains the local tunnel URL.
|
||||
|
||||
## Credential precedence
|
||||
|
||||
|
||||
@@ -30,6 +30,42 @@ Expected healthy signals:
|
||||
- `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`.
|
||||
|
||||
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
|
||||
openclaw --version
|
||||
openclaw gateway status --deep
|
||||
openclaw config get meta.lastTouchedVersion
|
||||
```
|
||||
|
||||
Fix options:
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Anthropic 429 extra usage required for long context
|
||||
|
||||
Use this when logs/errors include:
|
||||
|
||||
@@ -55,6 +55,15 @@ When debugging real providers/models (requires real creds):
|
||||
Slack DM with `/codex bind`, exercises `/codex fast` and
|
||||
`/codex permissions`, then verifies a plain reply and an image attachment
|
||||
route through the native plugin binding instead of ACP.
|
||||
- Codex app-server harness smoke: `pnpm test:docker:live-codex-harness`
|
||||
- Runs gateway agent turns through the plugin-owned Codex app-server harness,
|
||||
verifies `/codex status` and `/codex models`, and by default exercises image,
|
||||
cron MCP, sub-agent, and Guardian probes. Disable the sub-agent probe with
|
||||
`OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_PROBE=0` when isolating other Codex
|
||||
app-server failures. For a focused sub-agent check, disable the other probes:
|
||||
`OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE=0 OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_PROBE=1 pnpm test:docker:live-codex-harness`.
|
||||
This exits after the sub-agent probe unless
|
||||
`OPENCLAW_LIVE_CODEX_HARNESS_SUBAGENT_ONLY=0` is set.
|
||||
- Crestodian rescue command smoke: `pnpm test:live:crestodian-rescue-channel`
|
||||
- Opt-in belt-and-suspenders check for the message-channel rescue command
|
||||
surface. It exercises `/crestodian status`, queues a persistent model
|
||||
@@ -594,7 +603,7 @@ These Docker runners split into two buckets:
|
||||
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
|
||||
explicitly want the larger exhaustive scan.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the live Docker lanes. It also builds one shared `scripts/e2e/Dockerfile` image via `test:docker:e2e-build` and reuses it for the E2E container smoke runners that exercise the built app. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
|
||||
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
|
||||
|
||||
@@ -612,6 +621,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
|
||||
- Install Smoke CI skips the duplicate direct-npm global update with `OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL=1`; run the script locally without that env when direct `npm install -g` coverage is needed.
|
||||
- Agents delete shared workspace CLI smoke: `pnpm test:docker:agents-delete-shared-workspace` (script: `scripts/e2e/agents-delete-shared-workspace-docker.sh`) builds the root Dockerfile image by default, seeds two agents with one workspace in an isolated container home, runs `agents delete --json`, and verifies valid JSON plus retained workspace behavior. Reuse the install-smoke image with `OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE=openclaw-dockerfile-smoke:local OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD=1`.
|
||||
- Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`)
|
||||
- Browser CDP snapshot smoke: `pnpm test:docker:browser-cdp-snapshot` (script: `scripts/e2e/browser-cdp-snapshot-docker.sh`) builds the source E2E image plus a Chromium layer, starts Chromium with raw CDP, runs `browser doctor --deep`, and verifies CDP role snapshots cover link URLs, cursor-promoted clickables, iframe refs, and frame metadata.
|
||||
- OpenAI Responses web_search minimal reasoning regression: `pnpm test:docker:openai-web-search-minimal` (script: `scripts/e2e/openai-web-search-minimal-docker.sh`) runs a mocked OpenAI server through Gateway, verifies `web_search` raises `reasoning.effort` from `minimal` to `low`, then forces the provider schema reject and checks the raw detail appears in Gateway logs.
|
||||
- MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`)
|
||||
- Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`)
|
||||
|
||||
@@ -131,6 +131,7 @@ The setup script accepts these optional environment variables:
|
||||
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
|
||||
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
|
||||
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
|
||||
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
|
||||
|
||||
### Health checks
|
||||
|
||||
@@ -165,6 +166,19 @@ Use bind mode values in `gateway.bind` (`lan` / `loopback` / `custom` /
|
||||
`tailnet` / `auto`), not host aliases like `0.0.0.0` or `127.0.0.1`.
|
||||
</Note>
|
||||
|
||||
### Bonjour / mDNS
|
||||
|
||||
Docker bridge networking usually does not forward Bonjour/mDNS multicast
|
||||
(`224.0.0.251:5353`) reliably. The bundled Compose setup therefore defaults
|
||||
`OPENCLAW_DISABLE_BONJOUR=1` so the Gateway does not crash-loop or repeatedly
|
||||
restart advertising when the bridge drops multicast traffic.
|
||||
|
||||
Use the published Gateway URL, Tailscale, or wide-area DNS-SD for Docker hosts.
|
||||
Set `OPENCLAW_DISABLE_BONJOUR=0` only when running with host networking, macvlan,
|
||||
or another network where mDNS multicast is known to work.
|
||||
|
||||
For gotchas and troubleshooting, see [Bonjour discovery](/gateway/bonjour).
|
||||
|
||||
### Storage and persistence
|
||||
|
||||
Docker Compose bind-mounts `OPENCLAW_CONFIG_DIR` to `/home/node/.openclaw` and
|
||||
|
||||
@@ -79,12 +79,20 @@ ignores user npm prefix/global settings, so global-install npm config does not
|
||||
redirect bundled plugin dependencies into `~/node_modules` or the global package
|
||||
tree.
|
||||
|
||||
Before package updates and bundled runtime-dependency repairs, OpenClaw tries a
|
||||
best-effort disk-space check for the target volume. Low space produces a warning
|
||||
with the checked path, but does not block the update because filesystem quotas,
|
||||
snapshots, and network volumes can change after the check. The actual npm
|
||||
install, copy, and post-install verification remain authoritative.
|
||||
|
||||
### Bundled plugin runtime dependencies
|
||||
|
||||
Packaged installs keep bundled plugin runtime dependencies out of the read-only
|
||||
package tree. On startup and during `openclaw doctor --fix`, OpenClaw repairs
|
||||
runtime dependencies only for bundled plugins that are active in config, active
|
||||
through legacy channel config, or enabled by their bundled manifest default.
|
||||
Persisted channel auth state alone does not trigger Gateway startup
|
||||
runtime-dependency repair.
|
||||
|
||||
Explicit disablement wins. A disabled plugin or channel does not get its
|
||||
runtime dependencies repaired just because it exists in the package. External
|
||||
|
||||
@@ -23,6 +23,11 @@ By default, the Gateway writes a rolling log file under:
|
||||
|
||||
The date uses the gateway host's local timezone.
|
||||
|
||||
Each file rotates when it reaches `logging.maxFileBytes` (default: 100 MB).
|
||||
OpenClaw keeps up to five numbered archives beside the active file, such as
|
||||
`openclaw-YYYY-MM-DD.1.log`, and keeps writing to a fresh active log instead of
|
||||
suppressing diagnostics.
|
||||
|
||||
You can override this in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
@@ -167,7 +172,9 @@ Tool summaries can redact sensitive tokens before they hit the console:
|
||||
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
|
||||
- `logging.redactPatterns`: list of regex strings to override the default set
|
||||
|
||||
Redaction affects **console output only** and does not alter file logs.
|
||||
Redaction applies at the logging sinks for **console output**, **stderr-routed
|
||||
console diagnostics**, and **file logs**. File logs stay JSONL, but matching
|
||||
secret values are masked before the line is written to disk.
|
||||
|
||||
## Diagnostics and OpenTelemetry
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@ A **node** is a companion device (macOS/iOS/Android/headless) that connects to t
|
||||
Legacy transport: [Bridge protocol](/gateway/bridge-protocol) (TCP JSONL;
|
||||
historical only for current nodes).
|
||||
|
||||
macOS can also run in **node mode**: the menubar app connects to the Gateway’s WS server and exposes its local canvas/camera commands as a node (so `openclaw nodes …` works against this Mac).
|
||||
macOS can also run in **node mode**: the menubar app connects to the Gateway’s
|
||||
WS server and exposes its local canvas/camera commands as a node (so
|
||||
`openclaw nodes …` works against this Mac). In remote gateway mode, browser
|
||||
automation is handled by the CLI node host (`openclaw node run` or the
|
||||
installed node service), not by the native app node.
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -112,6 +116,7 @@ Notes:
|
||||
|
||||
```bash
|
||||
openclaw node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||
openclaw node start
|
||||
openclaw node restart
|
||||
```
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ Recommended defaults:
|
||||
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.
|
||||
- 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
|
||||
|
||||
@@ -37,9 +37,32 @@ Notes:
|
||||
- Triggers are normalized (trimmed, empties dropped). Empty lists fall back to defaults.
|
||||
- Limits are enforced for safety (count/length caps).
|
||||
|
||||
### Routing methods (trigger → target)
|
||||
|
||||
- `voicewake.routing.get` → `{ config: VoiceWakeRoutingConfig }`
|
||||
- `voicewake.routing.set` with params `{ config: VoiceWakeRoutingConfig }` → `{ config: VoiceWakeRoutingConfig }`
|
||||
|
||||
`VoiceWakeRoutingConfig` shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"defaultTarget": { "mode": "current" },
|
||||
"routes": [{ "trigger": "robot wake", "target": { "sessionKey": "agent:main:main" } }],
|
||||
"updatedAtMs": 1730000000000
|
||||
}
|
||||
```
|
||||
|
||||
Route targets support exactly one of:
|
||||
|
||||
- `{ "mode": "current" }`
|
||||
- `{ "agentId": "main" }`
|
||||
- `{ "sessionKey": "agent:main:main" }`
|
||||
|
||||
### Events
|
||||
|
||||
- `voicewake.changed` payload `{ triggers: string[] }`
|
||||
- `voicewake.routing.changed` payload `{ config: VoiceWakeRoutingConfig }`
|
||||
|
||||
Who receives it:
|
||||
|
||||
|
||||
@@ -22,6 +22,18 @@ Remote mode supports two transports:
|
||||
- **SSH tunnel** (default): Uses `ssh -N -L ...` to forward the gateway port to localhost. The gateway will see the node’s IP as `127.0.0.1` because the tunnel is loopback.
|
||||
- **Direct (ws/wss)**: Connects straight to the gateway URL. The gateway sees the real client IP.
|
||||
|
||||
In SSH tunnel mode, discovered LAN/tailnet hostnames are saved as
|
||||
`gateway.remote.sshTarget`. The app keeps `gateway.remote.url` on the local
|
||||
tunnel endpoint, for example `ws://127.0.0.1:18789`, so CLI, Web Chat, and
|
||||
the local node-host service all use the same safe loopback transport.
|
||||
|
||||
Browser automation in remote mode is owned by the CLI node host, not by the
|
||||
native macOS app node. The app starts the installed node host service when
|
||||
possible; if you need browser control from that Mac, install/start it with
|
||||
`openclaw node install ...` and `openclaw node start` (or run
|
||||
`openclaw node run ...` in the foreground), then target that browser-capable
|
||||
node.
|
||||
|
||||
## Prereqs on the remote host
|
||||
|
||||
1. Install Node + pnpm and build/install the OpenClaw CLI (`pnpm install && pnpm build && pnpm link --global`).
|
||||
|
||||
@@ -72,8 +72,8 @@ or fallback behavior without changing runtime loading semantics.
|
||||
Setup discovery now prefers descriptor-owned ids such as `setup.providers` and
|
||||
`setup.cliBackends` to narrow candidate plugins before it falls back to
|
||||
`setup-api` for plugins that still need setup-time runtime hooks. Provider
|
||||
setup flow uses manifest `providerAuthChoices` first, then falls back to
|
||||
runtime wizard choices and install-catalog choices for compatibility. Explicit
|
||||
setup lists use manifest `providerAuthChoices`, descriptor-derived setup
|
||||
choices, and install-catalog metadata without loading provider runtime. Explicit
|
||||
`setup.requiresRuntime: false` is a descriptor-only cutoff; omitted
|
||||
`requiresRuntime` keeps the legacy setup-api fallback for compatibility. If more
|
||||
than one discovered plugin claims the same normalized setup provider or CLI
|
||||
|
||||
@@ -20,6 +20,27 @@ If you are trying to orient yourself, start with
|
||||
`openai/gpt-5.5` is the model ref, `codex` is the runtime, and Telegram,
|
||||
Discord, Slack, or another channel remains the communication surface.
|
||||
|
||||
## What this plugin changes
|
||||
|
||||
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 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. |
|
||||
| Native hook relay | Plugin hooks around Codex-native events | Lets OpenClaw observe/block supported Codex-native tool/finalization events. |
|
||||
|
||||
Enabling the plugin makes those capabilities available. It does **not**:
|
||||
|
||||
- start using Codex for every OpenAI model
|
||||
- convert `openai-codex/*` model refs into the native runtime
|
||||
- make ACP/acpx the default Codex path
|
||||
- hot-switch existing sessions that already recorded a PI runtime
|
||||
- replace OpenClaw channel delivery, session files, auth-profile storage, or
|
||||
message routing
|
||||
|
||||
The same plugin also owns the native `/codex` chat-control command surface. If
|
||||
the plugin is enabled and the user asks to bind, resume, steer, stop, or inspect
|
||||
Codex threads from chat, agents should prefer `/codex ...` over ACP. ACP remains
|
||||
@@ -53,6 +74,32 @@ 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.
|
||||
|
||||
If the `codex` plugin is enabled but the primary model is still
|
||||
`openai-codex/*`, `openclaw doctor` warns instead of changing the route. That is
|
||||
intentional: `openai-codex/*` remains the PI Codex OAuth/subscription path, and
|
||||
native app-server execution stays an explicit runtime choice.
|
||||
|
||||
## Route map
|
||||
|
||||
Use this table before changing config:
|
||||
|
||||
| Desired behavior | Model ref | Runtime config | Plugin requirement | Expected status label |
|
||||
| ------------------------------------------- | -------------------------- | -------------------------------------- | --------------------------- | ------------------------------ |
|
||||
| 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 |
|
||||
| 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
|
||||
embedded turn?"
|
||||
- `/codex ...` answers "which native Codex conversation should this chat bind
|
||||
or control?"
|
||||
- ACP answers "which external harness process should acpx launch?"
|
||||
|
||||
## Pick the right model prefix
|
||||
|
||||
OpenAI-family routes are prefix-specific. Use `openai-codex/*` when you want
|
||||
@@ -91,6 +138,25 @@ and inspect the gateway's structured `agent harness selected` record. It
|
||||
includes the selected harness id, selection reason, runtime/fallback policy, and,
|
||||
in `auto` mode, each plugin candidate's support result.
|
||||
|
||||
### What doctor warnings mean
|
||||
|
||||
`openclaw doctor` warns when all of these are true:
|
||||
|
||||
- the bundled `codex` plugin is enabled or allowed
|
||||
- an agent's primary model is `openai-codex/*`
|
||||
- that agent's effective runtime is not `codex`
|
||||
|
||||
That warning exists because users often expect "Codex plugin enabled" to imply
|
||||
"native Codex app-server runtime." OpenClaw does not make that leap. The warning
|
||||
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
|
||||
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
|
||||
@@ -227,6 +293,25 @@ With this shape:
|
||||
- If Codex is missing or unsupported for the `codex` agent, the turn fails
|
||||
instead of quietly using PI.
|
||||
|
||||
## Agent command routing
|
||||
|
||||
Agents should route user requests by intent, not by the word "Codex" alone:
|
||||
|
||||
| User asks for... | Agent should use... |
|
||||
| -------------------------------------------------------- | ------------------------------------------------ |
|
||||
| "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 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 |
|
||||
|
||||
OpenClaw only advertises ACP spawn guidance to agents when ACP is enabled,
|
||||
dispatchable, and backed by a loaded runtime backend. If ACP is not available,
|
||||
the system prompt and plugin skills should not teach the agent about ACP
|
||||
routing.
|
||||
|
||||
## Codex-only deployments
|
||||
|
||||
Force the Codex harness when you need to prove that every embedded agent turn
|
||||
|
||||
@@ -169,8 +169,8 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
|
||||
|
||||
Each `providerAuthChoices` entry describes one onboarding or auth choice.
|
||||
OpenClaw reads this before provider runtime loads.
|
||||
Provider setup flow prefers these manifest choices, then falls back to runtime
|
||||
wizard metadata and install-catalog choices for compatibility.
|
||||
Provider setup lists use these manifest choices, descriptor-derived setup
|
||||
choices, and install-catalog metadata without loading provider runtime.
|
||||
|
||||
| Field | Required | Type | What it means |
|
||||
| --------------------- | -------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
@@ -708,7 +708,7 @@ Model fields:
|
||||
| `api` | `ModelApi` | Optional per-model API override. |
|
||||
| `baseUrl` | `string` | Optional per-model base URL override. |
|
||||
| `headers` | `Record<string, string>` | Optional per-model static headers. |
|
||||
| `input` | `Array<"text" \| "image" \| "document">` | Modalities the model accepts. |
|
||||
| `input` | `Array<"text" \| "image" \| "document" \| "audio" \| "video">` | Modalities the model accepts. |
|
||||
| `reasoning` | `boolean` | Whether the model exposes reasoning behavior. |
|
||||
| `contextWindow` | `number` | Native provider context window. |
|
||||
| `contextTokens` | `number` | Optional effective runtime context cap when different from `contextWindow`. |
|
||||
@@ -725,6 +725,37 @@ Do not put runtime-only data in `modelCatalog`. If a provider needs account
|
||||
state, an API request, or local process discovery to know the complete model
|
||||
set, declare that provider as `refreshable` or `runtime` in `discovery`.
|
||||
|
||||
### OpenClaw Provider Index
|
||||
|
||||
The OpenClaw Provider Index is OpenClaw-owned preview metadata for providers
|
||||
whose plugins may not be installed yet. It is not part of a plugin manifest.
|
||||
Plugin manifests remain the installed-plugin authority. The Provider Index is
|
||||
the internal fallback contract that future installable-provider and pre-install
|
||||
model picker surfaces will consume when a provider plugin is not installed.
|
||||
|
||||
Catalog authority order:
|
||||
|
||||
1. User config.
|
||||
2. Installed plugin manifest `modelCatalog`.
|
||||
3. Model catalog cache from explicit refresh.
|
||||
4. OpenClaw Provider Index preview rows.
|
||||
|
||||
The Provider Index must not contain secrets, enabled state, runtime hooks, or
|
||||
live account-specific model data. Its preview catalogs use the same
|
||||
`modelCatalog` provider row shape as plugin manifests, but should stay limited
|
||||
to stable display metadata unless runtime adapter fields such as `api`,
|
||||
`baseUrl`, pricing, or compatibility flags are intentionally kept aligned with
|
||||
the installed plugin manifest. Providers with live `/models` discovery should
|
||||
write refreshed rows through the explicit model catalog cache path instead of
|
||||
making normal listing or onboarding call provider APIs.
|
||||
|
||||
Provider Index entries may also carry installable-plugin metadata for providers
|
||||
whose plugin has moved out of core or is otherwise not installed yet. This
|
||||
metadata mirrors the channel catalog pattern: package name, npm install spec,
|
||||
expected integrity, and cheap auth-choice labels are enough to show an
|
||||
installable setup option. Once the plugin is installed, its manifest wins and
|
||||
the Provider Index entry is ignored for that provider.
|
||||
|
||||
Legacy top-level capability keys are deprecated. Use `openclaw doctor --fix` to
|
||||
move `speechProviders`, `realtimeTranscriptionProviders`,
|
||||
`realtimeVoiceProviders`, `mediaUnderstandingProviders`,
|
||||
|
||||
@@ -1,63 +1,95 @@
|
||||
---
|
||||
summary: "Voice Call plugin: outbound + inbound calls via Twilio/Telnyx/Plivo (plugin install + config + CLI)"
|
||||
summary: "Place outbound and accept inbound voice calls via Twilio, Telnyx, or Plivo, with optional realtime voice and streaming transcription"
|
||||
read_when:
|
||||
- You want to place an outbound voice call from OpenClaw
|
||||
- You are configuring or developing the voice-call plugin
|
||||
- You need realtime voice or streaming transcription on telephony
|
||||
title: "Voice call plugin"
|
||||
sidebarTitle: "Voice call"
|
||||
---
|
||||
|
||||
Voice calls for OpenClaw via a plugin. Supports outbound notifications and
|
||||
multi-turn conversations with inbound policies.
|
||||
Voice calls for OpenClaw via a plugin. Supports outbound notifications,
|
||||
multi-turn conversations, full-duplex realtime voice, streaming
|
||||
transcription, and inbound calls with allowlist policies.
|
||||
|
||||
Current providers:
|
||||
**Current providers:** `twilio` (Programmable Voice + Media Streams),
|
||||
`telnyx` (Call Control v2), `plivo` (Voice API + XML transfer + GetInput
|
||||
speech), `mock` (dev/no network).
|
||||
|
||||
- `twilio` (Programmable Voice + Media Streams)
|
||||
- `telnyx` (Call Control v2)
|
||||
- `plivo` (Voice API + XML transfer + GetInput speech)
|
||||
- `mock` (dev/no network)
|
||||
<Note>
|
||||
The Voice Call plugin runs **inside the Gateway process**. If you use a
|
||||
remote Gateway, install and configure the plugin on the machine running
|
||||
the Gateway, then restart the Gateway to load it.
|
||||
</Note>
|
||||
|
||||
Quick mental model:
|
||||
## Quick start
|
||||
|
||||
- Install plugin
|
||||
- Restart Gateway
|
||||
- Configure under `plugins.entries.voice-call.config`
|
||||
- Use `openclaw voicecall ...` or the `voice_call` tool
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
<Tabs>
|
||||
<Tab title="From npm (recommended)">
|
||||
```bash
|
||||
openclaw plugins install @openclaw/voice-call
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="From a local folder (dev)">
|
||||
```bash
|
||||
PLUGIN_SRC=./path/to/local/voice-call-plugin
|
||||
openclaw plugins install "$PLUGIN_SRC"
|
||||
cd "$PLUGIN_SRC" && pnpm install
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Where it runs (local vs remote)
|
||||
Restart the Gateway afterwards so the plugin loads.
|
||||
|
||||
The Voice Call plugin runs **inside the Gateway process**.
|
||||
</Step>
|
||||
<Step title="Configure provider and webhook">
|
||||
Set config under `plugins.entries.voice-call.config` (see
|
||||
[Configuration](#configuration) below for the full shape). At minimum:
|
||||
`provider`, provider credentials, `fromNumber`, and a publicly
|
||||
reachable webhook URL.
|
||||
</Step>
|
||||
<Step title="Verify setup">
|
||||
```bash
|
||||
openclaw voicecall setup
|
||||
```
|
||||
|
||||
If you use a remote Gateway, install/configure the plugin on the **machine running the Gateway**, then restart the Gateway to load it.
|
||||
The default output is readable in chat logs and terminals. It checks
|
||||
plugin enablement, provider credentials, webhook exposure, and that
|
||||
only one audio mode (`streaming` or `realtime`) is active. Use
|
||||
`--json` for scripts.
|
||||
|
||||
## Install
|
||||
</Step>
|
||||
<Step title="Smoke test">
|
||||
```bash
|
||||
openclaw voicecall smoke
|
||||
openclaw voicecall smoke --to "+15555550123"
|
||||
```
|
||||
|
||||
### Option A: install from npm (recommended)
|
||||
Both are dry runs by default. Add `--yes` to actually place a short
|
||||
outbound notify call:
|
||||
|
||||
```bash
|
||||
openclaw plugins install @openclaw/voice-call
|
||||
```
|
||||
```bash
|
||||
openclaw voicecall smoke --to "+15555550123" --yes
|
||||
```
|
||||
|
||||
Restart the Gateway afterwards.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Option B: install from a local folder (dev, no copying)
|
||||
<Warning>
|
||||
For Twilio, Telnyx, and Plivo, setup must resolve to a **public webhook URL**.
|
||||
If `publicUrl`, the tunnel URL, the Tailscale URL, or the serve fallback
|
||||
resolves to loopback or private network space, setup fails instead of
|
||||
starting a provider that cannot receive carrier webhooks.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
PLUGIN_SRC=./path/to/local/voice-call-plugin
|
||||
openclaw plugins install "$PLUGIN_SRC"
|
||||
cd "$PLUGIN_SRC" && pnpm install
|
||||
```
|
||||
## Configuration
|
||||
|
||||
Restart the Gateway afterwards.
|
||||
|
||||
## Config
|
||||
|
||||
Set config under `plugins.entries.voice-call.config`:
|
||||
|
||||
If `enabled` is true but the selected provider is missing credentials, Gateway
|
||||
startup logs a setup-incomplete warning with the missing keys and skips starting
|
||||
the runtime. Run `openclaw voicecall setup` to see the same readiness details.
|
||||
Commands, RPC calls, and agent tools still return the exact missing provider
|
||||
configuration when used.
|
||||
If `enabled: true` but the selected provider is missing credentials,
|
||||
Gateway startup logs a setup-incomplete warning with the missing keys and
|
||||
skips starting the runtime. Commands, RPC calls, and agent tools still
|
||||
return the exact missing provider configuration when used.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -74,15 +106,13 @@ configuration when used.
|
||||
accountSid: "ACxxxxxxxx",
|
||||
authToken: "...",
|
||||
},
|
||||
|
||||
telnyx: {
|
||||
apiKey: "...",
|
||||
connectionId: "...",
|
||||
// Telnyx webhook public key from the Telnyx Mission Control Portal
|
||||
// (Base64 string; can also be set via TELNYX_PUBLIC_KEY).
|
||||
// Telnyx webhook public key from the Mission Control Portal
|
||||
// (Base64; can also be set via TELNYX_PUBLIC_KEY).
|
||||
publicKey: "...",
|
||||
},
|
||||
|
||||
plivo: {
|
||||
authId: "MAxxxxxxxxxxxxxxxxxxxx",
|
||||
authToken: "...",
|
||||
@@ -103,41 +133,14 @@ configuration when used.
|
||||
// Public exposure (pick one)
|
||||
// publicUrl: "https://example.ngrok.app/voice/webhook",
|
||||
// tunnel: { provider: "ngrok" },
|
||||
// tailscale: { mode: "funnel", path: "/voice/webhook" }
|
||||
// tailscale: { mode: "funnel", path: "/voice/webhook" },
|
||||
|
||||
outbound: {
|
||||
defaultMode: "notify", // notify | conversation
|
||||
},
|
||||
|
||||
streaming: {
|
||||
enabled: true,
|
||||
provider: "openai", // optional; first registered realtime transcription provider when unset
|
||||
streamPath: "/voice/stream",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-...", // optional if OPENAI_API_KEY is set
|
||||
model: "gpt-4o-transcribe",
|
||||
silenceDurationMs: 800,
|
||||
vadThreshold: 0.5,
|
||||
},
|
||||
},
|
||||
preStartTimeoutMs: 5000,
|
||||
maxPendingConnections: 32,
|
||||
maxPendingConnectionsPerIp: 4,
|
||||
maxConnections: 128,
|
||||
},
|
||||
|
||||
realtime: {
|
||||
enabled: false,
|
||||
provider: "google", // optional; first registered realtime voice provider when unset
|
||||
toolPolicy: "safe-read-only",
|
||||
providers: {
|
||||
google: {
|
||||
model: "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
voice: "Kore",
|
||||
},
|
||||
},
|
||||
},
|
||||
streaming: { enabled: true /* see Streaming transcription */ },
|
||||
realtime: { enabled: false /* see Realtime voice */ },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -145,152 +148,135 @@ configuration when used.
|
||||
}
|
||||
```
|
||||
|
||||
Check setup before testing with a real provider:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Provider exposure and security notes">
|
||||
- Twilio, Telnyx, and Plivo all require a **publicly reachable** webhook URL.
|
||||
- `mock` is a local dev provider (no network calls).
|
||||
- Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
|
||||
- `skipSignatureVerification` is for local testing only.
|
||||
- On ngrok free tier, set `publicUrl` to the exact ngrok URL; signature verification is always enforced.
|
||||
- `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Local dev only.
|
||||
- Ngrok free-tier URLs can change or add interstitial behaviour; if `publicUrl` drifts, Twilio signatures fail. Production: prefer a stable domain or a Tailscale funnel.
|
||||
</Accordion>
|
||||
<Accordion title="Streaming connection caps">
|
||||
- `streaming.preStartTimeoutMs` closes sockets that never send a valid `start` frame.
|
||||
- `streaming.maxPendingConnections` caps total unauthenticated pre-start sockets.
|
||||
- `streaming.maxPendingConnectionsPerIp` caps unauthenticated pre-start sockets per source IP.
|
||||
- `streaming.maxConnections` caps total open media stream sockets (pending + active).
|
||||
</Accordion>
|
||||
<Accordion title="Legacy config migrations">
|
||||
Older configs using `provider: "log"`, `twilio.from`, or legacy
|
||||
`streaming.*` OpenAI keys are rewritten by `openclaw doctor --fix`.
|
||||
Runtime fallback still accepts the old voice-call keys for now, but
|
||||
the rewrite path is `openclaw doctor --fix` and the compat shim is
|
||||
temporary.
|
||||
|
||||
```bash
|
||||
openclaw voicecall setup
|
||||
```
|
||||
Auto-migrated streaming keys:
|
||||
|
||||
The default output is readable in chat logs and terminal sessions. It checks
|
||||
whether the plugin is enabled, the provider and credentials are present, webhook
|
||||
exposure is configured, and only one audio mode is active. Use
|
||||
`openclaw voicecall setup --json` for scripts.
|
||||
- `streaming.sttProvider` → `streaming.provider`
|
||||
- `streaming.openaiApiKey` → `streaming.providers.openai.apiKey`
|
||||
- `streaming.sttModel` → `streaming.providers.openai.model`
|
||||
- `streaming.silenceDurationMs` → `streaming.providers.openai.silenceDurationMs`
|
||||
- `streaming.vadThreshold` → `streaming.providers.openai.vadThreshold`
|
||||
|
||||
For Twilio, Telnyx, and Plivo, setup must resolve to a public webhook URL. If the
|
||||
configured `publicUrl`, tunnel URL, Tailscale URL, or serve fallback resolves to
|
||||
loopback or private network space, setup fails instead of starting a provider
|
||||
that cannot receive real carrier webhooks.
|
||||
|
||||
For a no-surprises smoke test, run:
|
||||
|
||||
```bash
|
||||
openclaw voicecall smoke
|
||||
openclaw voicecall smoke --to "+15555550123"
|
||||
```
|
||||
|
||||
The second command is still a dry run. Add `--yes` to place a short outbound
|
||||
notify call:
|
||||
|
||||
```bash
|
||||
openclaw voicecall smoke --to "+15555550123" --yes
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Twilio/Telnyx require a **publicly reachable** webhook URL.
|
||||
- Plivo requires a **publicly reachable** webhook URL.
|
||||
- `mock` is a local dev provider (no network calls).
|
||||
- If older configs still use `provider: "log"`, `twilio.from`, or legacy `streaming.*` OpenAI keys, run `openclaw doctor --fix` to rewrite them.
|
||||
- Telnyx requires `telnyx.publicKey` (or `TELNYX_PUBLIC_KEY`) unless `skipSignatureVerification` is true.
|
||||
- `skipSignatureVerification` is for local testing only.
|
||||
- If you use ngrok free tier, set `publicUrl` to the exact ngrok URL; signature verification is always enforced.
|
||||
- `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only.
|
||||
- Ngrok free tier URLs can change or add interstitial behavior; if `publicUrl` drifts, Twilio signatures will fail. For production, prefer a stable domain or Tailscale funnel.
|
||||
- `realtime.enabled` starts full voice-to-voice conversations; do not enable it together with `streaming.enabled`.
|
||||
- Streaming security defaults:
|
||||
- `streaming.preStartTimeoutMs` closes sockets that never send a valid `start` frame.
|
||||
- `streaming.maxPendingConnections` caps total unauthenticated pre-start sockets.
|
||||
- `streaming.maxPendingConnectionsPerIp` caps unauthenticated pre-start sockets per source IP.
|
||||
- `streaming.maxConnections` caps total open media stream sockets (pending + active).
|
||||
- Runtime fallback still accepts those old voice-call keys for now, but the rewrite path is `openclaw doctor --fix` and the compat shim is temporary.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Realtime voice conversations
|
||||
|
||||
`realtime` selects a full duplex realtime voice provider for live call audio.
|
||||
It is separate from `streaming`, which only forwards audio to realtime
|
||||
transcription providers.
|
||||
`realtime` selects a full-duplex realtime voice provider for live call
|
||||
audio. It is separate from `streaming`, which only forwards audio to
|
||||
realtime transcription providers.
|
||||
|
||||
Current runtime behavior:
|
||||
<Warning>
|
||||
`realtime.enabled` cannot be combined with `streaming.enabled`. Pick one
|
||||
audio mode per call.
|
||||
</Warning>
|
||||
|
||||
Current runtime behaviour:
|
||||
|
||||
- `realtime.enabled` is supported for Twilio Media Streams.
|
||||
- `realtime.enabled` cannot be combined with `streaming.enabled`.
|
||||
- `realtime.provider` is optional. If unset, Voice Call uses the first
|
||||
registered realtime voice provider.
|
||||
- Bundled realtime voice providers include Google Gemini Live (`google`) and
|
||||
OpenAI (`openai`), registered by their provider plugins.
|
||||
- `realtime.provider` is optional. If unset, Voice Call uses the first registered realtime voice provider.
|
||||
- Bundled realtime voice providers: Google Gemini Live (`google`) and OpenAI (`openai`), registered by their provider plugins.
|
||||
- Provider-owned raw config lives under `realtime.providers.<providerId>`.
|
||||
- Voice Call exposes the shared `openclaw_agent_consult` realtime tool by
|
||||
default. The realtime model can call it when the caller asks for deeper
|
||||
reasoning, current information, or normal OpenClaw tools.
|
||||
- `realtime.toolPolicy` controls the consult run:
|
||||
- `safe-read-only`: expose the consult tool and limit the regular agent to
|
||||
`read`, `web_search`, `web_fetch`, `x_search`, `memory_search`, and
|
||||
`memory_get`.
|
||||
- `owner`: expose the consult tool and let the regular agent use the normal
|
||||
agent tool policy.
|
||||
- `none`: do not expose the consult tool. Custom `realtime.tools` are still
|
||||
passed through to the realtime provider.
|
||||
- Consult session keys reuse the existing voice session when available, then
|
||||
fall back to the caller/callee phone number so follow-up consult calls keep
|
||||
context during the call.
|
||||
- If `realtime.provider` points at an unregistered provider, or no realtime
|
||||
voice provider is registered at all, Voice Call logs a warning and skips
|
||||
realtime media instead of failing the whole plugin.
|
||||
- Voice Call exposes the shared `openclaw_agent_consult` realtime tool by default. The realtime model can call it when the caller asks for deeper reasoning, current information, or normal OpenClaw tools.
|
||||
- If `realtime.provider` points at an unregistered provider, or no realtime voice provider is registered at all, Voice Call logs a warning and skips realtime media instead of failing the whole plugin.
|
||||
- Consult session keys reuse the existing voice session when available, then fall back to the caller/callee phone number so follow-up consult calls keep context during the call.
|
||||
|
||||
Google Gemini Live realtime defaults:
|
||||
### Tool policy
|
||||
|
||||
- API key: `realtime.providers.google.apiKey`, `GEMINI_API_KEY`, or
|
||||
`GOOGLE_GENERATIVE_AI_API_KEY`
|
||||
- model: `gemini-2.5-flash-native-audio-preview-12-2025`
|
||||
- voice: `Kore`
|
||||
`realtime.toolPolicy` controls the consult run:
|
||||
|
||||
Example:
|
||||
| Policy | Behavior |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `safe-read-only` | Expose the consult tool and limit the regular agent to `read`, `web_search`, `web_fetch`, `x_search`, `memory_search`, and `memory_get`. |
|
||||
| `owner` | Expose the consult tool and let the regular agent use the normal agent tool policy. |
|
||||
| `none` | Do not expose the consult tool. Custom `realtime.tools` are still passed through to the realtime provider. |
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
provider: "twilio",
|
||||
inboundPolicy: "allowlist",
|
||||
allowFrom: ["+15550005678"],
|
||||
realtime: {
|
||||
enabled: true,
|
||||
provider: "google",
|
||||
instructions: "Speak briefly. Call openclaw_agent_consult before using deeper tools.",
|
||||
toolPolicy: "safe-read-only",
|
||||
providers: {
|
||||
google: {
|
||||
apiKey: "${GEMINI_API_KEY}",
|
||||
model: "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
voice: "Kore",
|
||||
### Realtime provider examples
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Google Gemini Live">
|
||||
Defaults: API key from `realtime.providers.google.apiKey`,
|
||||
`GEMINI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY`; model
|
||||
`gemini-2.5-flash-native-audio-preview-12-2025`; voice `Kore`.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
provider: "twilio",
|
||||
inboundPolicy: "allowlist",
|
||||
allowFrom: ["+15550005678"],
|
||||
realtime: {
|
||||
enabled: true,
|
||||
provider: "google",
|
||||
instructions: "Speak briefly. Call openclaw_agent_consult before using deeper tools.",
|
||||
toolPolicy: "safe-read-only",
|
||||
providers: {
|
||||
google: {
|
||||
apiKey: "${GEMINI_API_KEY}",
|
||||
model: "gemini-2.5-flash-native-audio-preview-12-2025",
|
||||
voice: "Kore",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
Use OpenAI instead:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
realtime: {
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "${OPENAI_API_KEY}",
|
||||
</Tab>
|
||||
<Tab title="OpenAI">
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
realtime: {
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: { apiKey: "${OPENAI_API_KEY}" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
See [Google provider](/providers/google) and [OpenAI provider](/providers/openai)
|
||||
for provider-specific realtime voice options.
|
||||
See [Google provider](/providers/google) and
|
||||
[OpenAI provider](/providers/openai) for provider-specific realtime voice
|
||||
options.
|
||||
|
||||
## Streaming transcription
|
||||
|
||||
@@ -298,173 +284,84 @@ for provider-specific realtime voice options.
|
||||
|
||||
Current runtime behavior:
|
||||
|
||||
- `streaming.provider` is optional. If unset, Voice Call uses the first
|
||||
registered realtime transcription provider.
|
||||
- Bundled realtime transcription providers include Deepgram (`deepgram`),
|
||||
ElevenLabs (`elevenlabs`), Mistral (`mistral`), OpenAI (`openai`), and xAI
|
||||
(`xai`), registered by their provider plugins.
|
||||
- `streaming.provider` is optional. If unset, Voice Call uses the first registered realtime transcription provider.
|
||||
- Bundled realtime transcription providers: Deepgram (`deepgram`), ElevenLabs (`elevenlabs`), Mistral (`mistral`), OpenAI (`openai`), and xAI (`xai`), registered by their provider plugins.
|
||||
- Provider-owned raw config lives under `streaming.providers.<providerId>`.
|
||||
- If `streaming.provider` points at an unregistered provider, or no realtime
|
||||
transcription provider is registered at all, Voice Call logs a warning and
|
||||
skips media streaming instead of failing the whole plugin.
|
||||
- If `streaming.provider` points at an unregistered provider, or none is registered, Voice Call logs a warning and skips media streaming instead of failing the whole plugin.
|
||||
|
||||
OpenAI streaming transcription defaults:
|
||||
### Streaming provider examples
|
||||
|
||||
- API key: `streaming.providers.openai.apiKey` or `OPENAI_API_KEY`
|
||||
- model: `gpt-4o-transcribe`
|
||||
- `silenceDurationMs`: `800`
|
||||
- `vadThreshold`: `0.5`
|
||||
<Tabs>
|
||||
<Tab title="OpenAI">
|
||||
Defaults: API key `streaming.providers.openai.apiKey` or
|
||||
`OPENAI_API_KEY`; model `gpt-4o-transcribe`; `silenceDurationMs: 800`;
|
||||
`vadThreshold: 0.5`.
|
||||
|
||||
xAI streaming transcription defaults:
|
||||
|
||||
- API key: `streaming.providers.xai.apiKey` or `XAI_API_KEY`
|
||||
- endpoint: `wss://api.x.ai/v1/stt`
|
||||
- `encoding`: `mulaw`
|
||||
- `sampleRate`: `8000`
|
||||
- `endpointingMs`: `800`
|
||||
- `interimResults`: `true`
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
streaming: {
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
streamPath: "/voice/stream",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-...", // optional if OPENAI_API_KEY is set
|
||||
model: "gpt-4o-transcribe",
|
||||
silenceDurationMs: 800,
|
||||
vadThreshold: 0.5,
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
streaming: {
|
||||
enabled: true,
|
||||
provider: "openai",
|
||||
streamPath: "/voice/stream",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-...", // optional if OPENAI_API_KEY is set
|
||||
model: "gpt-4o-transcribe",
|
||||
silenceDurationMs: 800,
|
||||
vadThreshold: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
Use xAI instead:
|
||||
</Tab>
|
||||
<Tab title="xAI">
|
||||
Defaults: API key `streaming.providers.xai.apiKey` or `XAI_API_KEY`;
|
||||
endpoint `wss://api.x.ai/v1/stt`; encoding `mulaw`; sample rate `8000`;
|
||||
`endpointingMs: 800`; `interimResults: true`.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
streaming: {
|
||||
enabled: true,
|
||||
provider: "xai",
|
||||
streamPath: "/voice/stream",
|
||||
providers: {
|
||||
xai: {
|
||||
apiKey: "${XAI_API_KEY}", // optional if XAI_API_KEY is set
|
||||
endpointingMs: 800,
|
||||
language: "en",
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
streaming: {
|
||||
enabled: true,
|
||||
provider: "xai",
|
||||
streamPath: "/voice/stream",
|
||||
providers: {
|
||||
xai: {
|
||||
apiKey: "${XAI_API_KEY}", // optional if XAI_API_KEY is set
|
||||
endpointingMs: 800,
|
||||
language: "en",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
Legacy keys are still auto-migrated by `openclaw doctor --fix`:
|
||||
|
||||
- `streaming.sttProvider` → `streaming.provider`
|
||||
- `streaming.openaiApiKey` → `streaming.providers.openai.apiKey`
|
||||
- `streaming.sttModel` → `streaming.providers.openai.model`
|
||||
- `streaming.silenceDurationMs` → `streaming.providers.openai.silenceDurationMs`
|
||||
- `streaming.vadThreshold` → `streaming.providers.openai.vadThreshold`
|
||||
|
||||
## Stale call reaper
|
||||
|
||||
Use `staleCallReaperSeconds` to end calls that never receive a terminal webhook
|
||||
(for example, notify-mode calls that never complete). The default is `0`
|
||||
(disabled).
|
||||
|
||||
Recommended ranges:
|
||||
|
||||
- **Production:** `120`–`300` seconds for notify-style flows.
|
||||
- Keep this value **higher than `maxDurationSeconds`** so normal calls can
|
||||
finish. A good starting point is `maxDurationSeconds + 30–60` seconds.
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
maxDurationSeconds: 300,
|
||||
staleCallReaperSeconds: 360,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook Security
|
||||
|
||||
When a proxy or tunnel sits in front of the Gateway, the plugin reconstructs the
|
||||
public URL for signature verification. These options control which forwarded
|
||||
headers are trusted.
|
||||
|
||||
`webhookSecurity.allowedHosts` allowlists hosts from forwarding headers.
|
||||
|
||||
`webhookSecurity.trustForwardingHeaders` trusts forwarded headers without an allowlist.
|
||||
|
||||
`webhookSecurity.trustedProxyIPs` only trusts forwarded headers when the request
|
||||
remote IP matches the list.
|
||||
|
||||
Webhook replay protection is enabled for Twilio and Plivo. Replayed valid webhook
|
||||
requests are acknowledged but skipped for side effects.
|
||||
|
||||
Twilio conversation turns include a per-turn token in `<Gather>` callbacks, so
|
||||
stale/replayed speech callbacks cannot satisfy a newer pending transcript turn.
|
||||
|
||||
Unauthenticated webhook requests are rejected before body reads when the
|
||||
provider's required signature headers are missing.
|
||||
|
||||
The voice-call webhook uses the shared pre-auth body profile (64 KB / 5 seconds)
|
||||
plus a per-IP in-flight cap before signature verification.
|
||||
|
||||
Example with a stable public host:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
publicUrl: "https://voice.example.com/voice/webhook",
|
||||
webhookSecurity: {
|
||||
allowedHosts: ["voice.example.com"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## TTS for calls
|
||||
|
||||
Voice Call uses the core `messages.tts` configuration for
|
||||
streaming speech on calls. You can override it under the plugin config with the
|
||||
**same shape** — it deep‑merges with `messages.tts`.
|
||||
Voice Call uses the core `messages.tts` configuration for streaming
|
||||
speech on calls. You can override it under the plugin config with the
|
||||
**same shape** — it deep-merges with `messages.tts`.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -480,21 +377,23 @@ streaming speech on calls. You can override it under the plugin config with the
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
<Warning>
|
||||
**Microsoft speech is ignored for voice calls.** Telephony audio needs PCM;
|
||||
the current Microsoft transport does not expose telephony PCM output.
|
||||
</Warning>
|
||||
|
||||
Behavior notes:
|
||||
|
||||
- Legacy `tts.<provider>` keys inside plugin config (`openai`, `elevenlabs`, `microsoft`, `edge`) are repaired by `openclaw doctor --fix`; committed config should use `tts.providers.<provider>`.
|
||||
- **Microsoft speech is ignored for voice calls** (telephony audio needs PCM; the current Microsoft transport does not expose telephony PCM output).
|
||||
- Core TTS is used when Twilio media streaming is enabled; otherwise calls fall back to provider native voices.
|
||||
- Core TTS is used when Twilio media streaming is enabled; otherwise calls fall back to provider-native voices.
|
||||
- If a Twilio media stream is already active, Voice Call does not fall back to TwiML `<Say>`. If telephony TTS is unavailable in that state, the playback request fails instead of mixing two playback paths.
|
||||
- When telephony TTS falls back to a secondary provider, Voice Call logs a warning with the provider chain (`from`, `to`, `attempts`) for debugging.
|
||||
- When Twilio barge-in or stream teardown clears the pending TTS queue, queued
|
||||
playback requests settle instead of hanging callers that are awaiting playback
|
||||
completion.
|
||||
- When Twilio barge-in or stream teardown clears the pending TTS queue, queued playback requests settle instead of hanging callers awaiting playback completion.
|
||||
|
||||
### More examples
|
||||
|
||||
Use core TTS only (no override):
|
||||
### TTS examples
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Core TTS only">
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
@@ -507,9 +406,8 @@ Use core TTS only (no override):
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Override to ElevenLabs just for calls (keep core default elsewhere):
|
||||
|
||||
</Tab>
|
||||
<Tab title="Override to ElevenLabs (calls only)">
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
@@ -532,9 +430,8 @@ Override to ElevenLabs just for calls (keep core default elsewhere):
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Override only the OpenAI model for calls (deep‑merge example):
|
||||
|
||||
</Tab>
|
||||
<Tab title="OpenAI model override (deep-merge)">
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
@@ -555,6 +452,8 @@ Override only the OpenAI model for calls (deep‑merge example):
|
||||
},
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Inbound calls
|
||||
|
||||
@@ -568,50 +467,122 @@ Inbound policy defaults to `disabled`. To enable inbound calls, set:
|
||||
}
|
||||
```
|
||||
|
||||
`inboundPolicy: "allowlist"` is a low-assurance caller-ID screen. The plugin
|
||||
normalizes the provider-supplied `From` value and compares it to `allowFrom`.
|
||||
Webhook verification authenticates provider delivery and payload integrity, but
|
||||
it does not prove PSTN/VoIP caller-number ownership. Treat `allowFrom` as
|
||||
caller-ID filtering, not strong caller identity.
|
||||
<Warning>
|
||||
`inboundPolicy: "allowlist"` is a low-assurance caller-ID screen. The
|
||||
plugin normalizes the provider-supplied `From` value and compares it to
|
||||
`allowFrom`. Webhook verification authenticates provider delivery and
|
||||
payload integrity, but it does **not** prove PSTN/VoIP caller-number
|
||||
ownership. Treat `allowFrom` as caller-ID filtering, not strong caller
|
||||
identity.
|
||||
</Warning>
|
||||
|
||||
Auto-responses use the agent system. Tune with:
|
||||
|
||||
- `responseModel`
|
||||
- `responseSystemPrompt`
|
||||
- `responseTimeoutMs`
|
||||
Auto-responses use the agent system. Tune with `responseModel`,
|
||||
`responseSystemPrompt`, and `responseTimeoutMs`.
|
||||
|
||||
### Spoken output contract
|
||||
|
||||
For auto-responses, Voice Call appends a strict spoken-output contract to the system prompt:
|
||||
For auto-responses, Voice Call appends a strict spoken-output contract to
|
||||
the system prompt:
|
||||
|
||||
- `{"spoken":"..."}`
|
||||
```text
|
||||
{"spoken":"..."}
|
||||
```
|
||||
|
||||
Voice Call then extracts speech text defensively:
|
||||
Voice Call extracts speech text defensively:
|
||||
|
||||
- Ignores payloads marked as reasoning/error content.
|
||||
- Parses direct JSON, fenced JSON, or inline `"spoken"` keys.
|
||||
- Falls back to plain text and removes likely planning/meta lead-in paragraphs.
|
||||
|
||||
This keeps spoken playback focused on caller-facing text and avoids leaking planning text into audio.
|
||||
This keeps spoken playback focused on caller-facing text and avoids
|
||||
leaking planning text into audio.
|
||||
|
||||
### Conversation startup behavior
|
||||
|
||||
For outbound `conversation` calls, first-message handling is tied to live playback state:
|
||||
For outbound `conversation` calls, first-message handling is tied to live
|
||||
playback state:
|
||||
|
||||
- Barge-in queue clear and auto-response are suppressed only while the initial greeting is actively speaking.
|
||||
- If initial playback fails, the call returns to `listening` and the initial message remains queued for retry.
|
||||
- Initial playback for Twilio streaming starts on stream connect without extra delay.
|
||||
- Barge-in aborts active playback and clears queued-but-not-yet-playing Twilio
|
||||
TTS entries. Cleared entries resolve as skipped, so follow-up response logic
|
||||
can continue without waiting on audio that will never play.
|
||||
- Realtime voice conversations use the realtime stream's own opening turn. Voice Call does not post a legacy `<Say>` TwiML update for that initial message, so outbound `<Connect><Stream>` sessions stay attached.
|
||||
- Barge-in aborts active playback and clears queued-but-not-yet-playing Twilio TTS entries. Cleared entries resolve as skipped, so follow-up response logic can continue without waiting on audio that will never play.
|
||||
- Realtime voice conversations use the realtime stream's own opening turn. Voice Call does **not** post a legacy `<Say>` TwiML update for that initial message, so outbound `<Connect><Stream>` sessions stay attached.
|
||||
|
||||
### Twilio stream disconnect grace
|
||||
|
||||
When a Twilio media stream disconnects, Voice Call waits `2000ms` before auto-ending the call:
|
||||
When a Twilio media stream disconnects, Voice Call waits **2000 ms** before
|
||||
auto-ending the call:
|
||||
|
||||
- If the stream reconnects during that window, auto-end is canceled.
|
||||
- If no stream is re-registered after the grace period, the call is ended to prevent stuck active calls.
|
||||
- If no stream re-registers after the grace period, the call is ended to prevent stuck active calls.
|
||||
|
||||
## Stale call reaper
|
||||
|
||||
Use `staleCallReaperSeconds` to end calls that never receive a terminal
|
||||
webhook (for example, notify-mode calls that never complete). The default
|
||||
is `0` (disabled).
|
||||
|
||||
Recommended ranges:
|
||||
|
||||
- **Production:** `120`–`300` seconds for notify-style flows.
|
||||
- Keep this value **higher than `maxDurationSeconds`** so normal calls can finish. A good starting point is `maxDurationSeconds + 30–60` seconds.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
maxDurationSeconds: 300,
|
||||
staleCallReaperSeconds: 360,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook security
|
||||
|
||||
When a proxy or tunnel sits in front of the Gateway, the plugin
|
||||
reconstructs the public URL for signature verification. These options
|
||||
control which forwarded headers are trusted:
|
||||
|
||||
<ParamField path="webhookSecurity.allowedHosts" type="string[]">
|
||||
Allowlist hosts from forwarding headers.
|
||||
</ParamField>
|
||||
<ParamField path="webhookSecurity.trustForwardingHeaders" type="boolean">
|
||||
Trust forwarded headers without an allowlist.
|
||||
</ParamField>
|
||||
<ParamField path="webhookSecurity.trustedProxyIPs" type="string[]">
|
||||
Only trust forwarded headers when the request remote IP matches the list.
|
||||
</ParamField>
|
||||
|
||||
Additional protections:
|
||||
|
||||
- Webhook **replay protection** is enabled for Twilio and Plivo. Replayed valid webhook requests are acknowledged but skipped for side effects.
|
||||
- Twilio conversation turns include a per-turn token in `<Gather>` callbacks, so stale/replayed speech callbacks cannot satisfy a newer pending transcript turn.
|
||||
- Unauthenticated webhook requests are rejected before body reads when the provider's required signature headers are missing.
|
||||
- The voice-call webhook uses the shared pre-auth body profile (64 KB / 5 seconds) plus a per-IP in-flight cap before signature verification.
|
||||
|
||||
Example with a stable public host:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"voice-call": {
|
||||
config: {
|
||||
publicUrl: "https://voice.example.com/voice/webhook",
|
||||
webhookSecurity: {
|
||||
allowedHosts: ["voice.example.com"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -624,41 +595,43 @@ openclaw voicecall dtmf --call-id <id> --digits "ww123456#"
|
||||
openclaw voicecall end --call-id <id>
|
||||
openclaw voicecall status --call-id <id>
|
||||
openclaw voicecall tail
|
||||
openclaw voicecall latency # summarize turn latency from logs
|
||||
openclaw voicecall latency # summarize turn latency from logs
|
||||
openclaw voicecall expose --mode funnel
|
||||
```
|
||||
|
||||
`latency` reads `calls.jsonl` from the default voice-call storage path. Use
|
||||
`--file <path>` to point at a different log and `--last <n>` to limit analysis
|
||||
to the last N records (default 200). Output includes p50/p90/p99 for turn
|
||||
latency and listen-wait times.
|
||||
`latency` reads `calls.jsonl` from the default voice-call storage path.
|
||||
Use `--file <path>` to point at a different log and `--last <n>` to limit
|
||||
analysis to the last N records (default 200). Output includes p50/p90/p99
|
||||
for turn latency and listen-wait times.
|
||||
|
||||
## Agent tool
|
||||
|
||||
Tool name: `voice_call`
|
||||
Tool name: `voice_call`.
|
||||
|
||||
Actions:
|
||||
|
||||
- `initiate_call` (message, to?, mode?)
|
||||
- `continue_call` (callId, message)
|
||||
- `speak_to_user` (callId, message)
|
||||
- `send_dtmf` (callId, digits)
|
||||
- `end_call` (callId)
|
||||
- `get_status` (callId)
|
||||
| Action | Args |
|
||||
| --------------- | ------------------------- |
|
||||
| `initiate_call` | `message`, `to?`, `mode?` |
|
||||
| `continue_call` | `callId`, `message` |
|
||||
| `speak_to_user` | `callId`, `message` |
|
||||
| `send_dtmf` | `callId`, `digits` |
|
||||
| `end_call` | `callId` |
|
||||
| `get_status` | `callId` |
|
||||
|
||||
This repo ships a matching skill doc at `skills/voice-call/SKILL.md`.
|
||||
|
||||
## Gateway RPC
|
||||
|
||||
- `voicecall.initiate` (`to?`, `message`, `mode?`)
|
||||
- `voicecall.continue` (`callId`, `message`)
|
||||
- `voicecall.speak` (`callId`, `message`)
|
||||
- `voicecall.dtmf` (`callId`, `digits`)
|
||||
- `voicecall.end` (`callId`)
|
||||
- `voicecall.status` (`callId`)
|
||||
| Method | Args |
|
||||
| -------------------- | ------------------------- |
|
||||
| `voicecall.initiate` | `to?`, `message`, `mode?` |
|
||||
| `voicecall.continue` | `callId`, `message` |
|
||||
| `voicecall.speak` | `callId`, `message` |
|
||||
| `voicecall.dtmf` | `callId`, `digits` |
|
||||
| `voicecall.end` | `callId` |
|
||||
| `voicecall.status` | `callId` |
|
||||
|
||||
## Related
|
||||
|
||||
- [Text-to-speech](/tools/tts)
|
||||
- [Talk mode](/nodes/talk)
|
||||
- [Text-to-speech](/tools/tts)
|
||||
- [Voice wake](/nodes/voicewake)
|
||||
|
||||
@@ -260,6 +260,10 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
|
||||
|
||||
OpenClaw maps this to `anthropic-beta: context-1m-2025-08-07` on requests.
|
||||
|
||||
`params.context1m: true` also applies to the Claude CLI backend
|
||||
(`claude-cli/*`) for eligible Opus and Sonnet models, expanding the runtime
|
||||
context window for those CLI sessions to match the direct-API behavior.
|
||||
|
||||
<Warning>
|
||||
Requires long-context access on your Anthropic credential. Legacy token auth (`sk-ant-oat-*`) is rejected for 1M context requests — OpenClaw logs a warning and falls back to the standard context window.
|
||||
</Warning>
|
||||
|
||||
@@ -7,7 +7,13 @@ read_when:
|
||||
title: "OpenAI"
|
||||
---
|
||||
|
||||
OpenAI provides developer APIs for GPT models. OpenClaw supports three OpenAI-family routes. The model prefix selects the route:
|
||||
OpenAI provides developer APIs for GPT models, and Codex is also available as a
|
||||
ChatGPT-plan coding agent through OpenAI's Codex clients. OpenClaw keeps those
|
||||
surfaces separate so config stays predictable.
|
||||
|
||||
OpenClaw supports three OpenAI-family routes. The model prefix selects the
|
||||
provider/auth route; a separate runtime setting selects who executes the
|
||||
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)
|
||||
@@ -29,6 +35,24 @@ changing config.
|
||||
| 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
|
||||
|
||||
The names are similar but not interchangeable:
|
||||
|
||||
| Name you see | Layer | Meaning |
|
||||
| ---------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `/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. |
|
||||
|
||||
This means a config can intentionally contain both `openai-codex/*` and the
|
||||
`codex` plugin. That is valid when you want Codex OAuth through PI and also want
|
||||
native `/codex` chat controls available. `openclaw doctor` warns about that
|
||||
combination so you can confirm it is intentional; it does not rewrite it.
|
||||
|
||||
<Note>
|
||||
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`
|
||||
@@ -42,6 +66,8 @@ 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.
|
||||
If the bundled `codex` plugin is enabled but `openai-codex/*` still resolves
|
||||
through PI, `openclaw doctor` warns and leaves the route unchanged.
|
||||
</Note>
|
||||
|
||||
## OpenClaw feature coverage
|
||||
@@ -92,10 +118,11 @@ Choose your preferred auth method and follow the setup steps.
|
||||
|
||||
### Route summary
|
||||
|
||||
| Model ref | Route | Auth |
|
||||
|-----------|-------|------|
|
||||
| `openai/gpt-5.5` | Direct OpenAI Platform API | `OPENAI_API_KEY` |
|
||||
| `openai/gpt-5.4-mini` | Direct OpenAI Platform API | `OPENAI_API_KEY` |
|
||||
| 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 |
|
||||
|
||||
<Note>
|
||||
`openai/*` is the direct OpenAI API-key route unless you explicitly force
|
||||
@@ -154,10 +181,11 @@ Choose your preferred auth method and follow the setup steps.
|
||||
|
||||
### Route summary
|
||||
|
||||
| Model ref | Route | Auth |
|
||||
|-----------|-------|------|
|
||||
| `openai-codex/gpt-5.5` | ChatGPT/Codex OAuth through PI | Codex sign-in |
|
||||
| `openai/gpt-5.5` + `embeddedHarness.runtime: "codex"` | Codex app-server harness | Codex app-server auth |
|
||||
| Model ref | Runtime config | Route | Auth |
|
||||
|-----------|----------------|-------|------|
|
||||
| `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 |
|
||||
|
||||
<Note>
|
||||
Keep using the `openai-codex` provider id for auth/profile commands. The
|
||||
@@ -186,6 +214,15 @@ Choose your preferred auth method and follow the setup steps.
|
||||
`/new` or `/reset` after changing `embeddedHarness` if you want `/status` to
|
||||
reflect a new PI/Codex choice.
|
||||
|
||||
### Doctor warning
|
||||
|
||||
If the bundled `codex` plugin is enabled while this tab's
|
||||
`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
|
||||
app-server execution.
|
||||
|
||||
### Context window cap
|
||||
|
||||
OpenClaw treats model metadata and the runtime context cap as separate values.
|
||||
@@ -829,6 +866,7 @@ the Server-side compaction accordion below.
|
||||
- Use looser compat behavior
|
||||
- Strip Completions `store` from non-native `openai-completions` payloads
|
||||
- Accept advanced `params.extra_body`/`params.extraBody` pass-through JSON for OpenAI-compatible Completions proxies
|
||||
- Accept `params.chat_template_kwargs` for OpenAI-compatible Completions proxies such as vLLM
|
||||
- Do not force strict tool schemas or native-only headers
|
||||
|
||||
Azure OpenAI uses native transport and compat behavior but does not receive the hidden attribution headers.
|
||||
|
||||
@@ -82,6 +82,7 @@ Use explicit config when:
|
||||
- vLLM runs on a different host or port
|
||||
- You want to pin `contextWindow` or `maxTokens` values
|
||||
- Your server requires a real API key (or you want to control headers)
|
||||
- You connect to a trusted loopback, LAN, or Tailscale vLLM endpoint
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -91,6 +92,7 @@ Use explicit config when:
|
||||
baseUrl: "http://127.0.0.1:8000/v1",
|
||||
apiKey: "${VLLM_API_KEY}",
|
||||
api: "openai-completions",
|
||||
request: { allowPrivateNetwork: true },
|
||||
models: [
|
||||
{
|
||||
id: "your-model-id",
|
||||
@@ -126,6 +128,45 @@ Use explicit config when:
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Nemotron 3 thinking controls">
|
||||
vLLM/Nemotron 3 can use chat-template kwargs to control whether reasoning is
|
||||
returned as hidden reasoning or visible answer text. When an OpenClaw session
|
||||
uses `vllm/nemotron-3-*` with thinking off, OpenClaw sends:
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false,
|
||||
"force_nonempty_content": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To customize these values, set `chat_template_kwargs` under the model params.
|
||||
If you also set `params.extra_body.chat_template_kwargs`, that value has
|
||||
final precedence because `extra_body` is the last request-body override.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"vllm/nemotron-3-super": {
|
||||
params: {
|
||||
chat_template_kwargs: {
|
||||
enable_thinking: false,
|
||||
force_nonempty_content: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Custom base URL">
|
||||
If your vLLM server runs on a non-default host or port, set `baseUrl` in the explicit provider config:
|
||||
|
||||
@@ -137,6 +178,7 @@ Use explicit config when:
|
||||
baseUrl: "http://192.168.1.50:9000/v1",
|
||||
apiKey: "${VLLM_API_KEY}",
|
||||
api: "openai-completions",
|
||||
request: { allowPrivateNetwork: true },
|
||||
models: [
|
||||
{
|
||||
id: "my-custom-model",
|
||||
@@ -167,6 +209,10 @@ Use explicit config when:
|
||||
```
|
||||
|
||||
If you see a connection error, verify the host, port, and that vLLM started with the OpenAI-compatible server mode.
|
||||
For explicit loopback, LAN, or Tailscale endpoints, also set
|
||||
`models.providers.vllm.request.allowPrivateNetwork: true`; provider
|
||||
requests block private-network URLs by default unless the provider is
|
||||
explicitly trusted.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -132,6 +132,38 @@ GLM models are available as `zai/<model>` (example: `zai/glm-5`). The default bu
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Thinking and preserved thinking">
|
||||
Z.AI thinking follows OpenClaw's `/think` controls. With thinking off,
|
||||
OpenClaw sends `thinking: { type: "disabled" }` to avoid responses that
|
||||
spend the output budget on `reasoning_content` before visible text.
|
||||
|
||||
Preserved thinking is opt-in because Z.AI requires the full historical
|
||||
`reasoning_content` to be replayed, which increases prompt tokens. Enable it
|
||||
per model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"zai/glm-5.1": {
|
||||
params: { preserveThinking: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
When enabled and thinking is on, OpenClaw sends
|
||||
`thinking: { type: "enabled", clear_thinking: false }` and replays prior
|
||||
`reasoning_content` for the same OpenAI-compatible transcript.
|
||||
|
||||
Advanced users can still override the exact provider payload with
|
||||
`params.extra_body.thinking`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Image understanding">
|
||||
The bundled Z.AI plugin registers image understanding.
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ title: "Tests"
|
||||
- `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `threads` + `isolate: false` with adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=<n>` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs.
|
||||
- `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip.
|
||||
- `pnpm test:docker:all`: Builds the shared live-test image and Docker E2E image once, then runs the Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs are written under `.artifacts/docker-tests/<run-id>/`.
|
||||
- `pnpm test:docker:browser-cdp-snapshot`: Builds a Chromium-backed source E2E container, starts raw CDP plus an isolated Gateway, runs `browser doctor --deep`, and verifies CDP role snapshots include link URLs, cursor-promoted clickables, iframe refs, and frame metadata.
|
||||
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:codex`, `pnpm test:docker:live-cli-backend:codex:resume`, or `pnpm test:docker:live-cli-backend:codex:mcp`. Claude and Gemini have matching `:resume` and `:mcp` aliases.
|
||||
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
|
||||
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
|
||||
|
||||
@@ -11,6 +11,20 @@ For the overview, operator runbook, and concepts, see [ACP agents](/tools/acp-ag
|
||||
|
||||
The sections below cover acpx harness config, plugin setup for the MCP bridges, and permission configuration.
|
||||
|
||||
Use this page only when you are setting up the ACP/acpx route. For native Codex
|
||||
app-server runtime config, use [Codex harness](/plugins/codex-harness). For
|
||||
OpenAI API keys or Codex OAuth model-provider config, use
|
||||
[OpenAI](/providers/openai).
|
||||
|
||||
Codex has two OpenClaw routes:
|
||||
|
||||
| Route | Config/command | Setup page |
|
||||
| -------------------------- | ------------------------------------------------------ | --------------------------------------- |
|
||||
| Native Codex app-server | `/codex ...`, `embeddedHarness.runtime: "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.
|
||||
|
||||
## acpx harness support (current)
|
||||
|
||||
Current acpx built-in harness aliases:
|
||||
@@ -143,7 +157,10 @@ Then verify backend health:
|
||||
|
||||
### acpx command and version configuration
|
||||
|
||||
By default, the bundled `acpx` plugin uses its plugin-local pinned binary (`node_modules/.bin/acpx` inside the plugin package). Startup registers the backend as not-ready and a background job verifies `acpx --version`; if the binary is missing or mismatched, it runs `npm install --omit=dev --no-save acpx@<pinned>` and re-verifies. The gateway stays non-blocking throughout.
|
||||
By default, the bundled `acpx` plugin registers the embedded ACP backend without
|
||||
spawning an ACP agent during Gateway startup. Run `/acp doctor` for an explicit
|
||||
live probe. Set `OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE=1` only when you need the
|
||||
Gateway to probe the configured agent at startup.
|
||||
|
||||
Override the command or version in plugin config:
|
||||
|
||||
@@ -239,10 +256,11 @@ Restart the gateway after changing this value.
|
||||
|
||||
### Health probe agent configuration
|
||||
|
||||
The bundled `acpx` plugin probes one harness agent while deciding whether the
|
||||
embedded runtime backend is ready. If `acp.allowedAgents` is set, it defaults to
|
||||
the first allowed agent; otherwise it defaults to `codex`. If your deployment
|
||||
needs a different ACP agent for health checks, set the probe agent explicitly:
|
||||
When `/acp doctor` or the opt-in startup probe checks the backend, the bundled
|
||||
`acpx` plugin probes one harness agent. If `acp.allowedAgents` is set, it
|
||||
defaults to the first allowed agent; otherwise it defaults to `codex`. If your
|
||||
deployment needs a different ACP agent for health checks, set the probe agent
|
||||
explicitly:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.acpx.config.probeAgent claude
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,10 @@ a clear 501 error.
|
||||
What still works without Playwright:
|
||||
|
||||
- ARIA snapshots
|
||||
- Role-style accessibility snapshots (`--interactive`, `--compact`,
|
||||
`--depth`, `--efficient`) when a per-tab CDP WebSocket is available. This is
|
||||
a fallback for inspection and ref discovery; Playwright remains the primary
|
||||
action engine.
|
||||
- Page screenshots for the managed `openclaw` browser when a per-tab CDP
|
||||
WebSocket is available
|
||||
- Page screenshots for `existing-session` / Chrome MCP profiles
|
||||
@@ -84,7 +88,7 @@ What still needs Playwright:
|
||||
|
||||
- `navigate`
|
||||
- `act`
|
||||
- AI snapshots / role snapshots
|
||||
- AI snapshots that depend on Playwright's native AI snapshot format
|
||||
- CSS-selector element screenshots (`--element`)
|
||||
- full browser PDF export
|
||||
|
||||
@@ -256,9 +260,12 @@ OpenClaw supports two “snapshot” styles:
|
||||
- Output: the accessibility tree as structured nodes.
|
||||
- Actions: `openclaw browser click ax12` works when the snapshot path can bind
|
||||
the ref through Playwright and Chrome backend DOM ids.
|
||||
- If Playwright is unavailable, ARIA snapshots can still be useful for
|
||||
inspection, but refs may not be actionable. Re-snapshot with `--format ai`
|
||||
or `--interactive` when you need action refs.
|
||||
- If Playwright is unavailable, ARIA snapshots can still be useful for
|
||||
inspection, but refs may not be actionable. Re-snapshot with `--format ai`
|
||||
or `--interactive` when you need action refs.
|
||||
- Docker proof for the raw-CDP fallback path: `pnpm test:docker:browser-cdp-snapshot`
|
||||
starts Chromium with CDP, runs `browser doctor --deep`, and verifies role
|
||||
snapshots include link URLs, cursor-promoted clickables, and iframe metadata.
|
||||
|
||||
Ref behavior:
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ agent automation and verification.
|
||||
|
||||
```bash
|
||||
openclaw browser --browser-profile openclaw doctor
|
||||
openclaw browser --browser-profile openclaw doctor --deep
|
||||
openclaw browser --browser-profile openclaw status
|
||||
openclaw browser --browser-profile openclaw start
|
||||
openclaw browser --browser-profile openclaw open https://example.com
|
||||
|
||||
@@ -1,54 +1,93 @@
|
||||
---
|
||||
summary: "ClawHub guide: public registry, native OpenClaw install flows, and ClawHub CLI workflows"
|
||||
summary: "ClawHub: public registry for OpenClaw skills and plugins, native install flows, and the clawhub CLI"
|
||||
read_when:
|
||||
- Introducing ClawHub to new users
|
||||
- Installing, searching, or publishing skills or plugins
|
||||
- Explaining ClawHub CLI flags and sync behavior
|
||||
- Searching for, installing, or updating skills or plugins
|
||||
- Publishing skills or plugins to the registry
|
||||
- Configuring the clawhub CLI or its environment overrides
|
||||
title: "ClawHub"
|
||||
sidebarTitle: "ClawHub"
|
||||
---
|
||||
|
||||
ClawHub is the public registry for **OpenClaw skills and plugins**.
|
||||
|
||||
- Use native `openclaw` commands to search/install/update skills and install
|
||||
plugins from ClawHub.
|
||||
- Use the separate `clawhub` CLI when you need registry auth, publish, delete,
|
||||
undelete, or sync workflows.
|
||||
- Use native `openclaw` commands to search, install, and update skills, and to install plugins from ClawHub.
|
||||
- Use the separate `clawhub` CLI for registry auth, publish, delete/undelete, and sync workflows.
|
||||
|
||||
Site: [clawhub.ai](https://clawhub.ai)
|
||||
|
||||
## Quick start
|
||||
|
||||
<Steps>
|
||||
<Step title="Search">
|
||||
```bash
|
||||
openclaw skills search "calendar"
|
||||
```
|
||||
</Step>
|
||||
<Step title="Install">
|
||||
```bash
|
||||
openclaw skills install <skill-slug>
|
||||
```
|
||||
</Step>
|
||||
<Step title="Use">
|
||||
Start a new OpenClaw session — it picks up the new skill.
|
||||
</Step>
|
||||
<Step title="Publish (optional)">
|
||||
For registry-authenticated workflows (publish, sync, manage), install
|
||||
the separate `clawhub` CLI:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
# or
|
||||
pnpm add -g clawhub
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Native OpenClaw flows
|
||||
|
||||
Skills:
|
||||
<Tabs>
|
||||
<Tab title="Skills">
|
||||
```bash
|
||||
openclaw skills search "calendar"
|
||||
openclaw skills install <skill-slug>
|
||||
openclaw skills update --all
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw skills search "calendar"
|
||||
openclaw skills install <skill-slug>
|
||||
openclaw skills update --all
|
||||
```
|
||||
Native `openclaw` commands install into your active workspace and
|
||||
persist source metadata so later `update` calls can stay on ClawHub.
|
||||
|
||||
Plugins:
|
||||
</Tab>
|
||||
<Tab title="Plugins">
|
||||
```bash
|
||||
openclaw plugins install clawhub:<package>
|
||||
openclaw plugins update --all
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw plugins install clawhub:<package>
|
||||
openclaw plugins update --all
|
||||
```
|
||||
Bare npm-safe plugin specs are also tried against ClawHub before npm:
|
||||
|
||||
Bare npm-safe plugin specs are also tried against ClawHub before npm:
|
||||
```bash
|
||||
openclaw plugins install openclaw-codex-app-server
|
||||
```
|
||||
|
||||
```bash
|
||||
openclaw plugins install openclaw-codex-app-server
|
||||
```
|
||||
Plugin installs validate advertised `pluginApi` and
|
||||
`minGatewayVersion` compatibility before archive install runs, so
|
||||
incompatible hosts fail closed early instead of partially installing
|
||||
the package.
|
||||
|
||||
Native `openclaw` commands install into your active workspace and persist source
|
||||
metadata so later `update` calls can stay on ClawHub.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Plugin installs validate advertised `pluginApi` and `minGatewayVersion`
|
||||
compatibility before archive install runs, so incompatible hosts fail closed
|
||||
early instead of partially installing the package.
|
||||
<Note>
|
||||
`openclaw plugins install clawhub:...` only accepts installable plugin
|
||||
families. If a ClawHub package is actually a skill, OpenClaw stops and
|
||||
points you at `openclaw skills install <slug>` instead.
|
||||
|
||||
`openclaw plugins install clawhub:...` only accepts installable plugin families.
|
||||
If a ClawHub package is actually a skill, OpenClaw stops and points you at
|
||||
`openclaw skills install <slug>` instead.
|
||||
Anonymous ClawHub plugin installs also fail closed for private packages.
|
||||
Community or other non-official channels can still install, but OpenClaw
|
||||
warns so operators can review source and verification before enabling
|
||||
them.
|
||||
</Note>
|
||||
|
||||
## What ClawHub is
|
||||
|
||||
@@ -56,238 +95,224 @@ If a ClawHub package is actually a skill, OpenClaw stops and points you at
|
||||
- A versioned store of skill bundles and metadata.
|
||||
- A discovery surface for search, tags, and usage signals.
|
||||
|
||||
## How it works
|
||||
|
||||
1. A user publishes a skill bundle (files + metadata).
|
||||
2. ClawHub stores the bundle, parses metadata, and assigns a version.
|
||||
3. The registry indexes the skill for search and discovery.
|
||||
4. Users browse, download, and install skills in OpenClaw.
|
||||
|
||||
## What you can do
|
||||
|
||||
- Publish new skills and new versions of existing skills.
|
||||
- Discover skills by name, tags, or search.
|
||||
- Download skill bundles and inspect their files.
|
||||
- Report skills that are abusive or unsafe.
|
||||
- If you are a moderator, hide, unhide, delete, or ban.
|
||||
|
||||
## Who this is for (beginner-friendly)
|
||||
|
||||
If you want to add new capabilities to your OpenClaw agent, ClawHub is the easiest way to find and install skills. You do not need to know how the backend works. You can:
|
||||
|
||||
- Search for skills by plain language.
|
||||
- Install a skill into your workspace.
|
||||
- Update skills later with one command.
|
||||
- Back up your own skills by publishing them.
|
||||
|
||||
## Quick start (non-technical)
|
||||
|
||||
1. Search for something you need:
|
||||
- `openclaw skills search "calendar"`
|
||||
2. Install a skill:
|
||||
- `openclaw skills install <skill-slug>`
|
||||
3. Start a new OpenClaw session so it picks up the new skill.
|
||||
4. If you want to publish or manage registry auth, install the separate
|
||||
`clawhub` CLI too.
|
||||
|
||||
## Install the ClawHub CLI
|
||||
|
||||
You only need this for registry-authenticated workflows such as publish/sync:
|
||||
|
||||
```bash
|
||||
npm i -g clawhub
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g clawhub
|
||||
```
|
||||
|
||||
## How it fits into OpenClaw
|
||||
|
||||
Native `openclaw skills install` installs into the active workspace `skills/`
|
||||
directory. `openclaw plugins install clawhub:...` records a normal managed
|
||||
plugin install plus ClawHub source metadata for updates.
|
||||
|
||||
Anonymous ClawHub plugin installs also fail closed for private packages.
|
||||
Community or other non-official channels can still install, but OpenClaw warns
|
||||
so operators can review source and verification before enabling them.
|
||||
|
||||
The separate `clawhub` CLI also installs skills into `./skills` under your
|
||||
current working directory. If an OpenClaw workspace is configured, `clawhub`
|
||||
falls back to that workspace unless you override `--workdir` (or
|
||||
`CLAWHUB_WORKDIR`). OpenClaw loads workspace skills from `<workspace>/skills`
|
||||
and will pick them up in the **next** session. If you already use
|
||||
`~/.openclaw/skills` or bundled skills, workspace skills take precedence.
|
||||
|
||||
For more detail on how skills are loaded, shared, and gated, see
|
||||
[Skills](/tools/skills).
|
||||
|
||||
## Skill system overview
|
||||
|
||||
A skill is a versioned bundle of files that teaches OpenClaw how to perform a
|
||||
specific task. Each publish creates a new version, and the registry keeps a
|
||||
history of versions so users can audit changes.
|
||||
|
||||
A typical skill includes:
|
||||
A typical skill is a versioned bundle of files that includes:
|
||||
|
||||
- A `SKILL.md` file with the primary description and usage.
|
||||
- Optional configs, scripts, or supporting files used by the skill.
|
||||
- Metadata such as tags, summary, and install requirements.
|
||||
|
||||
ClawHub uses metadata to power discovery and safely expose skill capabilities.
|
||||
The registry also tracks usage signals (such as stars and downloads) to improve
|
||||
ranking and visibility.
|
||||
ClawHub uses metadata to power discovery and safely expose skill
|
||||
capabilities. The registry tracks usage signals (stars, downloads) to
|
||||
improve ranking and visibility. Each publish creates a new semver
|
||||
version, and the registry keeps version history so users can audit
|
||||
changes.
|
||||
|
||||
## What the service provides (features)
|
||||
## Workspace and skill loading
|
||||
|
||||
- **Public browsing** of skills and their `SKILL.md` content.
|
||||
- **Search** powered by embeddings (vector search), not just keywords.
|
||||
- **Versioning** with semver, changelogs, and tags (including `latest`).
|
||||
- **Downloads** as a zip per version.
|
||||
- **Stars and comments** for community feedback.
|
||||
- **Moderation** hooks for approvals and audits.
|
||||
- **CLI-friendly API** for automation and scripting.
|
||||
The separate `clawhub` CLI also installs skills into `./skills` under
|
||||
your current working directory. If an OpenClaw workspace is configured,
|
||||
`clawhub` falls back to that workspace unless you override `--workdir`
|
||||
(or `CLAWHUB_WORKDIR`). OpenClaw loads workspace skills from
|
||||
`<workspace>/skills` and picks them up in the **next** session.
|
||||
|
||||
If you already use `~/.openclaw/skills` or bundled skills, workspace
|
||||
skills take precedence. For more detail on how skills are loaded,
|
||||
shared, and gated, see [Skills](/tools/skills).
|
||||
|
||||
## Service features
|
||||
|
||||
| Feature | Notes |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| Public browsing | Skills and their `SKILL.md` content are publicly viewable. |
|
||||
| Search | Embedding-powered (vector search), not just keywords. |
|
||||
| Versioning | Semver, changelogs, and tags (including `latest`). |
|
||||
| Downloads | Zip per version. |
|
||||
| Stars and comments | Community feedback. |
|
||||
| Moderation | Approvals and audits. |
|
||||
| CLI-friendly API | Suitable for automation and scripting. |
|
||||
|
||||
## Security and moderation
|
||||
|
||||
ClawHub is open by default. Anyone can upload skills, but a GitHub account must
|
||||
be at least one week old to publish. This helps slow down abuse without blocking
|
||||
legitimate contributors.
|
||||
ClawHub is open by default — anyone can upload skills, but a GitHub
|
||||
account must be **at least one week old** to publish. This slows down
|
||||
abuse without blocking legitimate contributors.
|
||||
|
||||
Reporting and moderation:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Reporting">
|
||||
- Any signed-in user can report a skill.
|
||||
- Report reasons are required and recorded.
|
||||
- Each user can have up to 20 active reports at a time.
|
||||
- Skills with more than 3 unique reports are auto-hidden by default.
|
||||
</Accordion>
|
||||
<Accordion title="Moderation">
|
||||
- Moderators can view hidden skills, unhide them, delete them, or ban users.
|
||||
- Abusing the report feature can result in account bans.
|
||||
- Interested in becoming a moderator? Ask in the OpenClaw Discord and contact a moderator or maintainer.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
- Any signed in user can report a skill.
|
||||
- Report reasons are required and recorded.
|
||||
- Each user can have up to 20 active reports at a time.
|
||||
- Skills with more than 3 unique reports are auto hidden by default.
|
||||
- Moderators can view hidden skills, unhide them, delete them, or ban users.
|
||||
- Abusing the report feature can result in account bans.
|
||||
## ClawHub CLI
|
||||
|
||||
Interested in becoming a moderator? Ask in the OpenClaw Discord and contact a
|
||||
moderator or maintainer.
|
||||
You only need this for registry-authenticated workflows such as
|
||||
publish/sync.
|
||||
|
||||
## CLI commands and parameters
|
||||
### Global options
|
||||
|
||||
Global options (apply to all commands):
|
||||
<ParamField path="--workdir <dir>" type="string">
|
||||
Working directory. Default: current dir; falls back to OpenClaw workspace.
|
||||
</ParamField>
|
||||
<ParamField path="--dir <dir>" type="string" default="skills">
|
||||
Skills directory, relative to workdir.
|
||||
</ParamField>
|
||||
<ParamField path="--site <url>" type="string">
|
||||
Site base URL (browser login).
|
||||
</ParamField>
|
||||
<ParamField path="--registry <url>" type="string">
|
||||
Registry API base URL.
|
||||
</ParamField>
|
||||
<ParamField path="--no-input" type="boolean">
|
||||
Disable prompts (non-interactive).
|
||||
</ParamField>
|
||||
<ParamField path="-V, --cli-version" type="boolean">
|
||||
Print CLI version.
|
||||
</ParamField>
|
||||
|
||||
- `--workdir <dir>`: Working directory (default: current dir; falls back to OpenClaw workspace).
|
||||
- `--dir <dir>`: Skills directory, relative to workdir (default: `skills`).
|
||||
- `--site <url>`: Site base URL (browser login).
|
||||
- `--registry <url>`: Registry API base URL.
|
||||
- `--no-input`: Disable prompts (non-interactive).
|
||||
- `-V, --cli-version`: Print CLI version.
|
||||
### Commands
|
||||
|
||||
Auth:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Auth (login / logout / whoami)">
|
||||
```bash
|
||||
clawhub login # browser flow
|
||||
clawhub login --token <token>
|
||||
clawhub logout
|
||||
clawhub whoami
|
||||
```
|
||||
|
||||
- `clawhub login` (browser flow) or `clawhub login --token <token>`
|
||||
- `clawhub logout`
|
||||
- `clawhub whoami`
|
||||
Login options:
|
||||
|
||||
Options:
|
||||
- `--token <token>` — paste an API token.
|
||||
- `--label <label>` — label stored for browser login tokens (default: `CLI token`).
|
||||
- `--no-browser` — do not open a browser (requires `--token`).
|
||||
|
||||
- `--token <token>`: Paste an API token.
|
||||
- `--label <label>`: Label stored for browser login tokens (default: `CLI token`).
|
||||
- `--no-browser`: Do not open a browser (requires `--token`).
|
||||
</Accordion>
|
||||
<Accordion title="Search">
|
||||
```bash
|
||||
clawhub search "query"
|
||||
```
|
||||
|
||||
Search:
|
||||
- `--limit <n>` — max results.
|
||||
|
||||
- `clawhub search "query"`
|
||||
- `--limit <n>`: Max results.
|
||||
</Accordion>
|
||||
<Accordion title="Install / update / list">
|
||||
```bash
|
||||
clawhub install <slug>
|
||||
clawhub update <slug>
|
||||
clawhub update --all
|
||||
clawhub list
|
||||
```
|
||||
|
||||
Install:
|
||||
Options:
|
||||
|
||||
- `clawhub install <slug>`
|
||||
- `--version <version>`: Install a specific version.
|
||||
- `--force`: Overwrite if the folder already exists.
|
||||
- `--version <version>` — install or update to a specific version (single slug only on `update`).
|
||||
- `--force` — overwrite if the folder already exists, or when local files do not match any published version.
|
||||
- `clawhub list` reads `.clawhub/lock.json`.
|
||||
|
||||
Update:
|
||||
</Accordion>
|
||||
<Accordion title="Publish skills">
|
||||
```bash
|
||||
clawhub skill publish <path>
|
||||
```
|
||||
|
||||
- `clawhub update <slug>`
|
||||
- `clawhub update --all`
|
||||
- `--version <version>`: Update to a specific version (single slug only).
|
||||
- `--force`: Overwrite when local files do not match any published version.
|
||||
Options:
|
||||
|
||||
List:
|
||||
- `--slug <slug>` — skill slug.
|
||||
- `--name <name>` — display name.
|
||||
- `--version <version>` — semver version.
|
||||
- `--changelog <text>` — changelog text (can be empty).
|
||||
- `--tags <tags>` — comma-separated tags (default: `latest`).
|
||||
|
||||
- `clawhub list` (reads `.clawhub/lock.json`)
|
||||
</Accordion>
|
||||
<Accordion title="Publish plugins">
|
||||
```bash
|
||||
clawhub package publish <source>
|
||||
```
|
||||
|
||||
Publish skills:
|
||||
`<source>` can be a local folder, `owner/repo`, `owner/repo@ref`, or a
|
||||
GitHub URL.
|
||||
|
||||
- `clawhub skill publish <path>`
|
||||
- `--slug <slug>`: Skill slug.
|
||||
- `--name <name>`: Display name.
|
||||
- `--version <version>`: Semver version.
|
||||
- `--changelog <text>`: Changelog text (can be empty).
|
||||
- `--tags <tags>`: Comma-separated tags (default: `latest`).
|
||||
Options:
|
||||
|
||||
Publish plugins:
|
||||
- `--dry-run` — build the exact publish plan without uploading anything.
|
||||
- `--json` — emit machine-readable output for CI.
|
||||
- `--source-repo`, `--source-commit`, `--source-ref` — optional overrides when auto-detection is not enough.
|
||||
|
||||
- `clawhub package publish <source>`
|
||||
- `<source>` can be a local folder, `owner/repo`, `owner/repo@ref`, or a GitHub URL.
|
||||
- `--dry-run`: Build the exact publish plan without uploading anything.
|
||||
- `--json`: Emit machine-readable output for CI.
|
||||
- `--source-repo`, `--source-commit`, `--source-ref`: Optional overrides when auto-detection is not enough.
|
||||
</Accordion>
|
||||
<Accordion title="Delete / undelete (owner or admin)">
|
||||
```bash
|
||||
clawhub delete <slug> --yes
|
||||
clawhub undelete <slug> --yes
|
||||
```
|
||||
</Accordion>
|
||||
<Accordion title="Sync (scan local + publish new or updated)">
|
||||
```bash
|
||||
clawhub sync
|
||||
```
|
||||
|
||||
Delete/undelete (owner/admin only):
|
||||
Options:
|
||||
|
||||
- `clawhub delete <slug> --yes`
|
||||
- `clawhub undelete <slug> --yes`
|
||||
- `--root <dir...>` — extra scan roots.
|
||||
- `--all` — upload everything without prompts.
|
||||
- `--dry-run` — show what would be uploaded.
|
||||
- `--bump <type>` — `patch|minor|major` for updates (default: `patch`).
|
||||
- `--changelog <text>` — changelog for non-interactive updates.
|
||||
- `--tags <tags>` — comma-separated tags (default: `latest`).
|
||||
- `--concurrency <n>` — registry checks (default: `4`).
|
||||
|
||||
Sync (scan local skills + publish new/updated):
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
- `clawhub sync`
|
||||
- `--root <dir...>`: Extra scan roots.
|
||||
- `--all`: Upload everything without prompts.
|
||||
- `--dry-run`: Show what would be uploaded.
|
||||
- `--bump <type>`: `patch|minor|major` for updates (default: `patch`).
|
||||
- `--changelog <text>`: Changelog for non-interactive updates.
|
||||
- `--tags <tags>`: Comma-separated tags (default: `latest`).
|
||||
- `--concurrency <n>`: Registry checks (default: 4).
|
||||
## Common workflows
|
||||
|
||||
## Common workflows for agents
|
||||
<Tabs>
|
||||
<Tab title="Search">
|
||||
```bash
|
||||
clawhub search "postgres backups"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Install">
|
||||
```bash
|
||||
clawhub install my-skill-pack
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Update all">
|
||||
```bash
|
||||
clawhub update --all
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Publish a single skill">
|
||||
```bash
|
||||
clawhub skill publish ./my-skill --slug my-skill --name "My Skill" --version 1.0.0 --tags latest
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Sync many skills">
|
||||
```bash
|
||||
clawhub sync --all
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Publish a plugin from GitHub">
|
||||
```bash
|
||||
clawhub package publish your-org/your-plugin --dry-run
|
||||
clawhub package publish your-org/your-plugin
|
||||
clawhub package publish your-org/your-plugin@v1.0.0
|
||||
clawhub package publish https://github.com/your-org/your-plugin
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Search for skills
|
||||
### Plugin package metadata
|
||||
|
||||
```bash
|
||||
clawhub search "postgres backups"
|
||||
```
|
||||
|
||||
### Download new skills
|
||||
|
||||
```bash
|
||||
clawhub install my-skill-pack
|
||||
```
|
||||
|
||||
### Update installed skills
|
||||
|
||||
```bash
|
||||
clawhub update --all
|
||||
```
|
||||
|
||||
### Back up your skills (publish or sync)
|
||||
|
||||
For a single skill folder:
|
||||
|
||||
```bash
|
||||
clawhub skill publish ./my-skill --slug my-skill --name "My Skill" --version 1.0.0 --tags latest
|
||||
```
|
||||
|
||||
To scan and back up many skills at once:
|
||||
|
||||
```bash
|
||||
clawhub sync --all
|
||||
```
|
||||
|
||||
### Publish a plugin from GitHub
|
||||
|
||||
```bash
|
||||
clawhub package publish your-org/your-plugin --dry-run
|
||||
clawhub package publish your-org/your-plugin
|
||||
clawhub package publish your-org/your-plugin@v1.0.0
|
||||
clawhub package publish https://github.com/your-org/your-plugin
|
||||
```
|
||||
|
||||
Code plugins must include the required OpenClaw metadata in `package.json`:
|
||||
Code plugins must include the required OpenClaw metadata in
|
||||
`package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -309,50 +334,59 @@ Code plugins must include the required OpenClaw metadata in `package.json`:
|
||||
}
|
||||
```
|
||||
|
||||
Published packages should ship built JavaScript and point `runtimeExtensions`
|
||||
at that output. Git checkout installs can still fall back to TypeScript source
|
||||
when no built files exist, but built runtime entries avoid runtime TypeScript
|
||||
compilation in startup, doctor, and plugin loading paths.
|
||||
Published packages should ship **built JavaScript** and point
|
||||
`runtimeExtensions` at that output. Git checkout installs can still fall
|
||||
back to TypeScript source when no built files exist, but built runtime
|
||||
entries avoid runtime TypeScript compilation in startup, doctor, and
|
||||
plugin loading paths.
|
||||
|
||||
## Advanced details (technical)
|
||||
## Versioning, lockfile, and telemetry
|
||||
|
||||
### Versioning and tags
|
||||
<AccordionGroup>
|
||||
<Accordion title="Versioning and tags">
|
||||
- Each publish creates a new **semver** `SkillVersion`.
|
||||
- Tags (like `latest`) point to a version; moving tags lets you roll back.
|
||||
- Changelogs are attached per version and can be empty when syncing or publishing updates.
|
||||
</Accordion>
|
||||
<Accordion title="Local changes vs registry versions">
|
||||
Updates compare the local skill contents to registry versions using a
|
||||
content hash. If local files do not match any published version, the
|
||||
CLI asks before overwriting (or requires `--force` in
|
||||
non-interactive runs).
|
||||
</Accordion>
|
||||
<Accordion title="Sync scanning and fallback roots">
|
||||
`clawhub sync` scans your current workdir first. If no skills are
|
||||
found, it falls back to known legacy locations (for example
|
||||
`~/openclaw/skills` and `~/.openclaw/skills`). This is designed to
|
||||
find older skill installs without extra flags.
|
||||
</Accordion>
|
||||
<Accordion title="Storage and lockfile">
|
||||
- Installed skills are recorded in `.clawhub/lock.json` under your workdir.
|
||||
- Auth tokens are stored in the ClawHub CLI config file (override via `CLAWHUB_CONFIG_PATH`).
|
||||
</Accordion>
|
||||
<Accordion title="Telemetry (install counts)">
|
||||
When you run `clawhub sync` while logged in, the CLI sends a minimal
|
||||
snapshot to compute install counts. You can disable this entirely:
|
||||
|
||||
- Each publish creates a new **semver** `SkillVersion`.
|
||||
- Tags (like `latest`) point to a version; moving tags lets you roll back.
|
||||
- Changelogs are attached per version and can be empty when syncing or publishing updates.
|
||||
```bash
|
||||
export CLAWHUB_DISABLE_TELEMETRY=1
|
||||
```
|
||||
|
||||
### Local changes vs registry versions
|
||||
|
||||
Updates compare the local skill contents to registry versions using a content hash. If local files do not match any published version, the CLI asks before overwriting (or requires `--force` in non-interactive runs).
|
||||
|
||||
### Sync scanning and fallback roots
|
||||
|
||||
`clawhub sync` scans your current workdir first. If no skills are found, it falls back to known legacy locations (for example `~/openclaw/skills` and `~/.openclaw/skills`). This is designed to find older skill installs without extra flags.
|
||||
|
||||
### Storage and lockfile
|
||||
|
||||
- Installed skills are recorded in `.clawhub/lock.json` under your workdir.
|
||||
- Auth tokens are stored in the ClawHub CLI config file (override via `CLAWHUB_CONFIG_PATH`).
|
||||
|
||||
### Telemetry (install counts)
|
||||
|
||||
When you run `clawhub sync` while logged in, the CLI sends a minimal snapshot to compute install counts. You can disable this entirely:
|
||||
|
||||
```bash
|
||||
export CLAWHUB_DISABLE_TELEMETRY=1
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `CLAWHUB_SITE`: Override the site URL.
|
||||
- `CLAWHUB_REGISTRY`: Override the registry API URL.
|
||||
- `CLAWHUB_CONFIG_PATH`: Override where the CLI stores the token/config.
|
||||
- `CLAWHUB_WORKDIR`: Override the default workdir.
|
||||
- `CLAWHUB_DISABLE_TELEMETRY=1`: Disable telemetry on `sync`.
|
||||
| Variable | Effect |
|
||||
| ----------------------------- | ----------------------------------------------- |
|
||||
| `CLAWHUB_SITE` | Override the site URL. |
|
||||
| `CLAWHUB_REGISTRY` | Override the registry API URL. |
|
||||
| `CLAWHUB_CONFIG_PATH` | Override where the CLI stores the token/config. |
|
||||
| `CLAWHUB_WORKDIR` | Override the default workdir. |
|
||||
| `CLAWHUB_DISABLE_TELEMETRY=1` | Disable telemetry on `sync`. |
|
||||
|
||||
## Related
|
||||
|
||||
- [Plugin](/tools/plugin)
|
||||
- [Skills](/tools/skills)
|
||||
- [Community plugins](/plugins/community)
|
||||
- [Plugins](/tools/plugin)
|
||||
- [Skills](/tools/skills)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
summary: "Read-only diff viewer and file renderer for agents (optional plugin tool)"
|
||||
title: "Diffs"
|
||||
sidebarTitle: "Diffs"
|
||||
read_when:
|
||||
- You want agents to show code or markdown edits as diffs
|
||||
- You want a canvas-ready viewer URL or a rendered diff file
|
||||
@@ -24,24 +25,34 @@ When enabled, the plugin prepends concise usage guidance into system-prompt spac
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Enable the plugin.
|
||||
2. Call `diffs` with `mode: "view"` for canvas-first flows.
|
||||
3. Call `diffs` with `mode: "file"` for chat file delivery flows.
|
||||
4. Call `diffs` with `mode: "both"` when you need both artifacts.
|
||||
|
||||
## Enable the plugin
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
diffs: {
|
||||
enabled: true,
|
||||
<Steps>
|
||||
<Step title="Enable the plugin">
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
diffs: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Pick a mode">
|
||||
<Tabs>
|
||||
<Tab title="view">
|
||||
Canvas-first flows: agents call `diffs` with `mode: "view"` and open `details.viewerUrl` with `canvas present`.
|
||||
</Tab>
|
||||
<Tab title="file">
|
||||
Chat file delivery: agents call `diffs` with `mode: "file"` and send `details.filePath` with `message` using `path` or `filePath`.
|
||||
</Tab>
|
||||
<Tab title="both">
|
||||
Combined: agents call `diffs` with `mode: "both"` to get both artifacts in one call.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Disable built-in system guidance
|
||||
|
||||
@@ -68,122 +79,174 @@ If you want to disable both the guidance and the tool, disable the plugin instea
|
||||
|
||||
## Typical agent workflow
|
||||
|
||||
1. Agent calls `diffs`.
|
||||
2. Agent reads `details` fields.
|
||||
3. Agent either:
|
||||
- opens `details.viewerUrl` with `canvas present`
|
||||
- sends `details.filePath` with `message` using `path` or `filePath`
|
||||
- does both
|
||||
<Steps>
|
||||
<Step title="Call diffs">
|
||||
Agent calls the `diffs` tool with input.
|
||||
</Step>
|
||||
<Step title="Read details">
|
||||
Agent reads `details` fields from the response.
|
||||
</Step>
|
||||
<Step title="Present">
|
||||
Agent either opens `details.viewerUrl` with `canvas present`, sends `details.filePath` with `message` using `path` or `filePath`, or does both.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Input examples
|
||||
|
||||
Before and after:
|
||||
|
||||
```json
|
||||
{
|
||||
"before": "# Hello\n\nOne",
|
||||
"after": "# Hello\n\nTwo",
|
||||
"path": "docs/example.md",
|
||||
"mode": "view"
|
||||
}
|
||||
```
|
||||
|
||||
Patch:
|
||||
|
||||
```json
|
||||
{
|
||||
"patch": "diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-const x = 1;\n+const x = 2;\n",
|
||||
"mode": "both"
|
||||
}
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Before and after">
|
||||
```json
|
||||
{
|
||||
"before": "# Hello\n\nOne",
|
||||
"after": "# Hello\n\nTwo",
|
||||
"path": "docs/example.md",
|
||||
"mode": "view"
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Patch">
|
||||
```json
|
||||
{
|
||||
"patch": "diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-const x = 1;\n+const x = 2;\n",
|
||||
"mode": "both"
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Tool input reference
|
||||
|
||||
All fields are optional unless noted:
|
||||
All fields are optional unless noted.
|
||||
|
||||
- `before` (`string`): original text. Required with `after` when `patch` is omitted.
|
||||
- `after` (`string`): updated text. Required with `before` when `patch` is omitted.
|
||||
- `patch` (`string`): unified diff text. Mutually exclusive with `before` and `after`.
|
||||
- `path` (`string`): display filename for before and after mode.
|
||||
- `lang` (`string`): language override hint for before and after mode. Unknown values fall back to plain text.
|
||||
- `title` (`string`): viewer title override.
|
||||
- `mode` (`"view" | "file" | "both"`): output mode. Defaults to plugin default `defaults.mode`.
|
||||
Deprecated alias: `"image"` behaves like `"file"` and is still accepted for backward compatibility.
|
||||
- `theme` (`"light" | "dark"`): viewer theme. Defaults to plugin default `defaults.theme`.
|
||||
- `layout` (`"unified" | "split"`): diff layout. Defaults to plugin default `defaults.layout`.
|
||||
- `expandUnchanged` (`boolean`): expand unchanged sections when full context is available. Per-call option only (not a plugin default key).
|
||||
- `fileFormat` (`"png" | "pdf"`): rendered file format. Defaults to plugin default `defaults.fileFormat`.
|
||||
- `fileQuality` (`"standard" | "hq" | "print"`): quality preset for PNG or PDF rendering.
|
||||
- `fileScale` (`number`): device scale override (`1`-`4`).
|
||||
- `fileMaxWidth` (`number`): max render width in CSS pixels (`640`-`2400`).
|
||||
- `ttlSeconds` (`number`): artifact TTL in seconds for viewer and standalone file outputs. Default 1800, max 21600.
|
||||
- `baseUrl` (`string`): viewer URL origin override. Overrides plugin `viewerBaseUrl`. Must be `http` or `https`, no query/hash.
|
||||
<ParamField path="before" type="string">
|
||||
Original text. Required with `after` when `patch` is omitted.
|
||||
</ParamField>
|
||||
<ParamField path="after" type="string">
|
||||
Updated text. Required with `before` when `patch` is omitted.
|
||||
</ParamField>
|
||||
<ParamField path="patch" type="string">
|
||||
Unified diff text. Mutually exclusive with `before` and `after`.
|
||||
</ParamField>
|
||||
<ParamField path="path" type="string">
|
||||
Display filename for before and after mode.
|
||||
</ParamField>
|
||||
<ParamField path="lang" type="string">
|
||||
Language override hint for before and after mode. Unknown values fall back to plain text.
|
||||
</ParamField>
|
||||
<ParamField path="title" type="string">
|
||||
Viewer title override.
|
||||
</ParamField>
|
||||
<ParamField path="mode" type='"view" | "file" | "both"'>
|
||||
Output mode. Defaults to plugin default `defaults.mode`. Deprecated alias: `"image"` behaves like `"file"` and is still accepted for backward compatibility.
|
||||
</ParamField>
|
||||
<ParamField path="theme" type='"light" | "dark"'>
|
||||
Viewer theme. Defaults to plugin default `defaults.theme`.
|
||||
</ParamField>
|
||||
<ParamField path="layout" type='"unified" | "split"'>
|
||||
Diff layout. Defaults to plugin default `defaults.layout`.
|
||||
</ParamField>
|
||||
<ParamField path="expandUnchanged" type="boolean">
|
||||
Expand unchanged sections when full context is available. Per-call option only (not a plugin default key).
|
||||
</ParamField>
|
||||
<ParamField path="fileFormat" type='"png" | "pdf"'>
|
||||
Rendered file format. Defaults to plugin default `defaults.fileFormat`.
|
||||
</ParamField>
|
||||
<ParamField path="fileQuality" type='"standard" | "hq" | "print"'>
|
||||
Quality preset for PNG or PDF rendering.
|
||||
</ParamField>
|
||||
<ParamField path="fileScale" type="number">
|
||||
Device scale override (`1`-`4`).
|
||||
</ParamField>
|
||||
<ParamField path="fileMaxWidth" type="number">
|
||||
Max render width in CSS pixels (`640`-`2400`).
|
||||
</ParamField>
|
||||
<ParamField path="ttlSeconds" type="number" default="1800">
|
||||
Artifact TTL in seconds for viewer and standalone file outputs. Max 21600.
|
||||
</ParamField>
|
||||
<ParamField path="baseUrl" type="string">
|
||||
Viewer URL origin override. Overrides plugin `viewerBaseUrl`. Must be `http` or `https`, no query/hash.
|
||||
</ParamField>
|
||||
|
||||
Legacy input aliases still accepted for backward compatibility:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Legacy input aliases">
|
||||
Still accepted for backward compatibility:
|
||||
|
||||
- `format` -> `fileFormat`
|
||||
- `imageFormat` -> `fileFormat`
|
||||
- `imageQuality` -> `fileQuality`
|
||||
- `imageScale` -> `fileScale`
|
||||
- `imageMaxWidth` -> `fileMaxWidth`
|
||||
- `format` -> `fileFormat`
|
||||
- `imageFormat` -> `fileFormat`
|
||||
- `imageQuality` -> `fileQuality`
|
||||
- `imageScale` -> `fileScale`
|
||||
- `imageMaxWidth` -> `fileMaxWidth`
|
||||
|
||||
Validation and limits:
|
||||
|
||||
- `before` and `after` each max 512 KiB.
|
||||
- `patch` max 2 MiB.
|
||||
- `path` max 2048 bytes.
|
||||
- `lang` max 128 bytes.
|
||||
- `title` max 1024 bytes.
|
||||
- Patch complexity cap: max 128 files and 120000 total lines.
|
||||
- `patch` and `before` or `after` together are rejected.
|
||||
- Rendered file safety limits (apply to PNG and PDF):
|
||||
- `fileQuality: "standard"`: max 8 MP (8,000,000 rendered pixels).
|
||||
- `fileQuality: "hq"`: max 14 MP (14,000,000 rendered pixels).
|
||||
- `fileQuality: "print"`: max 24 MP (24,000,000 rendered pixels).
|
||||
- PDF also has a max of 50 pages.
|
||||
</Accordion>
|
||||
<Accordion title="Validation and limits">
|
||||
- `before` and `after` each max 512 KiB.
|
||||
- `patch` max 2 MiB.
|
||||
- `path` max 2048 bytes.
|
||||
- `lang` max 128 bytes.
|
||||
- `title` max 1024 bytes.
|
||||
- Patch complexity cap: max 128 files and 120000 total lines.
|
||||
- `patch` and `before` or `after` together are rejected.
|
||||
- Rendered file safety limits (apply to PNG and PDF):
|
||||
- `fileQuality: "standard"`: max 8 MP (8,000,000 rendered pixels).
|
||||
- `fileQuality: "hq"`: max 14 MP (14,000,000 rendered pixels).
|
||||
- `fileQuality: "print"`: max 24 MP (24,000,000 rendered pixels).
|
||||
- PDF also has a max of 50 pages.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Output details contract
|
||||
|
||||
The tool returns structured metadata under `details`.
|
||||
|
||||
Shared fields for modes that create a viewer:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Viewer fields">
|
||||
Shared fields for modes that create a viewer:
|
||||
|
||||
- `artifactId`
|
||||
- `viewerUrl`
|
||||
- `viewerPath`
|
||||
- `title`
|
||||
- `expiresAt`
|
||||
- `inputKind`
|
||||
- `fileCount`
|
||||
- `mode`
|
||||
- `context` (`agentId`, `sessionId`, `messageChannel`, `agentAccountId` when available)
|
||||
- `artifactId`
|
||||
- `viewerUrl`
|
||||
- `viewerPath`
|
||||
- `title`
|
||||
- `expiresAt`
|
||||
- `inputKind`
|
||||
- `fileCount`
|
||||
- `mode`
|
||||
- `context` (`agentId`, `sessionId`, `messageChannel`, `agentAccountId` when available)
|
||||
|
||||
File fields when PNG or PDF is rendered:
|
||||
</Accordion>
|
||||
<Accordion title="File fields">
|
||||
File fields when PNG or PDF is rendered:
|
||||
|
||||
- `artifactId`
|
||||
- `expiresAt`
|
||||
- `filePath`
|
||||
- `path` (same value as `filePath`, for message tool compatibility)
|
||||
- `fileBytes`
|
||||
- `fileFormat`
|
||||
- `fileQuality`
|
||||
- `fileScale`
|
||||
- `fileMaxWidth`
|
||||
- `artifactId`
|
||||
- `expiresAt`
|
||||
- `filePath`
|
||||
- `path` (same value as `filePath`, for message tool compatibility)
|
||||
- `fileBytes`
|
||||
- `fileFormat`
|
||||
- `fileQuality`
|
||||
- `fileScale`
|
||||
- `fileMaxWidth`
|
||||
|
||||
Compatibility aliases also returned for existing callers:
|
||||
</Accordion>
|
||||
<Accordion title="Compatibility aliases">
|
||||
Also returned for existing callers:
|
||||
|
||||
- `format` (same value as `fileFormat`)
|
||||
- `imagePath` (same value as `filePath`)
|
||||
- `imageBytes` (same value as `fileBytes`)
|
||||
- `imageQuality` (same value as `fileQuality`)
|
||||
- `imageScale` (same value as `fileScale`)
|
||||
- `imageMaxWidth` (same value as `fileMaxWidth`)
|
||||
- `format` (same value as `fileFormat`)
|
||||
- `imagePath` (same value as `filePath`)
|
||||
- `imageBytes` (same value as `fileBytes`)
|
||||
- `imageQuality` (same value as `fileQuality`)
|
||||
- `imageScale` (same value as `fileScale`)
|
||||
- `imageMaxWidth` (same value as `fileMaxWidth`)
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Mode behavior summary:
|
||||
|
||||
- `mode: "view"`: viewer fields only.
|
||||
- `mode: "file"`: file fields only, no viewer artifact.
|
||||
- `mode: "both"`: viewer fields plus file fields. If file rendering fails, viewer still returns with `fileError` and compatibility alias `imageError`.
|
||||
| Mode | What is returned |
|
||||
| -------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `"view"` | Viewer fields only. |
|
||||
| `"file"` | File fields only, no viewer artifact. |
|
||||
| `"both"` | Viewer fields plus file fields. If file rendering fails, viewer still returns with `fileError` and `imageError` alias. |
|
||||
|
||||
## Collapsed unchanged sections
|
||||
|
||||
@@ -246,13 +309,11 @@ Supported defaults:
|
||||
|
||||
Explicit tool parameters override these defaults.
|
||||
|
||||
Persistent viewer URL config:
|
||||
### Persistent viewer URL config
|
||||
|
||||
- `viewerBaseUrl` (`string`, optional)
|
||||
- Plugin-owned fallback for returned viewer links when a tool call does not pass `baseUrl`.
|
||||
- Must be `http` or `https`, no query/hash.
|
||||
|
||||
Example:
|
||||
<ParamField path="viewerBaseUrl" type="string">
|
||||
Plugin-owned fallback for returned viewer links when a tool call does not pass `baseUrl`. Must be `http` or `https`, no query/hash.
|
||||
</ParamField>
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -271,11 +332,9 @@ Example:
|
||||
|
||||
## Security config
|
||||
|
||||
- `security.allowRemoteViewer` (`boolean`, default `false`)
|
||||
- `false`: non-loopback requests to viewer routes are denied.
|
||||
- `true`: remote viewers are allowed if tokenized path is valid.
|
||||
|
||||
Example:
|
||||
<ParamField path="security.allowRemoteViewer" type="boolean" default="false">
|
||||
`false`: non-loopback requests to viewer routes are denied. `true`: remote viewers are allowed if tokenized path is valid.
|
||||
</ParamField>
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -336,23 +395,24 @@ URL construction behavior:
|
||||
|
||||
## Security model
|
||||
|
||||
Viewer hardening:
|
||||
|
||||
- Loopback-only by default.
|
||||
- Tokenized viewer paths with strict ID and token validation.
|
||||
- Viewer response CSP:
|
||||
- `default-src 'none'`
|
||||
- scripts and assets only from self
|
||||
- no outbound `connect-src`
|
||||
- Remote miss throttling when remote access is enabled:
|
||||
- 40 failures per 60 seconds
|
||||
- 60 second lockout (`429 Too Many Requests`)
|
||||
|
||||
File rendering hardening:
|
||||
|
||||
- Screenshot browser request routing is deny-by-default.
|
||||
- Only local viewer assets from `http://127.0.0.1/plugins/diffs/assets/*` are allowed.
|
||||
- External network requests are blocked.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Viewer hardening">
|
||||
- Loopback-only by default.
|
||||
- Tokenized viewer paths with strict ID and token validation.
|
||||
- Viewer response CSP:
|
||||
- `default-src 'none'`
|
||||
- scripts and assets only from self
|
||||
- no outbound `connect-src`
|
||||
- Remote miss throttling when remote access is enabled:
|
||||
- 40 failures per 60 seconds
|
||||
- 60 second lockout (`429 Too Many Requests`)
|
||||
</Accordion>
|
||||
<Accordion title="File rendering hardening">
|
||||
- Screenshot browser request routing is deny-by-default.
|
||||
- Only local viewer assets from `http://127.0.0.1/plugins/diffs/assets/*` are allowed.
|
||||
- External network requests are blocked.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Browser requirements for file mode
|
||||
|
||||
@@ -360,12 +420,19 @@ File rendering hardening:
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. `browser.executablePath` in OpenClaw config.
|
||||
2. Environment variables:
|
||||
- `OPENCLAW_BROWSER_EXECUTABLE_PATH`
|
||||
- `BROWSER_EXECUTABLE_PATH`
|
||||
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`
|
||||
3. Platform command/path discovery fallback.
|
||||
<Steps>
|
||||
<Step title="Config">
|
||||
`browser.executablePath` in OpenClaw config.
|
||||
</Step>
|
||||
<Step title="Environment variables">
|
||||
- `OPENCLAW_BROWSER_EXECUTABLE_PATH`
|
||||
- `BROWSER_EXECUTABLE_PATH`
|
||||
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`
|
||||
</Step>
|
||||
<Step title="Platform fallback">
|
||||
Platform command/path discovery fallback.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Common failure text:
|
||||
|
||||
@@ -375,42 +442,35 @@ Fix by installing Chrome, Chromium, Edge, or Brave, or setting one of the execut
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Input validation errors:
|
||||
|
||||
- `Provide patch or both before and after text.`
|
||||
- Include both `before` and `after`, or provide `patch`.
|
||||
- `Provide either patch or before/after input, not both.`
|
||||
- Do not mix input modes.
|
||||
- `Invalid baseUrl: ...`
|
||||
- Use `http(s)` origin with optional path, no query/hash.
|
||||
- `{field} exceeds maximum size (...)`
|
||||
- Reduce payload size.
|
||||
- Large patch rejection
|
||||
- Reduce patch file count or total lines.
|
||||
|
||||
Viewer accessibility issues:
|
||||
|
||||
- Viewer URL resolves to `127.0.0.1` by default.
|
||||
- For remote access scenarios, either:
|
||||
- set plugin `viewerBaseUrl`, or
|
||||
- pass `baseUrl` per tool call, or
|
||||
- use `gateway.bind=custom` and `gateway.customBindHost`
|
||||
- If `gateway.trustedProxies` includes loopback for a same-host proxy (for example Tailscale Serve), raw loopback viewer requests without forwarded client-IP headers fail closed by design.
|
||||
- For that proxy topology:
|
||||
- prefer `mode: "file"` or `mode: "both"` when you only need an attachment, or
|
||||
- intentionally enable `security.allowRemoteViewer` and set plugin `viewerBaseUrl` or pass a proxy/public `baseUrl` when you need a shareable viewer URL
|
||||
- Enable `security.allowRemoteViewer` only when you intend external viewer access.
|
||||
|
||||
Unmodified-lines row has no expand button:
|
||||
|
||||
- This can happen for patch input when the patch does not carry expandable context.
|
||||
- This is expected and does not indicate a viewer failure.
|
||||
|
||||
Artifact not found:
|
||||
|
||||
- Artifact expired due TTL.
|
||||
- Token or path changed.
|
||||
- Cleanup removed stale data.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Input validation errors">
|
||||
- `Provide patch or both before and after text.` — include both `before` and `after`, or provide `patch`.
|
||||
- `Provide either patch or before/after input, not both.` — do not mix input modes.
|
||||
- `Invalid baseUrl: ...` — use `http(s)` origin with optional path, no query/hash.
|
||||
- `{field} exceeds maximum size (...)` — reduce payload size.
|
||||
- Large patch rejection — reduce patch file count or total lines.
|
||||
</Accordion>
|
||||
<Accordion title="Viewer accessibility">
|
||||
- Viewer URL resolves to `127.0.0.1` by default.
|
||||
- For remote access scenarios, either:
|
||||
- set plugin `viewerBaseUrl`, or
|
||||
- pass `baseUrl` per tool call, or
|
||||
- use `gateway.bind=custom` and `gateway.customBindHost`
|
||||
- If `gateway.trustedProxies` includes loopback for a same-host proxy (for example Tailscale Serve), raw loopback viewer requests without forwarded client-IP headers fail closed by design.
|
||||
- For that proxy topology:
|
||||
- prefer `mode: "file"` or `mode: "both"` when you only need an attachment, or
|
||||
- intentionally enable `security.allowRemoteViewer` and set plugin `viewerBaseUrl` or pass a proxy/public `baseUrl` when you need a shareable viewer URL
|
||||
- Enable `security.allowRemoteViewer` only when you intend external viewer access.
|
||||
</Accordion>
|
||||
<Accordion title="Unmodified-lines row has no expand button">
|
||||
This can happen for patch input when the patch does not carry expandable context. This is expected and does not indicate a viewer failure.
|
||||
</Accordion>
|
||||
<Accordion title="Artifact not found">
|
||||
- Artifact expired due TTL.
|
||||
- Token or path changed.
|
||||
- Cleanup removed stale data.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Operational guidance
|
||||
|
||||
@@ -421,12 +481,12 @@ Artifact not found:
|
||||
- Avoid sending secrets in diff input when not required.
|
||||
- If your channel compresses images aggressively (for example Telegram or WhatsApp), prefer PDF output (`fileFormat: "pdf"`).
|
||||
|
||||
Diff rendering engine:
|
||||
<Note>
|
||||
Diff rendering engine powered by [Diffs](https://diffs.com).
|
||||
</Note>
|
||||
|
||||
- Powered by [Diffs](https://diffs.com).
|
||||
## Related
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Tools overview](/tools)
|
||||
- [Plugins](/tools/plugin)
|
||||
- [Browser](/tools/browser)
|
||||
- [Plugins](/tools/plugin)
|
||||
- [Tools overview](/tools)
|
||||
|
||||
@@ -1,77 +1,78 @@
|
||||
---
|
||||
summary: "Exec approvals, allowlists, and sandbox escape prompts"
|
||||
summary: "Host exec approvals: policy knobs, allowlists, and the YOLO/strict workflow"
|
||||
read_when:
|
||||
- Configuring exec approvals or allowlists
|
||||
- Implementing exec approval UX in the macOS app
|
||||
- Reviewing sandbox escape prompts and implications
|
||||
- Reviewing sandbox-escape prompts and their implications
|
||||
title: "Exec approvals"
|
||||
sidebarTitle: "Exec approvals"
|
||||
---
|
||||
|
||||
Exec approvals are the **companion app / node host guardrail** for letting a
|
||||
sandboxed agent run commands on a real host (`gateway` or `node`). A safety
|
||||
interlock: commands are allowed only when policy + allowlist + (optional) user
|
||||
approval all agree. Exec approvals stack **on top of** tool policy and elevated
|
||||
gating (unless elevated is set to `full`, which skips approvals).
|
||||
Exec approvals are the **companion app / node host guardrail** for letting
|
||||
a sandboxed agent run commands on a real host (`gateway` or `node`). A
|
||||
safety interlock: commands are allowed only when policy + allowlist +
|
||||
(optional) user approval all agree. Exec approvals stack **on top of**
|
||||
tool policy and elevated gating (unless elevated is set to `full`, which
|
||||
skips approvals).
|
||||
|
||||
<Note>
|
||||
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults;
|
||||
if an approvals field is omitted, the `tools.exec` value is used. Host exec
|
||||
also uses local approvals state on that machine — a host-local `ask: "always"`
|
||||
in `~/.openclaw/exec-approvals.json` keeps prompting even if session or config
|
||||
defaults request `ask: "on-miss"`.
|
||||
Effective policy is the **stricter** of `tools.exec.*` and approvals
|
||||
defaults; if an approvals field is omitted, the `tools.exec` value is
|
||||
used. Host exec also uses local approvals state on that machine — a
|
||||
host-local `ask: "always"` in `~/.openclaw/exec-approvals.json` keeps
|
||||
prompting even if session or config defaults request `ask: "on-miss"`.
|
||||
</Note>
|
||||
|
||||
## Inspecting the effective policy
|
||||
|
||||
- `openclaw approvals get`, `... --gateway`, `... --node <id|name|ip>` — show requested policy, host policy sources, and the effective result.
|
||||
- `openclaw exec-policy show` — local-machine merged view.
|
||||
- `openclaw exec-policy set|preset` — synchronize the local requested policy with the local host approvals file in one step.
|
||||
| Command | What it shows |
|
||||
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| `openclaw approvals get` / `--gateway` / `--node <id\|name\|ip>` | Requested policy, host policy sources, and the effective result. |
|
||||
| `openclaw exec-policy show` | Local-machine merged view. |
|
||||
| `openclaw exec-policy set` / `preset` | Synchronize the local requested policy with the local host approvals file in one step. |
|
||||
|
||||
When a local scope requests `host=node`, `exec-policy show` reports that scope
|
||||
as node-managed at runtime instead of pretending the local approvals file is
|
||||
the source of truth.
|
||||
When a local scope requests `host=node`, `exec-policy show` reports that
|
||||
scope as node-managed at runtime instead of pretending the local
|
||||
approvals file is the source of truth.
|
||||
|
||||
If the companion app UI is **not available**, any request that would normally
|
||||
prompt is resolved by the **ask fallback** (default: deny).
|
||||
If the companion app UI is **not available**, any request that would
|
||||
normally prompt is resolved by the **ask fallback** (default: `deny`).
|
||||
|
||||
<Tip>
|
||||
Native chat approval clients can seed channel-specific affordances on the
|
||||
pending approval message. For example, Matrix seeds reaction shortcuts (`✅`
|
||||
allow once, `❌` deny, `♾️` allow always) while still leaving `/approve ...`
|
||||
commands in the message as a fallback.
|
||||
pending approval message. For example, Matrix seeds reaction shortcuts
|
||||
(`✅` allow once, `❌` deny, `♾️` allow always) while still leaving
|
||||
`/approve ...` commands in the message as a fallback.
|
||||
</Tip>
|
||||
|
||||
## Where it applies
|
||||
|
||||
Exec approvals are enforced locally on the execution host:
|
||||
|
||||
- **gateway host** → `openclaw` process on the gateway machine
|
||||
- **node host** → node runner (macOS companion app or headless node host)
|
||||
- **Gateway host** → `openclaw` process on the gateway machine.
|
||||
- **Node host** → node runner (macOS companion app or headless node host).
|
||||
|
||||
Trust model note:
|
||||
### Trust model
|
||||
|
||||
- Gateway-authenticated callers are trusted operators for that Gateway.
|
||||
- Paired nodes extend that trusted operator capability onto the node host.
|
||||
- Exec approvals reduce accidental execution risk, but are not a per-user auth boundary.
|
||||
- Approved node-host runs bind canonical execution context: canonical cwd, exact argv, env
|
||||
binding when present, and pinned executable path when applicable.
|
||||
- For shell scripts and direct interpreter/runtime file invocations, OpenClaw also tries to bind
|
||||
one concrete local file operand. If that bound file changes after approval but before execution,
|
||||
the run is denied instead of executing drifted content.
|
||||
- This file binding is intentionally best-effort, not a complete semantic model of every
|
||||
interpreter/runtime loader path. If approval mode cannot identify exactly one concrete local
|
||||
file to bind, it refuses to mint an approval-backed run instead of pretending full coverage.
|
||||
- Exec approvals reduce accidental execution risk, but are **not** a per-user auth boundary.
|
||||
- Approved node-host runs bind canonical execution context: canonical cwd, exact argv, env binding when present, and pinned executable path when applicable.
|
||||
- For shell scripts and direct interpreter/runtime file invocations, OpenClaw also tries to bind one concrete local file operand. If that bound file changes after approval but before execution, the run is denied instead of executing drifted content.
|
||||
- File binding is intentionally best-effort, **not** a complete semantic model of every interpreter/runtime loader path. If approval mode cannot identify exactly one concrete local file to bind, it refuses to mint an approval-backed run instead of pretending full coverage.
|
||||
|
||||
macOS split:
|
||||
### macOS split
|
||||
|
||||
- **node host service** forwards `system.run` to the **macOS app** over local IPC.
|
||||
- **macOS app** enforces approvals + executes the command in UI context.
|
||||
- The **node host service** forwards `system.run` to the **macOS app** over local IPC.
|
||||
- The **macOS app** enforces approvals and executes the command in UI context.
|
||||
|
||||
## Settings and storage
|
||||
|
||||
Approvals live in a local JSON file on the execution host:
|
||||
|
||||
`~/.openclaw/exec-approvals.json`
|
||||
```text
|
||||
~/.openclaw/exec-approvals.json
|
||||
```
|
||||
|
||||
Example schema:
|
||||
|
||||
@@ -108,59 +109,120 @@ Example schema:
|
||||
}
|
||||
```
|
||||
|
||||
## No-approval "YOLO" mode
|
||||
## Policy knobs
|
||||
|
||||
If you want host exec to run without approval prompts, you must open **both** policy layers:
|
||||
### `exec.security`
|
||||
|
||||
- requested exec policy in OpenClaw config (`tools.exec.*`)
|
||||
- host-local approvals policy in `~/.openclaw/exec-approvals.json`
|
||||
<ParamField path="security" type='"deny" | "allowlist" | "full"'>
|
||||
- `deny` — block all host exec requests.
|
||||
- `allowlist` — allow only allowlisted commands.
|
||||
- `full` — allow everything (equivalent to elevated).
|
||||
</ParamField>
|
||||
|
||||
This is now the default host behavior unless you tighten it explicitly:
|
||||
### `exec.ask`
|
||||
|
||||
- `tools.exec.security`: `full` on `gateway`/`node`
|
||||
- `tools.exec.ask`: `off`
|
||||
- host `askFallback`: `full`
|
||||
<ParamField path="ask" type='"off" | "on-miss" | "always"'>
|
||||
- `off` — never prompt.
|
||||
- `on-miss` — prompt only when the allowlist does not match.
|
||||
- `always` — prompt on every command. `allow-always` durable trust does **not** suppress prompts when effective ask mode is `always`.
|
||||
</ParamField>
|
||||
|
||||
Important distinction:
|
||||
### `askFallback`
|
||||
|
||||
- `tools.exec.host=auto` chooses where exec runs: sandbox when available, otherwise gateway.
|
||||
- YOLO chooses how host exec is approved: `security=full` plus `ask=off`.
|
||||
- CLI-backed providers that expose their own noninteractive permission mode can follow this policy.
|
||||
Claude CLI adds `--permission-mode bypassPermissions` when OpenClaw's requested exec policy is
|
||||
YOLO. Override that backend behavior with explicit Claude args under
|
||||
`agents.defaults.cliBackends.claude-cli.args` / `resumeArgs`, for example
|
||||
`--permission-mode default`, `acceptEdits`, or `bypassPermissions`.
|
||||
- In YOLO mode, OpenClaw does not add a separate heuristic command-obfuscation approval gate or script-preflight rejection layer on top of the configured host exec policy.
|
||||
- `auto` does not make gateway routing a free override from a sandboxed session. A per-call `host=node` request is allowed from `auto`, and `host=gateway` is only allowed from `auto` when no sandbox runtime is active. If you want a stable non-auto default, set `tools.exec.host` or use `/exec host=...` explicitly.
|
||||
<ParamField path="askFallback" type='"deny" | "allowlist" | "full"'>
|
||||
Resolution when a prompt is required but no UI is reachable.
|
||||
|
||||
If you want a more conservative setup, tighten either layer back to `allowlist` / `on-miss`
|
||||
or `deny`.
|
||||
- `deny` — block.
|
||||
- `allowlist` — allow only if allowlist matches.
|
||||
- `full` — allow.
|
||||
</ParamField>
|
||||
|
||||
Persistent gateway-host "never prompt" setup:
|
||||
### `tools.exec.strictInlineEval`
|
||||
|
||||
```bash
|
||||
openclaw config set tools.exec.host gateway
|
||||
openclaw config set tools.exec.security full
|
||||
openclaw config set tools.exec.ask off
|
||||
openclaw gateway restart
|
||||
```
|
||||
<ParamField path="strictInlineEval" type="boolean">
|
||||
When `true`, OpenClaw treats inline code-eval forms as approval-only
|
||||
even if the interpreter binary itself is allowlisted. Defense-in-depth
|
||||
for interpreter loaders that do not map cleanly to one stable file
|
||||
operand.
|
||||
</ParamField>
|
||||
|
||||
Then set the host approvals file to match:
|
||||
Examples that strict mode catches:
|
||||
|
||||
```bash
|
||||
openclaw approvals set --stdin <<'EOF'
|
||||
{
|
||||
version: 1,
|
||||
defaults: {
|
||||
security: "full",
|
||||
ask: "off",
|
||||
askFallback: "full"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
- `python -c`
|
||||
- `node -e`, `node --eval`, `node -p`
|
||||
- `ruby -e`
|
||||
- `perl -e`, `perl -E`
|
||||
- `php -r`
|
||||
- `lua -e`
|
||||
- `osascript -e`
|
||||
|
||||
Local shortcut for the same gateway-host policy on the current machine:
|
||||
In strict mode these commands still need explicit approval, and
|
||||
`allow-always` does not persist new allowlist entries for them
|
||||
automatically.
|
||||
|
||||
## YOLO mode (no-approval)
|
||||
|
||||
If you want host exec to run without approval prompts, you must open
|
||||
**both** policy layers — requested exec policy in OpenClaw config
|
||||
(`tools.exec.*`) **and** host-local approvals policy in
|
||||
`~/.openclaw/exec-approvals.json`.
|
||||
|
||||
YOLO is the default host behavior unless you tighten it explicitly:
|
||||
|
||||
| Layer | YOLO setting |
|
||||
| --------------------- | -------------------------- |
|
||||
| `tools.exec.security` | `full` on `gateway`/`node` |
|
||||
| `tools.exec.ask` | `off` |
|
||||
| Host `askFallback` | `full` |
|
||||
|
||||
<Warning>
|
||||
**Important distinctions:**
|
||||
|
||||
- `tools.exec.host=auto` chooses **where** exec runs: sandbox when available, otherwise gateway.
|
||||
- YOLO chooses **how** host exec is approved: `security=full` plus `ask=off`.
|
||||
- In YOLO mode, OpenClaw does **not** add a separate heuristic command-obfuscation approval gate or script-preflight rejection layer on top of the configured host exec policy.
|
||||
- `auto` does not make gateway routing a free override from a sandboxed session. A per-call `host=node` request is allowed from `auto`; `host=gateway` is only allowed from `auto` when no sandbox runtime is active. For a stable non-auto default, set `tools.exec.host` or use `/exec host=...` explicitly.
|
||||
</Warning>
|
||||
|
||||
CLI-backed providers that expose their own noninteractive permission mode
|
||||
can follow this policy. Claude CLI adds
|
||||
`--permission-mode bypassPermissions` when OpenClaw's requested exec
|
||||
policy is YOLO. Override that backend behavior with explicit Claude args
|
||||
under `agents.defaults.cliBackends.claude-cli.args` / `resumeArgs` —
|
||||
for example `--permission-mode default`, `acceptEdits`, or
|
||||
`bypassPermissions`.
|
||||
|
||||
If you want a more conservative setup, tighten either layer back to
|
||||
`allowlist` / `on-miss` or `deny`.
|
||||
|
||||
### Persistent gateway-host "never prompt" setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Set the requested config policy">
|
||||
```bash
|
||||
openclaw config set tools.exec.host gateway
|
||||
openclaw config set tools.exec.security full
|
||||
openclaw config set tools.exec.ask off
|
||||
openclaw gateway restart
|
||||
```
|
||||
</Step>
|
||||
<Step title="Match the host approvals file">
|
||||
```bash
|
||||
openclaw approvals set --stdin <<'EOF'
|
||||
{
|
||||
version: 1,
|
||||
defaults: {
|
||||
security: "full",
|
||||
ask: "off",
|
||||
askFallback: "full"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Local shortcut
|
||||
|
||||
```bash
|
||||
openclaw exec-policy preset yolo
|
||||
@@ -168,13 +230,15 @@ openclaw exec-policy preset yolo
|
||||
|
||||
That local shortcut updates both:
|
||||
|
||||
- local `tools.exec.host/security/ask`
|
||||
- local `~/.openclaw/exec-approvals.json` defaults
|
||||
- Local `tools.exec.host/security/ask`.
|
||||
- Local `~/.openclaw/exec-approvals.json` defaults.
|
||||
|
||||
It is intentionally local-only. If you need to change gateway-host or node-host approvals
|
||||
remotely, continue using `openclaw approvals set --gateway` or
|
||||
It is intentionally local-only. To change gateway-host or node-host
|
||||
approvals remotely, use `openclaw approvals set --gateway` or
|
||||
`openclaw approvals set --node <id|name|ip>`.
|
||||
|
||||
### Node host
|
||||
|
||||
For a node host, apply the same approvals file on that node instead:
|
||||
|
||||
```bash
|
||||
@@ -190,71 +254,36 @@ openclaw approvals set --node <id|name|ip> --stdin <<'EOF'
|
||||
EOF
|
||||
```
|
||||
|
||||
Important local-only limitation:
|
||||
<Note>
|
||||
**Local-only limitations:**
|
||||
|
||||
- `openclaw exec-policy` does not synchronize node approvals
|
||||
- `openclaw exec-policy set --host node` is rejected
|
||||
- node exec approvals are fetched from the node at runtime, so node-targeted updates must use `openclaw approvals --node ...`
|
||||
- `openclaw exec-policy` does not synchronize node approvals.
|
||||
- `openclaw exec-policy set --host node` is rejected.
|
||||
- Node exec approvals are fetched from the node at runtime, so node-targeted updates must use `openclaw approvals --node ...`.
|
||||
</Note>
|
||||
|
||||
Session-only shortcut:
|
||||
### Session-only shortcut
|
||||
|
||||
- `/exec security=full ask=off` changes only the current session.
|
||||
- `/elevated full` is a break-glass shortcut that also skips exec approvals for that session.
|
||||
|
||||
If the host approvals file stays stricter than config, the stricter host policy still wins.
|
||||
|
||||
## Policy knobs
|
||||
|
||||
### Security (`exec.security`)
|
||||
|
||||
- **deny**: block all host exec requests.
|
||||
- **allowlist**: allow only allowlisted commands.
|
||||
- **full**: allow everything (equivalent to elevated).
|
||||
|
||||
### Ask (`exec.ask`)
|
||||
|
||||
- **off**: never prompt.
|
||||
- **on-miss**: prompt only when allowlist does not match.
|
||||
- **always**: prompt on every command.
|
||||
- `allow-always` durable trust does not suppress prompts when effective ask mode is `always`
|
||||
|
||||
### Ask fallback (`askFallback`)
|
||||
|
||||
If a prompt is required but no UI is reachable, fallback decides:
|
||||
|
||||
- **deny**: block.
|
||||
- **allowlist**: allow only if allowlist matches.
|
||||
- **full**: allow.
|
||||
|
||||
### Inline interpreter eval hardening (`tools.exec.strictInlineEval`)
|
||||
|
||||
When `tools.exec.strictInlineEval=true`, OpenClaw treats inline code-eval forms as approval-only even if the interpreter binary itself is allowlisted.
|
||||
|
||||
Examples:
|
||||
|
||||
- `python -c`
|
||||
- `node -e`, `node --eval`, `node -p`
|
||||
- `ruby -e`
|
||||
- `perl -e`, `perl -E`
|
||||
- `php -r`
|
||||
- `lua -e`
|
||||
- `osascript -e`
|
||||
|
||||
This is defense-in-depth for interpreter loaders that do not map cleanly to one stable file operand. In strict mode:
|
||||
|
||||
- these commands still need explicit approval;
|
||||
- `allow-always` does not persist new allowlist entries for them automatically.
|
||||
If the host approvals file stays stricter than config, the stricter host
|
||||
policy still wins.
|
||||
|
||||
## Allowlist (per agent)
|
||||
|
||||
Allowlists are **per agent**. If multiple agents exist, switch which agent you’re
|
||||
editing in the macOS app. Patterns are glob matches.
|
||||
Patterns can be resolved binary path globs or bare command-name globs. Bare names
|
||||
match only commands invoked through PATH, so `rg` can match `/opt/homebrew/bin/rg`
|
||||
when the command is `rg`, but not `./rg` or `/tmp/rg`. Use a path glob when you
|
||||
want to trust one specific binary location.
|
||||
Allowlists are **per agent**. If multiple agents exist, switch which agent
|
||||
you are editing in the macOS app. Patterns are glob matches.
|
||||
|
||||
Patterns can be resolved binary path globs or bare command-name globs.
|
||||
Bare names match only commands invoked through `PATH`, so `rg` can match
|
||||
`/opt/homebrew/bin/rg` when the command is `rg`, but **not** `./rg` or
|
||||
`/tmp/rg`. Use a path glob when you want to trust one specific binary
|
||||
location.
|
||||
|
||||
Legacy `agents.default` entries are migrated to `agents.main` on load.
|
||||
Shell chains such as `echo ok && pwd` still need every top-level segment to satisfy allowlist rules.
|
||||
Shell chains such as `echo ok && pwd` still need every top-level segment
|
||||
to satisfy allowlist rules.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -265,89 +294,94 @@ Examples:
|
||||
|
||||
Each allowlist entry tracks:
|
||||
|
||||
- **id** stable UUID used for UI identity (optional)
|
||||
- **last used** timestamp
|
||||
- **last used command**
|
||||
- **last resolved path**
|
||||
| Field | Meaning |
|
||||
| ------------------ | -------------------------------- |
|
||||
| `id` | Stable UUID used for UI identity |
|
||||
| `lastUsedAt` | Last-used timestamp |
|
||||
| `lastUsedCommand` | Last command that matched |
|
||||
| `lastResolvedPath` | Last resolved binary path |
|
||||
|
||||
## Auto-allow skill CLIs
|
||||
|
||||
When **Auto-allow skill CLIs** is enabled, executables referenced by known skills
|
||||
are treated as allowlisted on nodes (macOS node or headless node host). This uses
|
||||
`skills.bins` over the Gateway RPC to fetch the skill bin list. Disable this if you want strict manual allowlists.
|
||||
|
||||
Important trust notes:
|
||||
When **Auto-allow skill CLIs** is enabled, executables referenced by
|
||||
known skills are treated as allowlisted on nodes (macOS node or headless
|
||||
node host). This uses `skills.bins` over the Gateway RPC to fetch the
|
||||
skill bin list. Disable this if you want strict manual allowlists.
|
||||
|
||||
<Warning>
|
||||
- This is an **implicit convenience allowlist**, separate from manual path allowlist entries.
|
||||
- It is intended for trusted operator environments where Gateway and node are in the same trust boundary.
|
||||
- If you require strict explicit trust, keep `autoAllowSkills: false` and use manual path allowlist entries only.
|
||||
</Warning>
|
||||
|
||||
## Safe bins and approval forwarding
|
||||
|
||||
For safe bins (the stdin-only fast-path), interpreter binding details, and how
|
||||
to forward approval prompts to Slack/Discord/Telegram (or run them as native
|
||||
approval clients), see [Exec approvals — advanced](/tools/exec-approvals-advanced).
|
||||
|
||||
<!-- moved to /tools/exec-approvals-advanced -->
|
||||
For safe bins (the stdin-only fast-path), interpreter binding details, and
|
||||
how to forward approval prompts to Slack/Discord/Telegram (or run them as
|
||||
native approval clients), see
|
||||
[Exec approvals — advanced](/tools/exec-approvals-advanced).
|
||||
|
||||
## Control UI editing
|
||||
|
||||
Use the **Control UI → Nodes → Exec approvals** card to edit defaults, per‑agent
|
||||
overrides, and allowlists. Pick a scope (Defaults or an agent), tweak the policy,
|
||||
add/remove allowlist patterns, then **Save**. The UI shows **last used** metadata
|
||||
per pattern so you can keep the list tidy.
|
||||
Use the **Control UI → Nodes → Exec approvals** card to edit defaults,
|
||||
per-agent overrides, and allowlists. Pick a scope (Defaults or an agent),
|
||||
tweak the policy, add/remove allowlist patterns, then **Save**. The UI
|
||||
shows last-used metadata per pattern so you can keep the list tidy.
|
||||
|
||||
The target selector chooses **Gateway** (local approvals) or a **Node**. Nodes
|
||||
must advertise `system.execApprovals.get/set` (macOS app or headless node host).
|
||||
If a node does not advertise exec approvals yet, edit its local
|
||||
`~/.openclaw/exec-approvals.json` directly.
|
||||
The target selector chooses **Gateway** (local approvals) or a **Node**.
|
||||
Nodes must advertise `system.execApprovals.get/set` (macOS app or
|
||||
headless node host). If a node does not advertise exec approvals yet,
|
||||
edit its local `~/.openclaw/exec-approvals.json` directly.
|
||||
|
||||
CLI: `openclaw approvals` supports gateway or node editing (see [Approvals CLI](/cli/approvals)).
|
||||
CLI: `openclaw approvals` supports gateway or node editing — see
|
||||
[Approvals CLI](/cli/approvals).
|
||||
|
||||
## Approval flow
|
||||
|
||||
When a prompt is required, the gateway broadcasts `exec.approval.requested` to operator clients.
|
||||
The Control UI and macOS app resolve it via `exec.approval.resolve`, then the gateway forwards the
|
||||
When a prompt is required, the gateway broadcasts
|
||||
`exec.approval.requested` to operator clients. The Control UI and macOS
|
||||
app resolve it via `exec.approval.resolve`, then the gateway forwards the
|
||||
approved request to the node host.
|
||||
|
||||
For `host=node`, approval requests include a canonical `systemRunPlan` payload. The gateway uses
|
||||
that plan as the authoritative command/cwd/session context when forwarding approved `system.run`
|
||||
For `host=node`, approval requests include a canonical `systemRunPlan`
|
||||
payload. The gateway uses that plan as the authoritative
|
||||
command/cwd/session context when forwarding approved `system.run`
|
||||
requests.
|
||||
|
||||
That matters for async approval latency:
|
||||
|
||||
- the node exec path prepares one canonical plan up front
|
||||
- the approval record stores that plan and its binding metadata
|
||||
- once approved, the final forwarded `system.run` call reuses the stored plan
|
||||
instead of trusting later caller edits
|
||||
- if the caller changes `command`, `rawCommand`, `cwd`, `agentId`, or
|
||||
`sessionKey` after the approval request was created, the gateway rejects the
|
||||
forwarded run as an approval mismatch
|
||||
- The node exec path prepares one canonical plan up front.
|
||||
- The approval record stores that plan and its binding metadata.
|
||||
- Once approved, the final forwarded `system.run` call reuses the stored plan instead of trusting later caller edits.
|
||||
- If the caller changes `command`, `rawCommand`, `cwd`, `agentId`, or `sessionKey` after the approval request was created, the gateway rejects the forwarded run as an approval mismatch.
|
||||
|
||||
## System events
|
||||
|
||||
Exec lifecycle is surfaced as system messages:
|
||||
|
||||
- `Exec running` (only if the command exceeds the running notice threshold)
|
||||
- `Exec finished`
|
||||
- `Exec denied`
|
||||
- `Exec running` (only if the command exceeds the running notice threshold).
|
||||
- `Exec finished`.
|
||||
- `Exec denied`.
|
||||
|
||||
These are posted to the agent’s session after the node reports the event.
|
||||
Gateway-host exec approvals emit the same lifecycle events when the command finishes (and optionally when running longer than the threshold).
|
||||
Approval-gated execs reuse the approval id as the `runId` in these messages for easy correlation.
|
||||
These are posted to the agent's session after the node reports the event.
|
||||
Gateway-host exec approvals emit the same lifecycle events when the
|
||||
command finishes (and optionally when running longer than the threshold).
|
||||
Approval-gated execs reuse the approval id as the `runId` in these
|
||||
messages for easy correlation.
|
||||
|
||||
## Denied approval behavior
|
||||
|
||||
When an async exec approval is denied, OpenClaw prevents the agent from reusing
|
||||
output from any earlier run of the same command in the session. The denial reason
|
||||
is passed with explicit guidance that no command output is available, which stops
|
||||
the agent from claiming there is new output or repeating the denied command with
|
||||
stale results from a prior successful run.
|
||||
When an async exec approval is denied, OpenClaw prevents the agent from
|
||||
reusing output from any earlier run of the same command in the session.
|
||||
The denial reason is passed with explicit guidance that no command output
|
||||
is available, which stops the agent from claiming there is new output or
|
||||
repeating the denied command with stale results from a prior successful
|
||||
run.
|
||||
|
||||
## Implications
|
||||
|
||||
- **full** is powerful; prefer allowlists when possible.
|
||||
- **ask** keeps you in the loop while still allowing fast approvals.
|
||||
- **`full`** is powerful; prefer allowlists when possible.
|
||||
- **`ask`** keeps you in the loop while still allowing fast approvals.
|
||||
- Per-agent allowlists prevent one agent's approvals from leaking into others.
|
||||
- Approvals only apply to host exec requests from **authorized senders**. Unauthorized senders cannot issue `/exec`.
|
||||
- `/exec security=full` is a session-level convenience for authorized operators and skips approvals by design. To hard-block host exec, set approvals security to `deny` or deny the `exec` tool via tool policy.
|
||||
|
||||
@@ -1,50 +1,68 @@
|
||||
---
|
||||
summary: "Generate and edit images using configured providers (OpenAI, OpenAI Codex OAuth, Google Gemini, OpenRouter, LiteLLM, fal, MiniMax, ComfyUI, Vydra, xAI)"
|
||||
summary: "Generate and edit images via image_generate across OpenAI, Google, fal, MiniMax, ComfyUI, OpenRouter, LiteLLM, xAI, Vydra"
|
||||
read_when:
|
||||
- Generating images via the agent
|
||||
- Configuring image generation providers and models
|
||||
- Generating or editing images via the agent
|
||||
- Configuring image-generation providers and models
|
||||
- Understanding the image_generate tool parameters
|
||||
title: "Image generation"
|
||||
sidebarTitle: "Image generation"
|
||||
---
|
||||
|
||||
The `image_generate` tool lets the agent create and edit images using your configured providers. Generated images are delivered automatically as media attachments in the agent's reply.
|
||||
The `image_generate` tool lets the agent create and edit images using your
|
||||
configured providers. Generated images are delivered automatically as media
|
||||
attachments in the agent's reply.
|
||||
|
||||
<Note>
|
||||
The tool only appears when at least one image generation provider is available. If you don't see `image_generate` in your agent's tools, configure `agents.defaults.imageGenerationModel`, set up a provider API key, or sign in with OpenAI Codex OAuth.
|
||||
The tool only appears when at least one image-generation provider is
|
||||
available. If you do not see `image_generate` in your agent's tools,
|
||||
configure `agents.defaults.imageGenerationModel`, set up a provider API key,
|
||||
or sign in with OpenAI Codex OAuth.
|
||||
</Note>
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Set an API key for at least one provider (for example `OPENAI_API_KEY`, `GEMINI_API_KEY`, or `OPENROUTER_API_KEY`) or sign in with OpenAI Codex OAuth.
|
||||
2. Optionally set your preferred model:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-2",
|
||||
// Optional default provider request timeout for image_generate.
|
||||
timeoutMs: 180_000,
|
||||
<Steps>
|
||||
<Step title="Configure auth">
|
||||
Set an API key for at least one provider (for example `OPENAI_API_KEY`,
|
||||
`GEMINI_API_KEY`, `OPENROUTER_API_KEY`) or sign in with OpenAI Codex OAuth.
|
||||
</Step>
|
||||
<Step title="Pick a default model (optional)">
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openai/gpt-image-2",
|
||||
timeoutMs: 180_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
Codex OAuth uses the same `openai/gpt-image-2` model ref. When an
|
||||
`openai-codex` OAuth profile is configured, OpenClaw routes image requests
|
||||
through that same OAuth profile instead of first trying `OPENAI_API_KEY`.
|
||||
Explicit custom `models.providers.openai` image config, such as an API key or
|
||||
custom/Azure base URL, opts back into the direct OpenAI Images API route.
|
||||
Codex OAuth uses the same `openai/gpt-image-2` model ref. When an
|
||||
`openai-codex` OAuth profile is configured, OpenClaw routes image
|
||||
requests through that OAuth profile instead of first trying
|
||||
`OPENAI_API_KEY`. Explicit `models.providers.openai` config (API key,
|
||||
custom/Azure base URL) opts back into the direct OpenAI Images API
|
||||
route.
|
||||
|
||||
</Step>
|
||||
<Step title="Ask the agent">
|
||||
_"Generate an image of a friendly robot mascot."_
|
||||
|
||||
The agent calls `image_generate` automatically. No tool allow-listing
|
||||
needed — it is enabled by default when a provider is available.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Warning>
|
||||
For OpenAI-compatible LAN endpoints such as LocalAI, keep the custom
|
||||
`models.providers.openai.baseUrl` and explicitly opt in with
|
||||
`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork: true`; private/internal
|
||||
image endpoints remain blocked by default.
|
||||
|
||||
3. Ask the agent: _"Generate an image of a friendly robot mascot."_
|
||||
|
||||
The agent calls `image_generate` automatically. No tool allow-listing needed — it's enabled by default when a provider is available.
|
||||
`browser.ssrfPolicy.dangerouslyAllowPrivateNetwork: true`. Private and
|
||||
internal image endpoints remain blocked by default.
|
||||
</Warning>
|
||||
|
||||
## Common routes
|
||||
|
||||
@@ -61,97 +79,91 @@ The same `image_generate` tool handles text-to-image and reference-image
|
||||
editing. Use `image` for one reference or `images` for multiple references.
|
||||
Provider-supported output hints such as `quality`, `outputFormat`, and
|
||||
`background` are forwarded when available and reported as ignored when a
|
||||
provider does not support them. Current bundled transparent-background support
|
||||
is OpenAI-specific; other providers may still preserve PNG alpha if their
|
||||
provider does not support them. Bundled transparent-background support is
|
||||
OpenAI-specific; other providers may still preserve PNG alpha if their
|
||||
backend emits it.
|
||||
|
||||
## Supported providers
|
||||
|
||||
| Provider | Default model | Edit support | Auth |
|
||||
| ---------- | --------------------------------------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| ComfyUI | `workflow` | Yes (1 image, workflow-configured) | `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY` for cloud |
|
||||
| fal | `fal-ai/flux/dev` | Yes | `FAL_KEY` |
|
||||
| Google | `gemini-3.1-flash-image-preview` | Yes | `GEMINI_API_KEY` or `GOOGLE_API_KEY` |
|
||||
| LiteLLM | `gpt-image-2` | Yes (up to 5 input images) | `LITELLM_API_KEY` |
|
||||
| MiniMax | `image-01` | Yes (subject reference) | `MINIMAX_API_KEY` or MiniMax OAuth (`minimax-portal`) |
|
||||
| OpenAI | `gpt-image-2` | Yes (up to 4 images) | `OPENAI_API_KEY` or OpenAI Codex OAuth |
|
||||
| OpenRouter | `google/gemini-3.1-flash-image-preview` | Yes (up to 5 input images) | `OPENROUTER_API_KEY` |
|
||||
| LiteLLM | `gpt-image-2` | Yes (up to 5 input images) | `LITELLM_API_KEY` |
|
||||
| Google | `gemini-3.1-flash-image-preview` | Yes | `GEMINI_API_KEY` or `GOOGLE_API_KEY` |
|
||||
| fal | `fal-ai/flux/dev` | Yes | `FAL_KEY` |
|
||||
| MiniMax | `image-01` | Yes (subject reference) | `MINIMAX_API_KEY` or MiniMax OAuth (`minimax-portal`) |
|
||||
| ComfyUI | `workflow` | Yes (1 image, workflow-configured) | `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY` for cloud |
|
||||
| Vydra | `grok-imagine` | No | `VYDRA_API_KEY` |
|
||||
| xAI | `grok-imagine-image` | Yes (up to 5 images) | `XAI_API_KEY` |
|
||||
|
||||
Use `action: "list"` to inspect available providers and models at runtime:
|
||||
|
||||
```
|
||||
```text
|
||||
/tool image_generate action=list
|
||||
```
|
||||
|
||||
## Provider capabilities
|
||||
|
||||
| Capability | ComfyUI | fal | Google | MiniMax | OpenAI | Vydra | xAI |
|
||||
| --------------------- | ------------------ | ----------------- | -------------- | --------------------- | -------------- | ----- | -------------- |
|
||||
| Generate (max count) | Workflow-defined | 4 | 4 | 9 | 4 | 1 | 4 |
|
||||
| Edit / reference | 1 image (workflow) | 1 image | Up to 5 images | 1 image (subject ref) | Up to 5 images | — | Up to 5 images |
|
||||
| Size control | — | ✓ | ✓ | — | Up to 4K | — | — |
|
||||
| Aspect ratio | — | ✓ (generate only) | ✓ | ✓ | — | — | ✓ |
|
||||
| Resolution (1K/2K/4K) | — | ✓ | ✓ | — | — | — | 1K, 2K |
|
||||
|
||||
## Tool parameters
|
||||
|
||||
<ParamField path="prompt" type="string" required>
|
||||
Image generation prompt. Required for `action: "generate"`.
|
||||
Image generation prompt. Required for `action: "generate"`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="action" type="'generate' | 'list'" default="generate">
|
||||
Use `"list"` to inspect available providers and models at runtime.
|
||||
<ParamField path="action" type='"generate" | "list"' default="generate">
|
||||
Use `"list"` to inspect available providers and models at runtime.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="model" type="string">
|
||||
Provider/model override, e.g. `openai/gpt-image-2`; use
|
||||
`openai/gpt-image-1.5` for transparent OpenAI backgrounds.
|
||||
Provider/model override (e.g. `openai/gpt-image-2`). Use
|
||||
`openai/gpt-image-1.5` for transparent OpenAI backgrounds.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="image" type="string">
|
||||
Single reference image path or URL for edit mode.
|
||||
Single reference image path or URL for edit mode.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="images" type="string[]">
|
||||
Multiple reference images for edit mode (up to 5).
|
||||
Multiple reference images for edit mode (up to 5 on supporting providers).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="size" type="string">
|
||||
Size hint: `1024x1024`, `1536x1024`, `1024x1536`, `2048x2048`, `3840x2160`.
|
||||
Size hint: `1024x1024`, `1536x1024`, `1024x1536`, `2048x2048`, `3840x2160`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="aspectRatio" type="string">
|
||||
Aspect ratio: `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`.
|
||||
Aspect ratio: `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="resolution" type="'1K' | '2K' | '4K'">
|
||||
Resolution hint.
|
||||
<ParamField path="resolution" type='"1K" | "2K" | "4K"'>Resolution hint.</ParamField>
|
||||
<ParamField path="quality" type='"low" | "medium" | "high" | "auto"'>
|
||||
Quality hint when the provider supports it.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="quality" type="'low' | 'medium' | 'high' | 'auto'">
|
||||
Quality hint when the provider supports it.
|
||||
<ParamField path="outputFormat" type='"png" | "jpeg" | "webp"'>
|
||||
Output format hint when the provider supports it.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="outputFormat" type="'png' | 'jpeg' | 'webp'">
|
||||
Output format hint when the provider supports it.
|
||||
<ParamField path="background" type='"transparent" | "opaque" | "auto"'>
|
||||
Background hint when the provider supports it. Use `transparent` with
|
||||
`outputFormat: "png"` or `"webp"` for transparency-capable providers.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="background" type="'transparent' | 'opaque' | 'auto'">
|
||||
Background hint when the provider supports it. Use `transparent` with
|
||||
`outputFormat: "png"` or `"webp"` for transparency-capable providers.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="count" type="number">
|
||||
Number of images to generate (1–4).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="timeoutMs" type="number">
|
||||
Optional provider request timeout in milliseconds.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="filename" type="string">
|
||||
Output filename hint.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="count" type="number">Number of images to generate (1–4).</ParamField>
|
||||
<ParamField path="timeoutMs" type="number">Optional provider request timeout in milliseconds.</ParamField>
|
||||
<ParamField path="filename" type="string">Output filename hint.</ParamField>
|
||||
<ParamField path="openai" type="object">
|
||||
OpenAI-only hints: `background`, `moderation`, `outputCompression`, and `user`.
|
||||
OpenAI-only hints: `background`, `moderation`, `outputCompression`, and `user`.
|
||||
</ParamField>
|
||||
|
||||
Not all providers support all parameters. When a fallback provider supports a nearby geometry option instead of the exact requested one, OpenClaw remaps to the closest supported size, aspect ratio, or resolution before submission. Unsupported output hints such as `quality` or `outputFormat` are dropped for providers that do not declare support and are reported in the tool result.
|
||||
|
||||
Tool results report the applied settings. When OpenClaw remaps geometry during provider fallback, the returned `size`, `aspectRatio`, and `resolution` values reflect what was actually sent, and `details.normalization` captures the requested-to-applied translation.
|
||||
<Note>
|
||||
Not all providers support all parameters. When a fallback provider supports a
|
||||
nearby geometry option instead of the exact requested one, OpenClaw remaps to
|
||||
the closest supported size, aspect ratio, or resolution before submission.
|
||||
Unsupported output hints are dropped for providers that do not declare
|
||||
support and reported in the tool result. Tool results report the applied
|
||||
settings; `details.normalization` captures any requested-to-applied
|
||||
translation.
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -177,129 +189,177 @@ Tool results report the applied settings. When OpenClaw remaps geometry during p
|
||||
|
||||
### Provider selection order
|
||||
|
||||
When generating an image, OpenClaw tries providers in this order:
|
||||
OpenClaw tries providers in this order:
|
||||
|
||||
1. **`model` parameter** from the tool call (if the agent specifies one)
|
||||
2. **`imageGenerationModel.primary`** from config
|
||||
3. **`imageGenerationModel.fallbacks`** in order
|
||||
4. **Auto-detection** — uses auth-backed provider defaults only:
|
||||
- current default provider first
|
||||
- remaining registered image-generation providers in provider-id order
|
||||
1. **`model` parameter** from the tool call (if the agent specifies one).
|
||||
2. **`imageGenerationModel.primary`** from config.
|
||||
3. **`imageGenerationModel.fallbacks`** in order.
|
||||
4. **Auto-detection** — auth-backed provider defaults only:
|
||||
- current default provider first;
|
||||
- remaining registered image-generation providers in provider-id order.
|
||||
|
||||
If a provider fails (auth error, rate limit, etc.), the next configured candidate is tried automatically. If all fail, the error includes details from each attempt.
|
||||
If a provider fails (auth error, rate limit, etc.), the next configured
|
||||
candidate is tried automatically. If all fail, the error includes details
|
||||
from each attempt.
|
||||
|
||||
Notes:
|
||||
|
||||
- A per-call `model` override is exact: OpenClaw tries only that provider/model
|
||||
and does not continue to configured primary/fallback or auto-detected
|
||||
providers.
|
||||
- Auto-detection is auth-aware. A provider default only enters the candidate list
|
||||
when OpenClaw can actually authenticate that provider.
|
||||
- Auto-detection is enabled by default. Set
|
||||
`agents.defaults.mediaGenerationAutoProviderFallback: false` if you want image
|
||||
generation to use only the explicit `model`, `primary`, and `fallbacks`
|
||||
entries.
|
||||
- Set `agents.defaults.imageGenerationModel.timeoutMs` for slow image backends.
|
||||
A per-call `timeoutMs` tool parameter overrides the configured default.
|
||||
- Use `action: "list"` to inspect the currently registered providers, their
|
||||
default models, and auth env-var hints.
|
||||
<AccordionGroup>
|
||||
<Accordion title="Per-call model overrides are exact">
|
||||
A per-call `model` override tries only that provider/model and does
|
||||
not continue to configured primary/fallback or auto-detected providers.
|
||||
</Accordion>
|
||||
<Accordion title="Auto-detection is auth-aware">
|
||||
A provider default only enters the candidate list when OpenClaw can
|
||||
actually authenticate that provider. Set
|
||||
`agents.defaults.mediaGenerationAutoProviderFallback: false` to use only
|
||||
explicit `model`, `primary`, and `fallbacks` entries.
|
||||
</Accordion>
|
||||
<Accordion title="Timeouts">
|
||||
Set `agents.defaults.imageGenerationModel.timeoutMs` for slow image
|
||||
backends. A per-call `timeoutMs` tool parameter overrides the configured
|
||||
default.
|
||||
</Accordion>
|
||||
<Accordion title="Inspect at runtime">
|
||||
Use `action: "list"` to inspect the currently registered providers,
|
||||
their default models, and auth env-var hints.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Image editing
|
||||
|
||||
OpenAI, OpenRouter, Google, fal, MiniMax, ComfyUI, and xAI support editing reference images. Pass a reference image path or URL:
|
||||
OpenAI, OpenRouter, Google, fal, MiniMax, ComfyUI, and xAI support editing
|
||||
reference images. Pass a reference image path or URL:
|
||||
|
||||
```
|
||||
```text
|
||||
"Generate a watercolor version of this photo" + image: "/path/to/photo.jpg"
|
||||
```
|
||||
|
||||
OpenAI, OpenRouter, Google, and xAI support up to 5 reference images via the `images` parameter. fal, MiniMax, and ComfyUI support 1.
|
||||
OpenAI, OpenRouter, Google, and xAI support up to 5 reference images via the
|
||||
`images` parameter. fal, MiniMax, and ComfyUI support 1.
|
||||
|
||||
### OpenRouter image models
|
||||
## Provider deep dives
|
||||
|
||||
OpenRouter image generation uses the same `OPENROUTER_API_KEY` and routes through OpenRouter's chat completions image API. Select OpenRouter image models with the `openrouter/` prefix:
|
||||
<AccordionGroup>
|
||||
<Accordion title="OpenAI gpt-image-2 (and gpt-image-1.5)">
|
||||
OpenAI image generation defaults to `openai/gpt-image-2`. If an
|
||||
`openai-codex` OAuth profile is configured, OpenClaw reuses the same
|
||||
OAuth profile used by Codex subscription chat models and sends the
|
||||
image request through the Codex Responses backend. Legacy Codex base
|
||||
URLs such as `https://chatgpt.com/backend-api` are canonicalized to
|
||||
`https://chatgpt.com/backend-api/codex` for image requests. OpenClaw
|
||||
does **not** silently fall back to `OPENAI_API_KEY` for that request —
|
||||
to force direct OpenAI Images API routing, configure
|
||||
`models.providers.openai` explicitly with an API key, custom base URL,
|
||||
or Azure endpoint.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openrouter/google/gemini-3.1-flash-image-preview",
|
||||
The `openai/gpt-image-1.5`, `openai/gpt-image-1`, and
|
||||
`openai/gpt-image-1-mini` models can still be selected explicitly. Use
|
||||
`gpt-image-1.5` for transparent-background PNG/WebP output; the current
|
||||
`gpt-image-2` API rejects `background: "transparent"`.
|
||||
|
||||
`gpt-image-2` supports both text-to-image generation and
|
||||
reference-image editing through the same `image_generate` tool.
|
||||
OpenClaw forwards `prompt`, `count`, `size`, `quality`, `outputFormat`,
|
||||
and reference images to OpenAI. OpenAI does **not** receive
|
||||
`aspectRatio` or `resolution` directly; when possible OpenClaw maps
|
||||
those into a supported `size`, otherwise the tool reports them as
|
||||
ignored overrides.
|
||||
|
||||
OpenAI-specific options live under the `openai` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"quality": "low",
|
||||
"outputFormat": "jpeg",
|
||||
"openai": {
|
||||
"background": "opaque",
|
||||
"moderation": "low",
|
||||
"outputCompression": 60,
|
||||
"user": "end-user-42"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`openai.background` accepts `transparent`, `opaque`, or `auto`;
|
||||
transparent outputs require `outputFormat` `png` or `webp` and a
|
||||
transparency-capable OpenAI image model. OpenClaw routes default
|
||||
`gpt-image-2` transparent-background requests to `gpt-image-1.5`.
|
||||
`openai.outputCompression` applies to JPEG/WebP outputs.
|
||||
|
||||
The top-level `background` hint is provider-neutral and currently maps
|
||||
to the same OpenAI `background` request field when the OpenAI provider
|
||||
is selected. Providers that do not declare background support return
|
||||
it in `ignoredOverrides` instead of receiving the unsupported parameter.
|
||||
|
||||
To route OpenAI image generation through an Azure OpenAI deployment
|
||||
instead of `api.openai.com`, see
|
||||
[Azure OpenAI endpoints](/providers/openai#azure-openai-endpoints).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="OpenRouter image models">
|
||||
OpenRouter image generation uses the same `OPENROUTER_API_KEY` and
|
||||
routes through OpenRouter's chat completions image API. Select
|
||||
OpenRouter image models with the `openrouter/` prefix:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
imageGenerationModel: {
|
||||
primary: "openrouter/google/gemini-3.1-flash-image-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
OpenClaw forwards `prompt`, `count`, reference images, and
|
||||
Gemini-compatible `aspectRatio` / `resolution` hints to OpenRouter.
|
||||
Current built-in OpenRouter image model shortcuts include
|
||||
`google/gemini-3.1-flash-image-preview`,
|
||||
`google/gemini-3-pro-image-preview`, and `openai/gpt-5.4-image-2`. Use
|
||||
`action: "list"` to see what your configured plugin exposes.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="MiniMax dual-auth">
|
||||
MiniMax image generation is available through both bundled MiniMax
|
||||
auth paths:
|
||||
|
||||
- `minimax/image-01` for API-key setups
|
||||
- `minimax-portal/image-01` for OAuth setups
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="xAI grok-imagine-image">
|
||||
The bundled xAI provider uses `/v1/images/generations` for prompt-only
|
||||
requests and `/v1/images/edits` when `image` or `images` is present.
|
||||
|
||||
- Models: `xai/grok-imagine-image`, `xai/grok-imagine-image-pro`
|
||||
- Count: up to 4
|
||||
- References: one `image` or up to five `images`
|
||||
- Aspect ratios: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2:3`, `3:2`
|
||||
- Resolutions: `1K`, `2K`
|
||||
- Outputs: returned as OpenClaw-managed image attachments
|
||||
|
||||
OpenClaw intentionally does not expose xAI-native `quality`, `mask`,
|
||||
`user`, or extra native-only aspect ratios until those controls exist
|
||||
in the shared cross-provider `image_generate` contract.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Examples
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Generate (4K landscape)">
|
||||
```text
|
||||
/tool image_generate action=generate model=openai/gpt-image-2 prompt="A clean editorial poster for OpenClaw image generation" size=3840x2160 count=1
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Generate (transparent PNG)">
|
||||
```text
|
||||
/tool image_generate action=generate model=openai/gpt-image-1.5 prompt="A simple red circle sticker on a transparent background" outputFormat=png background=transparent
|
||||
```
|
||||
|
||||
OpenClaw forwards `prompt`, `count`, reference images, and Gemini-compatible `aspectRatio` / `resolution` hints to OpenRouter. Current built-in OpenRouter image model shortcuts include `google/gemini-3.1-flash-image-preview`, `google/gemini-3-pro-image-preview`, and `openai/gpt-5.4-image-2`; use `action: "list"` to see what your configured plugin exposes.
|
||||
|
||||
### OpenAI `gpt-image-2`
|
||||
|
||||
OpenAI image generation defaults to `openai/gpt-image-2`. If an
|
||||
`openai-codex` OAuth profile is configured, OpenClaw reuses the same OAuth
|
||||
profile used by Codex subscription chat models and sends the image request
|
||||
through the Codex Responses backend. Legacy Codex base URLs such as
|
||||
`https://chatgpt.com/backend-api` are canonicalized to
|
||||
`https://chatgpt.com/backend-api/codex` for image requests. It does not
|
||||
silently fall back to `OPENAI_API_KEY` for that request. To force direct OpenAI
|
||||
Images API routing, configure `models.providers.openai` explicitly with an API
|
||||
key, custom base URL, or Azure endpoint. The `openai/gpt-image-1.5`,
|
||||
`openai/gpt-image-1`, and `openai/gpt-image-1-mini` models can still be
|
||||
selected explicitly. Use `gpt-image-1.5` for transparent-background PNG/WebP
|
||||
output; the current `gpt-image-2` API rejects `background: "transparent"`.
|
||||
|
||||
`gpt-image-2` supports both text-to-image generation and reference-image
|
||||
editing through the same `image_generate` tool. OpenClaw forwards `prompt`,
|
||||
`count`, `size`, `quality`, `outputFormat`, and reference images to OpenAI.
|
||||
OpenAI does not receive `aspectRatio` or `resolution` directly; when possible
|
||||
OpenClaw maps those into a supported `size`, otherwise the tool reports them as
|
||||
ignored overrides.
|
||||
|
||||
OpenAI-specific options live under the `openai` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"quality": "low",
|
||||
"outputFormat": "jpeg",
|
||||
"openai": {
|
||||
"background": "opaque",
|
||||
"moderation": "low",
|
||||
"outputCompression": 60,
|
||||
"user": "end-user-42"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`openai.background` accepts `transparent`, `opaque`, or `auto`; transparent
|
||||
outputs require `outputFormat` `png` or `webp` and a transparency-capable OpenAI
|
||||
image model. OpenClaw routes default `gpt-image-2` transparent-background
|
||||
requests to `gpt-image-1.5`. `openai.outputCompression` applies to JPEG/WebP
|
||||
outputs.
|
||||
|
||||
The top-level `background` hint is provider-neutral and currently maps to the
|
||||
same OpenAI `background` request field when the OpenAI provider is selected.
|
||||
Providers that do not declare background support return it in `ignoredOverrides`
|
||||
instead of receiving the unsupported parameter.
|
||||
|
||||
When asking an agent for a transparent-background OpenAI image, the expected
|
||||
tool call is:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "openai/gpt-image-1.5",
|
||||
"prompt": "A simple red circle sticker on a transparent background",
|
||||
"outputFormat": "png",
|
||||
"background": "transparent"
|
||||
}
|
||||
```
|
||||
|
||||
The explicit `openai/gpt-image-1.5` model keeps the request portable across
|
||||
tool summaries and harnesses. If the agent instead uses the default
|
||||
`openai/gpt-image-2` with `openai.background: "transparent"` on the public
|
||||
OpenAI or OpenAI Codex OAuth route, OpenClaw rewrites the provider request to
|
||||
`gpt-image-1.5`. Azure and custom OpenAI-compatible endpoints keep their
|
||||
configured deployment/model names.
|
||||
|
||||
For headless CLI generation, use the equivalent `openclaw infer` flags:
|
||||
Equivalent CLI:
|
||||
|
||||
```bash
|
||||
openclaw infer image generate \
|
||||
@@ -310,86 +370,39 @@ openclaw infer image generate \
|
||||
--json
|
||||
```
|
||||
|
||||
The same `--output-format` and `--background` flags are available on
|
||||
`openclaw infer image edit`; `--openai-background` remains available as an
|
||||
OpenAI-specific alias. Current bundled providers other than OpenAI do not
|
||||
declare explicit background control, so `background: "transparent"` is reported
|
||||
as ignored for them.
|
||||
|
||||
Generate one 4K landscape image:
|
||||
|
||||
```
|
||||
/tool image_generate action=generate model=openai/gpt-image-2 prompt="A clean editorial poster for OpenClaw image generation" size=3840x2160 count=1
|
||||
```
|
||||
|
||||
Generate a transparent PNG:
|
||||
|
||||
```
|
||||
/tool image_generate action=generate model=openai/gpt-image-1.5 prompt="A simple red circle sticker on a transparent background" outputFormat=png background=transparent
|
||||
```
|
||||
|
||||
Generate two square images:
|
||||
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Generate (two square)">
|
||||
```text
|
||||
/tool image_generate action=generate model=openai/gpt-image-2 prompt="Two visual directions for a calm productivity app icon" size=1024x1024 count=2
|
||||
```
|
||||
|
||||
Edit one local reference image:
|
||||
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Edit (one reference)">
|
||||
```text
|
||||
/tool image_generate action=generate model=openai/gpt-image-2 prompt="Keep the subject, replace the background with a bright studio setup" image=/path/to/reference.png size=1024x1536
|
||||
```
|
||||
|
||||
Edit with multiple references:
|
||||
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Edit (multiple references)">
|
||||
```text
|
||||
/tool image_generate action=generate model=openai/gpt-image-2 prompt="Combine the character identity from the first image with the color palette from the second" images='["/path/to/character.png","/path/to/palette.jpg"]' size=1536x1024
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
To route OpenAI image generation through an Azure OpenAI deployment instead
|
||||
of `api.openai.com`, see [Azure OpenAI endpoints](/providers/openai#azure-openai-endpoints)
|
||||
in the OpenAI provider docs.
|
||||
|
||||
MiniMax image generation is available through both bundled MiniMax auth paths:
|
||||
|
||||
- `minimax/image-01` for API-key setups
|
||||
- `minimax-portal/image-01` for OAuth setups
|
||||
|
||||
## Provider capabilities
|
||||
|
||||
| Capability | OpenAI | Google | fal | MiniMax | ComfyUI | Vydra | xAI |
|
||||
| --------------------- | -------------------- | -------------------- | ------------------- | -------------------------- | ---------------------------------- | ------- | -------------------- |
|
||||
| Generate | Yes (up to 4) | Yes (up to 4) | Yes (up to 4) | Yes (up to 9) | Yes (workflow-defined outputs) | Yes (1) | Yes (up to 4) |
|
||||
| Edit/reference | Yes (up to 5 images) | Yes (up to 5 images) | Yes (1 image) | Yes (1 image, subject ref) | Yes (1 image, workflow-configured) | No | Yes (up to 5 images) |
|
||||
| Size control | Yes (up to 4K) | Yes | Yes | No | No | No | No |
|
||||
| Aspect ratio | No | Yes | Yes (generate only) | Yes | No | No | Yes |
|
||||
| Resolution (1K/2K/4K) | No | Yes | Yes | No | No | No | Yes (1K/2K) |
|
||||
|
||||
### xAI `grok-imagine-image`
|
||||
|
||||
The bundled xAI provider uses `/v1/images/generations` for prompt-only requests
|
||||
and `/v1/images/edits` when `image` or `images` is present.
|
||||
|
||||
- Models: `xai/grok-imagine-image`, `xai/grok-imagine-image-pro`
|
||||
- Count: up to 4
|
||||
- References: one `image` or up to five `images`
|
||||
- Aspect ratios: `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `2:3`, `3:2`
|
||||
- Resolutions: `1K`, `2K`
|
||||
- Outputs: returned as OpenClaw-managed image attachments
|
||||
|
||||
OpenClaw intentionally does not expose xAI-native `quality`, `mask`, `user`, or
|
||||
extra native-only aspect ratios until those controls exist in the shared
|
||||
cross-provider `image_generate` contract.
|
||||
The same `--output-format` and `--background` flags are available on
|
||||
`openclaw infer image edit`; `--openai-background` remains as an
|
||||
OpenAI-specific alias. Bundled providers other than OpenAI do not declare
|
||||
explicit background control today, so `background: "transparent"` is reported
|
||||
as ignored for them.
|
||||
|
||||
## Related
|
||||
|
||||
- [Tools Overview](/tools) — all available agent tools
|
||||
- [fal](/providers/fal) — fal image and video provider setup
|
||||
- [Tools overview](/tools) — all available agent tools
|
||||
- [ComfyUI](/providers/comfy) — local ComfyUI and Comfy Cloud workflow setup
|
||||
- [fal](/providers/fal) — fal image and video provider setup
|
||||
- [Google (Gemini)](/providers/google) — Gemini image provider setup
|
||||
- [MiniMax](/providers/minimax) — MiniMax image provider setup
|
||||
- [OpenAI](/providers/openai) — OpenAI Images provider setup
|
||||
- [Vydra](/providers/vydra) — Vydra image, video, and speech setup
|
||||
- [xAI](/providers/xai) — Grok image, video, search, code execution, and TTS setup
|
||||
- [Configuration Reference](/gateway/config-agents#agent-defaults) — `imageGenerationModel` config
|
||||
- [Configuration reference](/gateway/config-agents#agent-defaults) — `imageGenerationModel` config
|
||||
- [Models](/concepts/models) — model configuration and failover
|
||||
|
||||
@@ -95,6 +95,8 @@ active runtime model label from the latest transcript usage entry.
|
||||
|
||||
For partial changes, prefer `config.schema.lookup` then `config.patch`. Use
|
||||
`config.apply` only when you intentionally replace the entire config.
|
||||
For broader config docs, read [Configuration](/gateway/configuration) and
|
||||
[Configuration reference](/gateway/configuration-reference).
|
||||
The tool also refuses to change `tools.exec.ask` or `tools.exec.security`;
|
||||
legacy `tools.bash.*` aliases normalize to the same protected exec paths.
|
||||
|
||||
|
||||
@@ -1,83 +1,128 @@
|
||||
---
|
||||
summary: "Unified landing page for media generation, understanding, and speech capabilities"
|
||||
summary: "Image, video, music, speech, and media-understanding capabilities at a glance"
|
||||
read_when:
|
||||
- Looking for an overview of media capabilities
|
||||
- Looking for an overview of OpenClaw's media capabilities
|
||||
- Deciding which media provider to configure
|
||||
- Understanding how async media generation works
|
||||
title: "Media overview"
|
||||
sidebarTitle: "Media overview"
|
||||
---
|
||||
|
||||
# Media Generation and Understanding
|
||||
OpenClaw generates images, videos, and music, understands inbound media
|
||||
(images, audio, video), and speaks replies aloud with text-to-speech. All
|
||||
media capabilities are tool-driven: the agent decides when to use them based
|
||||
on the conversation, and each tool only appears when at least one backing
|
||||
provider is configured.
|
||||
|
||||
OpenClaw generates images, videos, and music, understands inbound media (images, audio, video), and speaks replies aloud with text-to-speech. All media capabilities are tool-driven: the agent decides when to use them based on the conversation, and each tool only appears when at least one backing provider is configured.
|
||||
## Capabilities
|
||||
|
||||
## Capabilities at a glance
|
||||
|
||||
| Capability | Tool | Providers | What it does |
|
||||
| -------------------- | ---------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| Image generation | `image_generate` | ComfyUI, fal, Google, MiniMax, OpenAI, Vydra, xAI | Creates or edits images from text prompts or references |
|
||||
| Video generation | `video_generate` | Alibaba, BytePlus, ComfyUI, fal, Google, MiniMax, OpenAI, Qwen, Runway, Together, Vydra, xAI | Creates videos from text, images, or existing videos |
|
||||
| Music generation | `music_generate` | ComfyUI, Google, MiniMax | Creates music or audio tracks from text prompts |
|
||||
| Text-to-speech (TTS) | `tts` | ElevenLabs, Google, Gradium, Local CLI, Microsoft, MiniMax, OpenAI, Vydra, xAI, Xiaomi MiMo | Converts outbound replies to spoken audio |
|
||||
| Media understanding | (automatic) | Any vision/audio-capable model provider, plus CLI fallbacks | Summarizes inbound images, audio, and video |
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Image generation" href="/tools/image-generation" icon="image">
|
||||
Create and edit images from text prompts or reference images via
|
||||
`image_generate`. Synchronous — completes inline with the reply.
|
||||
</Card>
|
||||
<Card title="Video generation" href="/tools/video-generation" icon="video">
|
||||
Text-to-video, image-to-video, and video-to-video via `video_generate`.
|
||||
Async — runs in the background and posts the result when ready.
|
||||
</Card>
|
||||
<Card title="Music generation" href="/tools/music-generation" icon="music">
|
||||
Generate music or audio tracks via `music_generate`. Async on shared
|
||||
providers; ComfyUI workflow path runs synchronously.
|
||||
</Card>
|
||||
<Card title="Text-to-speech" href="/tools/tts" icon="microphone">
|
||||
Convert outbound replies to spoken audio via the `tts` tool plus
|
||||
`messages.tts` config. Synchronous.
|
||||
</Card>
|
||||
<Card title="Media understanding" href="/nodes/media-understanding" icon="eye">
|
||||
Summarize inbound images, audio, and video using vision-capable model
|
||||
providers and dedicated media-understanding plugins.
|
||||
</Card>
|
||||
<Card title="Speech-to-text" href="/nodes/audio" icon="ear-listen">
|
||||
Transcribe inbound voice messages through batch STT or Voice Call
|
||||
streaming STT providers.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Provider capability matrix
|
||||
|
||||
This table shows which providers support which media capabilities across the platform.
|
||||
|
||||
| Provider | Image | Video | Music | TTS | STT / Transcription | Realtime Voice | Media Understanding |
|
||||
| ----------- | ----- | ----- | ----- | --- | ------------------- | -------------- | ------------------- |
|
||||
| Alibaba | | Yes | | | | | |
|
||||
| BytePlus | | Yes | | | | | |
|
||||
| ComfyUI | Yes | Yes | Yes | | | | |
|
||||
| Deepgram | | | | | Yes | Yes | |
|
||||
| ElevenLabs | | | | Yes | Yes | | |
|
||||
| fal | Yes | Yes | | | | | |
|
||||
| Google | Yes | Yes | Yes | Yes | | Yes | Yes |
|
||||
| Gradium | | | | Yes | | | |
|
||||
| Local CLI | | | | Yes | | | |
|
||||
| Microsoft | | | | Yes | | | |
|
||||
| MiniMax | Yes | Yes | Yes | Yes | | | |
|
||||
| Mistral | | | | | Yes | | |
|
||||
| OpenAI | Yes | Yes | | Yes | Yes | Yes | Yes |
|
||||
| Qwen | | Yes | | | | | |
|
||||
| Runway | | Yes | | | | | |
|
||||
| SenseAudio | | | | | Yes | | |
|
||||
| Together | | Yes | | | | | |
|
||||
| Vydra | Yes | Yes | | Yes | | | |
|
||||
| xAI | Yes | Yes | | Yes | Yes | | Yes |
|
||||
| Xiaomi MiMo | Yes | | | Yes | | | Yes |
|
||||
| Provider | Image | Video | Music | TTS | STT | Realtime voice | Media understanding |
|
||||
| ----------- | :---: | :---: | :---: | :-: | :-: | :------------: | :-----------------: |
|
||||
| Alibaba | | ✓ | | | | | |
|
||||
| BytePlus | | ✓ | | | | | |
|
||||
| ComfyUI | ✓ | ✓ | ✓ | | | | |
|
||||
| Deepgram | | | | | ✓ | ✓ | |
|
||||
| ElevenLabs | | | | ✓ | ✓ | | |
|
||||
| fal | ✓ | ✓ | | | | | |
|
||||
| Google | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ |
|
||||
| Gradium | | | | ✓ | | | |
|
||||
| Local CLI | | | | ✓ | | | |
|
||||
| Microsoft | | | | ✓ | | | |
|
||||
| MiniMax | ✓ | ✓ | ✓ | ✓ | | | |
|
||||
| Mistral | | | | | ✓ | | |
|
||||
| OpenAI | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ |
|
||||
| Qwen | | ✓ | | | | | |
|
||||
| Runway | | ✓ | | | | | |
|
||||
| SenseAudio | | | | | ✓ | | |
|
||||
| Together | | ✓ | | | | | |
|
||||
| Vydra | ✓ | ✓ | | ✓ | | | |
|
||||
| xAI | ✓ | ✓ | | ✓ | ✓ | | ✓ |
|
||||
| Xiaomi MiMo | ✓ | | | ✓ | | | ✓ |
|
||||
|
||||
<Note>
|
||||
Media understanding uses any vision-capable or audio-capable model registered in your provider config. The table above highlights providers with dedicated media-understanding support; most LLM providers with multimodal models (Anthropic, Google, OpenAI, etc.) can also understand inbound media when configured as the active reply model.
|
||||
Media understanding uses any vision-capable or audio-capable model registered
|
||||
in your provider config. The matrix above lists providers with dedicated
|
||||
media-understanding support; most multimodal LLM providers (Anthropic, Google,
|
||||
OpenAI, etc.) can also understand inbound media when configured as the active
|
||||
reply model.
|
||||
</Note>
|
||||
|
||||
## How async generation works
|
||||
## Async vs synchronous
|
||||
|
||||
Video and music generation run as background tasks because provider processing typically takes 30 seconds to several minutes. When the agent calls `video_generate` or `music_generate`, OpenClaw submits the request to the provider, returns a task ID immediately, and tracks the job in the task ledger. The agent continues responding to other messages while the job runs. When the provider finishes, OpenClaw wakes the agent so it can post the finished media back into the original channel. Image generation and TTS are synchronous and complete inline with the reply.
|
||||
| Capability | Mode | Why |
|
||||
| --------------- | ------------ | ------------------------------------------------------------------ |
|
||||
| Image | Synchronous | Provider responses return in seconds; completes inline with reply. |
|
||||
| Text-to-speech | Synchronous | Provider responses return in seconds; attached to the reply audio. |
|
||||
| Video | Asynchronous | Provider processing takes 30 s to several minutes. |
|
||||
| Music (shared) | Asynchronous | Same provider-processing characteristic as video. |
|
||||
| Music (ComfyUI) | Synchronous | Local workflow runs inline against the configured ComfyUI server. |
|
||||
|
||||
For async tools, OpenClaw submits the request to the provider, returns a task
|
||||
id immediately, and tracks the job in the task ledger. The agent continues
|
||||
responding to other messages while the job runs. When the provider finishes,
|
||||
OpenClaw wakes the agent so it can post the finished media back into the
|
||||
original channel.
|
||||
|
||||
## Speech-to-text and Voice Call
|
||||
|
||||
Deepgram, ElevenLabs, Mistral, OpenAI, SenseAudio, and xAI can all transcribe
|
||||
inbound audio through the batch `tools.media.audio` path when configured.
|
||||
Channel plugins that preflight a voice note for mention gating or command
|
||||
parsing mark the transcribed attachment on the inbound context, so the shared
|
||||
media-understanding pass reuses that transcript instead of making a second
|
||||
STT call for the same audio.
|
||||
|
||||
Deepgram, ElevenLabs, Mistral, OpenAI, and xAI also register Voice Call
|
||||
streaming STT providers, so live phone audio can be forwarded to the selected
|
||||
vendor without waiting for a completed recording.
|
||||
|
||||
Google maps to OpenClaw's image, video, music, batch TTS, backend realtime
|
||||
voice, and media-understanding surfaces. OpenAI maps to OpenClaw's image,
|
||||
video, batch TTS, batch STT, Voice Call streaming STT, backend realtime voice,
|
||||
and memory embedding surfaces. xAI currently maps to OpenClaw's image, video,
|
||||
search, code-execution, batch TTS, batch STT, and Voice Call streaming STT
|
||||
surfaces. xAI Realtime voice is an upstream capability, but it is not
|
||||
registered in OpenClaw until the shared realtime voice contract can represent
|
||||
it.
|
||||
## Provider mappings (how vendors split across surfaces)
|
||||
|
||||
## Quick links
|
||||
|
||||
- [Image Generation](/tools/image-generation) -- generating and editing images
|
||||
- [Video Generation](/tools/video-generation) -- text-to-video, image-to-video, and video-to-video
|
||||
- [Music Generation](/tools/music-generation) -- creating music and audio tracks
|
||||
- [Text-to-Speech](/tools/tts) -- converting replies to spoken audio
|
||||
- [Media Understanding](/nodes/media-understanding) -- understanding inbound images, audio, and video
|
||||
<AccordionGroup>
|
||||
<Accordion title="Google">
|
||||
Image, video, music, batch TTS, backend realtime voice, and
|
||||
media-understanding surfaces.
|
||||
</Accordion>
|
||||
<Accordion title="OpenAI">
|
||||
Image, video, batch TTS, batch STT, Voice Call streaming STT, backend
|
||||
realtime voice, and memory-embedding surfaces.
|
||||
</Accordion>
|
||||
<Accordion title="xAI">
|
||||
Image, video, search, code-execution, batch TTS, batch STT, and Voice
|
||||
Call streaming STT. xAI Realtime voice is an upstream capability but is
|
||||
not registered in OpenClaw until the shared realtime-voice contract can
|
||||
represent it.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Related
|
||||
|
||||
@@ -85,3 +130,5 @@ it.
|
||||
- [Video generation](/tools/video-generation)
|
||||
- [Music generation](/tools/music-generation)
|
||||
- [Text-to-speech](/tools/tts)
|
||||
- [Media understanding](/nodes/media-understanding)
|
||||
- [Audio nodes](/nodes/audio)
|
||||
|
||||
@@ -1,53 +1,82 @@
|
||||
---
|
||||
summary: "Generate music with shared providers, including workflow-backed plugins"
|
||||
summary: "Generate music via music_generate across Google Lyria, MiniMax, and ComfyUI workflows"
|
||||
read_when:
|
||||
- Generating music or audio via the agent
|
||||
- Configuring music generation providers and models
|
||||
- Configuring music-generation providers and models
|
||||
- Understanding the music_generate tool parameters
|
||||
title: "Music generation"
|
||||
sidebarTitle: "Music generation"
|
||||
---
|
||||
|
||||
The `music_generate` tool lets the agent create music or audio through the
|
||||
shared music-generation capability with configured providers such as Google,
|
||||
MiniMax, and workflow-configured ComfyUI.
|
||||
shared music-generation capability with configured providers — Google,
|
||||
MiniMax, and workflow-configured ComfyUI today.
|
||||
|
||||
For shared provider-backed agent sessions, OpenClaw starts music generation as a
|
||||
background task, tracks it in the task ledger, then wakes the agent again when
|
||||
the track is ready so the agent can post the finished audio back into the
|
||||
original channel.
|
||||
For session-backed agent runs, OpenClaw starts music generation as a
|
||||
background task, tracks it in the task ledger, then wakes the agent again
|
||||
when the track is ready so the agent can post the finished audio back into
|
||||
the original channel.
|
||||
|
||||
<Note>
|
||||
The built-in shared tool only appears when at least one music-generation provider is available. If you don't see `music_generate` in your agent's tools, configure `agents.defaults.musicGenerationModel` or set up a provider API key.
|
||||
The built-in shared tool only appears when at least one music-generation
|
||||
provider is available. If you do not see `music_generate` in your agent's
|
||||
tools, configure `agents.defaults.musicGenerationModel` or set up a
|
||||
provider API key.
|
||||
</Note>
|
||||
|
||||
## Quick start
|
||||
|
||||
### Shared provider-backed generation
|
||||
<Tabs>
|
||||
<Tab title="Shared provider-backed">
|
||||
<Steps>
|
||||
<Step title="Configure auth">
|
||||
Set an API key for at least one provider — for example
|
||||
`GEMINI_API_KEY` or `MINIMAX_API_KEY`.
|
||||
</Step>
|
||||
<Step title="Pick a default model (optional)">
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
musicGenerationModel: {
|
||||
primary: "google/lyria-3-clip-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
<Step title="Ask the agent">
|
||||
_"Generate an upbeat synthpop track about a night drive through a
|
||||
neon city."_
|
||||
|
||||
1. Set an API key for at least one provider, for example `GEMINI_API_KEY` or
|
||||
`MINIMAX_API_KEY`.
|
||||
2. Optionally set your preferred model:
|
||||
The agent calls `music_generate` automatically. No tool
|
||||
allow-listing needed.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
musicGenerationModel: {
|
||||
primary: "google/lyria-3-clip-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
For direct synchronous contexts without a session-backed agent run,
|
||||
the built-in tool still falls back to inline generation and returns
|
||||
the final media path in the tool result.
|
||||
|
||||
3. Ask the agent: _"Generate an upbeat synthpop track about a night drive
|
||||
through a neon city."_
|
||||
|
||||
The agent calls `music_generate` automatically. No tool allow-listing needed.
|
||||
|
||||
For direct synchronous contexts without a session-backed agent run, the built-in
|
||||
tool still falls back to inline generation and returns the final media path in
|
||||
the tool result.
|
||||
</Tab>
|
||||
<Tab title="ComfyUI workflow">
|
||||
<Steps>
|
||||
<Step title="Configure the workflow">
|
||||
Configure `plugins.entries.comfy.config.music` with a workflow
|
||||
JSON and prompt/output nodes.
|
||||
</Step>
|
||||
<Step title="Cloud auth (optional)">
|
||||
For Comfy Cloud, set `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY`.
|
||||
</Step>
|
||||
<Step title="Call the tool">
|
||||
```text
|
||||
/tool music_generate prompt="Warm ambient synth loop with soft tape texture"
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Example prompts:
|
||||
|
||||
@@ -59,40 +88,24 @@ Generate a cinematic piano track with soft strings and no vocals.
|
||||
Generate an energetic chiptune loop about launching a rocket at sunrise.
|
||||
```
|
||||
|
||||
### Workflow-driven Comfy generation
|
||||
## Supported providers
|
||||
|
||||
The bundled `comfy` plugin plugs into the shared `music_generate` tool through
|
||||
the music-generation provider registry.
|
||||
|
||||
1. Configure `plugins.entries.comfy.config.music` with a workflow JSON and
|
||||
prompt/output nodes.
|
||||
2. If you use Comfy Cloud, set `COMFY_API_KEY` or `COMFY_CLOUD_API_KEY`.
|
||||
3. Ask the agent for music or call the tool directly.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
/tool music_generate prompt="Warm ambient synth loop with soft tape texture"
|
||||
```
|
||||
|
||||
## Shared bundled provider support
|
||||
|
||||
| Provider | Default model | Reference inputs | Supported controls | API key |
|
||||
| Provider | Default model | Reference inputs | Supported controls | Auth |
|
||||
| -------- | ---------------------- | ---------------- | --------------------------------------------------------- | -------------------------------------- |
|
||||
| ComfyUI | `workflow` | Up to 1 image | Workflow-defined music or audio | `COMFY_API_KEY`, `COMFY_CLOUD_API_KEY` |
|
||||
| Google | `lyria-3-clip-preview` | Up to 10 images | `lyrics`, `instrumental`, `format` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` |
|
||||
| MiniMax | `music-2.6` | None | `lyrics`, `instrumental`, `durationSeconds`, `format=mp3` | `MINIMAX_API_KEY` or MiniMax OAuth |
|
||||
|
||||
### Declared capability matrix
|
||||
### Capability matrix
|
||||
|
||||
This is the explicit mode contract used by `music_generate`, contract tests,
|
||||
and the shared live sweep.
|
||||
The explicit mode contract used by `music_generate`, contract tests, and the
|
||||
shared live sweep:
|
||||
|
||||
| Provider | `generate` | `edit` | Edit limit | Shared live lanes |
|
||||
| -------- | ---------- | ------ | ---------- | ------------------------------------------------------------------------- |
|
||||
| ComfyUI | Yes | Yes | 1 image | Not in the shared sweep; covered by `extensions/comfy/comfy.live.test.ts` |
|
||||
| Google | Yes | Yes | 10 images | `generate`, `edit` |
|
||||
| MiniMax | Yes | No | None | `generate` |
|
||||
| -------- | :--------: | :----: | ---------- | ------------------------------------------------------------------------- |
|
||||
| ComfyUI | ✓ | ✓ | 1 image | Not in the shared sweep; covered by `extensions/comfy/comfy.live.test.ts` |
|
||||
| Google | ✓ | ✓ | 10 images | `generate`, `edit` |
|
||||
| MiniMax | ✓ | — | None | `generate` |
|
||||
|
||||
Use `action: "list"` to inspect available shared providers and models at
|
||||
runtime:
|
||||
@@ -113,48 +126,78 @@ Direct generation example:
|
||||
/tool music_generate prompt="Dreamy lo-fi hip hop with vinyl texture and gentle rain" instrumental=true
|
||||
```
|
||||
|
||||
## Built-in tool parameters
|
||||
## Tool parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ----------------- | -------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `prompt` | string | Music generation prompt (required for `action: "generate"`) |
|
||||
| `action` | string | `"generate"` (default), `"status"` for the current session task, or `"list"` to inspect providers |
|
||||
| `model` | string | Provider/model override, e.g. `google/lyria-3-pro-preview` or `comfy/workflow` |
|
||||
| `lyrics` | string | Optional lyrics when the provider supports explicit lyric input |
|
||||
| `instrumental` | boolean | Request instrumental-only output when the provider supports it |
|
||||
| `image` | string | Single reference image path or URL |
|
||||
| `images` | string[] | Multiple reference images (up to 10) |
|
||||
| `durationSeconds` | number | Target duration in seconds when the provider supports duration hints |
|
||||
| `timeoutMs` | number | Optional provider request timeout in milliseconds |
|
||||
| `format` | string | Output format hint (`mp3` or `wav`) when the provider supports it |
|
||||
| `filename` | string | Output filename hint |
|
||||
<ParamField path="prompt" type="string" required>
|
||||
Music generation prompt. Required for `action: "generate"`.
|
||||
</ParamField>
|
||||
<ParamField path="action" type='"generate" | "status" | "list"' default="generate">
|
||||
`"status"` returns the current session task; `"list"` inspects providers.
|
||||
</ParamField>
|
||||
<ParamField path="model" type="string">
|
||||
Provider/model override (e.g. `google/lyria-3-pro-preview`,
|
||||
`comfy/workflow`).
|
||||
</ParamField>
|
||||
<ParamField path="lyrics" type="string">
|
||||
Optional lyrics when the provider supports explicit lyric input.
|
||||
</ParamField>
|
||||
<ParamField path="instrumental" type="boolean">
|
||||
Request instrumental-only output when the provider supports it.
|
||||
</ParamField>
|
||||
<ParamField path="image" type="string">
|
||||
Single reference image path or URL.
|
||||
</ParamField>
|
||||
<ParamField path="images" type="string[]">
|
||||
Multiple reference images (up to 10 on supporting providers).
|
||||
</ParamField>
|
||||
<ParamField path="durationSeconds" type="number">
|
||||
Target duration in seconds when the provider supports duration hints.
|
||||
</ParamField>
|
||||
<ParamField path="format" type='"mp3" | "wav"'>
|
||||
Output format hint when the provider supports it.
|
||||
</ParamField>
|
||||
<ParamField path="filename" type="string">Output filename hint.</ParamField>
|
||||
<ParamField path="timeoutMs" type="number">Optional provider request timeout in milliseconds.</ParamField>
|
||||
|
||||
Not all providers support all parameters. OpenClaw still validates hard limits
|
||||
such as input counts before submission. When a provider supports duration but
|
||||
uses a shorter maximum than the requested value, OpenClaw automatically clamps
|
||||
to the closest supported duration. Truly unsupported optional hints are ignored
|
||||
with a warning when the selected provider or model cannot honor them.
|
||||
<Note>
|
||||
Not all providers support all parameters. OpenClaw still validates hard
|
||||
limits such as input counts before submission. When a provider supports
|
||||
duration but uses a shorter maximum than the requested value, OpenClaw
|
||||
clamps to the closest supported duration. Truly unsupported optional hints
|
||||
are ignored with a warning when the selected provider or model cannot honor
|
||||
them. Tool results report applied settings; `details.normalization`
|
||||
captures any requested-to-applied mapping.
|
||||
</Note>
|
||||
|
||||
Tool results report the applied settings. When OpenClaw clamps duration during provider fallback, the returned `durationSeconds` reflects the submitted value and `details.normalization.durationSeconds` shows the requested-to-applied mapping.
|
||||
## Async behavior
|
||||
|
||||
## Async behavior for the shared provider-backed path
|
||||
Session-backed music generation runs as a background task:
|
||||
|
||||
- Session-backed agent runs: `music_generate` creates a background task, returns a started/task response immediately, and posts the finished track later in a follow-up agent message.
|
||||
- Duplicate prevention: while that background task is still `queued` or `running`, later `music_generate` calls in the same session return task status instead of starting another generation.
|
||||
- Status lookup: use `action: "status"` to inspect the active session-backed music task without starting a new one.
|
||||
- Task tracking: use `openclaw tasks list` or `openclaw tasks show <taskId>` to inspect queued, running, and terminal status for the generation.
|
||||
- Completion wake: OpenClaw injects an internal completion event back into the same session so the model can write the user-facing follow-up itself.
|
||||
- Prompt hint: later user/manual turns in the same session get a small runtime hint when a music task is already in flight so the model does not blindly call `music_generate` again.
|
||||
- No-session fallback: direct/local contexts without a real agent session still run inline and return the final audio result in the same turn.
|
||||
- **Background task:** `music_generate` creates a background task, returns a
|
||||
started/task response immediately, and posts the finished track later in
|
||||
a follow-up agent message.
|
||||
- **Duplicate prevention:** while a task is `queued` or `running`, later
|
||||
`music_generate` calls in the same session return task status instead of
|
||||
starting another generation. Use `action: "status"` to check explicitly.
|
||||
- **Status lookup:** `openclaw tasks list` or `openclaw tasks show <taskId>`
|
||||
inspects queued, running, and terminal status.
|
||||
- **Completion wake:** OpenClaw injects an internal completion event back
|
||||
into the same session so the model can write the user-facing follow-up
|
||||
itself.
|
||||
- **Prompt hint:** later user/manual turns in the same session get a small
|
||||
runtime hint when a music task is already in flight, so the model does
|
||||
not blindly call `music_generate` again.
|
||||
- **No-session fallback:** direct/local contexts without a real agent
|
||||
session run inline and return the final audio result in the same turn.
|
||||
|
||||
### Task lifecycle
|
||||
|
||||
Each `music_generate` request moves through four states:
|
||||
|
||||
1. **queued** -- task created, waiting for the provider to accept it.
|
||||
2. **running** -- provider is processing (typically 30 seconds to 3 minutes depending on provider and duration).
|
||||
3. **succeeded** -- track ready; the agent wakes and posts it to the conversation.
|
||||
4. **failed** -- provider error or timeout; the agent wakes with error details.
|
||||
| State | Meaning |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `queued` | Task created, waiting for the provider to accept it. |
|
||||
| `running` | Provider is processing (typically 30 seconds to 3 minutes depending on provider and duration). |
|
||||
| `succeeded` | Track ready; the agent wakes and posts it to the conversation. |
|
||||
| `failed` | Provider error or timeout; the agent wakes with error details. |
|
||||
|
||||
Check status from the CLI:
|
||||
|
||||
@@ -164,8 +207,6 @@ openclaw tasks show <taskId>
|
||||
openclaw tasks cancel <taskId>
|
||||
```
|
||||
|
||||
Duplicate prevention: if a music task is already `queued` or `running` for the current session, `music_generate` returns the existing task status instead of starting a new one. Use `action: "status"` to check explicitly without triggering a new generation.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Model selection
|
||||
@@ -185,38 +226,59 @@ Duplicate prevention: if a music task is already `queued` or `running` for the c
|
||||
|
||||
### Provider selection order
|
||||
|
||||
When generating music, OpenClaw tries providers in this order:
|
||||
OpenClaw tries providers in this order:
|
||||
|
||||
1. `model` parameter from the tool call, if the agent specifies one
|
||||
2. `musicGenerationModel.primary` from config
|
||||
3. `musicGenerationModel.fallbacks` in order
|
||||
1. `model` parameter from the tool call (if the agent specifies one).
|
||||
2. `musicGenerationModel.primary` from config.
|
||||
3. `musicGenerationModel.fallbacks` in order.
|
||||
4. Auto-detection using auth-backed provider defaults only:
|
||||
- current default provider first
|
||||
- remaining registered music-generation providers in provider-id order
|
||||
- current default provider first;
|
||||
- remaining registered music-generation providers in provider-id order.
|
||||
|
||||
If a provider fails, the next candidate is tried automatically. If all fail, the
|
||||
error includes details from each attempt.
|
||||
If a provider fails, the next candidate is tried automatically. If all
|
||||
fail, the error includes details from each attempt.
|
||||
|
||||
Set `agents.defaults.mediaGenerationAutoProviderFallback: false` if you want
|
||||
music generation to use only the explicit `model`, `primary`, and `fallbacks`
|
||||
entries.
|
||||
Set `agents.defaults.mediaGenerationAutoProviderFallback: false` to use only
|
||||
explicit `model`, `primary`, and `fallbacks` entries.
|
||||
|
||||
## Provider notes
|
||||
|
||||
- Google uses Lyria 3 batch generation. The current bundled flow supports
|
||||
prompt, optional lyrics text, and optional reference images.
|
||||
- MiniMax uses the batch `music_generation` endpoint. The current bundled flow
|
||||
supports prompt, optional lyrics, instrumental mode, duration steering, and
|
||||
mp3 output through either `minimax` API-key auth or `minimax-portal` OAuth.
|
||||
- ComfyUI support is workflow-driven and depends on the configured graph plus
|
||||
node mapping for prompt/output fields.
|
||||
<AccordionGroup>
|
||||
<Accordion title="ComfyUI">
|
||||
Workflow-driven and depends on the configured graph plus node mapping
|
||||
for prompt/output fields. The bundled `comfy` plugin plugs into the
|
||||
shared `music_generate` tool through the music-generation provider
|
||||
registry.
|
||||
</Accordion>
|
||||
<Accordion title="Google (Lyria 3)">
|
||||
Uses Lyria 3 batch generation. The current bundled flow supports
|
||||
prompt, optional lyrics text, and optional reference images.
|
||||
</Accordion>
|
||||
<Accordion title="MiniMax">
|
||||
Uses the batch `music_generation` endpoint. Supports prompt, optional
|
||||
lyrics, instrumental mode, duration steering, and mp3 output through
|
||||
either `minimax` API-key auth or `minimax-portal` OAuth.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Choosing the right path
|
||||
|
||||
- **Shared provider-backed** when you want model selection, provider
|
||||
failover, and the built-in async task/status flow.
|
||||
- **Plugin path (ComfyUI)** when you need a custom workflow graph or a
|
||||
provider that is not part of the shared bundled music capability.
|
||||
|
||||
If you are debugging ComfyUI-specific behavior, see
|
||||
[ComfyUI](/providers/comfy). If you are debugging shared provider
|
||||
behavior, start with [Google (Gemini)](/providers/google) or
|
||||
[MiniMax](/providers/minimax).
|
||||
|
||||
## Provider capability modes
|
||||
|
||||
The shared music-generation contract now supports explicit mode declarations:
|
||||
The shared music-generation contract supports explicit mode declarations:
|
||||
|
||||
- `generate` for prompt-only generation
|
||||
- `edit` when the request includes one or more reference images
|
||||
- `generate` for prompt-only generation.
|
||||
- `edit` when the request includes one or more reference images.
|
||||
|
||||
New provider implementations should prefer explicit mode blocks:
|
||||
|
||||
@@ -237,15 +299,10 @@ capabilities: {
|
||||
```
|
||||
|
||||
Legacy flat fields such as `maxInputImages`, `supportsLyrics`, and
|
||||
`supportsFormat` are not enough to advertise edit support. Providers should
|
||||
declare `generate` and `edit` explicitly so live tests, contract tests, and
|
||||
the shared `music_generate` tool can validate mode support deterministically.
|
||||
|
||||
## Choosing the right path
|
||||
|
||||
- Use the shared provider-backed path when you want model selection, provider failover, and the built-in async task/status flow.
|
||||
- Use a plugin path such as ComfyUI when you need a custom workflow graph or a provider that is not part of the shared bundled music capability.
|
||||
- If you are debugging ComfyUI-specific behavior, see [ComfyUI](/providers/comfy). If you are debugging shared provider behavior, start with [Google (Gemini)](/providers/google) or [MiniMax](/providers/minimax).
|
||||
`supportsFormat` are **not** enough to advertise edit support. Providers
|
||||
should declare `generate` and `edit` explicitly so live tests, contract
|
||||
tests, and the shared `music_generate` tool can validate mode support
|
||||
deterministically.
|
||||
|
||||
## Live tests
|
||||
|
||||
@@ -263,9 +320,8 @@ pnpm test:live:media music
|
||||
|
||||
This live file loads missing provider env vars from `~/.profile`, prefers
|
||||
live/env API keys ahead of stored auth profiles by default, and runs both
|
||||
`generate` and declared `edit` coverage when the provider enables edit mode.
|
||||
|
||||
Today that means:
|
||||
`generate` and declared `edit` coverage when the provider enables edit
|
||||
mode. Coverage today:
|
||||
|
||||
- `google`: `generate` plus `edit`
|
||||
- `minimax`: `generate` only
|
||||
@@ -282,10 +338,10 @@ sections are configured.
|
||||
|
||||
## Related
|
||||
|
||||
- [Background Tasks](/automation/tasks) - task tracking for detached `music_generate` runs
|
||||
- [Configuration Reference](/gateway/config-agents#agent-defaults) - `musicGenerationModel` config
|
||||
- [Background tasks](/automation/tasks) — task tracking for detached `music_generate` runs
|
||||
- [ComfyUI](/providers/comfy)
|
||||
- [Configuration reference](/gateway/config-agents#agent-defaults) — `musicGenerationModel` config
|
||||
- [Google (Gemini)](/providers/google)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Models](/concepts/models) - model configuration and failover
|
||||
- [Tools Overview](/tools)
|
||||
- [Models](/concepts/models) — model configuration and failover
|
||||
- [Tools overview](/tools)
|
||||
|
||||
@@ -65,6 +65,8 @@ Packaged OpenClaw installs do not eagerly install every bundled plugin's
|
||||
runtime dependency tree. When a bundled OpenClaw-owned plugin is active from
|
||||
plugin config, legacy channel config, or a default-enabled manifest, startup
|
||||
repairs only that plugin's declared runtime dependencies before importing it.
|
||||
Persisted channel auth state alone does not activate a bundled channel for
|
||||
Gateway startup runtime-dependency repair.
|
||||
Explicit disablement still wins: `plugins.entries.<id>.enabled: false`,
|
||||
`plugins.deny`, `plugins.enabled: false`, and `channels.<id>.enabled: false`
|
||||
prevent automatic bundled runtime-dependency repair for that plugin/channel.
|
||||
@@ -178,7 +180,9 @@ OpenClaw scans for plugins in this order (first match wins):
|
||||
|
||||
<Steps>
|
||||
<Step title="Config paths">
|
||||
`plugins.load.paths` — explicit file or directory paths.
|
||||
`plugins.load.paths` — explicit file or directory paths. Paths that point
|
||||
back at OpenClaw's own packaged bundled plugin directories are ignored;
|
||||
run `openclaw doctor --fix` to remove those stale aliases.
|
||||
</Step>
|
||||
|
||||
<Step title="Workspace plugins">
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
---
|
||||
summary: "Skills: managed vs workspace, gating rules, and config/env wiring"
|
||||
summary: "Skills: managed vs workspace, gating rules, agent allowlists, and config wiring"
|
||||
read_when:
|
||||
- Adding or modifying skills
|
||||
- Changing skill gating or load rules
|
||||
- Changing skill gating, allowlists, or load rules
|
||||
- Understanding skill precedence and snapshot behavior
|
||||
title: "Skills"
|
||||
sidebarTitle: "Skills"
|
||||
---
|
||||
|
||||
OpenClaw uses **[AgentSkills](https://agentskills.io)-compatible** skill folders to teach the agent how to use tools. Each skill is a directory containing a `SKILL.md` with YAML frontmatter and instructions. OpenClaw loads **bundled skills** plus optional local overrides, and filters them at load time based on environment, config, and binary presence.
|
||||
OpenClaw uses **[AgentSkills](https://agentskills.io)-compatible** skill
|
||||
folders to teach the agent how to use tools. Each skill is a directory
|
||||
containing a `SKILL.md` with YAML frontmatter and instructions. OpenClaw
|
||||
loads bundled skills plus optional local overrides, and filters them at
|
||||
load time based on environment, config, and binary presence.
|
||||
|
||||
## Locations and precedence
|
||||
|
||||
OpenClaw loads skills from these sources:
|
||||
OpenClaw loads skills from these sources, **highest precedence first**:
|
||||
|
||||
1. **Extra skill folders**: configured with `skills.load.extraDirs`
|
||||
2. **Bundled skills**: shipped with the install (npm package or OpenClaw.app)
|
||||
3. **Managed/local skills**: `~/.openclaw/skills`
|
||||
4. **Personal agent skills**: `~/.agents/skills`
|
||||
5. **Project agent skills**: `<workspace>/.agents/skills`
|
||||
6. **Workspace skills**: `<workspace>/skills`
|
||||
| # | Source | Path |
|
||||
| --- | --------------------- | -------------------------------- |
|
||||
| 1 | Workspace skills | `<workspace>/skills` |
|
||||
| 2 | Project agent skills | `<workspace>/.agents/skills` |
|
||||
| 3 | Personal agent skills | `~/.agents/skills` |
|
||||
| 4 | Managed/local skills | `~/.openclaw/skills` |
|
||||
| 5 | Bundled skills | shipped with the install |
|
||||
| 6 | Extra skill folders | `skills.load.extraDirs` (config) |
|
||||
|
||||
If a skill name conflicts, precedence is:
|
||||
|
||||
`<workspace>/skills` (highest) → `<workspace>/.agents/skills` → `~/.agents/skills` → `~/.openclaw/skills` → bundled skills → `skills.load.extraDirs` (lowest)
|
||||
If a skill name conflicts, the highest source wins.
|
||||
|
||||
## Per-agent vs shared skills
|
||||
|
||||
In **multi-agent** setups, each agent has its own workspace. That means:
|
||||
In **multi-agent** setups each agent has its own workspace:
|
||||
|
||||
- **Per-agent skills** live in `<workspace>/skills` for that agent only.
|
||||
- **Project agent skills** live in `<workspace>/.agents/skills` and apply to
|
||||
that workspace before the normal workspace `skills/` folder.
|
||||
- **Personal agent skills** live in `~/.agents/skills` and apply across
|
||||
workspaces on that machine.
|
||||
- **Shared skills** live in `~/.openclaw/skills` (managed/local) and are visible
|
||||
to **all agents** on the same machine.
|
||||
- **Shared folders** can also be added via `skills.load.extraDirs` (lowest
|
||||
precedence) if you want a common skills pack used by multiple agents.
|
||||
| Scope | Path | Visible to |
|
||||
| -------------------- | ------------------------------------------- | --------------------------- |
|
||||
| Per-agent | `<workspace>/skills` | Only that agent |
|
||||
| Project-agent | `<workspace>/.agents/skills` | Only that workspace's agent |
|
||||
| Personal-agent | `~/.agents/skills` | All agents on that machine |
|
||||
| Shared managed/local | `~/.openclaw/skills` | All agents on that machine |
|
||||
| Shared extra dirs | `skills.load.extraDirs` (lowest precedence) | All agents on that machine |
|
||||
|
||||
If the same skill name exists in more than one place, the usual precedence
|
||||
applies: workspace wins, then project agent skills, then personal agent skills,
|
||||
then managed/local, then bundled, then extra dirs.
|
||||
Same name in multiple places → highest source wins. Workspace beats
|
||||
project-agent, beats personal-agent, beats managed/local, beats bundled,
|
||||
beats extra dirs.
|
||||
|
||||
## Agent skill allowlists
|
||||
|
||||
Skill **location** and skill **visibility** are separate controls.
|
||||
|
||||
- Location/precedence decides which copy of a same-named skill wins.
|
||||
- Agent allowlists decide which visible skills an agent can actually use.
|
||||
|
||||
Use `agents.defaults.skills` for a shared baseline, then override per agent with
|
||||
`agents.list[].skills`:
|
||||
Location/precedence decides which copy of a same-named skill wins; agent
|
||||
allowlists decide which skills an agent can actually use.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -66,84 +66,87 @@ Use `agents.defaults.skills` for a shared baseline, then override per agent with
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Allowlist rules">
|
||||
- Omit `agents.defaults.skills` for unrestricted skills by default.
|
||||
- Omit `agents.list[].skills` to inherit `agents.defaults.skills`.
|
||||
- Set `agents.list[].skills: []` for no skills.
|
||||
- A non-empty `agents.list[].skills` list is the **final** set for that
|
||||
agent — it does not merge with defaults.
|
||||
- The effective allowlist applies across prompt building, skill
|
||||
slash-command discovery, sandbox sync, and skill snapshots.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
- Omit `agents.defaults.skills` for unrestricted skills by default.
|
||||
- Omit `agents.list[].skills` to inherit `agents.defaults.skills`.
|
||||
- Set `agents.list[].skills: []` for no skills.
|
||||
- A non-empty `agents.list[].skills` list is the final set for that agent; it
|
||||
does not merge with defaults.
|
||||
|
||||
OpenClaw applies the effective agent skill set across prompt building, skill
|
||||
slash-command discovery, sandbox sync, and skill snapshots.
|
||||
|
||||
## Plugins + skills
|
||||
## Plugins and skills
|
||||
|
||||
Plugins can ship their own skills by listing `skills` directories in
|
||||
`openclaw.plugin.json` (paths relative to the plugin root). Plugin skills load
|
||||
when the plugin is enabled. This is the right place for tool-specific operating
|
||||
guides that are too long for the tool description but should be available
|
||||
whenever the plugin is installed; for example, the browser plugin ships a
|
||||
`browser-automation` skill for multi-step browser control. Today those
|
||||
directories are merged into the same low-precedence path as
|
||||
`skills.load.extraDirs`, so a same-named bundled, managed, agent, or workspace
|
||||
skill overrides them.
|
||||
You can gate them via `metadata.openclaw.requires.config` on the plugin’s config
|
||||
entry. See [Plugins](/tools/plugin) for discovery/config and [Tools](/tools) for the
|
||||
tool surface those skills teach.
|
||||
`openclaw.plugin.json` (paths relative to the plugin root). Plugin skills
|
||||
load when the plugin is enabled. This is the right place for tool-specific
|
||||
operating guides that are too long for the tool description but should be
|
||||
available whenever the plugin is installed — for example, the browser
|
||||
plugin ships a `browser-automation` skill for multi-step browser control.
|
||||
|
||||
Plugin skill directories are merged into the same low-precedence path as
|
||||
`skills.load.extraDirs`, so a same-named bundled, managed, agent, or
|
||||
workspace skill overrides them. You can gate them via
|
||||
`metadata.openclaw.requires.config` on the plugin's config entry.
|
||||
|
||||
See [Plugins](/tools/plugin) for discovery/config and [Tools](/tools) for
|
||||
the tool surface those skills teach.
|
||||
|
||||
## Skill Workshop
|
||||
|
||||
The optional, experimental Skill Workshop plugin can create or update workspace
|
||||
skills from reusable procedures observed during agent work. It is disabled by
|
||||
default and must be explicitly enabled through
|
||||
The optional, experimental **Skill Workshop** plugin can create or update
|
||||
workspace skills from reusable procedures observed during agent work. It
|
||||
is disabled by default and must be explicitly enabled via
|
||||
`plugins.entries.skill-workshop`.
|
||||
|
||||
Skill Workshop writes only to `<workspace>/skills`, scans generated content,
|
||||
supports pending approval or automatic safe writes, quarantines unsafe
|
||||
proposals, and refreshes the skill snapshot after successful writes so new
|
||||
skills can become available without a Gateway restart.
|
||||
Skill Workshop writes only to `<workspace>/skills`, scans generated
|
||||
content, supports pending approval or automatic safe writes, quarantines
|
||||
unsafe proposals, and refreshes the skill snapshot after successful
|
||||
writes so new skills become available without a Gateway restart.
|
||||
|
||||
Use it when you want corrections such as “next time, verify GIF attribution” or
|
||||
hard-won workflows such as media QA checklists to become durable procedural
|
||||
instructions. Start with pending approval; use automatic writes only in trusted
|
||||
workspaces after reviewing its proposals. Full guide:
|
||||
[Skill Workshop Plugin](/plugins/skill-workshop).
|
||||
Use it for corrections such as _"next time, verify GIF attribution"_ or
|
||||
hard-won workflows such as media QA checklists. Start with pending
|
||||
approval; use automatic writes only in trusted workspaces after reviewing
|
||||
its proposals. Full guide: [Skill Workshop plugin](/plugins/skill-workshop).
|
||||
|
||||
## ClawHub (install + sync)
|
||||
## ClawHub (install and sync)
|
||||
|
||||
ClawHub is the public skills registry for OpenClaw. Browse at
|
||||
[https://clawhub.ai](https://clawhub.ai). Use native `openclaw skills`
|
||||
commands to discover/install/update skills, or the separate `clawhub` CLI when
|
||||
you need publish/sync workflows.
|
||||
Full guide: [ClawHub](/tools/clawhub).
|
||||
[ClawHub](https://clawhub.ai) is the public skills registry for OpenClaw.
|
||||
Use native `openclaw skills` commands for discover/install/update, or the
|
||||
separate `clawhub` CLI for publish/sync workflows. Full guide:
|
||||
[ClawHub](/tools/clawhub).
|
||||
|
||||
Common flows:
|
||||
| Action | Command |
|
||||
| ---------------------------------- | -------------------------------------- |
|
||||
| Install a skill into the workspace | `openclaw skills install <skill-slug>` |
|
||||
| Update all installed skills | `openclaw skills update --all` |
|
||||
| Sync (scan + publish updates) | `clawhub sync --all` |
|
||||
|
||||
- Install a skill into your workspace:
|
||||
- `openclaw skills install <skill-slug>`
|
||||
- Update all installed skills:
|
||||
- `openclaw skills update --all`
|
||||
- Sync (scan + publish updates):
|
||||
- `clawhub sync --all`
|
||||
Native `openclaw skills install` installs into the active workspace
|
||||
`skills/` directory. The separate `clawhub` CLI also installs into
|
||||
`./skills` under your current working directory (or falls back to the
|
||||
configured OpenClaw workspace). OpenClaw picks that up as
|
||||
`<workspace>/skills` on the next session.
|
||||
|
||||
Native `openclaw skills install` installs into the active workspace `skills/`
|
||||
directory. The separate `clawhub` CLI also installs into `./skills` under your
|
||||
current working directory (or falls back to the configured OpenClaw workspace).
|
||||
OpenClaw picks that up as `<workspace>/skills` on the next session.
|
||||
## Security
|
||||
|
||||
## Security notes
|
||||
<Warning>
|
||||
Treat third-party skills as **untrusted code**. Read them before enabling.
|
||||
Prefer sandboxed runs for untrusted inputs and risky tools. See
|
||||
[Sandboxing](/gateway/sandboxing) for the agent-side controls.
|
||||
</Warning>
|
||||
|
||||
- Treat third-party skills as **untrusted code**. Read them before enabling.
|
||||
- Prefer sandboxed runs for untrusted inputs and risky tools. See [Sandboxing](/gateway/sandboxing).
|
||||
- Workspace and extra-dir skill discovery only accepts skill roots and `SKILL.md` files whose resolved realpath stays inside the configured root.
|
||||
- Gateway-backed skill dependency installs (`skills.install`, onboarding, and the Skills settings UI) run the built-in dangerous-code scanner before executing installer metadata. `critical` findings block by default unless the caller explicitly sets the dangerous override; suspicious findings still warn only.
|
||||
- `openclaw skills install <slug>` is different: it downloads a ClawHub skill folder into the workspace and does not use the installer-metadata path above.
|
||||
- `skills.entries.*.env` and `skills.entries.*.apiKey` inject secrets into the **host** process
|
||||
for that agent turn (not the sandbox). Keep secrets out of prompts and logs.
|
||||
- For a broader threat model and checklists, see [Security](/gateway/security).
|
||||
- `openclaw skills install <slug>` is different — it downloads a ClawHub skill folder into the workspace and does not use the installer-metadata path above.
|
||||
- `skills.entries.*.env` and `skills.entries.*.apiKey` inject secrets into the **host** process for that agent turn (not the sandbox). Keep secrets out of prompts and logs.
|
||||
|
||||
## Format (AgentSkills + Pi-compatible)
|
||||
For a broader threat model and checklists, see [Security](/gateway/security).
|
||||
|
||||
## SKILL.md format
|
||||
|
||||
`SKILL.md` must include at least:
|
||||
|
||||
@@ -154,26 +157,35 @@ description: Generate or edit images via a provider-backed image workflow
|
||||
---
|
||||
```
|
||||
|
||||
Notes:
|
||||
OpenClaw follows the AgentSkills spec for layout/intent. The parser used
|
||||
by the embedded agent supports **single-line** frontmatter keys only;
|
||||
`metadata` should be a **single-line JSON object**. Use `{baseDir}` in
|
||||
instructions to reference the skill folder path.
|
||||
|
||||
- We follow the AgentSkills spec for layout/intent.
|
||||
- The parser used by the embedded agent supports **single-line** frontmatter keys only.
|
||||
- `metadata` should be a **single-line JSON object**.
|
||||
- Use `{baseDir}` in instructions to reference the skill folder path.
|
||||
- Optional frontmatter keys:
|
||||
- `homepage` — URL surfaced as “Website” in the macOS Skills UI (also supported via `metadata.openclaw.homepage`).
|
||||
- `user-invocable` — `true|false` (default: `true`). When `true`, the skill is exposed as a user slash command.
|
||||
- `disable-model-invocation` — `true|false` (default: `false`). When `true`, the skill is excluded from the model prompt (still available via user invocation).
|
||||
- `command-dispatch` — `tool` (optional). When set to `tool`, the slash command bypasses the model and dispatches directly to a tool.
|
||||
- `command-tool` — tool name to invoke when `command-dispatch: tool` is set.
|
||||
- `command-arg-mode` — `raw` (default). For tool dispatch, forwards the raw args string to the tool (no core parsing).
|
||||
### Optional frontmatter keys
|
||||
|
||||
The tool is invoked with params:
|
||||
`{ command: "<raw args>", commandName: "<slash command>", skillName: "<skill name>" }`.
|
||||
<ParamField path="homepage" type="string">
|
||||
URL surfaced as "Website" in the macOS Skills UI. Also supported via `metadata.openclaw.homepage`.
|
||||
</ParamField>
|
||||
<ParamField path="user-invocable" type="boolean" default="true">
|
||||
When `true`, the skill is exposed as a user slash command.
|
||||
</ParamField>
|
||||
<ParamField path="disable-model-invocation" type="boolean" default="false">
|
||||
When `true`, the skill is excluded from the model prompt (still available via user invocation).
|
||||
</ParamField>
|
||||
<ParamField path="command-dispatch" type='"tool"'>
|
||||
When set to `tool`, the slash command bypasses the model and dispatches directly to a tool.
|
||||
</ParamField>
|
||||
<ParamField path="command-tool" type="string">
|
||||
Tool name to invoke when `command-dispatch: tool` is set.
|
||||
</ParamField>
|
||||
<ParamField path="command-arg-mode" type='"raw"' default="raw">
|
||||
For tool dispatch, forwards the raw args string to the tool (no core parsing). The tool is invoked with `{ command: "<raw args>", commandName: "<slash command>", skillName: "<skill name>" }`.
|
||||
</ParamField>
|
||||
|
||||
## Gating (load-time filters)
|
||||
|
||||
OpenClaw **filters skills at load time** using `metadata` (single-line JSON):
|
||||
OpenClaw filters skills at load time using `metadata` (single-line JSON):
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -192,33 +204,54 @@ metadata:
|
||||
|
||||
Fields under `metadata.openclaw`:
|
||||
|
||||
- `always: true` — always include the skill (skip other gates).
|
||||
- `emoji` — optional emoji used by the macOS Skills UI.
|
||||
- `homepage` — optional URL shown as “Website” in the macOS Skills UI.
|
||||
- `os` — optional list of platforms (`darwin`, `linux`, `win32`). If set, the skill is only eligible on those OSes.
|
||||
- `requires.bins` — list; each must exist on `PATH`.
|
||||
- `requires.anyBins` — list; at least one must exist on `PATH`.
|
||||
- `requires.env` — list; env var must exist **or** be provided in config.
|
||||
- `requires.config` — list of `openclaw.json` paths that must be truthy.
|
||||
- `primaryEnv` — env var name associated with `skills.entries.<name>.apiKey`.
|
||||
- `install` — optional array of installer specs used by the macOS Skills UI (brew/node/go/uv/download).
|
||||
<ParamField path="always" type="boolean">
|
||||
When `true`, always include the skill (skip other gates).
|
||||
</ParamField>
|
||||
<ParamField path="emoji" type="string">
|
||||
Optional emoji used by the macOS Skills UI.
|
||||
</ParamField>
|
||||
<ParamField path="homepage" type="string">
|
||||
Optional URL shown as "Website" in the macOS Skills UI.
|
||||
</ParamField>
|
||||
<ParamField path="os" type='"darwin" | "linux" | "win32"' >
|
||||
Optional list of platforms. If set, the skill is only eligible on those OSes.
|
||||
</ParamField>
|
||||
<ParamField path="requires.bins" type="string[]">
|
||||
Each must exist on `PATH`.
|
||||
</ParamField>
|
||||
<ParamField path="requires.anyBins" type="string[]">
|
||||
At least one must exist on `PATH`.
|
||||
</ParamField>
|
||||
<ParamField path="requires.env" type="string[]">
|
||||
Env var must exist or be provided in config.
|
||||
</ParamField>
|
||||
<ParamField path="requires.config" type="string[]">
|
||||
List of `openclaw.json` paths that must be truthy.
|
||||
</ParamField>
|
||||
<ParamField path="primaryEnv" type="string">
|
||||
Env var name associated with `skills.entries.<name>.apiKey`.
|
||||
</ParamField>
|
||||
<ParamField path="install" type="object[]">
|
||||
Optional installer specs used by the macOS Skills UI (brew/node/go/uv/download).
|
||||
</ParamField>
|
||||
|
||||
If no `metadata.openclaw` is present, the skill is always eligible (unless
|
||||
disabled in config or blocked by `skills.allowBundled` for bundled skills).
|
||||
|
||||
<Note>
|
||||
Legacy `metadata.clawdbot` blocks are still accepted when
|
||||
`metadata.openclaw` is absent, so older installed skills keep their dependency
|
||||
gates and installer hints. New and updated skills should use
|
||||
`metadata.openclaw` is absent, so older installed skills keep their
|
||||
dependency gates and installer hints. New and updated skills should use
|
||||
`metadata.openclaw`.
|
||||
</Note>
|
||||
|
||||
Note on sandboxing:
|
||||
### Sandboxing notes
|
||||
|
||||
- `requires.bins` is checked on the **host** at skill load time.
|
||||
- If an agent is sandboxed, the binary must also exist **inside the container**.
|
||||
Install it via `agents.defaults.sandbox.docker.setupCommand` (or a custom image).
|
||||
`setupCommand` runs once after the container is created.
|
||||
Package installs also require network egress, a writable root FS, and a root user in the sandbox.
|
||||
Example: the `summarize` skill (`skills/summarize/SKILL.md`) needs the `summarize` CLI
|
||||
in the sandbox container to run there.
|
||||
- If an agent is sandboxed, the binary must also exist **inside the container**. Install it via `agents.defaults.sandbox.docker.setupCommand` (or a custom image). `setupCommand` runs once after the container is created. Package installs also require network egress, a writable root FS, and a root user in the sandbox.
|
||||
- Example: the `summarize` skill (`skills/summarize/SKILL.md`) needs the `summarize` CLI in the sandbox container to run there.
|
||||
|
||||
Installer example:
|
||||
### Installer specs
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -245,29 +278,25 @@ metadata:
|
||||
---
|
||||
```
|
||||
|
||||
Notes:
|
||||
<AccordionGroup>
|
||||
<Accordion title="Installer selection rules">
|
||||
- If multiple installers are listed, the gateway picks a single preferred option (brew when available, otherwise node).
|
||||
- If all installers are `download`, OpenClaw lists each entry so you can see the available artifacts.
|
||||
- Installer specs can include `os: ["darwin"|"linux"|"win32"]` to filter options by platform.
|
||||
- Node installs honor `skills.install.nodeManager` in `openclaw.json` (default: npm; options: npm/pnpm/yarn/bun). This only affects skill installs; the Gateway runtime should still be Node — Bun is not recommended for WhatsApp/Telegram.
|
||||
- Gateway-backed installer selection is preference-driven: when install specs mix kinds, OpenClaw prefers Homebrew when `skills.install.preferBrew` is enabled and `brew` exists, then `uv`, then the configured node manager, then other fallbacks like `go` or `download`.
|
||||
- If every install spec is `download`, OpenClaw surfaces all download options instead of collapsing to one preferred installer.
|
||||
</Accordion>
|
||||
<Accordion title="Per-installer details">
|
||||
- **Go installs:** if `go` is missing and `brew` is available, the gateway installs Go via Homebrew first and sets `GOBIN` to Homebrew's `bin` when possible.
|
||||
- **Download installs:** `url` (required), `archive` (`tar.gz` | `tar.bz2` | `zip`), `extract` (default: auto when archive detected), `stripComponents`, `targetDir` (default: `~/.openclaw/tools/<skillKey>`).
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
- If multiple installers are listed, the gateway picks a **single** preferred option (brew when available, otherwise node).
|
||||
- If all installers are `download`, OpenClaw lists each entry so you can see the available artifacts.
|
||||
- Installer specs can include `os: ["darwin"|"linux"|"win32"]` to filter options by platform.
|
||||
- Node installs honor `skills.install.nodeManager` in `openclaw.json` (default: npm; options: npm/pnpm/yarn/bun).
|
||||
This only affects **skill installs**; the Gateway runtime should still be Node
|
||||
(Bun is not recommended for WhatsApp/Telegram).
|
||||
- Gateway-backed installer selection is preference-driven, not node-only:
|
||||
when install specs mix kinds, OpenClaw prefers Homebrew when
|
||||
`skills.install.preferBrew` is enabled and `brew` exists, then `uv`, then the
|
||||
configured node manager, then other fallbacks like `go` or `download`.
|
||||
- If every install spec is `download`, OpenClaw surfaces all download options
|
||||
instead of collapsing to one preferred installer.
|
||||
- Go installs: if `go` is missing and `brew` is available, the gateway installs Go via Homebrew first and sets `GOBIN` to Homebrew’s `bin` when possible.
|
||||
- Download installs: `url` (required), `archive` (`tar.gz` | `tar.bz2` | `zip`), `extract` (default: auto when archive detected), `stripComponents`, `targetDir` (default: `~/.openclaw/tools/<skillKey>`).
|
||||
## Config overrides
|
||||
|
||||
If no `metadata.openclaw` is present, the skill is always eligible (unless
|
||||
disabled in config or blocked by `skills.allowBundled` for bundled skills).
|
||||
|
||||
## Config overrides (`~/.openclaw/openclaw.json`)
|
||||
|
||||
Bundled/managed skills can be toggled and supplied with env values:
|
||||
Bundled and managed skills can be toggled and supplied with env values
|
||||
under `skills.entries` in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -291,69 +320,75 @@ Bundled/managed skills can be toggled and supplied with env values:
|
||||
}
|
||||
```
|
||||
|
||||
Note: if the skill name contains hyphens, quote the key (JSON5 allows quoted keys).
|
||||
<ParamField path="enabled" type="boolean">
|
||||
`false` disables the skill even if it is bundled or installed.
|
||||
</ParamField>
|
||||
<ParamField path="apiKey" type='string | { source, provider, id }'>
|
||||
Convenience for skills that declare `metadata.openclaw.primaryEnv`. Supports plaintext or SecretRef.
|
||||
</ParamField>
|
||||
<ParamField path="env" type="Record<string, string>">
|
||||
Injected only if the variable is not already set in the process.
|
||||
</ParamField>
|
||||
<ParamField path="config" type="object">
|
||||
Optional bag for custom per-skill fields. Custom keys must live here.
|
||||
</ParamField>
|
||||
<ParamField path="allowBundled" type="string[]">
|
||||
Optional allowlist for **bundled** skills only. If set, only bundled skills in the list are eligible (managed/workspace skills unaffected).
|
||||
</ParamField>
|
||||
|
||||
If you want stock image generation/editing inside OpenClaw itself, use the core
|
||||
`image_generate` tool with `agents.defaults.imageGenerationModel` instead of a
|
||||
bundled skill. Skill examples here are for custom or third-party workflows.
|
||||
If the skill name contains hyphens, quote the key (JSON5 allows quoted
|
||||
keys). Config keys match the **skill name** by default — if a skill
|
||||
defines `metadata.openclaw.skillKey`, use that key under `skills.entries`.
|
||||
|
||||
For native image analysis, use the `image` tool with `agents.defaults.imageModel`.
|
||||
For native image generation/editing, use `image_generate` with
|
||||
`agents.defaults.imageGenerationModel`. If you pick `openai/*`, `google/*`,
|
||||
`fal/*`, or another provider-specific image model, add that provider's auth/API
|
||||
key too.
|
||||
<Note>
|
||||
For stock image generation/editing inside OpenClaw, use the core
|
||||
`image_generate` tool with `agents.defaults.imageGenerationModel` instead
|
||||
of a bundled skill. Skill examples here are for custom or third-party
|
||||
workflows. For native image analysis use the `image` tool with
|
||||
`agents.defaults.imageModel`. If you pick `openai/*`, `google/*`,
|
||||
`fal/*`, or another provider-specific image model, add that provider's
|
||||
auth/API key too.
|
||||
</Note>
|
||||
|
||||
Config keys match the **skill name** by default. If a skill defines
|
||||
`metadata.openclaw.skillKey`, use that key under `skills.entries`.
|
||||
|
||||
Rules:
|
||||
|
||||
- `enabled: false` disables the skill even if it’s bundled/installed.
|
||||
- `env`: injected **only if** the variable isn’t already set in the process.
|
||||
- `apiKey`: convenience for skills that declare `metadata.openclaw.primaryEnv`.
|
||||
Supports plaintext string or SecretRef object (`{ source, provider, id }`).
|
||||
- `config`: optional bag for custom per-skill fields; custom keys must live here.
|
||||
- `allowBundled`: optional allowlist for **bundled** skills only. If set, only
|
||||
bundled skills in the list are eligible (managed/workspace skills unaffected).
|
||||
|
||||
## Environment injection (per agent run)
|
||||
## Environment injection
|
||||
|
||||
When an agent run starts, OpenClaw:
|
||||
|
||||
1. Reads skill metadata.
|
||||
2. Applies any `skills.entries.<key>.env` or `skills.entries.<key>.apiKey` to
|
||||
`process.env`.
|
||||
2. Applies `skills.entries.<key>.env` and `skills.entries.<key>.apiKey` to `process.env`.
|
||||
3. Builds the system prompt with **eligible** skills.
|
||||
4. Restores the original environment after the run ends.
|
||||
|
||||
This is **scoped to the agent run**, not a global shell environment.
|
||||
Environment injection is **scoped to the agent run**, not a global shell
|
||||
environment.
|
||||
|
||||
For the bundled `claude-cli` backend, OpenClaw also materializes the same
|
||||
eligible snapshot as a temporary Claude Code plugin and passes it with
|
||||
`--plugin-dir`. Claude Code can then use its native skill resolver while
|
||||
OpenClaw still owns precedence, per-agent allowlists, gating, and
|
||||
`skills.entries.*` env/API key injection. Other CLI backends use the prompt
|
||||
catalog only.
|
||||
`skills.entries.*` env/API key injection. Other CLI backends use the
|
||||
prompt catalog only.
|
||||
|
||||
## Session snapshot (performance)
|
||||
## Snapshots and refresh
|
||||
|
||||
OpenClaw snapshots the eligible skills **when a session starts** and reuses that list for subsequent turns in the same session. Changes to skills or config take effect on the next new session.
|
||||
OpenClaw snapshots the eligible skills **when a session starts** and
|
||||
reuses that list for subsequent turns in the same session. Changes to
|
||||
skills or config take effect on the next new session.
|
||||
|
||||
Skills can also refresh mid-session when the skills watcher is enabled or when a new eligible remote node appears (see below). Think of this as a **hot reload**: the refreshed list is picked up on the next agent turn.
|
||||
Skills can refresh mid-session in two cases:
|
||||
|
||||
If the effective agent skill allowlist changes for that session, OpenClaw
|
||||
refreshes the snapshot so the visible skills stay aligned with the current
|
||||
agent.
|
||||
- The skills watcher is enabled.
|
||||
- A new eligible remote node appears.
|
||||
|
||||
## Remote macOS nodes (Linux gateway)
|
||||
Think of this as a **hot reload**: the refreshed list is picked up on the
|
||||
next agent turn. If the effective agent skill allowlist changes for that
|
||||
session, OpenClaw refreshes the snapshot so visible skills stay aligned
|
||||
with the current agent.
|
||||
|
||||
If the Gateway is running on Linux but a **macOS node** is connected **with `system.run` allowed** (Exec approvals security not set to `deny`), OpenClaw can treat macOS-only skills as eligible when the required binaries are present on that node. The agent should execute those skills via the `exec` tool with `host=node`.
|
||||
### Skills watcher
|
||||
|
||||
This relies on the node reporting its command support and on a bin probe via `system.which` or `system.run`. Offline nodes do not make remote-only skills visible. If a connected node stops answering bin probes, OpenClaw clears its cached bin matches so agents no longer see skills that cannot currently run there.
|
||||
|
||||
## Skills watcher (auto-refresh)
|
||||
|
||||
By default, OpenClaw watches skill folders and bumps the skills snapshot when `SKILL.md` files change. Configure this under `skills.load`:
|
||||
By default, OpenClaw watches skill folders and bumps the skills snapshot
|
||||
when `SKILL.md` files change. Configure under `skills.load`:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -366,44 +401,58 @@ By default, OpenClaw watches skill folders and bumps the skills snapshot when `S
|
||||
}
|
||||
```
|
||||
|
||||
## Token impact (skills list)
|
||||
### Remote macOS nodes (Linux gateway)
|
||||
|
||||
When skills are eligible, OpenClaw injects a compact XML list of available skills into the system prompt (via `formatSkillsForPrompt` in `pi-coding-agent`). The cost is deterministic:
|
||||
If the Gateway runs on Linux but a **macOS node** is connected with
|
||||
`system.run` allowed (Exec approvals security not set to `deny`),
|
||||
OpenClaw can treat macOS-only skills as eligible when the required
|
||||
binaries are present on that node. The agent should execute those skills
|
||||
via the `exec` tool with `host=node`.
|
||||
|
||||
- **Base overhead (only when ≥1 skill):** 195 characters.
|
||||
This relies on the node reporting its command support and on a bin probe
|
||||
via `system.which` or `system.run`. Offline nodes do **not** make
|
||||
remote-only skills visible. If a connected node stops answering bin
|
||||
probes, OpenClaw clears its cached bin matches so agents no longer see
|
||||
skills that cannot currently run there.
|
||||
|
||||
## Token impact
|
||||
|
||||
When skills are eligible, OpenClaw injects a compact XML list of available
|
||||
skills into the system prompt (via `formatSkillsForPrompt` in
|
||||
`pi-coding-agent`). The cost is deterministic:
|
||||
|
||||
- **Base overhead** (only when ≥1 skill): 195 characters.
|
||||
- **Per skill:** 97 characters + the length of the XML-escaped `<name>`, `<description>`, and `<location>` values.
|
||||
|
||||
Formula (characters):
|
||||
|
||||
```
|
||||
```text
|
||||
total = 195 + Σ (97 + len(name_escaped) + len(description_escaped) + len(location_escaped))
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- XML escaping expands `& < > " '` into entities (`&`, `<`, etc.), increasing length.
|
||||
- Token counts vary by model tokenizer. A rough OpenAI-style estimate is ~4 chars/token, so **97 chars ≈ 24 tokens** per skill plus your actual field lengths.
|
||||
XML escaping expands `& < > " '` into entities (`&`, `<`, etc.),
|
||||
increasing length. Token counts vary by model tokenizer. A rough
|
||||
OpenAI-style estimate is ~4 chars/token, so **97 chars ≈ 24 tokens** per
|
||||
skill plus your actual field lengths.
|
||||
|
||||
## Managed skills lifecycle
|
||||
|
||||
OpenClaw ships a baseline set of skills as **bundled skills** as part of the
|
||||
install (npm package or OpenClaw.app). `~/.openclaw/skills` exists for local
|
||||
overrides (for example, pinning/patching a skill without changing the bundled
|
||||
copy). Workspace skills are user-owned and override both on name conflicts.
|
||||
|
||||
## Config reference
|
||||
|
||||
See [Skills config](/tools/skills-config) for the full configuration schema.
|
||||
OpenClaw ships a baseline set of skills as **bundled skills** with the
|
||||
install (npm package or OpenClaw.app). `~/.openclaw/skills` exists for
|
||||
local overrides — for example, pinning or patching a skill without
|
||||
changing the bundled copy. Workspace skills are user-owned and override
|
||||
both on name conflicts.
|
||||
|
||||
## Looking for more skills?
|
||||
|
||||
Browse [https://clawhub.ai](https://clawhub.ai).
|
||||
|
||||
---
|
||||
Browse [https://clawhub.ai](https://clawhub.ai). Full configuration
|
||||
schema: [Skills config](/tools/skills-config).
|
||||
|
||||
## Related
|
||||
|
||||
- [Creating Skills](/tools/creating-skills) — building custom skills
|
||||
- [Skills Config](/tools/skills-config) — skill configuration reference
|
||||
- [Slash Commands](/tools/slash-commands) — all available slash commands
|
||||
- [ClawHub](/tools/clawhub) — public skills registry
|
||||
- [Creating skills](/tools/creating-skills) — building custom skills
|
||||
- [Plugins](/tools/plugin) — plugin system overview
|
||||
- [Skill Workshop plugin](/plugins/skill-workshop) — generate skills from agent work
|
||||
- [Skills config](/tools/skills-config) — skill configuration reference
|
||||
- [Slash commands](/tools/slash-commands) — all available slash commands
|
||||
|
||||
@@ -134,7 +134,7 @@ Built-in commands available today:
|
||||
- `/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|provider|limit|summary|audio|help` controls TTS. See [/tools/tts](/tools/tts).
|
||||
- `/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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user