mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
4849106b3b
commit
a0ad38e71a
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"core": 2292,
|
||||
"channel": 3716,
|
||||
"plugin": 4030
|
||||
"plugin": 4040
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`).
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -848,7 +848,7 @@ Socket Mode configuration:
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
identity: "user",
|
||||
postAs: "user",
|
||||
userToken: "<xoxp>",
|
||||
appToken: "<xapp>",
|
||||
},
|
||||
@@ -862,7 +862,7 @@ HTTP Request URL configuration:
|
||||
{
|
||||
channels: {
|
||||
slack: {
|
||||
identity: "user",
|
||||
postAs: "user",
|
||||
mode: "http",
|
||||
userToken: "<xoxp>",
|
||||
signingSecret: "<signing-secret>",
|
||||
@@ -1242,7 +1242,7 @@ Status snapshot behavior:
|
||||
`userTokenStatus` + `appTokenStatus` for user identity.
|
||||
|
||||
<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>
|
||||
|
||||
## Actions and gates
|
||||
|
||||
+19
-3
@@ -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.<accountName>` - 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.<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.markdown.tables` - Markdown table rendering mode (`off` | `bullets` | `code` | `block`)
|
||||
|
||||
@@ -312,6 +325,9 @@ Full example:
|
||||
channels: {
|
||||
twitch: {
|
||||
enabled: true,
|
||||
defaultAccount: "default",
|
||||
accounts: {
|
||||
default: {
|
||||
username: "openclaw",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
@@ -319,7 +335,7 @@ Full example:
|
||||
clientSecret: "secret123...",
|
||||
refreshToken: "refresh456...",
|
||||
allowFrom: ["123456789"],
|
||||
accounts: {
|
||||
},
|
||||
second: {
|
||||
username: "mybot",
|
||||
accessToken: "oauth:def456...",
|
||||
|
||||
@@ -459,23 +459,18 @@ Per-account override: `channels.whatsapp.accounts.<id>.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.<id>.enabled`, `accounts.<id>.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.<id>.historyLimit` |
|
||||
| Prompts | `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt` |
|
||||
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
+1
-1
@@ -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": [
|
||||
|
||||
@@ -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" } },
|
||||
},
|
||||
|
||||
+1
-24
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
<Accordion title="Kimi Coding">
|
||||
```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
|
||||
<Accordion title="Moonshot AI (Kimi)">
|
||||
```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
|
||||
<Accordion title="Synthetic (Anthropic-compatible)">
|
||||
```json5
|
||||
{
|
||||
env: { SYNTHETIC_API_KEY: "sk-..." },
|
||||
env: { vars: { SYNTHETIC_API_KEY: "sk-..." } },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M3" },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -643,8 +643,10 @@ Neither file overrides existing env vars. You can also set inline env vars in co
|
||||
```json5
|
||||
{
|
||||
env: {
|
||||
vars: {
|
||||
OPENROUTER_API_KEY: "sk-or-...",
|
||||
vars: { GROQ_API_KEY: "gsk-..." },
|
||||
GROQ_API_KEY: "gsk-...",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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": "…",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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: "..." } },
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+4
-2
@@ -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: {
|
||||
vars: {
|
||||
OPENROUTER_API_KEY: "sk-or-...",
|
||||
vars: { GROQ_API_KEY: "gsk-..." },
|
||||
GROQ_API_KEY: "gsk-...",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -306,22 +306,23 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
{
|
||||
tools: {
|
||||
media: {
|
||||
audio: {
|
||||
enabled: true,
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
|
||||
{
|
||||
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"],
|
||||
},
|
||||
video: {
|
||||
enabled: true,
|
||||
maxChars: 500,
|
||||
models: [
|
||||
{ provider: "google", model: "gemini-3-flash-preview" },
|
||||
{
|
||||
type: "cli",
|
||||
command: "gemini",
|
||||
@@ -332,8 +333,15 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
"read_file",
|
||||
"Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
|
||||
],
|
||||
capabilities: ["video"],
|
||||
},
|
||||
],
|
||||
audio: {
|
||||
enabled: true,
|
||||
},
|
||||
video: {
|
||||
enabled: true,
|
||||
maxChars: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -345,13 +353,9 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
{
|
||||
tools: {
|
||||
media: {
|
||||
image: {
|
||||
enabled: true,
|
||||
maxBytes: 10485760,
|
||||
maxChars: 500,
|
||||
models: [
|
||||
{ provider: "openai", model: "gpt-5.6-sol" },
|
||||
{ provider: "anthropic", model: "claude-opus-5" },
|
||||
{ provider: "openai", model: "gpt-5.6-sol", capabilities: ["image"] },
|
||||
{ provider: "anthropic", model: "claude-opus-5", capabilities: ["image"] },
|
||||
{
|
||||
type: "cli",
|
||||
command: "gemini",
|
||||
@@ -362,8 +366,13 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
"read_file",
|
||||
"Read the media at {{AttachmentPath}} and describe it in <= {{MaxChars}} characters.",
|
||||
],
|
||||
capabilities: ["image"],
|
||||
},
|
||||
],
|
||||
image: {
|
||||
enabled: true,
|
||||
maxBytes: 10485760,
|
||||
maxChars: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -375,7 +384,6 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
{
|
||||
tools: {
|
||||
media: {
|
||||
image: {
|
||||
models: [
|
||||
{
|
||||
provider: "google",
|
||||
@@ -384,25 +392,6 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
|
||||
},
|
||||
],
|
||||
},
|
||||
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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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" } },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -92,9 +92,11 @@ response back in OpenAI format.
|
||||
```json5
|
||||
{
|
||||
env: {
|
||||
vars: {
|
||||
OPENAI_API_KEY: "not-needed",
|
||||
OPENAI_BASE_URL: "http://localhost:3456/v1",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/claude-opus-4" },
|
||||
|
||||
@@ -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" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ export DEEPINFRA_API_KEY="<your-deepinfra-api-key>" # pragma: allowlist secret
|
||||
|
||||
```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: {
|
||||
defaults: {
|
||||
model: { primary: "deepinfra/deepseek-ai/DeepSeek-V4-Flash" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
@@ -100,8 +100,8 @@ 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` |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| Shared model path | `tools.media.models` |
|
||||
| Default base URL | `https://api.groq.com/openai/v1` |
|
||||
| Default model | `whisper-large-v3-turbo` |
|
||||
| Auto priority | 20 |
|
||||
@@ -113,9 +113,7 @@ To make Groq the default audio backend:
|
||||
{
|
||||
tools: {
|
||||
media: {
|
||||
audio: {
|
||||
models: [{ provider: "groq" }],
|
||||
},
|
||||
models: [{ provider: "groq", capabilities: ["audio"] }],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ Any model on the gateway is addressable as `kilocode/<upstream-id>` (for example
|
||||
|
||||
```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: {
|
||||
defaults: {
|
||||
model: { primary: "kilocode/kilo-auto/balanced" },
|
||||
|
||||
@@ -94,7 +94,7 @@ Capabilities:
|
||||
|
||||
```json5
|
||||
{
|
||||
env: { MODEL_API_KEY: "<key>" },
|
||||
env: { vars: { MODEL_API_KEY: "<key>" } },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "meta/muse-spark-1.1" },
|
||||
|
||||
@@ -120,7 +120,7 @@ Model refs follow the auth path: `minimax/<model>` 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: {
|
||||
|
||||
@@ -52,7 +52,7 @@ media understanding (Voxtral batch transcription), realtime STT for Voice Call
|
||||
<Step title="Set a default model">
|
||||
```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" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
audio: {
|
||||
models: [
|
||||
{
|
||||
type: "provider",
|
||||
provider: "openai",
|
||||
model: "gpt-4o-transcribe",
|
||||
capabilities: ["audio"],
|
||||
},
|
||||
],
|
||||
audio: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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" } } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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" } } },
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -100,6 +100,6 @@ Local image and video references are supported via data URIs.
|
||||
Shared tool parameters, provider selection, and async behavior.
|
||||
</Card>
|
||||
<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>
|
||||
</CardGroup>
|
||||
|
||||
@@ -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" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -138,7 +138,7 @@ A single auth flow writes region-matched profiles for both `stepfun` and `stepfu
|
||||
<Accordion title="Full config: Standard provider">
|
||||
```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
|
||||
<Accordion title="Full config: Step Plan provider">
|
||||
```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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -216,7 +216,7 @@ More help: [Troubleshooting](/help/troubleshooting) and [FAQ](/help/faq).
|
||||
<Accordion title="Config file example">
|
||||
```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",
|
||||
|
||||
@@ -393,13 +393,15 @@ stale context metadata on active 4.20 rows. It does not pin active 4.20
|
||||
{
|
||||
tools: {
|
||||
media: {
|
||||
audio: {
|
||||
models: [
|
||||
{
|
||||
type: "provider",
|
||||
provider: "xai",
|
||||
capabilities: ["audio"],
|
||||
},
|
||||
],
|
||||
audio: {
|
||||
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-..." } },
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -147,7 +147,6 @@ Point OpenClaw at the address reachable from WSL2:
|
||||
remote: {
|
||||
cdpUrl: "http://WINDOWS_HOST_OR_IP:9222",
|
||||
attachOnly: true,
|
||||
color: "#00AA00",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+8
-21
@@ -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,16 +253,12 @@ browser-specific model settings.
|
||||
{
|
||||
tools: {
|
||||
media: {
|
||||
image: {
|
||||
models: [
|
||||
{ provider: "bytedance", model: "doubao-seed-2.0-pro" },
|
||||
{ provider: "bytedance", model: "doubao-seed-2.0-pro", capabilities: ["image"] },
|
||||
// Add fallback candidates; first success wins
|
||||
{ provider: "openai", model: "gpt-4o" },
|
||||
{ provider: "openai", model: "gpt-4o", capabilities: ["image"] },
|
||||
],
|
||||
},
|
||||
// Shared media models also work when tagged for image support.
|
||||
// models: [{ provider: "openai", model: "gpt-4o", capabilities: ["image"] }],
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -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=<BROWSERLESS_API_KEY>",
|
||||
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=<BROWSERBASE_API_KEY>",
|
||||
color: "#F97316",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -618,7 +607,6 @@ WebSocket gateway.
|
||||
profiles: {
|
||||
notte: {
|
||||
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)
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@ openclaw config set browser.defaultProfile chrome
|
||||
{
|
||||
browser: {
|
||||
profiles: {
|
||||
chrome: { driver: "extension", color: "#FF4500" },
|
||||
chrome: { driver: "extension" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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_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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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"];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user