refactor(ui): graduate Labs features into settings (#145460)

* refactor(ui): graduate Labs features into settings

Move Swarm, CLI agents, loop detection, and message auditing to their existing settings homes. Keep local-model lean mode in advanced troubleshooting and preserve runtime defaults, opt-outs, Swarm limits, audit scope, and translated chat summaries.

* test: cover graduated settings and scoped palette search

Record only the ten approved common settings. Keep the Custodian search fixture scoped to matching sessions while retaining its original work session and all open, history, and reload assertions.

* test(ui): repair retained machine screenshot locator

Reuse the visible machine:fast locator in the optional capture path. The E2E typecheck and both cloud-dispatch cases pass with screenshot capture enabled.
This commit is contained in:
Peter Steinberger 2026-09-11 20:17:11 -07:00 • committed by GitHub
parent 6f1185305d
commit 1e802bc698
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 172 additions and 433 deletions

View file

@ -1,4 +1,4 @@
789c416fb4a192afc0c878e035e7489c37c9e904476a75740f8123d94f167bbb config-baseline.json
94ffedec888ac382e973df3b48c9af6f7c07c0b21b722b186f0bba1af5e241d2 config-baseline.core.json
64dc9f7997a26fff90d651718e603bf19d0482c5ddfa34a30e56d18fd368b7b4 config-baseline.json
033ecad7ffc9ffe68672808870f6c30d2fd817575b6562e5f8834affcda8603c config-baseline.core.json
a1ef11d09477575b264d3e3b6f59172f5040b2963cd91b4a9601a3c10b87be9c config-baseline.channel.json
c806615766d365633bc52a039110e04b7286c9c7a7e34cf90bc6bc33e299a79c config-baseline.plugin.json

View file

@ -9,7 +9,7 @@ read_when:
Experimental features are preview surfaces controlled by config flags. They need more real-world mileage before their shape and behavior become long-lived contracts.
- Off by default unless the feature docs state otherwise. Swarm is enabled by default with an explicit opt-out.
- Off by default unless the feature docs state otherwise.
- Shape and behavior can change faster than stable config.
- Prefer a stable path when one already exists.
- Roll out broadly only after testing in a smaller environment first.
@ -20,14 +20,14 @@ Custom plugin UI flag below controls user-installed native browser code only.
## Currently documented flags
| Surface | Key | Use it when | More |
| ------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Local model runtime | `agents.defaults.experimental.localModelLean`, `agents.entries.*.experimental.localModelLean` | A smaller or stricter local backend chokes on OpenClaw's full default tool surface | [Local Models](/gateway/local-models) |
| Codex harness | `plugins.entries.codex.config.appServer.experimental.sandboxExecServer` | You want native Codex app-server 0.143.0 or newer to target an OpenClaw sandbox-backed exec-server instead of disabling Code Mode | [Codex harness reference](/plugins/codex-harness-reference#sandboxed-native-execution) |
| Code Mode | `tools.codeMode.enabled` | You want compact code-orchestrated access to a hidden OpenClaw tool catalog | [Code Mode](/tools/code-mode) |
| Cloud workers | `cloudWorkers.desktop` | You want to watch or control desktop-capable cloud worker environments from the Control UI | [Cloud Worker Desktop](/gateway/cloud-workers#desktop-interactive) |
| Custom plugin UI | `gateway.controlUi.experimental.customPlugins` | You want trusted user-installed plugins to add native Control UI views or replace built-in views | [Feature plugins](/plugins/feature-plugins#enable-custom-plugin-ui) |
| Swarm | `tools.swarm.enabled` | You want Code Mode scripts to orchestrate bounded groups of sub-agents in parallel | [Swarm](/tools/swarm) |
| Surface | Key | Use it when | More |
| ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Codex harness | `plugins.entries.codex.config.appServer.experimental.sandboxExecServer` | You want native Codex app-server 0.143.0 or newer to target an OpenClaw sandbox-backed exec-server instead of disabling Code Mode | [Codex harness reference](/plugins/codex-harness-reference#sandboxed-native-execution) |
| Code Mode | `tools.codeMode.enabled` | You want compact code-orchestrated access to a hidden OpenClaw tool catalog | [Code Mode](/tools/code-mode) |
| Cloud workers | `cloudWorkers.desktop` | You want to watch or control desktop-capable cloud worker environments from the Control UI | [Cloud Worker Desktop](/gateway/cloud-workers#desktop-interactive) |
| Custom plugin UI | `gateway.controlUi.experimental.customPlugins` | You want trusted user-installed plugins to add native Control UI views or replace built-in views | [Feature plugins](/plugins/feature-plugins#enable-custom-plugin-ui) |
| Host Desktop | `desktop.host.enabled` | You want to watch or control the Gateway host through its VNC or Screen Sharing server | [Desktop](/gateway/configuration-reference#desktop) |
| Tool Search | `tools.toolSearch.enabled` | You want to defer tool schemas for all models instead of using each model's default | [Tool Search](/tools/tool-search) |
## Control UI Labs
@ -36,12 +36,11 @@ Control UI switch. Enabling or disabling a lab patches the canonical Gateway
config immediately; the page shows a restart hint only when a feature requires
one.
Labs includes Code Mode, Swarm, Tool Search, Custom plugin UI,
Tool-loop detection, Lean tools for local models, Message audit metadata, and
Cloud Worker Desktop. Message audit metadata, Cloud Worker Desktop, and Custom
plugin UI require a Gateway restart. Custom plugin UI also requires reloading
connected browser tabs; the other listed switches normally take effect for
future agent runs without restarting.
Labs includes Code Mode, Tool Search for all models, Custom plugin UI,
Host Desktop, and Cloud Worker Desktop. Custom plugin UI and both desktop
features require a Gateway restart. Custom plugin UI also requires reloading
connected browser tabs. Code Mode and Tool Search changes normally take effect
for future agent runs without restarting.
Custom plugin UI is off by default. Enabled bundled plugins, including
Workboard, retain their native UI with the setting off. Backend APIs and
@ -53,83 +52,20 @@ Code Mode remains disabled until you turn on its Labs switch or explicitly set
engages only for models marked as preferred Code Mode performers; it does not
force Code Mode on for every model.
Swarm is enabled by default, including when `tools.swarm` is omitted or sets
only limits. Turn off its Labs switch, set `tools.swarm: false`, or set
`tools.swarm.enabled: false` to opt out. Per-agent overrides remain available;
an agent that sets only limits inherits global enablement. Swarm does not
enable Code Mode or grant tools: Code Mode's Swarm API requires an executable
native `sessions_spawn` tool, while the low-level flow also requires
`agents_wait`. See [Swarm requirements](/tools/swarm#requirements).
Local models use Tool Search automatically when `tools.toolSearch` is unset.
The Labs switch enables an override for all models.
## Local model lean mode
Lean mode is an explicit capability restriction. Local inference normally uses [Tool Search](/tools/tool-search) to defer schemas while preserving capabilities, so leave lean mode off unless you deliberately want a smaller tool set.
Lean mode is an advanced troubleshooting override, configured outside Labs.
Its existing `experimental.localModelLean` keys remain supported. See
[Local model lean mode](/gateway/local-models#local-model-lean-mode) for capability
restrictions, config examples, and recovery guidance.
`agents.defaults.experimental.localModelLean: true` removes optional tools before catalog construction: `browser`, `automations`, `message`, `image_generate`, `music_generate`, `video_generate`, `tts`, and `pdf`. These removed tools cannot be found through Tool Search. Explicitly allowed or delivery-required tools remain available, though Tool Search may catalog them instead of exposing them directly. Lean mode also defaults catalogs to structured Tool Search (`tool_search`, `tool_describe`, `tool_call`) when `tools.toolSearch` is not already set. Use `agents.entries.*.experimental.localModelLean` to scope this to one agent.
Setup no longer writes this flag. For older installations, `openclaw doctor --fix` removes an onboarding-owned `true` when its ownership marker still matches the default model. Explicit settings and settings with stale ownership markers are preserved. Set a retained flag to `false` to restore optional capabilities; automatic Tool Search still applies to local routes.
If you already tune Tool Search globally, OpenClaw leaves that config alone. Set `tools.toolSearch: false` to opt out of the lean-mode Tool Search default.
In structured `tools` mode, lean runs keep `exec` directly visible beside the Tool Search controls so coding-tuned local models can still choose their familiar shell path. This changes schema visibility only: normal tool policy, sandboxing, and exec approvals still apply. Explicit `code` and `directory` modes keep their normal compaction behavior.
### Why these tools
These tools have the largest descriptions, broadest parameter shapes, or highest chance of distracting a small model from the normal coding and conversation path. On a small-context or stricter OpenAI-compatible backend that is the difference between:
- Tool schemas fitting the prompt vs. crowding out conversation history.
- The model picking the right tool vs. emitting malformed tool calls from too many similar schemas.
- The Chat Completions adapter staying inside structured-output limits vs. a 400 on tool-call payload size.
The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, image understanding, web search/fetch (when configured), memory, and session/agent tools. Remaining catalog tools stay reachable through Tool Search unless you set `tools.toolSearch: false`; explicit tool allows can restore a capability removed by lean mode.
### When to turn it on
Enable lean mode once you have proved the model can talk to the Gateway but full agent turns misbehave:
1. `openclaw infer model run --gateway --model <ref> --prompt "Reply with exactly: pong"` succeeds.
2. A normal agent turn fails with malformed tool calls, oversized prompts, or the model ignoring its tools.
3. Toggling `localModelLean: true` clears the failure.
### When to leave it off
Leave lean mode unset or set `agents.defaults.experimental.localModelLean: false` to retain the full policy-approved tool set. Setup preserves explicit choices and never enables lean mode automatically.
Lean mode does not replace `tools.profile`, `tools.allow`/`tools.deny`, or the model `compat.supportsTools: false` escape hatch. For a permanent narrower tool surface on a specific agent, prefer those stable knobs.
### Enable
```json5
{
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
}
```
For one agent only:
```json5
{
agents: {
entries: {
local: {
default: true,
model: "lmstudio/gemma-4-e4b-it",
experimental: {
localModelLean: true,
},
},
},
},
}
```
Restart the Gateway after changing the flag in the config file. Toggling **Lean tools for local models** from Control UI Labs instead takes effect for future agent runs without a restart. Lean filtering removes `browser`, `automations`, `message`, `image_generate`, `music_generate`, `video_generate`, `tts`, and `pdf` unless you explicitly preserve them with `tools.allow` or `tools.alsoAllow`; Tool Search may still catalog preserved tools instead of exposing them directly.
- <a id="why-these-tools" />[Why these tools](/gateway/local-models#why-these-tools)
- <a id="when-to-turn-it-on" />[When to turn it on](/gateway/local-models#when-to-turn-it-on)
- <a id="when-to-leave-it-off" />[When to leave it off](/gateway/local-models#when-to-leave-it-off)
- <a id="enable" />[Enable](/gateway/local-models#enable)
## Experimental does not mean hidden

View file

@ -352,8 +352,9 @@ See [Audit records](/cli/audit) for the full field reference and query filters.
## Message lifecycle events
Set [`logging.audit.messages`](/gateway/config-observability#audit) to choose what
is recorded, then restart the Gateway:
Choose message audit metadata in **Settings → Advanced → Logging**, or set
[`logging.audit.messages`](/gateway/config-observability#audit), then restart
the Gateway:
- `off` (default): no message records.
- `direct`: only messages in direct conversations.

View file

@ -179,9 +179,11 @@ For the full key index and the other top-level config domains, see [Configuratio
- Tool activity descriptions appear automatically when supplied by the acting agent; viewing tool calls does not request utility-model completions. The former `controlUi.toolTitles` setting is retired. Run `openclaw doctor --fix` to remove it from existing configs.
- `controlUi.automaticallyFetchFavicons`: link favicons in Control UI chat. Default: `true`. The authenticated browser asks its same-origin Gateway for each hostname. The Gateway requests only `https://<hostname>/favicon.ico`, rejects IP literals and private/internal destinations, pins public DNS results, revalidates every redirect under the same strict SSRF policy, limits redirects/time/bytes/concurrency, validates the image, and returns a private-cacheable image blob. OpenClaw does not use Google or another favicon service for this flow. This discloses linked hostnames and the Gateway's network address to those destination sites. Set `false` to prevent the browser from requesting favicon routes and the Gateway from contacting link destinations.
- `controlUi.dangerouslyAllowHostHeaderOriginFallback`: dangerous mode that enables Host-header origin fallback for deployments that intentionally rely on Host-header origin policy.
- `cliAgents.enabled`: show the experimental **CLI agents** group in the Control UI new-session model picker. Default: `true`; set `false` to disable CLI agents and native CLI session creation. The group appears only when the Gateway advertises `sessions.catalog.list`, and it includes only catalog providers that support creating sessions. Selecting one opens the same catalog-target new-session flow used by the sidebar catalog action.
- `cliAgents.enabled`: show the **CLI agents** group in the Control UI new-session model picker. Default: `true`; set `false` to disable CLI agents and native CLI session creation. The group appears only when the Gateway advertises `sessions.catalog.list`, and it includes only catalog providers that support creating sessions. Selecting one opens the same catalog-target new-session flow used by the sidebar catalog action.
Catalog providers can also advertise terminal-based session creation. The method is available only when Labs `cliAgents.enabled` is on, the Gateway terminal is available, and the selected provider exposes the capability. Callers supply `cwd`; create a fresh worktree first with `worktrees.create` when needed, because terminal start does not provision one.
Configure CLI agents in **Settings → Infrastructure → Gateway**.
Catalog providers can also advertise terminal-based session creation. The method is available only when `cliAgents.enabled` is on, the Gateway terminal is available, and the selected provider exposes the capability. Callers supply `cwd`; create a fresh worktree first with `worktrees.create` when needed, because terminal start does not provision one.
- `terminal.enabled`: the admin-scoped operator terminal. Default: `true`; set `false` to opt out. The terminal starts a host PTY in the selected agent workspace, inherits the Gateway process environment, and is refused for agents with `sandbox.mode: "all"`. Changes hot-apply: disabling closes attached, detached, and conversation-owned sessions and cancels pending opens; re-enabling allows fresh sessions. Reload open Control UI pages to pick up the updated content security policy.
- `terminal.shell`: optional shell executable. When unset, OpenClaw uses `$SHELL` on Unix and `%ComSpec%` on Windows. Changes hot-apply to newly opened terminals; existing terminals keep running their original shell.

View file

@ -289,6 +289,78 @@ If the model loads cleanly but full agent turns misbehave, check transport first
- **Tool calls show up as raw JSON/XML/ReAct text, or the provider returns an empty `tool_calls` array?** Do not add a proxy that blindly converts assistant text into tool execution. Fix the server's chat template and parser first. If the model only works when tool use is forced, add the `params.extra_body.tool_choice: "required"` override above. Use that model entry only for sessions where a tool call is expected every turn.
- **Safety**: local models skip provider-side filters. Keep agents narrow and compaction on to limit prompt-injection blast radius.
### Local model lean mode
Configure lean mode in **Settings → Agent Defaults → Agents** with advanced settings shown, or use the config examples below. The retained `experimental.localModelLean` key remains supported.
Lean mode is an advanced troubleshooting override that explicitly restricts capabilities. Local inference normally uses [Tool Search](/tools/tool-search) to defer schemas while preserving capabilities, so leave lean mode off unless you deliberately want a smaller tool set.
`agents.defaults.experimental.localModelLean: true` removes optional tools before catalog construction: `browser`, `automations`, `message`, `image_generate`, `music_generate`, `video_generate`, `tts`, and `pdf`. These removed tools cannot be found through Tool Search. Explicitly allowed or delivery-required tools remain available, though Tool Search may catalog them instead of exposing them directly. Lean mode also defaults catalogs to structured Tool Search (`tool_search`, `tool_describe`, `tool_call`) when `tools.toolSearch` is not already set. Use `agents.entries.*.experimental.localModelLean` to scope this to one agent.
Setup no longer writes this flag. For older installations, `openclaw doctor --fix` removes an onboarding-owned `true` when its ownership marker still matches the default model. Explicit settings and settings with stale ownership markers are preserved. Set a retained flag to `false` to restore optional capabilities; automatic Tool Search still applies to local routes.
If you already tune Tool Search globally, OpenClaw leaves that config alone. Set `tools.toolSearch: false` to opt out of the lean-mode Tool Search default.
In structured `tools` mode, lean runs keep `exec` directly visible beside the Tool Search controls so coding-tuned local models can still choose their familiar shell path. This changes schema visibility only: normal tool policy, sandboxing, and exec approvals still apply. Explicit `code` and `directory` modes keep their normal compaction behavior.
#### Why these tools
These tools have the largest descriptions, broadest parameter shapes, or highest chance of distracting a small model from the normal coding and conversation path. On a small-context or stricter OpenAI-compatible backend that is the difference between:
- Tool schemas fitting the prompt vs. crowding out conversation history.
- The model picking the right tool vs. emitting malformed tool calls from too many similar schemas.
- The Chat Completions adapter staying inside structured-output limits vs. a 400 on tool-call payload size.
The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, image understanding, web search/fetch (when configured), memory, and session/agent tools. Remaining catalog tools stay reachable through Tool Search unless you set `tools.toolSearch: false`; explicit tool allows can restore a capability removed by lean mode.
#### When to turn it on
Enable lean mode once you have proved the model can talk to the Gateway but full agent turns misbehave:
1. `openclaw infer model run --gateway --model <ref> --prompt "Reply with exactly: pong"` succeeds.
2. A normal agent turn fails with malformed tool calls, oversized prompts, or the model ignoring its tools.
3. Toggling `localModelLean: true` clears the failure.
#### When to leave it off
Leave lean mode unset or set `agents.defaults.experimental.localModelLean: false` to retain the full policy-approved tool set. Setup preserves explicit choices and never enables lean mode automatically.
Lean mode does not replace `tools.profile`, `tools.allow`/`tools.deny`, or the model `compat.supportsTools: false` escape hatch. For a permanent narrower tool surface on a specific agent, prefer those stable knobs.
#### Enable
```json5
{
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
}
```
For one agent only:
```json5
{
agents: {
entries: {
local: {
default: true,
model: "lmstudio/gemma-4-e4b-it",
experimental: {
localModelLean: true,
},
},
},
},
}
```
Restart the Gateway after changing the flag in the config file. Lean filtering removes `browser`, `automations`, `message`, `image_generate`, `music_generate`, `video_generate`, `tts`, and `pdf` unless you explicitly preserve them with `tools.allow` or `tools.alsoAllow`; Tool Search may still catalog preserved tools instead of exposing them directly.
## Related
- [Configuration reference](/gateway/configuration-reference)

View file

@ -64,7 +64,7 @@ Per-agent override (optional, at `agents.entries.*.tools.loopDetection`):
The per-agent setting overrides the global setting.
You can also enable the global rolling-history detectors in **Settings -> Labs** in the Control UI.
You can also enable the global rolling-history detectors in **Settings → Agent Defaults → Tools** in the Control UI. Reset the setting to its default to disable the rolling detectors while keeping the post-compaction guard; explicitly turning it off disables both.
### Field behavior

View file

@ -9,7 +9,7 @@ read_when:
- You want to observe collector children in chat
---
Swarm is an experimental way to orchestrate many sub-agents from a
Swarm orchestrates many sub-agents from a
[Code Mode](/tools/code-mode) script. It is enabled by default, with an explicit
opt-out. Use normal JavaScript or TypeScript control flow such as `Promise.all`,
`while`, and `if` to fan out work, collect results, and make decisions.
@ -35,10 +35,8 @@ separately opt-in, and normal tool policy still applies. Existing Codex sessions
can retain an older tool catalog. See the
[fresh-session guidance](/tools/swarm#use-swarm-from-other-harnesses) below.
To opt out, turn off **Settings → Agents & Tools → Labs → Swarm** in the
Control UI. The switch saves `tools.swarm.enabled: false` immediately and
applies to future runs without restarting the Gateway. Or set the boolean
shorthand in `openclaw.json`:
To opt out, disable Swarm in **Settings → Agent Defaults → Tools**, or set
`tools.swarm: false` in `openclaw.json`:
```json5
{
@ -50,7 +48,7 @@ shorthand in `openclaw.json`:
`swarm: { enabled: false }` has the same effect while preserving configured
limits. To re-enable Swarm, remove the explicit opt-out, set `swarm: true` or
`swarm: { enabled: true }`, or turn the Labs switch back on.
`swarm: { enabled: true }`, or enable it in **Settings → Agent Defaults → Tools**.
To tune the limits, use object form. These are the defaults. You only need to
include values you want to change:

View file

@ -59,7 +59,8 @@ Control UI capabilities grouped by area, each with the Gateway RPC methods behin
- Privacy & Security: curated rows for gateway auth, exec policy, browser enablement, tool profile, device auth, and mobile pairing, above the schema-backed `security`/`approvals` sections.
- Secrets (`/settings/secrets`) manages team-scoped secret and environment entries through `secrets.store.*`. Environment values remain visible, secret values are never returned after saving, Bulk Add accepts quoted multiline dotenv values, and mutation actions are hidden when the connected Gateway does not advertise them.
- Approvals includes newest-first, 30-day history for resolved exec, plugin, and system-agent requests. Filter by kind or page through older rows to review the decision, reason, source session, and resolver attribution recorded by the Gateway.
- Labs exposes shipped experimental switches. Code Mode defaults off; turning it on writes `tools.codeMode.enabled: "auto"`, which engages only for models marked as preferred Code Mode performers. Swarm defaults on; turn it off to write `tools.swarm.enabled: false`. Swarm does not enable Code Mode or grant tools denied by policy. Code Mode and Swarm changes save immediately and apply to future runs without restarting; unshipped experiments do not appear or write speculative config keys.
- Labs exposes Code Mode, Tool Search for all models, Custom plugin UI, Host Desktop, and Cloud Worker Desktop. Code Mode defaults off; turning it on writes `tools.codeMode.enabled: "auto"`, which engages only for models marked as preferred Code Mode performers. Changes save immediately; features that require a Gateway restart show a restart hint. Swarm and CLI agents are available by default, with their existing configuration opt-outs and tool permissions.
- Swarm and tool-loop detection live in **Settings → Agent Defaults → Tools**. CLI agents live in **Settings → Infrastructure → Gateway**. Message audit metadata lives in **Settings → Advanced → Logging** and remains off by default; restart the Gateway after changing it. Lean local-model mode remains an advanced troubleshooting setting under **Settings → Agent Defaults → Agents**.
- Notifications: browser web-push status, subscribe/unsubscribe, and a test send.
- Advanced: every config section without a curated home, plus the raw JSON5 editor.
- **Advanced → Setup** is collapsed by default. Expand it to edit discovery access and app recommendation consent or inspect read-only setup history. Internal bookkeeping fields are absent from the form; the raw JSON5 editor remains unchanged.

View file

@ -167,6 +167,7 @@ exports[`config tier coverage > keeps the curated common leaf set reviewable 1`]
"gateway.auth.trustedProxy.allowUsers.*",
"gateway.auth.trustedProxy.userHeader",
"gateway.bind",
"gateway.cliAgents.enabled",
"gateway.controlUi.allowedOrigins.*",
"gateway.http.endpoints.chatCompletions.images.urlAllowlist.*",
"gateway.http.endpoints.responses.files.urlAllowlist.*",
@ -204,6 +205,7 @@ exports[`config tier coverage > keeps the curated common leaf set reviewable 1`]
"hooks.mappings.*.agentId",
"hooks.mappings.*.model",
"hooks.token",
"logging.audit.messages",
"mcp.apps.enabled",
"mcp.servers.*.args.*",
"mcp.servers.*.auth",
@ -355,6 +357,7 @@ exports[`config tier coverage > keeps the curated common leaf set reviewable 1`]
"tools.github.gitAuthor.name",
"tools.github.kind",
"tools.github.profileId",
"tools.loopDetection.enabled",
"tools.media.audio.attachments.mode",
"tools.media.audio.attachments.prefer",
"tools.media.audio.baseUrl",
@ -572,6 +575,13 @@ exports[`config tier coverage > keeps the curated common leaf set reviewable 1`]
"tools.message.crossContext.marker.suffix",
"tools.profile",
"tools.sessions.visibility",
"tools.swarm",
"tools.swarm.defaultAgentId",
"tools.swarm.enabled",
"tools.swarm.maxChildrenPerGroup",
"tools.swarm.maxConcurrent",
"tools.swarm.maxTotalPerGroup",
"tools.swarm.waitTimeoutSecondsMax",
"tools.web.fetch.enabled",
"tools.web.fetch.headers.*",
"tools.web.fetch.provider",

View file

@ -106,7 +106,7 @@ export const CORE_FIELD_HELP: Record<string, string> = {
"gateway.controlUi.enabled":
"Enables serving the gateway Control UI from the gateway HTTP process when true. Keep enabled for local administration, and disable when an external control surface replaces it.",
"gateway.cliAgents":
"Experimental Control UI discovery for external CLI session engines exposed by the Gateway session catalog. Enabled by default; disable to prevent starting those engines from the new-session model picker.",
"Control UI discovery for external CLI session engines exposed by the Gateway session catalog. Enabled by default; disable to prevent starting those engines from the new-session model picker.",
"gateway.cliAgents.enabled":
"Shows catalog-backed CLI agents in the Control UI new-session model picker when true (default: true). Set false to disable CLI agents and native CLI session creation. Only catalogs that advertise session creation are listed, and the picker stays hidden when the Gateway does not advertise session catalog support.",
"gateway.terminal":
@ -304,7 +304,7 @@ export const CORE_FIELD_HELP: Record<string, string> = {
"agents.entries.*.experimental":
"Per-agent experimental flags. Omitted fields inherit agents.defaults.experimental.",
"agents.entries.*.experimental.localModelLean":
"Per-agent override for lean local-model mode. Enable it for one smaller local-model agent without trimming tools from every agent.",
"Per-agent troubleshooting override for lean local-model mode. Enable it only when restricting optional tools resolves a demonstrated model failure, without trimming tools from every agent.",
"agents.defaults.contextLimits":
"Focused per-agent-context budget defaults for selected high-volume excerpts and injected prompt blocks. Use this to tune bounded read/injection sizes without reopening any unbounded call paths.",
"agents.defaults.contextLimits.memoryGetMaxChars":

View file

@ -166,7 +166,7 @@ export const MODEL_FIELD_HELP: Record<string, string> = {
"agents.defaults.experimental":
"Experimental agent-default flags. Keep these off unless you are intentionally testing a preview surface.",
"agents.defaults.experimental.localModelLean":
"Explicitly restrict optional tools such as browser, automations, and message. Off by default; supported local runtimes use automatic Tool Search without this restriction. Explicit tool allows and required delivery tools are preserved.",
"Advanced troubleshooting override that restricts optional tools such as browser, automations, and message. Off by default; supported local runtimes use automatic Tool Search without this restriction. Explicit tool allows and required delivery tools are preserved.",
"agents.defaults.startupContext":
'Runtime-owned first-turn prelude for bare "/new" and "/reset". Use this to control whether recent daily memory files are preloaded into the first prompt instead of asking the model to decide what to read.',
"agents.defaults.startupContext.enabled":

View file

@ -405,7 +405,7 @@ export const RUNTIME_FIELD_HELP: Record<string, string> = {
"tools.exec.applyPatch.allowModels":
'Optional allowlist of model ids (e.g. "gpt-5.4" or "openai/gpt-5.4").',
"tools.loopDetection.enabled":
"Enable repetitive tool-call loop detection and backoff safety checks (default: false).",
"Controls rolling-history tool-loop detection and the post-compaction guard. Omit to keep rolling detection off and post-compaction protection on. Set true to enable both, or false to disable both.",
"tools.exec.notifyOnExit":
"When true (default), backgrounded exec sessions on exit and node exec lifecycle events enqueue a system event and request a heartbeat.",
"tools.exec.notifyOnExitEmptySuccess":

View file

@ -98,6 +98,8 @@ const SECTION_DOCS_URLS = {
} as const satisfies Record<string, string>;
const FIELD_PLACEHOLDERS: Record<string, string> = {
"gateway.cliAgents.enabled": "Default (enabled)",
"tools.loopDetection.enabled": "Default (post-compaction protection only)",
"gateway.publicOrigin": "https://gateway.example.com",
"gateway.remote.url": "ws://host:18789",
"gateway.remote.tlsFingerprint": "sha256:ab12cd34…",

View file

@ -1335,6 +1335,17 @@ describe("config schema", () => {
expect(baseSchema.uiHints["gateway.reload.mode"]?.advanced).toBe(true);
expect(baseSchema.uiHints["agents.defaults.workspace"]?.advanced).toBe(false);
expect(baseSchema.uiHints["agents.defaults.compaction.timeoutSeconds"]?.advanced).toBe(true);
for (const path of [
"tools.swarm",
"tools.swarm.enabled",
"tools.swarm.maxConcurrent",
"tools.loopDetection.enabled",
"gateway.cliAgents.enabled",
"logging.audit.messages",
]) {
expect(baseSchema.uiHints[path]?.advanced, path).toBe(false);
}
expect(baseSchema.uiHints["agents.defaults.experimental.localModelLean"]?.advanced).toBe(true);
});
it("preserves explicit common hints on numeric leaves while defaulting tuning advanced", () => {

View file

@ -119,6 +119,7 @@ channels.telegram.accounts.*.groups.*.topics.*.groupPolicy
channels.telegram.direct.*.topics.*.groupPolicy
channels.whatsapp.groups.*.requireMention channels.whatsapp.selfChatMode
cron.enabled env.vars gateway.auth.mode gateway.auth.password gateway.auth.token
gateway.cliAgents.enabled
gateway.auth.trustedProxy.allowUsers gateway.auth.trustedProxy.userHeader gateway.bind
gateway.controlUi.allowedOrigins gateway.http.endpoints.chatCompletions.images.urlAllowlist
gateway.http.endpoints.responses.files.urlAllowlist
@ -130,6 +131,7 @@ gateway.trustedProxies hooks.allowedAgentIds hooks.enabled hooks.gmail.account h
hooks.gmail.pushToken hooks.gmail.subscription hooks.gmail.topic
hooks.gmail.model hooks.gmail.serve.port hooks.internal.entries.*.enabled
hooks.mappings.*.agentId hooks.mappings.*.model hooks.token
logging.audit.messages
mcp.apps.enabled mcp.servers.*.args mcp.servers.*.auth mcp.servers.*.command
mcp.servers.*.cwd mcp.servers.*.enabled mcp.servers.*.env mcp.servers.*.headers
mcp.servers.*.oauth.authProfileId mcp.servers.*.transport mcp.servers.*.url
@ -156,6 +158,9 @@ tools.github
tools.fs tools.media.audio tools.media.image tools.media.video tools.message
tools.exec.reviewer.model.primary tools.media.models.*.model
tools.media.models.*.request.auth.token tools.profile tools.sessions
tools.loopDetection.enabled tools.swarm tools.swarm.enabled
tools.swarm.maxConcurrent tools.swarm.maxChildrenPerGroup tools.swarm.maxTotalPerGroup
tools.swarm.waitTimeoutSecondsMax tools.swarm.defaultAgentId
tools.web transcripts.enabled
tts.auto tts.persona tts.personas.*.providers.*.apiKey tts.provider
tts.providers.* tts.providers.*.apiKey

View file

@ -294,7 +294,7 @@ export type GatewayTerminalConfig = {
detachedSessionTimeoutSeconds?: number;
};
/** Labs-gated external CLI session targets in the Control UI. */
/** External CLI session targets in the Control UI. */
export type GatewayCliAgentsConfig = {
/** Show catalog-backed CLI agents in the new-session model picker. Default: true. */
enabled?: boolean;

View file

@ -9,6 +9,7 @@ import {
installMockGateway,
resolvePlaywrightChromiumExecutablePath,
startControlUiE2eServer,
type ControlUiMockGatewayScenario,
type ControlUiE2eServer,
} from "../test-helpers/control-ui-e2e.ts";
@ -28,9 +29,10 @@ const WORK_SESSION_KEY = "agent:main:work";
let browser: Browser;
let server: ControlUiE2eServer;
function custodianGatewayScenario() {
function custodianGatewayScenario(): ControlUiMockGatewayScenario {
return {
sessionKey: WORK_SESSION_KEY,
sessions: [{ key: WORK_SESSION_KEY, label: "Main", kind: "direct", updatedAt: Date.now() }],
featureMethods: [
"chat.metadata",
"chat.startup",
@ -40,6 +42,15 @@ function custodianGatewayScenario() {
"openclaw.chat.history",
],
methodResponses: {
"sessions.list": {
cases: [
// The work session does not match the Ask OpenClaw palette query.
{
match: { search: "Ask OpenClaw" },
response: { count: 0, sessions: [] },
},
],
},
"openclaw.chat": {
sessionId: MOCK_SESSION_ID,
reply: "Machine is healthy. Ask me anything.",

View file

@ -3294,10 +3294,10 @@ export const en: TranslationMap & {
description:
"Set the global default for compact, sandboxed JavaScript tool workflows. On selects Auto for evaluated models; Off disables the default. Per-model Code Mode overrides are in Agent Defaults → Models (Advanced).",
},
swarm: {
title: "Swarm",
groupTitle: "Parallel tasks",
description: "Coordinate parallel subagents and collect their results.",
defaultPhase: "Unphased",
progress: "{complete} of {total}",
active: "{running} running · {queued} queued · {failed} failed",
@ -3313,21 +3313,7 @@ export const en: TranslationMap & {
description:
"Local models use Tool Search automatically. Enable this override to defer tool schemas for all models; turning it off restores each model's default.",
},
loopDetection: {
title: "Tool-loop detection",
description:
"Enable rolling-history guards that warn or block repeated tool calls when an agent stops making progress.",
},
localModelLean: {
title: "Lean tools for local models",
description:
"Remove optional tools such as browser and automations. Leave this off to keep those capabilities available through Tool Search.",
},
cliAgents: {
title: "CLI agents",
description:
"Show external CLI session engines in the new-session model picker when their plugins support creating sessions.",
},
customPluginUi: {
title: "Custom plugin UI",
description:
@ -3335,11 +3321,7 @@ export const en: TranslationMap & {
restartRequired:
"Restart the Gateway and reload this browser tab after changing this setting.",
},
auditMessages: {
title: "Message audit metadata",
description:
"Record content-free metadata for direct conversations in the audit ledger. Message content is never stored.",
},
hostDesktop: {
title: "Host Desktop",
description:

View file

@ -136,7 +136,7 @@ describe("LabsPage", () => {
});
it("renders every registered experimental entry with its documentation link", async () => {
const { page } = await mountPage({
const { page, runtimeConfig } = await mountPage({
tools: { codeMode: { enabled: true }, swarm: { enabled: true } },
});
@ -147,7 +147,16 @@ describe("LabsPage", () => {
expect(introLink?.href).toBe("https://docs.openclaw.ai/concepts/experimental-features");
expect(page.querySelectorAll(".settings-row")).toHaveLength(LAB_FEATURES.length);
expect(page.textContent).toContain("Code Mode");
expect(page.textContent).toContain("Swarm");
for (const title of [
"Swarm",
"CLI agents",
"Tool-loop detection",
"Message audit metadata",
"Lean tools for local models",
]) {
expect(page.textContent).not.toContain(title);
}
expect(runtimeConfig.patch).not.toHaveBeenCalled();
expect(page.textContent).toContain("Host Desktop");
expect(page.textContent).toContain("Cloud Worker Desktop");
expect(codeModeToggle(page).checked).toBe(true);
@ -177,12 +186,6 @@ describe("LabsPage", () => {
expectedPatch: { tools: { codeMode: { enabled: null } } },
note: "labs: update codeMode",
},
{
label: "Lean tools for local models",
sourceConfig: { agents: { defaults: { experimental: { localModelLean: true } } } },
expectedPatch: { agents: { defaults: { experimental: { localModelLean: null } } } },
note: "labs: update localModelLean",
},
{
label: "Custom plugin UI",
sourceConfig: { gateway: { controlUi: { experimental: { customPlugins: true } } } },
@ -248,38 +251,12 @@ describe("LabsPage", () => {
expectedPatch: { tools: { toolSearch: { enabled: true, mode: "directory" } } },
note: "labs: update toolSearch",
},
{
label: "Tool-loop detection",
sourceConfig: { tools: { loopDetection: { enabled: false } } },
expectedPatch: { tools: { loopDetection: { enabled: true } } },
note: "labs: update loopDetection",
},
{
label: "Lean tools for local models",
sourceConfig: {},
expectedPatch: { agents: { defaults: { experimental: { localModelLean: true } } } },
note: "labs: update localModelLean",
},
{
label: "CLI agents",
sourceConfig: { gateway: { cliAgents: { enabled: false } } },
expectedPatch: { gateway: { cliAgents: { enabled: null } } },
note: "labs: update cliAgents",
},
{
label: "Custom plugin UI",
sourceConfig: {},
expectedPatch: { gateway: { controlUi: { experimental: { customPlugins: true } } } },
note: "labs: update customPluginUi",
},
{
// Not a boolean gate: the on state is the conservative `direct` mode, so
// enabling here cannot start recording group or unknown conversations.
label: "Message audit metadata",
sourceConfig: { logging: { audit: { messages: "off" } } },
expectedPatch: { logging: { audit: { messages: "direct" } } },
note: "labs: update auditMessages",
},
{
label: "Host Desktop",
sourceConfig: { desktop: { host: { enabled: false } } },
@ -307,47 +284,14 @@ describe("LabsPage", () => {
});
});
it("reads a mode-valued gate as on only for the mode this row offers", async () => {
const off = await mountPage({ logging: { audit: { messages: "off" } } });
expect(labToggle(off.page, "Message audit metadata").checked).toBe(false);
off.provider.remove();
const direct = await mountPage({ logging: { audit: { messages: "direct" } } });
expect(labToggle(direct.page, "Message audit metadata").checked).toBe(true);
direct.provider.remove();
// `all` is broader than the mode this row offers, but it is still on. Showing
// it as off would make the switch look available and quietly narrow a choice
// the operator made deliberately somewhere else.
const all = await mountPage({ logging: { audit: { messages: "all" } } });
expect(labToggle(all.page, "Message audit metadata").checked).toBe(true);
});
it("restores the default off mode from a broader audit mode", async () => {
const { page, runtimeConfig } = await mountPage({
logging: { audit: { messages: "all" } },
});
const toggle = labToggle(page, "Message audit metadata");
toggle.checked = false;
toggle.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledOnce());
expect(runtimeConfig.patch).toHaveBeenCalledWith({
raw: { logging: { audit: { messages: null } } },
note: "labs: update auditMessages",
});
});
it("marks startup-scoped entries as needing a restart", async () => {
const { page } = await mountPage({});
const rows = [...page.querySelectorAll(".settings-row")];
const restartRows = rows.filter((row) => row.textContent?.toLowerCase().includes("restart"));
expect(restartRows).toHaveLength(4);
expect(restartRows).toHaveLength(3);
expect(restartRows.map((row) => row.textContent)).toEqual(
expect.arrayContaining([
expect.stringContaining("Message audit metadata"),
expect.stringContaining("Custom plugin UI"),
expect.stringContaining("Host Desktop"),
expect.stringContaining("Cloud Worker Desktop"),
@ -361,7 +305,6 @@ describe("LabsPage", () => {
it("shows default provenance", async () => {
const inherited = await mountPage({});
expect(labRow(inherited.page, "Code Mode").textContent).toContain("Using default: Disabled");
expect(labRow(inherited.page, "Swarm").textContent).toContain("Using default: Enabled");
inherited.provider.remove();
const overridden = await mountPage({
@ -371,107 +314,6 @@ describe("LabsPage", () => {
},
});
expect(labRow(overridden.page, "Code Mode").textContent).toContain("Default: Disabled");
expect(labRow(overridden.page, "Swarm").textContent).toContain("Default: Enabled");
});
});
describe("LabsPage CLI agents enablement", () => {
afterEach(() => {
document.body.replaceChildren();
});
it.each([
["unset", true, {}, false],
["empty object", true, { gateway: { cliAgents: {} } }, false],
["explicit enabled", true, { gateway: { cliAgents: { enabled: true } } }, true],
["explicit disabled", false, { gateway: { cliAgents: { enabled: false } } }, true],
])("reads %s as %s with an enabled default", async (_label, expected, config, overridden) => {
const { page } = await mountPage(config);
expect(labToggle(page, "CLI agents").checked).toBe(expected);
expect(labRow(page, "CLI agents").textContent).toContain(
overridden ? "Default: Enabled" : "Using default: Enabled",
);
});
it("writes an explicit opt-out when disabling the default", async () => {
const { page, runtimeConfig } = await mountPage({});
const toggle = labToggle(page, "CLI agents");
expect(toggle.checked).toBe(true);
toggle.checked = false;
toggle.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledOnce());
expect(runtimeConfig.patch).toHaveBeenCalledWith({
raw: { gateway: { cliAgents: { enabled: false } } },
note: "labs: update cliAgents",
});
});
});
describe("LabsPage swarm enablement", () => {
afterEach(() => {
document.body.replaceChildren();
});
it.each([
["unset", true, {}, false],
["empty object", true, { tools: { swarm: {} } }, false],
["limits-only object", true, { tools: { swarm: { maxConcurrent: 3 } } }, false],
["boolean true", true, { tools: { swarm: true } }, true],
["explicit enabled", true, { tools: { swarm: { enabled: true } } }, true],
["boolean false", false, { tools: { swarm: false } }, true],
[
"explicit disabled with limits",
false,
{ tools: { swarm: { enabled: false, maxConcurrent: 3 } } },
true,
],
])("reads %s as %s with an enabled default", async (_label, expected, config, overridden) => {
const { page } = await mountPage(config);
expect(labToggle(page, "Swarm").checked).toBe(expected);
expect(labRow(page, "Swarm").textContent).toContain(
overridden ? "Default: Enabled" : "Using default: Enabled",
);
});
it("writes an explicit opt-out when disabling the default", async () => {
const { page, runtimeConfig } = await mountPage({});
const toggle = labToggle(page, "Swarm");
expect(toggle.checked).toBe(true);
toggle.checked = false;
toggle.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledOnce());
expect(runtimeConfig.patch).toHaveBeenCalledWith({
raw: { tools: { swarm: { enabled: false } } },
note: "labs: update swarm",
});
});
it.each([
{
label: "object gate without removing limits",
swarm: { enabled: false, maxConcurrent: 3 },
reset: { enabled: null },
},
{ label: "boolean shorthand", swarm: false, reset: null },
])("restores the enabled default by resetting the $label", async ({ swarm, reset }) => {
const { page, runtimeConfig } = await mountPage({ tools: { swarm } });
const toggle = labToggle(page, "Swarm");
expect(toggle.checked).toBe(false);
toggle.checked = true;
toggle.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledOnce());
expect(runtimeConfig.patch).toHaveBeenCalledWith({
raw: { tools: { swarm: reset } },
note: "labs: update swarm",
});
});
});
@ -580,58 +422,3 @@ describe("LabsPage tool search enablement", () => {
});
});
});
describe("LabsPage tool loop detection enablement", () => {
// Mirrors resolveToolLoopDetectionConfig and the detector default: only an
// explicit true enables the rolling-history detectors.
it.each([
{ label: "unset", config: {}, expected: false },
{
label: "explicit enabled",
config: { tools: { loopDetection: { enabled: true } } },
expected: true,
},
{
label: "explicit disabled",
config: { tools: { loopDetection: { enabled: false } } },
expected: false,
},
])("reads $label as $expected", async ({ config, expected }) => {
const { page, provider } = await mountPage(config);
expect(labToggle(page, "Tool-loop detection").checked).toBe(expected);
provider.remove();
});
it("patches only enabled so sibling settings remain untouched", async () => {
const { page, runtimeConfig } = await mountPage({
tools: { loopDetection: { enabled: false, warningThreshold: 12 } },
});
const toggle = labToggle(page, "Tool-loop detection");
toggle.checked = true;
toggle.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledOnce());
expect(runtimeConfig.patch).toHaveBeenCalledWith({
raw: { tools: { loopDetection: { enabled: true } } },
note: "labs: update loopDetection",
});
});
it("restores the disabled default instead of pinning false", async () => {
const { page, runtimeConfig } = await mountPage({
tools: { loopDetection: { enabled: true, warningThreshold: 12 } },
});
const toggle = labToggle(page, "Tool-loop detection");
toggle.checked = false;
toggle.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
await vi.waitFor(() => expect(runtimeConfig.patch).toHaveBeenCalledOnce());
expect(runtimeConfig.patch).toHaveBeenCalledWith({
raw: { tools: { loopDetection: { enabled: null } } },
note: "labs: update loopDetection",
});
});
});

View file

@ -87,22 +87,6 @@ export const LAB_FEATURES = [
resetScope: "gate",
restartHint: null,
},
{
id: "swarm",
title: () => t("labsPage.swarm.title"),
description: () => t("labsPage.swarm.description"),
docsUrl: "https://docs.openclaw.ai/tools/swarm",
configPath: ["tools", "swarm", "enabled"],
onValue: true,
offValue: false,
activeValues: [true],
// Mirrors resolveSwarmConfig: only an explicit false opts out; limits-only
// objects inherit the enabled default without owning the gate.
readEnabled: (raw) => raw !== false && (!isRecord(raw) || raw.enabled !== false),
enableAlso: null,
resetScope: "gate",
restartHint: null,
},
{
id: "toolSearch",
title: () => t("labsPage.toolSearch.title"),
@ -124,50 +108,6 @@ export const LAB_FEATURES = [
resetScope: "parent",
restartHint: null,
},
{
id: "loopDetection",
title: () => t("labsPage.loopDetection.title"),
description: () => t("labsPage.loopDetection.description"),
docsUrl: "https://docs.openclaw.ai/tools/loop-detection",
configPath: ["tools", "loopDetection", "enabled"],
onValue: true,
offValue: false,
activeValues: [true],
// ToolLoopDetectionSchema accepts object form only, and
// resolveToolLoopDetectionConfig reads this enabled leaf directly.
readEnabled: null,
enableAlso: null,
resetScope: "gate",
restartHint: null,
},
{
id: "localModelLean",
title: () => t("labsPage.localModelLean.title"),
description: () => t("labsPage.localModelLean.description"),
docsUrl: "https://docs.openclaw.ai/gateway/local-models",
configPath: ["agents", "defaults", "experimental", "localModelLean"],
onValue: true,
offValue: false,
activeValues: [true],
readEnabled: null,
enableAlso: null,
resetScope: "gate",
restartHint: null,
},
{
id: "cliAgents",
title: () => t("labsPage.cliAgents.title"),
description: () => t("labsPage.cliAgents.description"),
docsUrl: "https://docs.openclaw.ai/gateway/configuration-reference#gateway",
configPath: ["gateway", "cliAgents", "enabled"],
onValue: true,
offValue: false,
activeValues: [true],
readEnabled: (raw) => !isRecord(raw) || raw.enabled !== false,
enableAlso: null,
resetScope: "gate",
restartHint: null,
},
{
id: "customPluginUi",
title: () => t("labsPage.customPluginUi.title"),
@ -182,25 +122,6 @@ export const LAB_FEATURES = [
resetScope: "gate",
restartHint: () => t("labsPage.customPluginUi.restartRequired"),
},
{
id: "auditMessages",
title: () => t("labsPage.auditMessages.title"),
description: () => t("labsPage.auditMessages.description"),
docsUrl: "https://docs.openclaw.ai/gateway/audit",
// Not a boolean: `off` | `direct` | `all`. Labs offers the conservative
// `direct`, so turning it on cannot start recording group or unknown
// conversations that the operator never opted into.
configPath: ["logging", "audit", "messages"],
onValue: "direct",
offValue: "off",
activeValues: ["direct", "all"],
readEnabled: null,
enableAlso: null,
resetScope: "gate",
// startGatewayEventSubscriptions resolves the mode once and bakes it into
// the recorder, so this outlives the reload plan's `logging: none` rule.
restartHint: () => t("labsPage.restartRequired"),
},
{
id: "hostDesktop",
title: () => t("labsPage.hostDesktop.title"),