From a0ad38e71ab727a269679bce096191d50cdffedc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 07:57:56 -0700 Subject: [PATCH] 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 --- docs/.generated/config-baseline.counts.json | 2 +- docs/.generated/config-baseline.sha256 | 4 +- docs/AGENTS.md | 1 + docs/channels/discord.md | 7 +- docs/channels/slack.md | 6 +- docs/channels/twitch.md | 34 ++- docs/channels/whatsapp.md | 20 +- docs/channels/zalouser.md | 5 +- docs/cli/acp.md | 2 +- docs/cli/policy.md | 2 +- docs/concepts/model-providers.md | 2 +- docs/concepts/retry.md | 25 +- docs/concepts/system-prompt.md | 2 +- docs/gateway/config-agents.md | 33 ++- docs/gateway/config-channels.md | 10 +- docs/gateway/config-tools.md | 10 +- docs/gateway/configuration-examples.md | 2 +- docs/gateway/configuration-reference.md | 2 +- docs/gateway/configuration.md | 6 +- docs/gateway/protocol.md | 4 +- docs/gateway/security/index.md | 8 +- docs/help/environment.md | 6 +- docs/help/faq-models.md | 6 +- docs/help/faq.md | 8 +- docs/nodes/media-understanding.md | 117 ++++---- docs/plugins/sdk-agent-harness.md | 2 +- docs/providers/anthropic.md | 10 +- docs/providers/baseten.md | 2 +- docs/providers/cerebras.md | 2 +- docs/providers/claude-max-api-proxy.md | 6 +- docs/providers/deepgram.md | 8 +- docs/providers/deepinfra.md | 2 +- docs/providers/deepseek.md | 2 +- docs/providers/elevenlabs.md | 2 +- docs/providers/groq.md | 20 +- docs/providers/kilocode.md | 2 +- docs/providers/meta.md | 2 +- docs/providers/minimax.md | 4 +- docs/providers/mistral.md | 4 +- docs/providers/moonshot.md | 4 +- docs/providers/nvidia.md | 2 +- docs/providers/ollama.md | 11 +- docs/providers/openai.md | 29 +- docs/providers/opencode-go.md | 2 +- docs/providers/opencode.md | 2 +- docs/providers/qianfan.md | 2 +- docs/providers/runway.md | 2 +- docs/providers/senseaudio.md | 8 +- docs/providers/stepfun.md | 4 +- docs/providers/synthetic.md | 2 +- docs/providers/venice.md | 2 +- docs/providers/xai.md | 16 +- docs/providers/xiaomi.md | 4 +- docs/providers/zai.md | 2 +- docs/reference/prompt-caching.md | 13 +- docs/start/openclaw.md | 7 +- ...wsl2-windows-remote-cdp-troubleshooting.md | 1 - docs/tools/browser.md | 35 +-- docs/tools/chrome-extension.md | 2 +- docs/tools/tts.md | 4 +- extensions/ollama/openclaw.plugin.json | 19 ++ extensions/openai/openclaw.plugin.json | 2 +- extensions/voice-call/openclaw.plugin.json | 40 ++- package.json | 3 +- scripts/check-docs-config-examples.mjs | 7 + scripts/check-docs-config-examples.mts | 30 ++ scripts/test-projects.test-support.mts | 4 + src/config/config.plugin-validation.test.ts | 4 +- src/config/docs-config-examples.test.ts | 138 +++++++++ src/config/docs-config-examples.ts | 278 ++++++++++++++++++ src/scripts/test-projects.test.ts | 10 + 71 files changed, 786 insertions(+), 295 deletions(-) create mode 100644 scripts/check-docs-config-examples.mjs create mode 100644 scripts/check-docs-config-examples.mts create mode 100644 src/config/docs-config-examples.test.ts create mode 100644 src/config/docs-config-examples.ts diff --git a/docs/.generated/config-baseline.counts.json b/docs/.generated/config-baseline.counts.json index c17094f375c1..3b51a1460e0d 100644 --- a/docs/.generated/config-baseline.counts.json +++ b/docs/.generated/config-baseline.counts.json @@ -1,5 +1,5 @@ { "core": 2292, "channel": 3716, - "plugin": 4030 + "plugin": 4040 } diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 1176d15ebcaf..231099c38d95 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -05150969476529ddc78ae3836efb00419571bf706696e91f23af226571fd2562 config-baseline.json +f47ba9b32b6d42dff5e0f2cef4d7b16a629ff35b659b3f66beb6748c13944566 config-baseline.json 52a8577126e605f70ef6c34cafaaebd83ec7a658b70da20ebb24b6eb37a0e513 config-baseline.core.json 3a8d0cbdbf9d7d603204fba5b93493050cb76fd9141fcbbec56159ff00158d6f config-baseline.channel.json -4498ac72bb6b9cd5205f7bb8741110a4c53c4e2ec504a1dbb7e7e59c3495f492 config-baseline.plugin.json +d93639a3d59b9b7ecaa27ff38b844a9ec90ac074c9e53f02930146ed21665c66 config-baseline.plugin.json diff --git a/docs/AGENTS.md b/docs/AGENTS.md index eb5e48ff4bba..3c41568c3200 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -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. - 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`. - 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. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index b6b0724eb265..9fa0e121b528 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1100,18 +1100,13 @@ See [Slash commands](/tools/slash-commands) for the command catalog and behavior enabled: true, intervalMs: 30000, 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: - - - `autoPresence.healthyText` - - `autoPresence.degradedText` - - `autoPresence.exhaustedText` (supports `{reason}` placeholder) + 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`). diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 11a392e55365..497746fafc98 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -848,7 +848,7 @@ Socket Mode configuration: { channels: { slack: { - identity: "user", + postAs: "user", userToken: "", appToken: "", }, @@ -862,7 +862,7 @@ HTTP Request URL configuration: { channels: { slack: { - identity: "user", + postAs: "user", mode: "http", userToken: "", signingSecret: "", @@ -1242,7 +1242,7 @@ Status snapshot behavior: `userTokenStatus` + `appTokenStatus` for user identity. -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`. ## Actions and gates diff --git a/docs/channels/twitch.md b/docs/channels/twitch.md index f504441f52eb..21aecef9f8e9 100644 --- a/docs/channels/twitch.md +++ b/docs/channels/twitch.md @@ -108,6 +108,10 @@ For automatic refresh, create your own app at the [Twitch Developer Console](htt { channels: { twitch: { + username: "openclaw", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "yourchannel", clientSecret: "your_client_secret", refreshToken: "your_refresh_token", }, @@ -164,6 +168,9 @@ Every account entry needs its own `accessToken` (the env var covers only the def twitch: { accounts: { default: { + username: "openclaw", + accessToken: "oauth:abc123...", + channel: "yourchannel", allowFrom: ["123456789", "987654321"], }, }, @@ -179,6 +186,9 @@ Every account entry needs its own `accessToken` (the env var covers only the def twitch: { accounts: { default: { + username: "openclaw", + accessToken: "oauth:abc123...", + channel: "yourchannel", allowedRoles: ["moderator", "vip"], }, }, @@ -196,6 +206,9 @@ Every account entry needs its own `accessToken` (the env var covers only the def twitch: { accounts: { default: { + username: "openclaw", + accessToken: "oauth:abc123...", + channel: "yourchannel", requireMention: false, }, }, @@ -300,8 +313,8 @@ openclaw channels status --probe ### Provider options - `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.accounts.` - Multi-account config (all account fields above) +- `channels.twitch.username` / `accessToken` / `clientId` / `channel` - Simplified single-account config with an implicit `default` account +- `channels.twitch.accounts.` - 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.markdown.tables` - Markdown table rendering mode (`off` | `bullets` | `code` | `block`) @@ -312,14 +325,17 @@ Full example: channels: { twitch: { enabled: true, - username: "openclaw", - accessToken: "oauth:abc123...", - clientId: "xyz789...", - channel: "yourchannel", - clientSecret: "secret123...", - refreshToken: "refresh456...", - allowFrom: ["123456789"], + defaultAccount: "default", accounts: { + default: { + username: "openclaw", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "yourchannel", + clientSecret: "secret123...", + refreshToken: "refresh456...", + allowFrom: ["123456789"], + }, second: { username: "mybot", accessToken: "oauth:def456...", diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 83722252207b..42598d3c4cba 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -459,23 +459,18 @@ Per-account override: `channels.whatsapp.accounts..reactionLevel`. ## 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 { - channels: { - whatsapp: { - ackReaction: { - emoji: "👀", - direct: true, - group: "mentions", // always | mentions | never - }, - }, + messages: { + ackReaction: "👀", + ackReactionScope: "group-mentions", // all | direct | group-all | group-mentions | off }, } ``` -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 @@ -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 @@ -681,10 +676,11 @@ Primary reference: [Configuration reference - WhatsApp](/gateway/config-channels | Area | Fields | | ---------------- | -------------------------------------------------------------------------------------------------------------- | | Access | `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` | -| Delivery | `textChunkLimit`, `streaming.chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel` | +| Delivery | `textChunkLimit`, `streaming.chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `reactionLevel` | | Multi-account | `accounts..enabled`, `accounts..authDir`, and other per-account overrides | | Operations | `configWrites`, `enabled` | | Inbound batching | `messages.inbound.debounceMs`, `messages.inbound.byChannel.whatsapp` | +| Acknowledgments | `messages.ackReaction`, `messages.ackReactionScope` | | Session behavior | `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms..historyLimit` | | Prompts | `groups..systemPrompt`, `groups["*"].systemPrompt`, `direct..systemPrompt`, `direct["*"].systemPrompt` | diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md index 1efc25633d9f..13b75489404d 100644 --- a/docs/channels/zalouser.md +++ b/docs/channels/zalouser.md @@ -159,9 +159,10 @@ Accounts map to `zalouser` profiles in OpenClaw state. Example: channels: { zalouser: { enabled: true, - defaultAccount: "default", + groupPolicy: "allowlist", + defaultAccount: "work", accounts: { - work: { enabled: true, profile: "work" }, + work: { enabled: true, profile: "work", groupPolicy: "allowlist" }, }, }, }, diff --git a/docs/cli/acp.md b/docs/cli/acp.md index 743711239d4d..c2536ffd7f7a 100644 --- a/docs/cli/acp.md +++ b/docs/cli/acp.md @@ -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`: -```json +```json validate=false { "agents": { "openclaw": { diff --git a/docs/cli/policy.md b/docs/cli/policy.md index 354d11d2bd70..b48b9184151e 100644 --- a/docs/cli/policy.md +++ b/docs/cli/policy.md @@ -45,7 +45,7 @@ is present under it (unsupported sections or keys fail as `policy/policy-jsonc-invalid` instead of being silently ignored). Minimal example covering every supported section: -```jsonc +```jsonc validate=false { "channels": { "denyRules": [ diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 3a99b31ebc8b..5f644479a81b 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -388,7 +388,7 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint: ```json5 { - env: { KIMI_API_KEY: "sk-..." }, + env: { vars: { KIMI_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "kimi/kimi-for-coding" } }, }, diff --git a/docs/concepts/retry.md b/docs/concepts/retry.md index dc7255de5b99..cf57728d003d 100644 --- a/docs/concepts/retry.md +++ b/docs/concepts/retry.md @@ -43,30 +43,7 @@ title: "Retry policy" ## Configuration -Set retry policy per provider in `~/.openclaw/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, - }, - }, - }, -} -``` +Discord and Telegram channel retry timings are built in and are not configurable in `openclaw.json`. ## Notes diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 6648f22c7feb..b89a22dfbf86 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -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. -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 diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index 8e979515c6d0..7ed2e810cc0c 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -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, 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 -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 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. -The retired `agents.defaults.promptOverlays` key is rejected by config -validation; `openclaw doctor --fix` migrates its personality value into -`plugins.entries.openai.config.personality` when that key is unset. +```json5 +{ + 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` @@ -1333,6 +1342,8 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`. - `byChannel`: per-channel `mode` overrides 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 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`. diff --git a/docs/gateway/config-channels.md b/docs/gateway/config-channels.md index c54d108d2dba..87234c2fc71f 100644 --- a/docs/gateway/config-channels.md +++ b/docs/gateway/config-channels.md @@ -114,11 +114,9 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat ```json5 { - web: { - enabled: true, - }, channels: { whatsapp: { + enabled: true, dmPolicy: "pairing", // pairing | allowlist | open | disabled allowFrom: ["+15555550123", "+447700900123"], textChunkLimit: 4000, @@ -197,12 +195,6 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat actions: { reactions: true, sendMessage: true }, reactionNotifications: "own", // off | own | all mediaMaxMb: 100, - retry: { - attempts: 3, - minDelayMs: 400, - maxDelayMs: 30000, - jitter: 0.1, - }, network: { autoSelectFamily: true, dnsResultOrder: "ipv4first", diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 71b232d0a529..c5f8ca2a15a2 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -210,7 +210,7 @@ Controls elevated exec access outside the sandbox: tools: { exec: { backgroundMs: 10000, - timeoutSec: 1800, + timeoutSeconds: 1800, cleanupMs: 1800000, approvalRunningNoticeMs: 10000, notifyOnExit: true, @@ -607,7 +607,7 @@ Interactive custom-provider onboarding infers image input for known vision-model ```json5 { - env: { CEREBRAS_API_KEY: "sk-..." }, + env: { vars: { CEREBRAS_API_KEY: "sk-..." } }, agents: { defaults: { model: { @@ -643,7 +643,7 @@ Interactive custom-provider onboarding infers image input for known vision-model ```json5 { - env: { KIMI_API_KEY: "sk-..." }, + env: { vars: { KIMI_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "kimi/kimi-for-coding" }, @@ -738,7 +738,7 @@ Interactive custom-provider onboarding infers image input for known vision-model ```json5 { - env: { MOONSHOT_API_KEY: "sk-..." }, + env: { vars: { MOONSHOT_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "moonshot/kimi-k2.6" }, @@ -792,7 +792,7 @@ Interactive custom-provider onboarding infers image input for known vision-model ```json5 { - env: { SYNTHETIC_API_KEY: "sk-..." }, + env: { vars: { SYNTHETIC_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" }, diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 0ecbbd9959f6..1d40ddd3ffbd 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -65,8 +65,8 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number. { // Environment + shell env: { - OPENROUTER_API_KEY: "sk-or-...", vars: { + OPENROUTER_API_KEY: "sk-or-...", GROQ_API_KEY: "gsk-...", }, shellEnv: { diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index aa023b1af741..18721d54074e 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1086,8 +1086,8 @@ Setup: `openclaw dns setup --apply`. ```json5 { env: { - OPENROUTER_API_KEY: "sk-or-...", vars: { + OPENROUTER_API_KEY: "sk-or-...", GROQ_API_KEY: "gsk-...", }, shellEnv: { diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index a6a38be80593..c1fa4008a516 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -643,8 +643,10 @@ Neither file overrides existing env vars. You can also set inline env vars in co ```json5 { env: { - OPENROUTER_API_KEY: "sk-or-...", - vars: { GROQ_API_KEY: "gsk-..." }, + vars: { + OPENROUTER_API_KEY: "sk-or-...", + GROQ_API_KEY: "gsk-...", + }, }, } ``` diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index c0274098f45a..6bb3f4899e29 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -226,7 +226,7 @@ a terminal handshake failure. When a device token is issued, `hello-ok.auth` adds it: -```json +```json validate=false { "auth": { "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 operator token: -```json +```json validate=false { "auth": { "deviceToken": "…", diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index e9e355cfff92..e1a4c4aec9d7 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -431,6 +431,10 @@ Common patterns: personal agent (full access, no sandbox), family/work agent (sa ```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: { entries: { public: { @@ -438,10 +442,6 @@ Common patterns: personal agent (full access, no sandbox), family/work agent (sa workspace: "~/.openclaw/workspace-public", sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" }, 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: [ "sessions_list", "sessions_history", diff --git a/docs/help/environment.md b/docs/help/environment.md index 45975de5e2e2..264821aa73f2 100644 --- a/docs/help/environment.md +++ b/docs/help/environment.md @@ -101,20 +101,20 @@ See [Workspace `.env` files](/gateway/security#workspace-env-files) for the secu ## 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 { env: { - OPENROUTER_API_KEY: "sk-or-...", vars: { + OPENROUTER_API_KEY: "sk-or-...", 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"` is passed to providers as that exact string. diff --git a/docs/help/faq-models.md b/docs/help/faq-models.md index 0039ff7521c7..5770e886d72b 100644 --- a/docs/help/faq-models.md +++ b/docs/help/faq-models.md @@ -220,7 +220,7 @@ troubleshooting, see the main [FAQ](/help/faq). ```json5 { - env: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." }, + env: { vars: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "minimax/MiniMax-M3" }, @@ -296,7 +296,7 @@ troubleshooting, see the main [FAQ](/help/faq). 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": {} }, }, }, - env: { ZAI_API_KEY: "..." }, + env: { vars: { ZAI_API_KEY: "..." } }, } ``` diff --git a/docs/help/faq.md b/docs/help/faq.md index 5dd68b651c52..2e53c3998fe7 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -883,15 +883,17 @@ First-run Q&A - install, onboard, auth routes, subscriptions, initial failures - - `.env` from the current working directory. - 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: ```json5 { env: { - OPENROUTER_API_KEY: "sk-or-...", - vars: { GROQ_API_KEY: "gsk-..." }, + vars: { + OPENROUTER_API_KEY: "sk-or-...", + GROQ_API_KEY: "gsk-...", + }, }, } ``` diff --git a/docs/nodes/media-understanding.md b/docs/nodes/media-understanding.md index 9909497c1a76..ac01e02c6fca 100644 --- a/docs/nodes/media-understanding.md +++ b/docs/nodes/media-understanding.md @@ -306,34 +306,42 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc. { tools: { 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: { enabled: true, - models: [ - { provider: "openai", model: "gpt-4o-mini-transcribe" }, - { - type: "cli", - command: "whisper", - args: ["--model", "base", "{{AttachmentPath}}"], - }, - ], }, video: { enabled: true, maxChars: 500, - models: [ - { provider: "google", model: "gemini-3-flash-preview" }, - { - type: "cli", - command: "gemini", - args: [ - "-m", - "gemini-3-flash", - "--allowed-tools", - "read_file", - "Read the media at {{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: { 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: { enabled: true, maxBytes: 10485760, 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: { media: { - image: { - models: [ - { - provider: "google", - model: "gemini-3.1-pro-preview", - capabilities: ["image", "video", "audio"], - }, - ], - }, - audio: { - models: [ - { - provider: "google", - model: "gemini-3.1-pro-preview", - capabilities: ["image", "video", "audio"], - }, - ], - }, - video: { - models: [ - { - provider: "google", - model: "gemini-3.1-pro-preview", - capabilities: ["image", "video", "audio"], - }, - ], - }, + models: [ + { + provider: "google", + model: "gemini-3.1-pro-preview", + capabilities: ["image", "video", "audio"], + }, + ], }, }, } diff --git a/docs/plugins/sdk-agent-harness.md b/docs/plugins/sdk-agent-harness.md index 05b3ce2a833e..dec780b2af0e 100644 --- a/docs/plugins/sdk-agent-harness.md +++ b/docs/plugins/sdk-agent-harness.md @@ -465,7 +465,7 @@ Per-agent overrides use the same model-scoped shape: Legacy whole-agent runtime examples like this are ignored: -```json +```json validate=false { "agents": { "defaults": { diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index 2636ca607483..cc29d26e6a1c 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -79,7 +79,7 @@ OpenClaw release: ```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" } } }, } ``` @@ -469,10 +469,10 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth. }, }, }, - list: [ - { id: "research", default: true }, - { id: "alerts", params: { cacheRetention: "none" } }, - ], + entries: { + research: { default: true }, + alerts: { params: { cacheRetention: "none" } }, + }, }, } ``` diff --git a/docs/providers/baseten.md b/docs/providers/baseten.md index 3a9779039676..dd8a2054818f 100644 --- a/docs/providers/baseten.md +++ b/docs/providers/baseten.md @@ -110,7 +110,7 @@ Most setups only need the API key. To pin the provider explicitly: ```json5 { - env: { BASETEN_API_KEY: "..." }, + env: { vars: { BASETEN_API_KEY: "..." } }, agents: { defaults: { model: { primary: "baseten/thinkingmachines/inkling" }, diff --git a/docs/providers/cerebras.md b/docs/providers/cerebras.md index 195a7fbd9c73..666bf757f0f0 100644 --- a/docs/providers/cerebras.md +++ b/docs/providers/cerebras.md @@ -89,7 +89,7 @@ Most setups only need the API key. Use explicit `models.providers.cerebras` conf ```json5 { - env: { CEREBRAS_API_KEY: "csk-..." }, + env: { vars: { CEREBRAS_API_KEY: "csk-..." } }, agents: { defaults: { model: { primary: "cerebras/gemma-4-31b" }, diff --git a/docs/providers/claude-max-api-proxy.md b/docs/providers/claude-max-api-proxy.md index e18168098cda..f7c0b4d9abb2 100644 --- a/docs/providers/claude-max-api-proxy.md +++ b/docs/providers/claude-max-api-proxy.md @@ -92,8 +92,10 @@ response back in OpenAI format. ```json5 { env: { - OPENAI_API_KEY: "not-needed", - OPENAI_BASE_URL: "http://localhost:3456/v1", + vars: { + OPENAI_API_KEY: "not-needed", + OPENAI_BASE_URL: "http://localhost:3456/v1", + }, }, agents: { defaults: { diff --git a/docs/providers/deepgram.md b/docs/providers/deepgram.md index 3d050e1a5470..99281712f3de 100644 --- a/docs/providers/deepgram.md +++ b/docs/providers/deepgram.md @@ -36,9 +36,9 @@ returns them. { tools: { media: { + models: [{ provider: "deepgram", model: "nova-3", capabilities: ["audio"] }], audio: { enabled: true, - models: [{ provider: "deepgram", model: "nova-3" }], }, }, }, @@ -68,9 +68,11 @@ Deepgram `/listen` request, so any Deepgram-supported param name works { tools: { media: { + models: [ + { provider: "deepgram", model: "nova-3", language: "en", capabilities: ["audio"] }, + ], audio: { 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: { media: { + models: [{ provider: "deepgram", model: "nova-3", capabilities: ["audio"] }], audio: { enabled: true, providerOptions: { @@ -91,7 +94,6 @@ Deepgram `/listen` request, so any Deepgram-supported param name works smart_format: true, }, }, - models: [{ provider: "deepgram", model: "nova-3" }], }, }, }, diff --git a/docs/providers/deepinfra.md b/docs/providers/deepinfra.md index 68b70c5b186e..051dd9bd23d7 100644 --- a/docs/providers/deepinfra.md +++ b/docs/providers/deepinfra.md @@ -38,7 +38,7 @@ export DEEPINFRA_API_KEY="" # pragma: allowlist secret ```json5 { - env: { DEEPINFRA_API_KEY: "" }, // pragma: allowlist secret + env: { vars: { DEEPINFRA_API_KEY: "" } }, // pragma: allowlist secret agents: { defaults: { model: { primary: "deepinfra/deepseek-ai/DeepSeek-V4-Flash" }, diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md index 5d081dd48da7..377bd923532d 100644 --- a/docs/providers/deepseek.md +++ b/docs/providers/deepseek.md @@ -135,7 +135,7 @@ preserve the replay payload DeepSeek requires. ```json5 { - env: { DEEPSEEK_API_KEY: "sk-..." }, + env: { vars: { DEEPSEEK_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "deepseek/deepseek-v4-pro" }, diff --git a/docs/providers/elevenlabs.md b/docs/providers/elevenlabs.md index cc9b31c15164..da48f3ddfec8 100644 --- a/docs/providers/elevenlabs.md +++ b/docs/providers/elevenlabs.md @@ -60,9 +60,9 @@ Use Scribe v2 for inbound audio attachments and short recorded voice segments: { tools: { media: { + models: [{ provider: "elevenlabs", model: "scribe_v2", capabilities: ["audio"] }], audio: { enabled: true, - models: [{ provider: "elevenlabs", model: "scribe_v2" }], }, }, }, diff --git a/docs/providers/groq.md b/docs/providers/groq.md index 8a2151ea05a2..cdf3056e1612 100644 --- a/docs/providers/groq.md +++ b/docs/providers/groq.md @@ -61,7 +61,7 @@ export GROQ_API_KEY=gsk_... ```json5 { - env: { GROQ_API_KEY: "gsk_..." }, + env: { vars: { GROQ_API_KEY: "gsk_..." } }, agents: { defaults: { 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. -| Property | Value | -| ------------------ | ----------------------------------------- | -| Shared config path | `tools.media.audio` | -| Default base URL | `https://api.groq.com/openai/v1` | -| Default model | `whisper-large-v3-turbo` | -| Auto priority | 20 | -| API endpoint | OpenAI-compatible `/audio/transcriptions` | +| Property | Value | +| ----------------- | ----------------------------------------- | +| Shared model path | `tools.media.models` | +| Default base URL | `https://api.groq.com/openai/v1` | +| Default model | `whisper-large-v3-turbo` | +| Auto priority | 20 | +| API endpoint | OpenAI-compatible `/audio/transcriptions` | To make Groq the default audio backend: @@ -113,9 +113,7 @@ To make Groq the default audio backend: { tools: { media: { - audio: { - models: [{ provider: "groq" }], - }, + models: [{ provider: "groq", capabilities: ["audio"] }], }, }, } diff --git a/docs/providers/kilocode.md b/docs/providers/kilocode.md index f4fbb68461aa..3c9c48d52b66 100644 --- a/docs/providers/kilocode.md +++ b/docs/providers/kilocode.md @@ -66,7 +66,7 @@ Any model on the gateway is addressable as `kilocode/` (for example ```json5 { - env: { KILOCODE_API_KEY: "" }, // pragma: allowlist secret + env: { vars: { KILOCODE_API_KEY: "" } }, // pragma: allowlist secret agents: { defaults: { model: { primary: "kilocode/kilo-auto/balanced" }, diff --git a/docs/providers/meta.md b/docs/providers/meta.md index 2667a826ae49..fa032a867644 100644 --- a/docs/providers/meta.md +++ b/docs/providers/meta.md @@ -94,7 +94,7 @@ Capabilities: ```json5 { - env: { MODEL_API_KEY: "" }, + env: { vars: { MODEL_API_KEY: "" } }, agents: { defaults: { model: { primary: "meta/muse-spark-1.1" }, diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 41684fed0589..faa075c99fbf 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -120,7 +120,7 @@ Model refs follow the auth path: `minimax/` for API-key setups, `minimax- ```json5 { - env: { MINIMAX_API_KEY: "sk-..." }, + env: { vars: { MINIMAX_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "minimax/MiniMax-M3" } } }, models: { mode: "merge", @@ -360,7 +360,7 @@ See [MiniMax Search](/tools/minimax-search) for full web search configuration an ```json5 { - env: { MINIMAX_API_KEY: "sk-..." }, + env: { vars: { MINIMAX_API_KEY: "sk-..." } }, agents: { defaults: { models: { diff --git a/docs/providers/mistral.md b/docs/providers/mistral.md index 5a52217ada96..0932ef4630b1 100644 --- a/docs/providers/mistral.md +++ b/docs/providers/mistral.md @@ -52,7 +52,7 @@ media understanding (Voxtral batch transcription), realtime STT for Voice Call ```json5 { - env: { MISTRAL_API_KEY: "sk-..." }, + env: { vars: { MISTRAL_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "mistral/mistral-large-latest" } } }, } ``` @@ -99,9 +99,9 @@ Use Voxtral for batch audio transcription through the media understanding pipeli { tools: { media: { + models: [{ provider: "mistral", model: "voxtral-mini-latest", capabilities: ["audio"] }], audio: { enabled: true, - models: [{ provider: "mistral", model: "voxtral-mini-latest" }], }, }, }, diff --git a/docs/providers/moonshot.md b/docs/providers/moonshot.md index 3e979b5800bf..f57a62b4922d 100644 --- a/docs/providers/moonshot.md +++ b/docs/providers/moonshot.md @@ -112,7 +112,7 @@ onboarding. ```json5 { - env: { MOONSHOT_API_KEY: "sk-..." }, + env: { vars: { MOONSHOT_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "moonshot/kimi-k3" }, @@ -244,7 +244,7 @@ onboarding. ```json5 { - env: { KIMI_API_KEY: "sk-..." }, + env: { vars: { KIMI_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "kimi/kimi-for-coding" }, diff --git a/docs/providers/nvidia.md b/docs/providers/nvidia.md index d40b23e4b8ab..bd8954ee62f0 100644 --- a/docs/providers/nvidia.md +++ b/docs/providers/nvidia.md @@ -47,7 +47,7 @@ openclaw onboard --auth-choice nvidia-api-key --nvidia-api-key "nvapi-..." ```json5 { - env: { NVIDIA_API_KEY: "nvapi-..." }, + env: { vars: { NVIDIA_API_KEY: "nvapi-..." } }, models: { providers: { nvidia: { diff --git a/docs/providers/ollama.md b/docs/providers/ollama.md index c9453df4afff..139fe17ee8cf 100644 --- a/docs/providers/ollama.md +++ b/docs/providers/ollama.md @@ -421,9 +421,16 @@ timeout and cap `num_ctx`: }, tools: { media: { + models: [ + { + provider: "ollama", + model: "qwen2.5vl:7b", + timeoutSeconds: 300, + capabilities: ["image"], + }, + ], image: { 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: { models: { "ollama/gemma4": { - thinking: "low", + params: { thinking: "low" }, }, }, }, diff --git a/docs/providers/openai.md b/docs/providers/openai.md index f90f25c5872b..df83595bf639 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -265,7 +265,7 @@ for the full example. ```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" } } }, } ``` @@ -279,7 +279,7 @@ for the full example. ```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" } } }, } ``` @@ -806,12 +806,9 @@ request reaches the provider, so aspect-ratio requests generally still work. ## GPT-5 prompt contribution -OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family models on -the `openai` provider (including legacy pre-repair Codex refs that normalize -to `openai/*`). Other providers that also serve GPT-5-family model ids, such -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. +OpenClaw adds a shared GPT-5 prompt contribution to matching GPT-5-family +OpenClaw-assembled prompts. The OpenAI plugin setting below controls the +friendly style on OpenAI-family routes. Older GPT-4.x model ids do not match. The native Codex app-server harness does not receive the persona/tool- 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: { media: { + models: [ + { + type: "provider", + provider: "openai", + model: "gpt-4o-transcribe", + capabilities: ["audio"], + }, + ], audio: { - models: [ - { - type: "provider", - provider: "openai", - model: "gpt-4o-transcribe", - }, - ], + enabled: true, }, }, }, diff --git a/docs/providers/opencode-go.md b/docs/providers/opencode-go.md index 534fda33fa6c..33ef1167276b 100644 --- a/docs/providers/opencode-go.md +++ b/docs/providers/opencode-go.md @@ -66,7 +66,7 @@ interactive onboarding or pass the shared OpenCode API key directly. ```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" } } }, } ``` diff --git a/docs/providers/opencode.md b/docs/providers/opencode.md index e63529582f01..2275130aee63 100644 --- a/docs/providers/opencode.md +++ b/docs/providers/opencode.md @@ -90,7 +90,7 @@ provider ids split so upstream per-model routing stays correct. ```json5 { - env: { OPENCODE_API_KEY: "sk-..." }, + env: { vars: { OPENCODE_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "opencode/gpt-5.6-sol" } } }, } ``` diff --git a/docs/providers/qianfan.md b/docs/providers/qianfan.md index c1349782cd6f..e6396310acd1 100644 --- a/docs/providers/qianfan.md +++ b/docs/providers/qianfan.md @@ -74,7 +74,7 @@ This example explicitly selects the current DeepSeek flagship instead of the onb ```json5 { - env: { QIANFAN_API_KEY: "bce-v3/ALTAK-..." }, + env: { vars: { QIANFAN_API_KEY: "bce-v3/ALTAK-..." } }, agents: { defaults: { model: { primary: "qianfan/deepseek-v4-pro" }, diff --git a/docs/providers/runway.md b/docs/providers/runway.md index 2967ce5d737e..a4439910a93e 100644 --- a/docs/providers/runway.md +++ b/docs/providers/runway.md @@ -100,6 +100,6 @@ Local image and video references are supported via data URIs. Shared tool parameters, provider selection, and async behavior. - Agent default settings including video generation model. + Agent default settings including `mediaModels.video`. diff --git a/docs/providers/senseaudio.md b/docs/providers/senseaudio.md index bbc8c7724f50..64ecf05661a6 100644 --- a/docs/providers/senseaudio.md +++ b/docs/providers/senseaudio.md @@ -32,9 +32,15 @@ SenseAudio transcribes inbound audio and voice-note attachments through OpenClaw { tools: { media: { + models: [ + { + provider: "senseaudio", + model: "senseaudio-asr-pro-1.5-260319", + capabilities: ["audio"], + }, + ], audio: { enabled: true, - models: [{ provider: "senseaudio", model: "senseaudio-asr-pro-1.5-260319" }], }, }, }, diff --git a/docs/providers/stepfun.md b/docs/providers/stepfun.md index c878e72db4b1..305b9a99aa17 100644 --- a/docs/providers/stepfun.md +++ b/docs/providers/stepfun.md @@ -138,7 +138,7 @@ A single auth flow writes region-matched profiles for both `stepfun` and `stepfu ```json5 { - env: { STEPFUN_API_KEY: "your-key" }, + env: { vars: { STEPFUN_API_KEY: "your-key" } }, agents: { defaults: { model: { primary: "stepfun/step-3.5-flash" } } }, models: { mode: "merge", @@ -178,7 +178,7 @@ A single auth flow writes region-matched profiles for both `stepfun` and `stepfu ```json5 { - env: { STEPFUN_API_KEY: "your-key" }, + env: { vars: { STEPFUN_API_KEY: "your-key" } }, agents: { defaults: { model: { primary: "stepfun-plan/step-3.5-flash" } } }, models: { mode: "merge", diff --git a/docs/providers/synthetic.md b/docs/providers/synthetic.md index 073cdc93a2f9..06529d0adf29 100644 --- a/docs/providers/synthetic.md +++ b/docs/providers/synthetic.md @@ -53,7 +53,7 @@ changes its base URL, override `models.providers.synthetic.baseUrl`. ```json5 { - env: { SYNTHETIC_API_KEY: "sk-..." }, + env: { vars: { SYNTHETIC_API_KEY: "sk-..." } }, agents: { defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" }, diff --git a/docs/providers/venice.md b/docs/providers/venice.md index e24d459c9215..55b32897b60b 100644 --- a/docs/providers/venice.md +++ b/docs/providers/venice.md @@ -216,7 +216,7 @@ More help: [Troubleshooting](/help/troubleshooting) and [FAQ](/help/faq). ```json5 { - env: { VENICE_API_KEY: "vapi_..." }, + env: { vars: { VENICE_API_KEY: "vapi_..." } }, agents: { defaults: { model: { primary: "venice/zai-org-glm-4.7" } } }, models: { mode: "merge", diff --git a/docs/providers/xai.md b/docs/providers/xai.md index d44db2078acf..0bb4aa3d2edc 100644 --- a/docs/providers/xai.md +++ b/docs/providers/xai.md @@ -393,13 +393,15 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20 { tools: { media: { + models: [ + { + type: "provider", + provider: "xai", + capabilities: ["audio"], + }, + ], audio: { - models: [ - { - type: "provider", - provider: "xai", - }, - ], + enabled: true, }, }, }, @@ -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-..." } }, } ``` diff --git a/docs/providers/xiaomi.md b/docs/providers/xiaomi.md index 76aefc368274..8cbaf53b877e 100644 --- a/docs/providers/xiaomi.md +++ b/docs/providers/xiaomi.md @@ -170,7 +170,7 @@ mono Opus with `ffmpeg` before delivery. ```json5 { - env: { XIAOMI_API_KEY: "your-key" }, + env: { vars: { XIAOMI_API_KEY: "your-key" } }, agents: { defaults: { model: { primary: "xiaomi/mimo-v2.5" } } }, models: { mode: "merge", @@ -210,7 +210,7 @@ Token Plan: ```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" } } }, models: { mode: "merge", diff --git a/docs/providers/zai.md b/docs/providers/zai.md index 4e6f3d346c75..0936d31d5e6c 100644 --- a/docs/providers/zai.md +++ b/docs/providers/zai.md @@ -141,7 +141,7 @@ you want to force a specific Coding Plan or general API surface. ```json5 { - env: { ZAI_API_KEY: "sk-..." }, + env: { vars: { ZAI_API_KEY: "sk-..." } }, models: { providers: { zai: { diff --git a/docs/reference/prompt-caching.md b/docs/reference/prompt-caching.md index 9ffa79f308b9..b655f29d9c1c 100644 --- a/docs/reference/prompt-caching.md +++ b/docs/reference/prompt-caching.md @@ -221,20 +221,9 @@ Why the assertions differ: Anthropic exposes explicit cache breakpoints and movi diagnostics: cacheTrace: enabled: true - filePath: "~/.openclaw/logs/cache-trace.jsonl" # optional - includeMessages: false # default true - includePrompt: false # default true - includeSystem: false # default true ``` -Defaults: - -| Key | Default | -| ----------------- | -------------------------------------------- | -| `filePath` | `$OPENCLAW_STATE_DIR/logs/cache-trace.jsonl` | -| `includeMessages` | `true` | -| `includePrompt` | `true` | -| `includeSystem` | `true` | +`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. ### Env toggles (one-off debugging) diff --git a/docs/start/openclaw.md b/docs/start/openclaw.md index 107bdb7bf77d..b71d6c9ab234 100644 --- a/docs/start/openclaw.md +++ b/docs/start/openclaw.md @@ -127,15 +127,14 @@ Example: // Start with 0; enable later. heartbeat: { every: "0m" }, }, - list: [ - { - id: "main", + entries: { + main: { default: true, groupChat: { mentionPatterns: ["@openclaw", "openclaw"], }, }, - ], + }, }, channels: { whatsapp: { diff --git a/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md b/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md index f8158a7474e0..f6837d060cf7 100644 --- a/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md +++ b/docs/tools/browser-wsl2-windows-remote-cdp-troubleshooting.md @@ -147,7 +147,6 @@ Point OpenClaw at the address reachable from WSL2: remote: { cdpUrl: "http://WINDOWS_HOST_OR_IP:9222", attachOnly: true, - color: "#00AA00", }, }, }, diff --git a/docs/tools/browser.md b/docs/tools/browser.md index ad7948640fbb..af74b2a704a7 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -169,31 +169,27 @@ Browser settings live in `~/.openclaw/openclaw.json`. }, // snapshotDefaults: { mode: "efficient" }, // default snapshot mode when the caller omits one defaultProfile: "openclaw", - color: "#FF4500", headless: false, noSandbox: false, attachOnly: false, executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", profiles: { - openclaw: { cdpPort: 18800, color: "#FF4500" }, + openclaw: { cdpPort: 18800 }, work: { cdpPort: 18801, - color: "#0066CC", headless: true, executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", }, user: { driver: "existing-session", attachOnly: true, - color: "#00AA00", }, brave: { driver: "existing-session", attachOnly: true, 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: { media: { - image: { - models: [ - { provider: "bytedance", model: "doubao-seed-2.0-pro" }, - // Add fallback candidates; first success wins - { provider: "openai", model: "gpt-4o" }, - ], - }, - // Shared media models also work when tagged for image support. - // models: [{ provider: "openai", model: "gpt-4o", capabilities: ["image"] }], + models: [ + { provider: "bytedance", model: "doubao-seed-2.0-pro", capabilities: ["image"] }, + // Add fallback candidates; first success wins + { provider: "openai", model: "gpt-4o", capabilities: ["image"] }, + ], }, }, 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 screenshot, ask the agent to send it explicitly with the message tool. -Use the existing `tools.media.image` / `tools.media.models` fields for model -fallbacks, timeouts, byte limits, profiles, and provider request settings. +Use `tools.media.models` for model fallbacks, timeouts, byte limits, profiles, +and provider request settings. Tag screenshot-capable entries with the `image` +capability. If the active main model already supports vision and no explicit image 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, 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. -- `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. - 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. @@ -494,7 +486,6 @@ Example: profiles: { browserless: { cdpUrl: "wss://production-sfo.browserless.io?token=", - color: "#00AA00", }, }, }, @@ -523,7 +514,6 @@ Browserless as an externally managed CDP service: browserless: { cdpUrl: "ws://127.0.0.1:3000", attachOnly: true, - color: "#00AA00", }, }, }, @@ -586,7 +576,6 @@ proxies. profiles: { browserbase: { cdpUrl: "wss://connect.browserbase.com?apiKey=", - color: "#F97316", }, }, }, @@ -618,7 +607,6 @@ WebSocket gateway. profiles: { notte: { cdpUrl: "wss://us-prod.notte.cc/sessions/connect?token=", - color: "#7C3AED", }, }, }, @@ -695,7 +683,7 @@ Official background and setup references: - [Chrome DevTools MCP README](https://github.com/ChromeDevTools/chrome-devtools-mcp) 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 targets the default local Google Chrome profile. Use `userDataDir` for Brave, @@ -710,7 +698,6 @@ directory: driver: "existing-session", attachOnly: true, userDataDir: "~/Library/Application Support/BraveSoftware/Brave-Browser", - color: "#FB542B", }, }, }, diff --git a/docs/tools/chrome-extension.md b/docs/tools/chrome-extension.md index 69b53f822d48..37074feb3455 100644 --- a/docs/tools/chrome-extension.md +++ b/docs/tools/chrome-extension.md @@ -91,7 +91,7 @@ openclaw config set browser.defaultProfile chrome { browser: { profiles: { - chrome: { driver: "extension", color: "#FF4500" }, + chrome: { driver: "extension" }, }, }, } diff --git a/docs/tools/tts.md b/docs/tools/tts.md index 424a431e317c..f3cb96787b6b 100644 --- a/docs/tools/tts.md +++ b/docs/tools/tts.md @@ -744,13 +744,13 @@ directive warnings. **Disable model overrides entirely:** ```json5 -{ messages: { tts: { modelOverrides: { enabled: false } } } } +{ tts: { modelOverrides: { enabled: false } } } ``` **Allow provider switching while keeping other knobs configurable:** ```json5 -{ messages: { tts: { modelOverrides: { enabled: true, allowProvider: true, allowSeed: false } } } } +{ tts: { modelOverrides: { enabled: true, allowProvider: true, allowSeed: false } } } ``` ## Slash commands diff --git a/extensions/ollama/openclaw.plugin.json b/extensions/ollama/openclaw.plugin.json index ee74ea9f2d20..98f9090dfe6d 100644 --- a/extensions/ollama/openclaw.plugin.json +++ b/extensions/ollama/openclaw.plugin.json @@ -219,6 +219,15 @@ "type": "boolean" } } + }, + "webSearch": { + "type": "object", + "additionalProperties": false, + "properties": { + "baseUrl": { + "type": "string" + } + } } } }, @@ -238,6 +247,16 @@ "nodeInference.enabled": { "label": "Enable Node Inference", "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 } } } diff --git a/extensions/openai/openclaw.plugin.json b/extensions/openai/openclaw.plugin.json index e5892085a71f..b3a3feedfb3d 100644 --- a/extensions/openai/openclaw.plugin.json +++ b/extensions/openai/openclaw.plugin.json @@ -306,7 +306,7 @@ "type": "string", "enum": ["friendly", "on", "off"], "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." } } } diff --git a/extensions/voice-call/openclaw.plugin.json b/extensions/voice-call/openclaw.plugin.json index ea63e2ef8ff4..a660657ef7b9 100644 --- a/extensions/voice-call/openclaw.plugin.json +++ b/extensions/voice-call/openclaw.plugin.json @@ -712,9 +712,17 @@ "model": { "type": "string" }, - "voice": { + "speakerVoice": { "type": "string" }, + "voice": { + "type": "string", + "description": "Legacy alias for speakerVoice." + }, + "voiceId": { + "type": "string", + "description": "Legacy alias for speakerVoice." + }, "speed": { "type": "number", "minimum": 0.25, @@ -735,9 +743,17 @@ "baseUrl": { "type": "string" }, - "voiceId": { + "speakerVoiceId": { "type": "string" }, + "voice": { + "type": "string", + "description": "Legacy alias for speakerVoiceId." + }, + "voiceId": { + "type": "string", + "description": "Legacy alias for speakerVoiceId." + }, "modelId": { "type": "string" }, @@ -791,9 +807,17 @@ "enabled": { "type": "boolean" }, - "voice": { + "speakerVoice": { "type": "string" }, + "voice": { + "type": "string", + "description": "Legacy alias for speakerVoice." + }, + "voiceId": { + "type": "string", + "description": "Legacy alias for speakerVoice." + }, "lang": { "type": "string" }, @@ -829,9 +853,17 @@ "enabled": { "type": "boolean" }, - "voice": { + "speakerVoice": { "type": "string" }, + "voice": { + "type": "string", + "description": "Legacy alias for speakerVoice." + }, + "voiceId": { + "type": "string", + "description": "Legacy alias for speakerVoice." + }, "lang": { "type": "string" }, diff --git a/package.json b/package.json index 730777bb47d2..eecfcbe64571 100644 --- a/package.json +++ b/package.json @@ -1535,7 +1535,7 @@ "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: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: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", @@ -1595,6 +1595,7 @@ "db:kysely:gen": "node --import tsx scripts/generate-kysely-types.mts", "dev": "node scripts/run-node.mjs", "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-links": "node scripts/docs-link-audit.mjs", "docs:check-links:anchors": "node scripts/docs-link-audit.mjs --anchors", diff --git a/scripts/check-docs-config-examples.mjs b/scripts/check-docs-config-examples.mjs new file mode 100644 index 000000000000..1deec333f25c --- /dev/null +++ b/scripts/check-docs-config-examples.mjs @@ -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", +}); diff --git a/scripts/check-docs-config-examples.mts b/scripts/check-docs-config-examples.mts new file mode 100644 index 000000000000..247e8e0f641b --- /dev/null +++ b/scripts/check-docs-config-examples.mts @@ -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); +} diff --git a/scripts/test-projects.test-support.mts b/scripts/test-projects.test-support.mts index 38fb5cc1da06..bce8fc2fc632 100644 --- a/scripts/test-projects.test-support.mts +++ b/scripts/test-projects.test-support.mts @@ -538,6 +538,7 @@ const PRECISE_SOURCE_TEST_TARGETS = new Map([ ], ], ]); +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_PATH_CONSUMER_TEST_TARGETS = [ ...RUNTIME_SIDECAR_BASELINE_OWNER_TEST_TARGETS, @@ -2949,6 +2950,9 @@ function resolvePreciseChangedTestTargets( const cwd = options.cwd ?? process.cwd(); const mappedTargets = SOURCE_TEST_TARGETS.get(changedPath) ?? + (/^extensions\/[^/]+\/openclaw\.plugin\.json$/u.test(changedPath) + ? [changedPath, DOCS_CONFIG_EXAMPLES_TEST_TARGET] + : null) ?? resolveToolingTestTargets(changedPath, cwd) ?? resolveAppcastTargets(changedPath) ?? resolvePromptSnapshotFixtureTargets(changedPath); diff --git a/src/config/config.plugin-validation.test.ts b/src/config/config.plugin-validation.test.ts index 8772720ce1e9..be77553f214d 100644 --- a/src/config/config.plugin-validation.test.ts +++ b/src/config/config.plugin-validation.test.ts @@ -2133,7 +2133,7 @@ describe("config plugin validation", () => { 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({ agents: { list: [{ id: "openclaw" }] }, plugins: { @@ -2146,7 +2146,7 @@ describe("config plugin validation", () => { providers: { openai: { baseUrl: "http://localhost:8880/v1", - voice: "alloy", + speakerVoice: "alloy", speed: 1.5, instructions: "Speak in a cheerful tone", }, diff --git a/src/config/docs-config-examples.test.ts b/src/config/docs-config-examples.test.ts new file mode 100644 index 000000000000..2381506d5558 --- /dev/null +++ b/src/config/docs-config-examples.test.ts @@ -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 { + 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: [ + '', + " ```json5", + ' { agents: { list: [{ id: "main" }] } }', + " ````", + "", + ].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([]); + }); +}); diff --git a/src/config/docs-config-examples.ts b/src/config/docs-config-examples.ts new file mode 100644 index 000000000000..cb0a185d106f --- /dev/null +++ b/src/config/docs-config-examples.ts @@ -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; +}; + +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): 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(); + 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 }; +} diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index 2ce87b2dae45..39501edc6593 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -701,6 +701,16 @@ describe("test-projects args", () => { ).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", () => { const changedPaths = ["scripts/setup-auth-system.sh"];