feat(docs-ci): validate docs config examples against the schema (#121336)

* feat(docs-ci): validate config examples against schema

* docs: teach canonical agent roster configuration

* docs: nest inline environment variables under vars

* docs: update canonical media model keys

* docs: remove retired config keys from examples

* fix(docs-ci): keep audit internals private

* feat(docs-ci): validate plugin-owned config examples

* fix(docs-ci): validate indented config fences

* fix(ci): route manifest changes through docs audit

* chore(docs): refresh generated config baselines
This commit is contained in:
Peter Steinberger
2026-08-10 07:57:56 -07:00
committed by GitHub
parent 4849106b3b
commit a0ad38e71a
71 changed files with 786 additions and 295 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"core": 2292, "core": 2292,
"channel": 3716, "channel": 3716,
"plugin": 4030 "plugin": 4040
} }
+2 -2
View File
@@ -1,4 +1,4 @@
05150969476529ddc78ae3836efb00419571bf706696e91f23af226571fd2562 config-baseline.json f47ba9b32b6d42dff5e0f2cef4d7b16a629ff35b659b3f66beb6748c13944566 config-baseline.json
52a8577126e605f70ef6c34cafaaebd83ec7a658b70da20ebb24b6eb37a0e513 config-baseline.core.json 52a8577126e605f70ef6c34cafaaebd83ec7a658b70da20ebb24b6eb37a0e513 config-baseline.core.json
3a8d0cbdbf9d7d603204fba5b93493050cb76fd9141fcbbec56159ff00158d6f config-baseline.channel.json 3a8d0cbdbf9d7d603204fba5b93493050cb76fd9141fcbbec56159ff00158d6f config-baseline.channel.json
4498ac72bb6b9cd5205f7bb8741110a4c53c4e2ec504a1dbb7e7e59c3495f492 config-baseline.plugin.json d93639a3d59b9b7ecaa27ff38b844a9ec90ac074c9e53f02930146ed21665c66 config-baseline.plugin.json
+1
View File
@@ -15,6 +15,7 @@ This directory owns docs authoring, Mintlify link rules, and docs i18n policy.
- For docs, UI copy, and picker lists, order services/providers alphabetically unless the section is explicitly describing runtime order or auto-detection order. - For docs, UI copy, and picker lists, order services/providers alphabetically unless the section is explicitly describing runtime order or auto-detection order.
- Keep bundled plugin naming consistent with the repo-wide plugin terminology rules in the root `AGENTS.md`. - Keep bundled plugin naming consistent with the repo-wide plugin terminology rules in the root `AGENTS.md`.
- JSON5/JSON config fences that look like whole `openclaw.json` documents are schema-validated in CI with `pnpm docs:check-config-examples`; deliberately partial or legacy snippets opt out with `validate=false` in the fence info string.
- Generated docs, never hand-edit: `docs/plugins/reference/**`, `docs/plugins/reference.md`, and `docs/plugins/plugin-inventory.md` come from `pnpm plugins:inventory:gen`; `docs/maturity/**` from `pnpm maturity:render`. - Generated docs, never hand-edit: `docs/plugins/reference/**`, `docs/plugins/reference.md`, and `docs/plugins/plugin-inventory.md` come from `pnpm plugins:inventory:gen`; `docs/maturity/**` from `pnpm maturity:render`.
- The public and packaged docs map is generated from `pnpm docs:list --headings` during publishing and packaging. Keep only the small source stub at `docs/docs_map.md`; never commit the expanded heading mirror. - The public and packaged docs map is generated from `pnpm docs:list --headings` during publishing and packaging. Keep only the small source stub at `docs/docs_map.md`; never commit the expanded heading mirror.
+1 -6
View File
@@ -1100,18 +1100,13 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior
enabled: true, enabled: true,
intervalMs: 30000, intervalMs: 30000,
minUpdateIntervalMs: 15000, minUpdateIntervalMs: 15000,
exhaustedText: "token exhausted",
}, },
}, },
}, },
} }
``` ```
Auto presence maps runtime availability to Discord status: healthy => online, degraded or unknown => idle, exhausted or unavailable => dnd. Defaults: `intervalMs` 30000, `minUpdateIntervalMs` 15000 (must be less than or equal to `intervalMs`). Optional text overrides: Auto presence maps runtime availability to Discord status: healthy => online, degraded or unknown => idle, exhausted or unavailable => dnd. Defaults: `intervalMs` 30000, `minUpdateIntervalMs` 15000 (must be less than or equal to `intervalMs`).
- `autoPresence.healthyText`
- `autoPresence.degradedText`
- `autoPresence.exhaustedText` (supports `{reason}` placeholder)
</Accordion> </Accordion>
+3 -3
View File
@@ -848,7 +848,7 @@ Socket Mode configuration:
{ {
channels: { channels: {
slack: { slack: {
identity: "user", postAs: "user",
userToken: "<xoxp>", userToken: "<xoxp>",
appToken: "<xapp>", appToken: "<xapp>",
}, },
@@ -862,7 +862,7 @@ HTTP Request URL configuration:
{ {
channels: { channels: {
slack: { slack: {
identity: "user", postAs: "user",
mode: "http", mode: "http",
userToken: "<xoxp>", userToken: "<xoxp>",
signingSecret: "<signing-secret>", signingSecret: "<signing-secret>",
@@ -1242,7 +1242,7 @@ Status snapshot behavior:
`userTokenStatus` + `appTokenStatus` for user identity. `userTokenStatus` + `appTokenStatus` for user identity.
<Tip> <Tip>
For bot identity, actions and directory reads can prefer an optional user token; writes continue to use the bot token unless `userTokenReadOnly: false` allows fallback. For `identity: "user"`, reads and writes always use `userToken`. For bot identity, actions and directory reads can prefer an optional user token; writes continue to use the bot token unless `userTokenReadOnly: false` allows fallback. For `postAs: "user"`, reads and writes always use `userToken`.
</Tip> </Tip>
## Actions and gates ## Actions and gates
+25 -9
View File
@@ -108,6 +108,10 @@ For automatic refresh, create your own app at the [Twitch Developer Console](htt
{ {
channels: { channels: {
twitch: { twitch: {
username: "openclaw",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "yourchannel",
clientSecret: "your_client_secret", clientSecret: "your_client_secret",
refreshToken: "your_refresh_token", refreshToken: "your_refresh_token",
}, },
@@ -164,6 +168,9 @@ Every account entry needs its own `accessToken` (the env var covers only the def
twitch: { twitch: {
accounts: { accounts: {
default: { default: {
username: "openclaw",
accessToken: "oauth:abc123...",
channel: "yourchannel",
allowFrom: ["123456789", "987654321"], allowFrom: ["123456789", "987654321"],
}, },
}, },
@@ -179,6 +186,9 @@ Every account entry needs its own `accessToken` (the env var covers only the def
twitch: { twitch: {
accounts: { accounts: {
default: { default: {
username: "openclaw",
accessToken: "oauth:abc123...",
channel: "yourchannel",
allowedRoles: ["moderator", "vip"], allowedRoles: ["moderator", "vip"],
}, },
}, },
@@ -196,6 +206,9 @@ Every account entry needs its own `accessToken` (the env var covers only the def
twitch: { twitch: {
accounts: { accounts: {
default: { default: {
username: "openclaw",
accessToken: "oauth:abc123...",
channel: "yourchannel",
requireMention: false, requireMention: false,
}, },
}, },
@@ -300,8 +313,8 @@ openclaw channels status --probe
### Provider options ### Provider options
- `channels.twitch.enabled` - Enable/disable channel startup - `channels.twitch.enabled` - Enable/disable channel startup
- `channels.twitch.username` / `accessToken` / `clientId` / `channel` - Simplified single-account config (implicit `default` account; takes precedence over `accounts.default`) - `channels.twitch.username` / `accessToken` / `clientId` / `channel` - Simplified single-account config with an implicit `default` account
- `channels.twitch.accounts.<accountName>` - Multi-account config (all account fields above) - `channels.twitch.accounts.<accountName>` - Multi-account config (all account fields above); do not combine it with top-level account credentials
- `channels.twitch.defaultAccount` - Which account name is the default - `channels.twitch.defaultAccount` - Which account name is the default
- `channels.twitch.markdown.tables` - Markdown table rendering mode (`off` | `bullets` | `code` | `block`) - `channels.twitch.markdown.tables` - Markdown table rendering mode (`off` | `bullets` | `code` | `block`)
@@ -312,14 +325,17 @@ Full example:
channels: { channels: {
twitch: { twitch: {
enabled: true, enabled: true,
username: "openclaw", defaultAccount: "default",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "yourchannel",
clientSecret: "secret123...",
refreshToken: "refresh456...",
allowFrom: ["123456789"],
accounts: { accounts: {
default: {
username: "openclaw",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "yourchannel",
clientSecret: "secret123...",
refreshToken: "refresh456...",
allowFrom: ["123456789"],
},
second: { second: {
username: "mybot", username: "mybot",
accessToken: "oauth:def456...", accessToken: "oauth:def456...",
+8 -12
View File
@@ -459,23 +459,18 @@ Per-account override: `channels.whatsapp.accounts.<id>.reactionLevel`.
## Acknowledgment reactions ## Acknowledgment reactions
`channels.whatsapp.ackReaction` sends an immediate reaction on inbound receipt, gated by `reactionLevel` (suppressed when `"off"`): `messages.ackReaction` sends an immediate reaction on inbound receipt, gated by the active WhatsApp account's `reactionLevel` (suppressed when `"off"`). `messages.ackReactionScope` selects direct messages, groups, or both:
```json5 ```json5
{ {
channels: { messages: {
whatsapp: { ackReaction: "👀",
ackReaction: { ackReactionScope: "group-mentions", // all | direct | group-all | group-mentions | off
emoji: "👀",
direct: true,
group: "mentions", // always | mentions | never
},
},
}, },
} }
``` ```
Notes: sent immediately after inbound is accepted (pre-reply); if `ackReaction` is present without `emoji`, WhatsApp uses the routed agent's identity emoji falling back to "👀" (omit `ackReaction` or set `emoji: ""` for no ack); failures are logged but do not block reply delivery; group mode `mentions` reacts only on mention-triggered turns, while group activation `always` bypasses that check; WhatsApp uses `channels.whatsapp.ackReaction` only (legacy `messages.ackReaction` does not apply here). Notes: the reaction is sent immediately after inbound is accepted (pre-reply); omit `messages.ackReaction` or set it to `""` for no acknowledgment. Failures are logged but do not block reply delivery. The default scope is `"group-mentions"`; use `"all"` for direct messages and all eligible groups.
## Lifecycle status reactions ## Lifecycle status reactions
@@ -491,7 +486,7 @@ Set `messages.statusReactions.enabled: true` to let WhatsApp replace the ack rea
} }
``` ```
Notes: `channels.whatsapp.ackReaction` still controls eligibility for direct messages and groups; the queued state uses the same effective emoji as plain ack reactions; WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place and restore the ack after the final done/error state. Notes: `messages.ackReactionScope` still controls eligibility for direct messages and groups; the queued state uses the same effective emoji as plain acknowledgment reactions. WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place and restore the acknowledgment after the final done/error state.
## Active-turn typing ## Active-turn typing
@@ -681,10 +676,11 @@ Primary reference: [Configuration reference - WhatsApp](/gateway/config-channels
| Area | Fields | | Area | Fields |
| ---------------- | -------------------------------------------------------------------------------------------------------------- | | ---------------- | -------------------------------------------------------------------------------------------------------------- |
| Access | `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` | | Access | `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` |
| Delivery | `textChunkLimit`, `streaming.chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel` | | Delivery | `textChunkLimit`, `streaming.chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `reactionLevel` |
| Multi-account | `accounts.<id>.enabled`, `accounts.<id>.authDir`, and other per-account overrides | | Multi-account | `accounts.<id>.enabled`, `accounts.<id>.authDir`, and other per-account overrides |
| Operations | `configWrites`, `enabled` | | Operations | `configWrites`, `enabled` |
| Inbound batching | `messages.inbound.debounceMs`, `messages.inbound.byChannel.whatsapp` | | Inbound batching | `messages.inbound.debounceMs`, `messages.inbound.byChannel.whatsapp` |
| Acknowledgments | `messages.ackReaction`, `messages.ackReactionScope` |
| Session behavior | `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit` | | Session behavior | `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit` |
| Prompts | `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt` | | Prompts | `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt` |
+3 -2
View File
@@ -159,9 +159,10 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example:
channels: { channels: {
zalouser: { zalouser: {
enabled: true, enabled: true,
defaultAccount: "default", groupPolicy: "allowlist",
defaultAccount: "work",
accounts: { accounts: {
work: { enabled: true, profile: "work" }, work: { enabled: true, profile: "work", groupPolicy: "allowlist" },
}, },
}, },
}, },
+1 -1
View File
@@ -196,7 +196,7 @@ acpx openclaw -s codex-bridge --cwd /path/to/repo \
If you want `acpx openclaw` to target a specific Gateway and session key every time, override the `openclaw` agent command in `~/.acpx/config.json`: If you want `acpx openclaw` to target a specific Gateway and session key every time, override the `openclaw` agent command in `~/.acpx/config.json`:
```json ```json validate=false
{ {
"agents": { "agents": {
"openclaw": { "openclaw": {
+1 -1
View File
@@ -45,7 +45,7 @@ is present under it (unsupported sections or keys fail as
`policy/policy-jsonc-invalid` instead of being silently ignored). Minimal `policy/policy-jsonc-invalid` instead of being silently ignored). Minimal
example covering every supported section: example covering every supported section:
```jsonc ```jsonc validate=false
{ {
"channels": { "channels": {
"denyRules": [ "denyRules": [
+1 -1
View File
@@ -388,7 +388,7 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint:
```json5 ```json5
{ {
env: { KIMI_API_KEY: "sk-..." }, env: { vars: { KIMI_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { model: { primary: "kimi/kimi-for-coding" } }, defaults: { model: { primary: "kimi/kimi-for-coding" } },
}, },
+1 -24
View File
@@ -43,30 +43,7 @@ title: "Retry policy"
## Configuration ## Configuration
Set retry policy per provider in `~/.openclaw/openclaw.json`: Discord and Telegram channel retry timings are built in and are not configurable in `openclaw.json`.
```json5
{
channels: {
telegram: {
retry: {
attempts: 3,
minDelayMs: 400,
maxDelayMs: 30000,
jitter: 0.1,
},
},
discord: {
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1,
},
},
},
}
```
## Notes ## Notes
+1 -1
View File
@@ -24,7 +24,7 @@ Provider plugins can contribute cache-aware guidance without replacing the OpenC
Use provider-owned contributions for model-family-specific tuning. Reserve the legacy `before_prompt_build` hook for compatibility or truly global prompt changes. Use provider-owned contributions for model-family-specific tuning. Reserve the legacy `before_prompt_build` hook for compatibility or truly global prompt changes.
The bundled OpenAI/Codex GPT-5-family overlay (`resolveGpt5SystemPromptContribution`) uses this mechanism: a `stablePrefix` behavior contract (execution policy, tool discipline, output contract, completion contract) plus an optional `interaction_style` override for a friendlier tone. It applies to any `gpt-5*` model id routed through the OpenAI or Codex plugins, controlled by the OpenAI plugin setting `plugins.entries.openai.config.personality` (`"friendly"`/`"on"` or `"off"`). The built-in GPT-5-family prompt contribution (`resolveGpt5SystemPromptContribution`) uses this mechanism: a `stablePrefix` behavior contract (execution policy, tool discipline, output contract, completion contract) plus an optional `interaction_style` override for a friendlier tone. For OpenAI-family routes, `plugins.entries.openai.config.personality` controls that style layer: `"friendly"` is the default, `"on"` aliases `"friendly"`, and `"off"` removes only the friendly override; the stable behavior contract remains.
## Structure ## Structure
+22 -11
View File
@@ -495,19 +495,28 @@ as shown above. See [CLI backends](/gateway/cli-backends) for operations and
[building CLI backend plugins](/plugins/cli-backend-plugins) for command, [building CLI backend plugins](/plugins/cli-backend-plugins) for command,
session, image, and parser registration. session, image, and parser registration.
### GPT-5 prompt overlay ### OpenAI GPT-5 personality
The GPT-5 prompt overlay is provider-owned, not an agent default. GPT-5-family The bundled OpenAI plugin owns the GPT-5 friendly interaction-style setting. Matching GPT-5-family prompts receive the shared behavior contract; `personality` controls only the friendly style layer. Native Codex app-server routes keep Codex-owned base/model instructions instead of this OpenClaw GPT-5 contribution, and OpenClaw disables Codex's built-in personality for native threads.
model ids on OpenAI-family providers receive a tagged behavior contract on
OpenClaw-assembled prompts, and the OpenAI plugin's
`plugins.entries.openai.config.personality` setting (`"friendly"`/`"on"` or
`"off"`) toggles only the friendly interaction-style layer. Native Codex
app-server routes keep Codex-owned base/model instructions instead of this
overlay. See [GPT-5 prompt contribution](/providers/openai#gpt-5-prompt-contribution).
The retired `agents.defaults.promptOverlays` key is rejected by config ```json5
validation; `openclaw doctor --fix` migrates its personality value into {
`plugins.entries.openai.config.personality` when that key is unset. plugins: {
entries: {
openai: {
config: {
personality: "friendly", // friendly | on | off
},
},
},
},
}
```
- `"friendly"` (default) and `"on"` enable the friendly interaction-style layer.
- `"off"` disables only the friendly layer; the tagged GPT-5 behavior contract remains enabled.
See [OpenAI GPT-5 prompt contribution](/providers/openai#gpt-5-prompt-contribution) for provider and native Codex behavior.
### `agents.defaults.heartbeat` ### `agents.defaults.heartbeat`
@@ -1333,6 +1342,8 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
- `byChannel`: per-channel `mode` overrides keyed by provider id. - `byChannel`: per-channel `mode` overrides keyed by provider id.
- `debounceMsByChannel`: per-channel debounce overrides in milliseconds, keyed by provider id. - `debounceMsByChannel`: per-channel debounce overrides in milliseconds, keyed by provider id.
Use `messages.inbound.debounceMs` for the global pre-queue debounce window.
### Inbound debounce ### Inbound debounce
Batches rapid text-only messages from the same sender into a single agent turn. Media/attachments flush immediately. Control commands bypass debouncing. Default `debounceMs`: `2000`. Batches rapid text-only messages from the same sender into a single agent turn. Media/attachments flush immediately. Control commands bypass debouncing. Default `debounceMs`: `2000`.
+1 -9
View File
@@ -114,11 +114,9 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
```json5 ```json5
{ {
web: {
enabled: true,
},
channels: { channels: {
whatsapp: { whatsapp: {
enabled: true,
dmPolicy: "pairing", // pairing | allowlist | open | disabled dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["+15555550123", "+447700900123"], allowFrom: ["+15555550123", "+447700900123"],
textChunkLimit: 4000, textChunkLimit: 4000,
@@ -197,12 +195,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
actions: { reactions: true, sendMessage: true }, actions: { reactions: true, sendMessage: true },
reactionNotifications: "own", // off | own | all reactionNotifications: "own", // off | own | all
mediaMaxMb: 100, mediaMaxMb: 100,
retry: {
attempts: 3,
minDelayMs: 400,
maxDelayMs: 30000,
jitter: 0.1,
},
network: { network: {
autoSelectFamily: true, autoSelectFamily: true,
dnsResultOrder: "ipv4first", dnsResultOrder: "ipv4first",
+5 -5
View File
@@ -210,7 +210,7 @@ Controls elevated exec access outside the sandbox:
tools: { tools: {
exec: { exec: {
backgroundMs: 10000, backgroundMs: 10000,
timeoutSec: 1800, timeoutSeconds: 1800,
cleanupMs: 1800000, cleanupMs: 1800000,
approvalRunningNoticeMs: 10000, approvalRunningNoticeMs: 10000,
notifyOnExit: true, notifyOnExit: true,
@@ -607,7 +607,7 @@ Interactive custom-provider onboarding infers image input for known vision-model
```json5 ```json5
{ {
env: { CEREBRAS_API_KEY: "sk-..." }, env: { vars: { CEREBRAS_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { model: {
@@ -643,7 +643,7 @@ Interactive custom-provider onboarding infers image input for known vision-model
<Accordion title="Kimi Coding"> <Accordion title="Kimi Coding">
```json5 ```json5
{ {
env: { KIMI_API_KEY: "sk-..." }, env: { vars: { KIMI_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "kimi/kimi-for-coding" }, model: { primary: "kimi/kimi-for-coding" },
@@ -738,7 +738,7 @@ Interactive custom-provider onboarding infers image input for known vision-model
<Accordion title="Moonshot AI (Kimi)"> <Accordion title="Moonshot AI (Kimi)">
```json5 ```json5
{ {
env: { MOONSHOT_API_KEY: "sk-..." }, env: { vars: { MOONSHOT_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "moonshot/kimi-k2.6" }, model: { primary: "moonshot/kimi-k2.6" },
@@ -792,7 +792,7 @@ Interactive custom-provider onboarding infers image input for known vision-model
<Accordion title="Synthetic (Anthropic-compatible)"> <Accordion title="Synthetic (Anthropic-compatible)">
```json5 ```json5
{ {
env: { SYNTHETIC_API_KEY: "sk-..." }, env: { vars: { SYNTHETIC_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" }, model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" },
+1 -1
View File
@@ -65,8 +65,8 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
{ {
// Environment + shell // Environment + shell
env: { env: {
OPENROUTER_API_KEY: "sk-or-...",
vars: { vars: {
OPENROUTER_API_KEY: "sk-or-...",
GROQ_API_KEY: "gsk-...", GROQ_API_KEY: "gsk-...",
}, },
shellEnv: { shellEnv: {
+1 -1
View File
@@ -1086,8 +1086,8 @@ Setup: `openclaw dns setup --apply`.
```json5 ```json5
{ {
env: { env: {
OPENROUTER_API_KEY: "sk-or-...",
vars: { vars: {
OPENROUTER_API_KEY: "sk-or-...",
GROQ_API_KEY: "gsk-...", GROQ_API_KEY: "gsk-...",
}, },
shellEnv: { shellEnv: {
+4 -2
View File
@@ -643,8 +643,10 @@ Neither file overrides existing env vars. You can also set inline env vars in co
```json5 ```json5
{ {
env: { env: {
OPENROUTER_API_KEY: "sk-or-...", vars: {
vars: { GROQ_API_KEY: "gsk-..." }, OPENROUTER_API_KEY: "sk-or-...",
GROQ_API_KEY: "gsk-...",
},
}, },
} }
``` ```
+2 -2
View File
@@ -226,7 +226,7 @@ a terminal handshake failure.
When a device token is issued, `hello-ok.auth` adds it: When a device token is issued, `hello-ok.auth` adds it:
```json ```json validate=false
{ {
"auth": { "auth": {
"deviceToken": "…", "deviceToken": "…",
@@ -240,7 +240,7 @@ Built-in QR/setup-code bootstrap is a mobile handoff path. A successful
baseline setup-code connect returns a primary node token plus one bounded baseline setup-code connect returns a primary node token plus one bounded
operator token: operator token:
```json ```json validate=false
{ {
"auth": { "auth": {
"deviceToken": "…", "deviceToken": "…",
+4 -4
View File
@@ -431,6 +431,10 @@ Common patterns: personal agent (full access, no sandbox), family/work agent (sa
```json5 ```json5
{ {
// Session tools can reveal transcript data. Default scope is current + spawned;
// reads also include same-agent groups watched through ambient group awareness.
// Use visibility: "self" to exclude those watched sessions.
tools: { sessions: { visibility: "tree" } }, // self | tree | agent | all
agents: { agents: {
entries: { entries: {
public: { public: {
@@ -438,10 +442,6 @@ Common patterns: personal agent (full access, no sandbox), family/work agent (sa
workspace: "~/.openclaw/workspace-public", workspace: "~/.openclaw/workspace-public",
sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" }, sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" },
tools: { tools: {
// Session tools can reveal transcript data. Default scope is current + spawned;
// reads also include same-agent groups watched through ambient group awareness.
// Use visibility: "self" to exclude those watched sessions.
sessions: { visibility: "tree" }, // self | tree | agent | all
allow: [ allow: [
"sessions_list", "sessions_list",
"sessions_history", "sessions_history",
+3 -3
View File
@@ -101,20 +101,20 @@ See [Workspace `.env` files](/gateway/security#workspace-env-files) for the secu
## Config `env` block ## Config `env` block
Two equivalent ways to set inline env vars (both are non-overriding): Set inline env vars under `env.vars` (values are non-overriding):
```json5 ```json5
{ {
env: { env: {
OPENROUTER_API_KEY: "sk-or-...",
vars: { vars: {
OPENROUTER_API_KEY: "sk-or-...",
GROQ_API_KEY: "gsk-...", GROQ_API_KEY: "gsk-...",
}, },
}, },
} }
``` ```
The config `env` block accepts literal string values only. It does not expand The config `env.vars` block accepts literal string values only. It does not expand
`file:...` values; for example, `XAI_API_KEY: "file:secrets/xai-api-key.txt"` `file:...` values; for example, `XAI_API_KEY: "file:secrets/xai-api-key.txt"`
is passed to providers as that exact string. is passed to providers as that exact string.
+3 -3
View File
@@ -220,7 +220,7 @@ troubleshooting, see the main [FAQ](/help/faq).
```json5 ```json5
{ {
env: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." }, env: { vars: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "minimax/MiniMax-M3" }, model: { primary: "minimax/MiniMax-M3" },
@@ -296,7 +296,7 @@ troubleshooting, see the main [FAQ](/help/faq).
models: { "openrouter/anthropic/claude-sonnet-4-6": {} }, models: { "openrouter/anthropic/claude-sonnet-4-6": {} },
}, },
}, },
env: { OPENROUTER_API_KEY: "sk-or-..." }, env: { vars: { OPENROUTER_API_KEY: "sk-or-..." } },
} }
``` ```
@@ -310,7 +310,7 @@ troubleshooting, see the main [FAQ](/help/faq).
models: { "zai/glm-5.1": {} }, models: { "zai/glm-5.1": {} },
}, },
}, },
env: { ZAI_API_KEY: "..." }, env: { vars: { ZAI_API_KEY: "..." } },
} }
``` ```
+5 -3
View File
@@ -883,15 +883,17 @@ First-run Q&A - install, onboard, auth routes, subscriptions, initial failures -
- `.env` from the current working directory. - `.env` from the current working directory.
- a global fallback `.env` from `~/.openclaw/.env` (`$OPENCLAW_STATE_DIR/.env`). - a global fallback `.env` from `~/.openclaw/.env` (`$OPENCLAW_STATE_DIR/.env`).
Normally, neither `.env` file overrides existing env vars. For an OpenClaw-installed systemd service, the global `.env` may replace only service values that OpenClaw recorded as managed; operator-owned service values still take precedence. Provider credential and endpoint-routing keys are an exception for workspace `.env`: keys such as `GEMINI_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, or any key ending in `_ENDPOINT` (and other bundled-provider auth or endpoint env vars) are ignored from workspace `.env` and should live in the process environment, `~/.openclaw/.env`, or config `env`. Normally, neither `.env` file overrides existing env vars. For an OpenClaw-installed systemd service, the global `.env` may replace only service values that OpenClaw recorded as managed; operator-owned service values still take precedence. Provider credential and endpoint-routing keys are an exception for workspace `.env`: keys such as `GEMINI_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, or any key ending in `_ENDPOINT` (and other bundled-provider auth or endpoint env vars) are ignored from workspace `.env` and should live in the process environment, `~/.openclaw/.env`, or config `env.vars`.
Inline env vars in config apply only if missing from the process env: Inline env vars in config apply only if missing from the process env:
```json5 ```json5
{ {
env: { env: {
OPENROUTER_API_KEY: "sk-or-...", vars: {
vars: { GROQ_API_KEY: "gsk-..." }, OPENROUTER_API_KEY: "sk-or-...",
GROQ_API_KEY: "gsk-...",
},
}, },
} }
``` ```
+53 -64
View File
@@ -306,34 +306,42 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
{ {
tools: { tools: {
media: { media: {
models: [
{
provider: "openai",
model: "gpt-4o-mini-transcribe",
capabilities: ["audio"],
},
{
type: "cli",
command: "whisper",
args: ["--model", "base", "{{AttachmentPath}}"],
capabilities: ["audio"],
},
{
provider: "google",
model: "gemini-3-flash-preview",
capabilities: ["video"],
},
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
],
capabilities: ["video"],
},
],
audio: { audio: {
enabled: true, enabled: true,
models: [
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{
type: "cli",
command: "whisper",
args: ["--model", "base", "{{AttachmentPath}}"],
},
],
}, },
video: { video: {
enabled: true, enabled: true,
maxChars: 500, maxChars: 500,
models: [
{ provider: "google", model: "gemini-3-flash-preview" },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
],
},
],
}, },
}, },
}, },
@@ -345,25 +353,26 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
{ {
tools: { tools: {
media: { media: {
models: [
{ provider: "openai", model: "gpt-5.6-sol", capabilities: ["image"] },
{ provider: "anthropic", model: "claude-opus-5", capabilities: ["image"] },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
],
capabilities: ["image"],
},
],
image: { image: {
enabled: true, enabled: true,
maxBytes: 10485760, maxBytes: 10485760,
maxChars: 500, maxChars: 500,
models: [
{ provider: "openai", model: "gpt-5.6-sol" },
{ provider: "anthropic", model: "claude-opus-5" },
{
type: "cli",
command: "gemini",
args: [
"-m",
"gemini-3-flash",
"--allowed-tools",
"read_file",
"Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
],
},
],
}, },
}, },
}, },
@@ -375,33 +384,13 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
{ {
tools: { tools: {
media: { media: {
image: { models: [
models: [ {
{ provider: "google",
provider: "google", model: "gemini-3.1-pro-preview",
model: "gemini-3.1-pro-preview", capabilities: ["image", "video", "audio"],
capabilities: ["image", "video", "audio"], },
}, ],
],
},
audio: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
},
],
},
video: {
models: [
{
provider: "google",
model: "gemini-3.1-pro-preview",
capabilities: ["image", "video", "audio"],
},
],
},
}, },
}, },
} }
+1 -1
View File
@@ -465,7 +465,7 @@ Per-agent overrides use the same model-scoped shape:
Legacy whole-agent runtime examples like this are ignored: Legacy whole-agent runtime examples like this are ignored:
```json ```json validate=false
{ {
"agents": { "agents": {
"defaults": { "defaults": {
+5 -5
View File
@@ -79,7 +79,7 @@ OpenClaw release:
```json5 ```json5
{ {
env: { ANTHROPIC_API_KEY: "example-anthropic-key-not-real" }, env: { vars: { ANTHROPIC_API_KEY: "example-anthropic-key-not-real" } },
agents: { defaults: { model: { primary: "anthropic/claude-opus-5" } } }, agents: { defaults: { model: { primary: "anthropic/claude-opus-5" } } },
} }
``` ```
@@ -469,10 +469,10 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
}, },
}, },
}, },
list: [ entries: {
{ id: "research", default: true }, research: { default: true },
{ id: "alerts", params: { cacheRetention: "none" } }, alerts: { params: { cacheRetention: "none" } },
], },
}, },
} }
``` ```
+1 -1
View File
@@ -110,7 +110,7 @@ Most setups only need the API key. To pin the provider explicitly:
```json5 ```json5
{ {
env: { BASETEN_API_KEY: "..." }, env: { vars: { BASETEN_API_KEY: "..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "baseten/thinkingmachines/inkling" }, model: { primary: "baseten/thinkingmachines/inkling" },
+1 -1
View File
@@ -89,7 +89,7 @@ Most setups only need the API key. Use explicit `models.providers.cerebras` conf
```json5 ```json5
{ {
env: { CEREBRAS_API_KEY: "csk-..." }, env: { vars: { CEREBRAS_API_KEY: "csk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "cerebras/gemma-4-31b" }, model: { primary: "cerebras/gemma-4-31b" },
+4 -2
View File
@@ -92,8 +92,10 @@ response back in OpenAI format.
```json5 ```json5
{ {
env: { env: {
OPENAI_API_KEY: "not-needed", vars: {
OPENAI_BASE_URL: "http://localhost:3456/v1", OPENAI_API_KEY: "not-needed",
OPENAI_BASE_URL: "http://localhost:3456/v1",
},
}, },
agents: { agents: {
defaults: { defaults: {
+5 -3
View File
@@ -36,9 +36,9 @@ returns them.
{ {
tools: { tools: {
media: { media: {
models: [{ provider: "deepgram", model: "nova-3", capabilities: ["audio"] }],
audio: { audio: {
enabled: true, enabled: true,
models: [{ provider: "deepgram", model: "nova-3" }],
}, },
}, },
}, },
@@ -68,9 +68,11 @@ Deepgram `/listen` request, so any Deepgram-supported param name works
{ {
tools: { tools: {
media: { media: {
models: [
{ provider: "deepgram", model: "nova-3", language: "en", capabilities: ["audio"] },
],
audio: { audio: {
enabled: true, enabled: true,
models: [{ provider: "deepgram", model: "nova-3", language: "en" }],
}, },
}, },
}, },
@@ -82,6 +84,7 @@ Deepgram `/listen` request, so any Deepgram-supported param name works
{ {
tools: { tools: {
media: { media: {
models: [{ provider: "deepgram", model: "nova-3", capabilities: ["audio"] }],
audio: { audio: {
enabled: true, enabled: true,
providerOptions: { providerOptions: {
@@ -91,7 +94,6 @@ Deepgram `/listen` request, so any Deepgram-supported param name works
smart_format: true, smart_format: true,
}, },
}, },
models: [{ provider: "deepgram", model: "nova-3" }],
}, },
}, },
}, },
+1 -1
View File
@@ -38,7 +38,7 @@ export DEEPINFRA_API_KEY="<your-deepinfra-api-key>" # pragma: allowlist secret
```json5 ```json5
{ {
env: { DEEPINFRA_API_KEY: "<your-deepinfra-api-key>" }, // pragma: allowlist secret env: { vars: { DEEPINFRA_API_KEY: "<your-deepinfra-api-key>" } }, // pragma: allowlist secret
agents: { agents: {
defaults: { defaults: {
model: { primary: "deepinfra/deepseek-ai/DeepSeek-V4-Flash" }, model: { primary: "deepinfra/deepseek-ai/DeepSeek-V4-Flash" },
+1 -1
View File
@@ -135,7 +135,7 @@ preserve the replay payload DeepSeek requires.
```json5 ```json5
{ {
env: { DEEPSEEK_API_KEY: "sk-..." }, env: { vars: { DEEPSEEK_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "deepseek/deepseek-v4-pro" }, model: { primary: "deepseek/deepseek-v4-pro" },
+1 -1
View File
@@ -60,9 +60,9 @@ Use Scribe v2 for inbound audio attachments and short recorded voice segments:
{ {
tools: { tools: {
media: { media: {
models: [{ provider: "elevenlabs", model: "scribe_v2", capabilities: ["audio"] }],
audio: { audio: {
enabled: true, enabled: true,
models: [{ provider: "elevenlabs", model: "scribe_v2" }],
}, },
}, },
}, },
+9 -11
View File
@@ -61,7 +61,7 @@ export GROQ_API_KEY=gsk_...
```json5 ```json5
{ {
env: { GROQ_API_KEY: "gsk_..." }, env: { vars: { GROQ_API_KEY: "gsk_..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "groq/openai/gpt-oss-120b" }, model: { primary: "groq/openai/gpt-oss-120b" },
@@ -99,13 +99,13 @@ See [Thinking modes](/tools/thinking) for the shared `/think` levels and how Ope
Groq's plugin also registers an **audio media-understanding provider** so voice messages can be transcribed through the shared `tools.media.audio` surface. Groq's plugin also registers an **audio media-understanding provider** so voice messages can be transcribed through the shared `tools.media.audio` surface.
| Property | Value | | Property | Value |
| ------------------ | ----------------------------------------- | | ----------------- | ----------------------------------------- |
| Shared config path | `tools.media.audio` | | Shared model path | `tools.media.models` |
| Default base URL | `https://api.groq.com/openai/v1` | | Default base URL | `https://api.groq.com/openai/v1` |
| Default model | `whisper-large-v3-turbo` | | Default model | `whisper-large-v3-turbo` |
| Auto priority | 20 | | Auto priority | 20 |
| API endpoint | OpenAI-compatible `/audio/transcriptions` | | API endpoint | OpenAI-compatible `/audio/transcriptions` |
To make Groq the default audio backend: To make Groq the default audio backend:
@@ -113,9 +113,7 @@ To make Groq the default audio backend:
{ {
tools: { tools: {
media: { media: {
audio: { models: [{ provider: "groq", capabilities: ["audio"] }],
models: [{ provider: "groq" }],
},
}, },
}, },
} }
+1 -1
View File
@@ -66,7 +66,7 @@ Any model on the gateway is addressable as `kilocode/<upstream-id>` (for example
```json5 ```json5
{ {
env: { KILOCODE_API_KEY: "<your-kilocode-api-key>" }, // pragma: allowlist secret env: { vars: { KILOCODE_API_KEY: "<your-kilocode-api-key>" } }, // pragma: allowlist secret
agents: { agents: {
defaults: { defaults: {
model: { primary: "kilocode/kilo-auto/balanced" }, model: { primary: "kilocode/kilo-auto/balanced" },
+1 -1
View File
@@ -94,7 +94,7 @@ Capabilities:
```json5 ```json5
{ {
env: { MODEL_API_KEY: "<key>" }, env: { vars: { MODEL_API_KEY: "<key>" } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "meta/muse-spark-1.1" }, model: { primary: "meta/muse-spark-1.1" },
+2 -2
View File
@@ -120,7 +120,7 @@ Model refs follow the auth path: `minimax/<model>` for API-key setups, `minimax-
```json5 ```json5
{ {
env: { MINIMAX_API_KEY: "sk-..." }, env: { vars: { MINIMAX_API_KEY: "sk-..." } },
agents: { defaults: { model: { primary: "minimax/MiniMax-M3" } } }, agents: { defaults: { model: { primary: "minimax/MiniMax-M3" } } },
models: { models: {
mode: "merge", mode: "merge",
@@ -360,7 +360,7 @@ See [MiniMax Search](/tools/minimax-search) for full web search configuration an
```json5 ```json5
{ {
env: { MINIMAX_API_KEY: "sk-..." }, env: { vars: { MINIMAX_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
models: { models: {
+2 -2
View File
@@ -52,7 +52,7 @@ media understanding (Voxtral batch transcription), realtime STT for Voice Call
<Step title="Set a default model"> <Step title="Set a default model">
```json5 ```json5
{ {
env: { MISTRAL_API_KEY: "sk-..." }, env: { vars: { MISTRAL_API_KEY: "sk-..." } },
agents: { defaults: { model: { primary: "mistral/mistral-large-latest" } } }, agents: { defaults: { model: { primary: "mistral/mistral-large-latest" } } },
} }
``` ```
@@ -99,9 +99,9 @@ Use Voxtral for batch audio transcription through the media understanding pipeli
{ {
tools: { tools: {
media: { media: {
models: [{ provider: "mistral", model: "voxtral-mini-latest", capabilities: ["audio"] }],
audio: { audio: {
enabled: true, enabled: true,
models: [{ provider: "mistral", model: "voxtral-mini-latest" }],
}, },
}, },
}, },
+2 -2
View File
@@ -112,7 +112,7 @@ onboarding.
```json5 ```json5
{ {
env: { MOONSHOT_API_KEY: "sk-..." }, env: { vars: { MOONSHOT_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "moonshot/kimi-k3" }, model: { primary: "moonshot/kimi-k3" },
@@ -244,7 +244,7 @@ onboarding.
```json5 ```json5
{ {
env: { KIMI_API_KEY: "sk-..." }, env: { vars: { KIMI_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "kimi/kimi-for-coding" }, model: { primary: "kimi/kimi-for-coding" },
+1 -1
View File
@@ -47,7 +47,7 @@ openclaw onboard --auth-choice nvidia-api-key --nvidia-api-key "nvapi-..."
```json5 ```json5
{ {
env: { NVIDIA_API_KEY: "nvapi-..." }, env: { vars: { NVIDIA_API_KEY: "nvapi-..." } },
models: { models: {
providers: { providers: {
nvidia: { nvidia: {
+9 -2
View File
@@ -421,9 +421,16 @@ timeout and cap `num_ctx`:
}, },
tools: { tools: {
media: { media: {
models: [
{
provider: "ollama",
model: "qwen2.5vl:7b",
timeoutSeconds: 300,
capabilities: ["image"],
},
],
image: { image: {
timeoutSeconds: 180, timeoutSeconds: 180,
models: [{ provider: "ollama", model: "qwen2.5vl:7b", timeoutSeconds: 300 }],
}, },
}, },
}, },
@@ -1024,7 +1031,7 @@ For full setup and behavior, see [Ollama Web Search](/tools/ollama-search).
defaults: { defaults: {
models: { models: {
"ollama/gemma4": { "ollama/gemma4": {
thinking: "low", params: { thinking: "low" },
}, },
}, },
}, },
+14 -15
View File
@@ -265,7 +265,7 @@ for the full example.
```json5 ```json5
{ {
env: { OPENAI_API_KEY: "example-openai-key-not-real" }, env: { vars: { OPENAI_API_KEY: "example-openai-key-not-real" } },
agents: { defaults: { model: { primary: "openai/gpt-5.6" } } }, agents: { defaults: { model: { primary: "openai/gpt-5.6" } } },
} }
``` ```
@@ -279,7 +279,7 @@ for the full example.
```json5 ```json5
{ {
env: { OPENAI_API_KEY: "example-openai-key-not-real" }, env: { vars: { OPENAI_API_KEY: "example-openai-key-not-real" } },
agents: { defaults: { model: { primary: "openai/chat-latest" } } }, agents: { defaults: { model: { primary: "openai/chat-latest" } } },
} }
``` ```
@@ -806,12 +806,9 @@ request reaches the provider, so aspect-ratio requests generally still work.
## GPT-5 prompt contribution ## GPT-5 prompt contribution
OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family models on OpenClaw adds a shared GPT-5 prompt contribution to matching GPT-5-family
the `openai` provider (including legacy pre-repair Codex refs that normalize OpenClaw-assembled prompts. The OpenAI plugin setting below controls the
to `openai/*`). Other providers that also serve GPT-5-family model ids, such friendly style on OpenAI-family routes. Older GPT-4.x model ids do not match.
as OpenRouter or opencode routes, do not receive this overlay; it is gated on
provider id `openai`, not on model id alone. Older GPT-4.x models never
receive it.
The native Codex app-server harness does not receive the persona/tool- The native Codex app-server harness does not receive the persona/tool-
discipline behavior contract or the friendly interaction-style overlay through discipline behavior contract or the friendly interaction-style overlay through
@@ -935,14 +932,16 @@ value into `plugins.entries.openai.config.personality` when that key is unset.
{ {
tools: { tools: {
media: { media: {
models: [
{
type: "provider",
provider: "openai",
model: "gpt-4o-transcribe",
capabilities: ["audio"],
},
],
audio: { audio: {
models: [ enabled: true,
{
type: "provider",
provider: "openai",
model: "gpt-4o-transcribe",
},
],
}, },
}, },
}, },
+1 -1
View File
@@ -66,7 +66,7 @@ interactive onboarding or pass the shared OpenCode API key directly.
```json5 ```json5
{ {
env: { OPENCODE_API_KEY: "YOUR_API_KEY_HERE" }, // pragma: allowlist secret env: { vars: { OPENCODE_API_KEY: "YOUR_API_KEY_HERE" } }, // pragma: allowlist secret
agents: { defaults: { model: { primary: "opencode-go/kimi-k3" } } }, agents: { defaults: { model: { primary: "opencode-go/kimi-k3" } } },
} }
``` ```
+1 -1
View File
@@ -90,7 +90,7 @@ provider ids split so upstream per-model routing stays correct.
```json5 ```json5
{ {
env: { OPENCODE_API_KEY: "sk-..." }, env: { vars: { OPENCODE_API_KEY: "sk-..." } },
agents: { defaults: { model: { primary: "opencode/gpt-5.6-sol" } } }, agents: { defaults: { model: { primary: "opencode/gpt-5.6-sol" } } },
} }
``` ```
+1 -1
View File
@@ -74,7 +74,7 @@ This example explicitly selects the current DeepSeek flagship instead of the onb
```json5 ```json5
{ {
env: { QIANFAN_API_KEY: "bce-v3/ALTAK-..." }, env: { vars: { QIANFAN_API_KEY: "bce-v3/ALTAK-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "qianfan/deepseek-v4-pro" }, model: { primary: "qianfan/deepseek-v4-pro" },
+1 -1
View File
@@ -100,6 +100,6 @@ Local image and video references are supported via data URIs.
Shared tool parameters, provider selection, and async behavior. Shared tool parameters, provider selection, and async behavior.
</Card> </Card>
<Card title="Configuration reference" href="/gateway/config-agents#agent-defaults" icon="gear"> <Card title="Configuration reference" href="/gateway/config-agents#agent-defaults" icon="gear">
Agent default settings including video generation model. Agent default settings including `mediaModels.video`.
</Card> </Card>
</CardGroup> </CardGroup>
+7 -1
View File
@@ -32,9 +32,15 @@ SenseAudio transcribes inbound audio and voice-note attachments through OpenClaw
{ {
tools: { tools: {
media: { media: {
models: [
{
provider: "senseaudio",
model: "senseaudio-asr-pro-1.5-260319",
capabilities: ["audio"],
},
],
audio: { audio: {
enabled: true, enabled: true,
models: [{ provider: "senseaudio", model: "senseaudio-asr-pro-1.5-260319" }],
}, },
}, },
}, },
+2 -2
View File
@@ -138,7 +138,7 @@ A single auth flow writes region-matched profiles for both `stepfun` and `stepfu
<Accordion title="Full config: Standard provider"> <Accordion title="Full config: Standard provider">
```json5 ```json5
{ {
env: { STEPFUN_API_KEY: "your-key" }, env: { vars: { STEPFUN_API_KEY: "your-key" } },
agents: { defaults: { model: { primary: "stepfun/step-3.5-flash" } } }, agents: { defaults: { model: { primary: "stepfun/step-3.5-flash" } } },
models: { models: {
mode: "merge", mode: "merge",
@@ -178,7 +178,7 @@ A single auth flow writes region-matched profiles for both `stepfun` and `stepfu
<Accordion title="Full config: Step Plan provider"> <Accordion title="Full config: Step Plan provider">
```json5 ```json5
{ {
env: { STEPFUN_API_KEY: "your-key" }, env: { vars: { STEPFUN_API_KEY: "your-key" } },
agents: { defaults: { model: { primary: "stepfun-plan/step-3.5-flash" } } }, agents: { defaults: { model: { primary: "stepfun-plan/step-3.5-flash" } } },
models: { models: {
mode: "merge", mode: "merge",
+1 -1
View File
@@ -53,7 +53,7 @@ changes its base URL, override `models.providers.synthetic.baseUrl`.
```json5 ```json5
{ {
env: { SYNTHETIC_API_KEY: "sk-..." }, env: { vars: { SYNTHETIC_API_KEY: "sk-..." } },
agents: { agents: {
defaults: { defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" }, model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" },
+1 -1
View File
@@ -216,7 +216,7 @@ More help: [Troubleshooting](/help/troubleshooting) and [FAQ](/help/faq).
<Accordion title="Config file example"> <Accordion title="Config file example">
```json5 ```json5
{ {
env: { VENICE_API_KEY: "vapi_..." }, env: { vars: { VENICE_API_KEY: "vapi_..." } },
agents: { defaults: { model: { primary: "venice/zai-org-glm-4.7" } } }, agents: { defaults: { model: { primary: "venice/zai-org-glm-4.7" } } },
models: { models: {
mode: "merge", mode: "merge",
+9 -7
View File
@@ -393,13 +393,15 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20
{ {
tools: { tools: {
media: { media: {
models: [
{
type: "provider",
provider: "xai",
capabilities: ["audio"],
},
],
audio: { audio: {
models: [ enabled: true,
{
type: "provider",
provider: "xai",
},
],
}, },
}, },
}, },
@@ -495,7 +497,7 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20
}, },
}, },
}, },
env: { XAI_API_KEY: "xai-..." }, env: { vars: { XAI_API_KEY: "xai-..." } },
} }
``` ```
+2 -2
View File
@@ -170,7 +170,7 @@ mono Opus with `ffmpeg` before delivery.
```json5 ```json5
{ {
env: { XIAOMI_API_KEY: "your-key" }, env: { vars: { XIAOMI_API_KEY: "your-key" } },
agents: { defaults: { model: { primary: "xiaomi/mimo-v2.5" } } }, agents: { defaults: { model: { primary: "xiaomi/mimo-v2.5" } } },
models: { models: {
mode: "merge", mode: "merge",
@@ -210,7 +210,7 @@ Token Plan:
```json5 ```json5
{ {
env: { XIAOMI_TOKEN_PLAN_API_KEY: "tp-your-key" }, env: { vars: { XIAOMI_TOKEN_PLAN_API_KEY: "tp-your-key" } },
agents: { defaults: { model: { primary: "xiaomi-token-plan/mimo-v2.5-pro" } } }, agents: { defaults: { model: { primary: "xiaomi-token-plan/mimo-v2.5-pro" } } },
models: { models: {
mode: "merge", mode: "merge",
+1 -1
View File
@@ -141,7 +141,7 @@ you want to force a specific Coding Plan or general API surface.
```json5 ```json5
{ {
env: { ZAI_API_KEY: "sk-..." }, env: { vars: { ZAI_API_KEY: "sk-..." } },
models: { models: {
providers: { providers: {
zai: { zai: {
+1 -12
View File
@@ -221,20 +221,9 @@ Why the assertions differ: Anthropic exposes explicit cache breakpoints and movi
diagnostics: diagnostics:
cacheTrace: cacheTrace:
enabled: true enabled: true
filePath: "~/.openclaw/logs/cache-trace.jsonl" # optional
includeMessages: false # default true
includePrompt: false # default true
includeSystem: false # default true
``` ```
Defaults: `enabled` defaults to `false`. Cache traces otherwise write to `$OPENCLAW_STATE_DIR/logs/cache-trace.jsonl` and include messages, prompt text, and the system prompt by default. Output-path and payload-inclusion overrides are environment-only controls for one-off debugging.
| Key | Default |
| ----------------- | -------------------------------------------- |
| `filePath` | `$OPENCLAW_STATE_DIR/logs/cache-trace.jsonl` |
| `includeMessages` | `true` |
| `includePrompt` | `true` |
| `includeSystem` | `true` |
### Env toggles (one-off debugging) ### Env toggles (one-off debugging)
+3 -4
View File
@@ -127,15 +127,14 @@ Example:
// Start with 0; enable later. // Start with 0; enable later.
heartbeat: { every: "0m" }, heartbeat: { every: "0m" },
}, },
list: [ entries: {
{ main: {
id: "main",
default: true, default: true,
groupChat: { groupChat: {
mentionPatterns: ["@openclaw", "openclaw"], mentionPatterns: ["@openclaw", "openclaw"],
}, },
}, },
], },
}, },
channels: { channels: {
whatsapp: { whatsapp: {
@@ -147,7 +147,6 @@ Point OpenClaw at the address reachable from WSL2:
remote: { remote: {
cdpUrl: "http://WINDOWS_HOST_OR_IP:9222", cdpUrl: "http://WINDOWS_HOST_OR_IP:9222",
attachOnly: true, attachOnly: true,
color: "#00AA00",
}, },
}, },
}, },
+11 -24
View File
@@ -169,31 +169,27 @@ Browser settings live in `~/.openclaw/openclaw.json`.
}, },
// snapshotDefaults: { mode: "efficient" }, // default snapshot mode when the caller omits one // snapshotDefaults: { mode: "efficient" }, // default snapshot mode when the caller omits one
defaultProfile: "openclaw", defaultProfile: "openclaw",
color: "#FF4500",
headless: false, headless: false,
noSandbox: false, noSandbox: false,
attachOnly: false, attachOnly: false,
executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
profiles: { profiles: {
openclaw: { cdpPort: 18800, color: "#FF4500" }, openclaw: { cdpPort: 18800 },
work: { work: {
cdpPort: 18801, cdpPort: 18801,
color: "#0066CC",
headless: true, headless: true,
executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
}, },
user: { user: {
driver: "existing-session", driver: "existing-session",
attachOnly: true, attachOnly: true,
color: "#00AA00",
}, },
brave: { brave: {
driver: "existing-session", driver: "existing-session",
attachOnly: true, attachOnly: true,
userDataDir: "~/Library/Application Support/BraveSoftware/Brave-Browser", userDataDir: "~/Library/Application Support/BraveSoftware/Brave-Browser",
color: "#FB542B",
}, },
remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" }, remote: { cdpUrl: "http://10.0.0.42:9222" },
}, },
}, },
} }
@@ -257,15 +253,11 @@ browser-specific model settings.
{ {
tools: { tools: {
media: { media: {
image: { models: [
models: [ { provider: "bytedance", model: "doubao-seed-2.0-pro", capabilities: ["image"] },
{ provider: "bytedance", model: "doubao-seed-2.0-pro" }, // Add fallback candidates; first success wins
// Add fallback candidates; first success wins { provider: "openai", model: "gpt-4o", capabilities: ["image"] },
{ provider: "openai", model: "gpt-4o" }, ],
],
},
// Shared media models also work when tagged for image support.
// models: [{ provider: "openai", model: "gpt-4o", capabilities: ["image"] }],
}, },
}, },
agents: { agents: {
@@ -293,8 +285,9 @@ Screenshot image blocks are private tool results: the agent can inspect them,
but OpenClaw does not automatically attach them to channel replies. To share a but OpenClaw does not automatically attach them to channel replies. To share a
screenshot, ask the agent to send it explicitly with the message tool. screenshot, ask the agent to send it explicitly with the message tool.
Use the existing `tools.media.image` / `tools.media.models` fields for model Use `tools.media.models` for model fallbacks, timeouts, byte limits, profiles,
fallbacks, timeouts, byte limits, profiles, and provider request settings. and provider request settings. Tag screenshot-capable entries with the `image`
capability.
If the active main model already supports vision and no explicit image If the active main model already supports vision and no explicit image
understanding model is configured, OpenClaw keeps the normal image result so the understanding model is configured, OpenClaw keeps the normal image result so the
@@ -369,7 +362,6 @@ main model can read the screenshot directly.
The diagnostics do not enable acceleration, add a global acceleration setting, The diagnostics do not enable acceleration, add a global acceleration setting,
or grant sandbox browser device access. or grant sandbox browser device access.
- `executablePath` can be set globally or per local managed profile. Per-profile values override `browser.executablePath`, so different managed profiles can launch different Chromium-based browsers. Both forms accept `~` for your OS home directory. - `executablePath` can be set globally or per local managed profile. Per-profile values override `browser.executablePath`, so different managed profiles can launch different Chromium-based browsers. Both forms accept `~` for your OS home directory.
- `color` (top-level and per-profile) tints the browser UI so you can see which profile is active.
- Default profile is `openclaw` (managed standalone). Use `defaultProfile: "user"` to opt into the signed-in user browser. - Default profile is `openclaw` (managed standalone). Use `defaultProfile: "user"` to opt into the signed-in user browser.
- Auto-detect order: system default browser if Chromium-based; otherwise Chrome, Brave, Edge, Chromium, Chrome Canary. - Auto-detect order: system default browser if Chromium-based; otherwise Chrome, Brave, Edge, Chromium, Chrome Canary.
- `driver: "existing-session"` uses Chrome DevTools MCP instead of raw CDP. It can attach through Chrome MCP auto-connect, or through `cdpUrl` when you already have a DevTools endpoint for the running browser. - `driver: "existing-session"` uses Chrome DevTools MCP instead of raw CDP. It can attach through Chrome MCP auto-connect, or through `cdpUrl` when you already have a DevTools endpoint for the running browser.
@@ -494,7 +486,6 @@ Example:
profiles: { profiles: {
browserless: { browserless: {
cdpUrl: "wss://production-sfo.browserless.io?token=<BROWSERLESS_API_KEY>", cdpUrl: "wss://production-sfo.browserless.io?token=<BROWSERLESS_API_KEY>",
color: "#00AA00",
}, },
}, },
}, },
@@ -523,7 +514,6 @@ Browserless as an externally managed CDP service:
browserless: { browserless: {
cdpUrl: "ws://127.0.0.1:3000", cdpUrl: "ws://127.0.0.1:3000",
attachOnly: true, attachOnly: true,
color: "#00AA00",
}, },
}, },
}, },
@@ -586,7 +576,6 @@ proxies.
profiles: { profiles: {
browserbase: { browserbase: {
cdpUrl: "wss://connect.browserbase.com?apiKey=<BROWSERBASE_API_KEY>", cdpUrl: "wss://connect.browserbase.com?apiKey=<BROWSERBASE_API_KEY>",
color: "#F97316",
}, },
}, },
}, },
@@ -618,7 +607,6 @@ WebSocket gateway.
profiles: { profiles: {
notte: { notte: {
cdpUrl: "wss://us-prod.notte.cc/sessions/connect?token=<NOTTE_API_KEY>", cdpUrl: "wss://us-prod.notte.cc/sessions/connect?token=<NOTTE_API_KEY>",
color: "#7C3AED",
}, },
}, },
}, },
@@ -695,7 +683,7 @@ Official background and setup references:
- [Chrome DevTools MCP README](https://github.com/ChromeDevTools/chrome-devtools-mcp) - [Chrome DevTools MCP README](https://github.com/ChromeDevTools/chrome-devtools-mcp)
Built-in profile: `user`. Create your own custom existing-session profile if Built-in profile: `user`. Create your own custom existing-session profile if
you want a different name, color, or browser data directory. you want a different name or browser data directory.
By default the built-in `user` profile uses Chrome MCP auto-connect, which By default the built-in `user` profile uses Chrome MCP auto-connect, which
targets the default local Google Chrome profile. Use `userDataDir` for Brave, targets the default local Google Chrome profile. Use `userDataDir` for Brave,
@@ -710,7 +698,6 @@ directory:
driver: "existing-session", driver: "existing-session",
attachOnly: true, attachOnly: true,
userDataDir: "~/Library/Application Support/BraveSoftware/Brave-Browser", userDataDir: "~/Library/Application Support/BraveSoftware/Brave-Browser",
color: "#FB542B",
}, },
}, },
}, },
+1 -1
View File
@@ -91,7 +91,7 @@ openclaw config set browser.defaultProfile chrome
{ {
browser: { browser: {
profiles: { profiles: {
chrome: { driver: "extension", color: "#FF4500" }, chrome: { driver: "extension" },
}, },
}, },
} }
+2 -2
View File
@@ -744,13 +744,13 @@ directive warnings.
**Disable model overrides entirely:** **Disable model overrides entirely:**
```json5 ```json5
{ messages: { tts: { modelOverrides: { enabled: false } } } } { tts: { modelOverrides: { enabled: false } } }
``` ```
**Allow provider switching while keeping other knobs configurable:** **Allow provider switching while keeping other knobs configurable:**
```json5 ```json5
{ messages: { tts: { modelOverrides: { enabled: true, allowProvider: true, allowSeed: false } } } } { tts: { modelOverrides: { enabled: true, allowProvider: true, allowSeed: false } } }
``` ```
## Slash commands ## Slash commands
+19
View File
@@ -219,6 +219,15 @@
"type": "boolean" "type": "boolean"
} }
} }
},
"webSearch": {
"type": "object",
"additionalProperties": false,
"properties": {
"baseUrl": {
"type": "string"
}
}
} }
} }
}, },
@@ -238,6 +247,16 @@
"nodeInference.enabled": { "nodeInference.enabled": {
"label": "Enable Node Inference", "label": "Enable Node Inference",
"help": "When false, this node host does not advertise or accept Ollama node-inference commands." "help": "When false, this node host does not advertise or accept Ollama node-inference commands."
},
"webSearch": {
"label": "Web Search",
"help": "Plugin-owned Ollama web search connection settings.",
"advanced": true
},
"webSearch.baseUrl": {
"label": "Web Search Base URL",
"help": "Optional Ollama host override used only for web search.",
"advanced": true
} }
} }
} }
+1 -1
View File
@@ -306,7 +306,7 @@
"type": "string", "type": "string",
"enum": ["friendly", "on", "off"], "enum": ["friendly", "on", "off"],
"default": "friendly", "default": "friendly",
"description": "Controls the shared GPT-5 friendly interaction-style overlay for OpenAI-family providers. `friendly` and `on` enable the style overlay; `off` disables only that style layer while the tagged GPT-5 behavior contract stays active." "description": "Controls the canonical OpenAI-family GPT-5 friendly interaction style. Defaults to `friendly`; `on` aliases `friendly`; `off` removes only the style layer while shared stable behavior remains."
} }
} }
} }
+36 -4
View File
@@ -712,9 +712,17 @@
"model": { "model": {
"type": "string" "type": "string"
}, },
"voice": { "speakerVoice": {
"type": "string" "type": "string"
}, },
"voice": {
"type": "string",
"description": "Legacy alias for speakerVoice."
},
"voiceId": {
"type": "string",
"description": "Legacy alias for speakerVoice."
},
"speed": { "speed": {
"type": "number", "type": "number",
"minimum": 0.25, "minimum": 0.25,
@@ -735,9 +743,17 @@
"baseUrl": { "baseUrl": {
"type": "string" "type": "string"
}, },
"voiceId": { "speakerVoiceId": {
"type": "string" "type": "string"
}, },
"voice": {
"type": "string",
"description": "Legacy alias for speakerVoiceId."
},
"voiceId": {
"type": "string",
"description": "Legacy alias for speakerVoiceId."
},
"modelId": { "modelId": {
"type": "string" "type": "string"
}, },
@@ -791,9 +807,17 @@
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"voice": { "speakerVoice": {
"type": "string" "type": "string"
}, },
"voice": {
"type": "string",
"description": "Legacy alias for speakerVoice."
},
"voiceId": {
"type": "string",
"description": "Legacy alias for speakerVoice."
},
"lang": { "lang": {
"type": "string" "type": "string"
}, },
@@ -829,9 +853,17 @@
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
}, },
"voice": { "speakerVoice": {
"type": "string" "type": "string"
}, },
"voice": {
"type": "string",
"description": "Legacy alias for speakerVoice."
},
"voiceId": {
"type": "string",
"description": "Legacy alias for speakerVoice."
},
"lang": { "lang": {
"type": "string" "type": "string"
}, },
+2 -1
View File
@@ -1535,7 +1535,7 @@
"check:deprecated-api-usage": "node --import tsx scripts/check-deprecated-api-usage.mts", "check:deprecated-api-usage": "node --import tsx scripts/check-deprecated-api-usage.mts",
"check:deprecated-jsdoc": "node --import tsx scripts/check-deprecated-jsdoc.mts", "check:deprecated-jsdoc": "node --import tsx scripts/check-deprecated-jsdoc.mts",
"check:doctor-deprecation-registry": "node --import tsx scripts/check-doctor-deprecation-registry.ts", "check:doctor-deprecation-registry": "node --import tsx scripts/check-doctor-deprecation-registry.ts",
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-mdx && pnpm docs:check-i18n-glossary && pnpm docs:check-links", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-mdx && pnpm docs:check-i18n-glossary && pnpm docs:check-links && pnpm docs:check-config-examples",
"check:env-var-count": "node --import tsx scripts/check-env-var-count.mts", "check:env-var-count": "node --import tsx scripts/check-env-var-count.mts",
"check:host-env-policy:swift": "node --import tsx scripts/generate-host-env-security-policy-swift.mts --check", "check:host-env-policy:swift": "node --import tsx scripts/generate-host-env-security-policy-swift.mts --check",
"check:import-cycles": "node --import tsx scripts/check-import-cycles.ts", "check:import-cycles": "node --import tsx scripts/check-import-cycles.ts",
@@ -1595,6 +1595,7 @@
"db:kysely:gen": "node --import tsx scripts/generate-kysely-types.mts", "db:kysely:gen": "node --import tsx scripts/generate-kysely-types.mts",
"dev": "node scripts/run-node.mjs", "dev": "node scripts/run-node.mjs",
"dev:ui:mock": "node --import tsx scripts/control-ui-mock-dev.ts", "dev:ui:mock": "node --import tsx scripts/control-ui-mock-dev.ts",
"docs:check-config-examples": "node scripts/check-docs-config-examples.mjs",
"docs:check-i18n-glossary": "node --import tsx scripts/check-docs-i18n-glossary.mts", "docs:check-i18n-glossary": "node --import tsx scripts/check-docs-i18n-glossary.mts",
"docs:check-links": "node scripts/docs-link-audit.mjs", "docs:check-links": "node scripts/docs-link-audit.mjs",
"docs:check-links:anchors": "node scripts/docs-link-audit.mjs --anchors", "docs:check-links:anchors": "node scripts/docs-link-audit.mjs --anchors",
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env node
import { runTsxCliShim } from "./lib/tsx-cli-shim.mjs";
await runTsxCliShim(import.meta.url, {
implementation: "./check-docs-config-examples.mts",
failureTool: "check-docs-config-examples",
});
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env node
import path from "node:path";
import { fileURLToPath } from "node:url";
import { auditDocsConfigExamples } from "../src/config/docs-config-examples.js";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const result = auditDocsConfigExamples({ repoRoot });
console.log(`docs_config_files_scanned=${result.stats.filesScanned}`);
console.log(`docs_config_fences_seen=${result.stats.fencesSeen}`);
console.log(`docs_config_fences_validated=${result.stats.candidatesValidated}`);
console.log(`docs_config_fences_skipped=${result.stats.fencesSkipped}`);
console.log(`docs_config_skipped_unsupported_language=${result.stats.skippedUnsupportedLanguage}`);
console.log(`docs_config_skipped_opt_out=${result.stats.skippedOptOut}`);
console.log(`docs_config_skipped_parse_failure=${result.stats.skippedParseFailure}`);
console.log(`docs_config_skipped_non_object=${result.stats.skippedNonObject}`);
console.log(`docs_config_skipped_fragment=${result.stats.skippedFragment}`);
for (const finding of result.findings) {
console.log(
`${finding.filePath}:${finding.fenceStartLine} :: ${finding.issuePath} :: ${finding.message}`,
);
}
if (result.findings.length > 0) {
console.log(
"Update each example to the current schema, or annotate a deliberately partial or illustrative fence with validate=false.",
);
process.exit(1);
}
+4
View File
@@ -538,6 +538,7 @@ const PRECISE_SOURCE_TEST_TARGETS = new Map<string, string[]>([
], ],
], ],
]); ]);
const DOCS_CONFIG_EXAMPLES_TEST_TARGET = "src/config/docs-config-examples.test.ts";
const RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS = ["src/plugins/bundled-plugin-metadata.test.ts"]; const RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS = ["src/plugins/bundled-plugin-metadata.test.ts"];
const RUNTIME_SIDECAR_PATH_CONSUMER_TEST_TARGETS = [ const RUNTIME_SIDECAR_PATH_CONSUMER_TEST_TARGETS = [
...RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS, ...RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS,
@@ -2949,6 +2950,9 @@ function resolvePreciseChangedTestTargets(
const cwd = options.cwd ?? process.cwd(); const cwd = options.cwd ?? process.cwd();
const mappedTargets = const mappedTargets =
SOURCE_TEST_TARGETS.get(changedPath) ?? SOURCE_TEST_TARGETS.get(changedPath) ??
(/^extensions\/[^/]+\/openclaw\.plugin\.json$/u.test(changedPath)
? [changedPath, DOCS_CONFIG_EXAMPLES_TEST_TARGET]
: null) ??
resolveToolingTestTargets(changedPath, cwd) ?? resolveToolingTestTargets(changedPath, cwd) ??
resolveAppcastTargets(changedPath) ?? resolveAppcastTargets(changedPath) ??
resolvePromptSnapshotFixtureTargets(changedPath); resolvePromptSnapshotFixtureTargets(changedPath);
+2 -2
View File
@@ -2133,7 +2133,7 @@ describe("config plugin validation", () => {
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
}); });
it("accepts voice-call OpenAI TTS speed, instructions, and baseUrl config fields", () => { it("accepts voice-call OpenAI TTS speakerVoice, speed, instructions, and baseUrl fields", () => {
const res = validateInSuite({ const res = validateInSuite({
agents: { list: [{ id: "openclaw" }] }, agents: { list: [{ id: "openclaw" }] },
plugins: { plugins: {
@@ -2146,7 +2146,7 @@ describe("config plugin validation", () => {
providers: { providers: {
openai: { openai: {
baseUrl: "http://localhost:8880/v1", baseUrl: "http://localhost:8880/v1",
voice: "alloy", speakerVoice: "alloy",
speed: 1.5, speed: 1.5,
instructions: "Speak in a cheerful tone", instructions: "Speak in a cheerful tone",
}, },
+138
View File
@@ -0,0 +1,138 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { auditDocsConfigExamples } from "./docs-config-examples.js";
type SkipStat = "skippedFragment" | "skippedNonObject" | "skippedOptOut" | "skippedParseFailure";
function auditMarkdown(markdown: string): ReturnType<typeof auditDocsConfigExamples> {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-config-"));
const docsRoot = path.join(repoRoot, "docs");
fs.mkdirSync(docsRoot);
fs.writeFileSync(path.join(docsRoot, "fixture.md"), markdown);
try {
return auditDocsConfigExamples({ repoRoot });
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
}
describe("docs config examples", () => {
it.each([
{
name: "reports a retired nested key in a whole config",
markdown: "```json5\n{ agents: { defaults: { promptOverlays: {} } } }\n```",
findings: 1,
skipped: undefined,
issuePath: "agents.defaults",
},
{
name: "reports a retired key in an indented MDX component fence",
markdown: [
'<Accordion title="Legacy roster">',
" ```json5",
' { agents: { list: [{ id: "main" }] } }',
" ````",
"</Accordion>",
].join("\n"),
findings: 1,
skipped: undefined,
issuePath: "agents",
},
{
name: "skips a fragment without a recognized-key majority",
markdown: "```json5\n{ agents: {}, payloads: [] }\n```",
findings: 0,
skipped: "skippedFragment" as SkipStat,
},
{
name: "skips an explicit opt-out",
markdown:
'```json5 title="legacy config" validate=false\n{ agents: { defaults: { promptOverlays: {} } } }\n```',
findings: 0,
skipped: "skippedOptOut" as SkipStat,
},
{
name: "skips invalid JSON5",
markdown: "```json5\n{ agents: {\n```",
findings: 0,
skipped: "skippedParseFailure" as SkipStat,
},
{
name: "validates JSON fences",
markdown: '```json\n{ "gateway": { "port": 18789 } }\n```',
findings: 0,
skipped: undefined,
},
{
name: "validates JSONC tilde fences",
markdown: '~~~JSONC\n{ // comment\n "gateway": { "port": 18789 }\n}\n~~~',
findings: 0,
skipped: undefined,
},
{
name: "skips arrays",
markdown: "```json5\n[{ gateway: {} }]\n```",
findings: 0,
skipped: "skippedNonObject" as SkipStat,
},
{
name: "reports a retired root key beside a recognized key",
markdown: "```json5\n{ agents: {}, gateway: {}, promptOverlays: {} }\n```",
findings: 1,
skipped: undefined,
issuePath: "",
},
{
name: "reports a retired bundled channel key",
markdown: '```json5\n{ channels: { slack: { identity: "bot" } } }\n```',
findings: 1,
skipped: undefined,
issuePath: "channels.slack",
},
{
name: "reports an unsupported OpenAI plugin config key",
markdown:
'```json5\n{ plugins: { entries: { openai: { config: { personalityy: "friendly" } } } } }\n```',
findings: 1,
skipped: undefined,
issuePath: "plugins.entries.openai.config",
},
{
name: "accepts a supported OpenAI plugin config value",
markdown:
'```json5\n{ plugins: { entries: { openai: { config: { personality: "off" } } } } }\n```',
findings: 0,
skipped: undefined,
},
{
name: "drops nested include directives before validation",
markdown:
'```json5\n{ agents: { $include: "./agents.json5" }, gateway: { port: 18789 } }\n```',
findings: 0,
skipped: undefined,
},
])("$name", ({ markdown, findings, skipped, issuePath }) => {
const audit = auditMarkdown(markdown);
expect(audit.findings).toHaveLength(findings);
expect(audit.stats.candidatesValidated).toBe(skipped ? 0 : 1);
if (skipped) {
expect(audit.stats[skipped]).toBe(1);
}
if (issuePath !== undefined) {
expect(audit.findings[0]?.issuePath).toBe(issuePath);
}
});
it("keeps real docs aligned with the config schema", () => {
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
// This test is selected when src/config changes, so retiring a key checks docs even
// without a docs edit. The check:docs script covers docs-only PRs; together the two
// CI lanes leave no change-classification gap.
expect(auditDocsConfigExamples({ repoRoot }).findings).toEqual([]);
});
});
+278
View File
@@ -0,0 +1,278 @@
import fs from "node:fs";
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import JSON5 from "json5";
import {
loadPluginMetadataSnapshot,
type PluginMetadataSnapshot,
} from "../plugins/plugin-metadata-snapshot.js";
import { validateConfigObjectRaw, validateConfigObjectRawWithPlugins } from "./validation.js";
import { OpenClawSchemaShape } from "./zod-schema.root-shape.js";
type DocsConfigFinding = {
filePath: string;
fenceStartLine: number;
issuePath: string;
message: string;
};
type DocsConfigStats = {
filesScanned: number;
fencesSeen: number;
candidatesValidated: number;
fencesSkipped: number;
skippedUnsupportedLanguage: number;
skippedOptOut: number;
skippedParseFailure: number;
skippedNonObject: number;
skippedFragment: number;
};
type DocsConfigAudit = {
findings: DocsConfigFinding[];
stats: DocsConfigStats;
};
type MarkdownFence = {
info: string;
body: string;
startLine: number;
};
type DocsConfigValidationContext = {
env: NodeJS.ProcessEnv;
pluginMetadataSnapshot: Pick<PluginMetadataSnapshot, "manifestRegistry">;
};
const ROOT_CONFIG_KEYS = new Set(Object.keys(OpenClawSchemaShape));
function emptyStats(filesScanned = 0): DocsConfigStats {
return {
filesScanned,
fencesSeen: 0,
candidatesValidated: 0,
fencesSkipped: 0,
skippedUnsupportedLanguage: 0,
skippedOptOut: 0,
skippedParseFailure: 0,
skippedNonObject: 0,
skippedFragment: 0,
};
}
function extractMarkdownFences(markdown: string): MarkdownFence[] {
const lines = markdown.split(/\r?\n/u);
const fences: MarkdownFence[] = [];
for (let index = 0; index < lines.length; index += 1) {
// MDX component-nested fences (Accordion/Tabs) are commonly indented 4+ spaces;
// CommonMark's indented-code rule does not apply inside these docs components.
const opening = lines[index]?.match(/^([ \t]*)(`{3,}|~{3,})(.*)$/u);
if (!opening) {
continue;
}
const indent = opening[1];
const marker = opening[2];
if (indent === undefined || !marker) {
continue;
}
const body: string[] = [];
const startLine = index + 1;
const closing = new RegExp(`^[ \\t]*${marker.charAt(0)}{${marker.length},}[ \\t]*$`, "u");
index += 1;
while (index < lines.length && !closing.test(lines[index] ?? "")) {
body.push(lines[index] ?? "");
index += 1;
}
fences.push({
info: opening[3]?.trim() ?? "",
body: body.join("\n"),
startLine,
});
}
return fences;
}
function isConfigFence(info: string): boolean {
return /^(?:json5|json|jsonc)(?:\s|$)/iu.test(info);
}
function isUnrecognizedKeyMessage(message: string): boolean {
return /(?:Unrecognized keys?|must not have additional properties):\s*"/iu.test(message);
}
function isWholeConfig(parsed: Record<string, unknown>): boolean {
const topLevelKeys = Object.keys(parsed);
const recognizedKeys = topLevelKeys.filter((key) => ROOT_CONFIG_KEYS.has(key));
// Accepted tradeoff: equal config/non-config mixes and documents containing only
// retired root keys look like fragments and are skipped.
return recognizedKeys.length > topLevelKeys.length / 2;
}
function stripIncludeKeys(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(stripIncludeKeys);
}
if (!isRecord(value)) {
return value;
}
// src/config/includes.ts resolves these directives before schema validation.
// Docs validation drops them recursively to mirror that pipeline boundary.
return Object.fromEntries(
Object.entries(value).flatMap(([key, child]) =>
key === "$include" ? [] : [[key, stripIncludeKeys(child)]],
),
);
}
function createDocsConfigValidationContext(): DocsConfigValidationContext {
const env = {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(process.cwd(), "extensions"),
};
return {
env,
pluginMetadataSnapshot: loadPluginMetadataSnapshot({
config: {},
env,
preferPersisted: false,
allowCurrent: false,
}),
};
}
function auditConfigMarkdown(
params: { markdown: string; filePath: string },
validationContext: DocsConfigValidationContext,
): DocsConfigAudit {
const findings: DocsConfigFinding[] = [];
const stats = emptyStats(1);
for (const fence of extractMarkdownFences(params.markdown)) {
stats.fencesSeen += 1;
if (!isConfigFence(fence.info)) {
stats.fencesSkipped += 1;
stats.skippedUnsupportedLanguage += 1;
continue;
}
if (/\bvalidate=false\b/iu.test(fence.info)) {
stats.fencesSkipped += 1;
stats.skippedOptOut += 1;
continue;
}
let parsed: unknown;
try {
parsed = JSON5.parse(fence.body);
} catch {
stats.fencesSkipped += 1;
stats.skippedParseFailure += 1;
continue;
}
if (!isRecord(parsed)) {
stats.fencesSkipped += 1;
stats.skippedNonObject += 1;
continue;
}
if (!isWholeConfig(parsed)) {
stats.fencesSkipped += 1;
stats.skippedFragment += 1;
continue;
}
const validationInput = stripIncludeKeys(parsed);
// Keep these validators separate so ValidateConfigWithPluginsParams stays outside the
// Plugin SDK declaration closure; changing it shifts every SDK module baseline hash.
const results = [
validateConfigObjectRaw(validationInput, { validateBundledChannels: true }),
validateConfigObjectRawWithPlugins(validationInput, validationContext),
];
stats.candidatesValidated += 1;
// This gate catches retired keys only. Placeholder type errors and incomplete
// illustrative values remain outside its contract.
const seenIssues = new Set<string>();
for (const result of results) {
if (result.ok) {
continue;
}
for (const issue of result.issues) {
if (!isUnrecognizedKeyMessage(issue.message)) {
continue;
}
const issueKey = JSON.stringify([issue.path, issue.message]);
if (seenIssues.has(issueKey)) {
continue;
}
seenIssues.add(issueKey);
findings.push({
filePath: params.filePath,
fenceStartLine: fence.startLine,
issuePath: issue.path,
message: issue.message,
});
}
}
}
return {
findings: findings.toSorted(
(left, right) =>
left.filePath.localeCompare(right.filePath) ||
left.fenceStartLine - right.fenceStartLine ||
left.issuePath.localeCompare(right.issuePath),
),
stats,
};
}
// Keep this in sync with the generated-doc locale test in scripts/docs-link-audit.mts.
function isLocalizedDocPath(filePath: string): boolean {
return /^\/?[a-z]{2}(?:-[A-Za-z]{2,8})+\//u.test(filePath);
}
function listDocsFiles(docsRoot: string): string[] {
const files: string[] = [];
const walk = (directory: string): void => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (entry.name.startsWith(".")) {
continue;
}
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
walk(entryPath);
} else if (entry.isFile() && /\.mdx?$/iu.test(entry.name)) {
files.push(entryPath);
}
}
};
walk(docsRoot);
return files.toSorted((left, right) => left.localeCompare(right));
}
/** Audits English docs config examples against the current strict schema. */
export function auditDocsConfigExamples(params: { repoRoot: string }): DocsConfigAudit {
const docsRoot = path.join(params.repoRoot, "docs");
const findings: DocsConfigFinding[] = [];
const stats = emptyStats();
const validationContext = createDocsConfigValidationContext();
for (const filePath of listDocsFiles(docsRoot)) {
const docsRelativePath = path.relative(docsRoot, filePath).split(path.sep).join("/");
if (isLocalizedDocPath(docsRelativePath)) {
continue;
}
const repoRelativePath = path.posix.join("docs", docsRelativePath);
const audit = auditConfigMarkdown(
{
markdown: fs.readFileSync(filePath, "utf8"),
filePath: repoRelativePath,
},
validationContext,
);
findings.push(...audit.findings);
for (const [key, value] of Object.entries(audit.stats)) {
stats[key as keyof DocsConfigStats] += value;
}
}
return { findings, stats };
}
+10
View File
@@ -701,6 +701,16 @@ describe("test-projects args", () => {
).toStrictEqual([]); ).toStrictEqual([]);
}); });
it("routes bundled plugin manifest changes through the docs config audit", () => {
expect(resolveChangedTestTargetPlan(["extensions/voice-call/openclaw.plugin.json"])).toEqual({
mode: "targets",
targets: [
"extensions/voice-call/openclaw.plugin.json",
"src/config/docs-config-examples.test.ts",
],
});
});
it("routes auth setup script changes to the focused auth monitor test", () => { it("routes auth setup script changes to the focused auth monitor test", () => {
const changedPaths = ["scripts/setup-auth-system.sh"]; const changedPaths = ["scripts/setup-auth-system.sh"];