diff --git a/docs/docs_map.md b/docs/docs_map.md index 850d8517731c..733917cd23df 100644 --- a/docs/docs_map.md +++ b/docs/docs_map.md @@ -9908,7 +9908,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H2: What it does - H2: Why use it - H2: Quickstart - - H3: Enable Code Mode + - H3: Defaults and overrides - H3: What the model does - H3: Verify the active surface - H2: Use Swarm for agent fan-out diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 7cd311591c6a..b80cf90af10c 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -84,17 +84,23 @@ Without that sandbox-layer entry, the MCP server can still load successfully whi ### `tools.codeMode` -`tools.codeMode` enables the generic OpenClaw code-mode surface. When enabled +`tools.codeMode` gates the generic OpenClaw code-mode surface. When engaged for a run with tools, normal OpenClaw tools move behind the in-sandbox `tools.*` catalog bridge, and MCP tools are available through the generated `MCP` namespace. The model normally sees `exec` and `wait`; tools such as `computer` whose structured results cannot cross the JSON-only bridge stay direct. +`enabled` defaults to `"auto"`, which engages code mode only for models whose +catalog entry flags `compat.codeMode: "preferred"`. See +[Code Mode - automatic per-model activation](/tools/code-mode#automatic-per-model-activation). + +To opt out for every run: + ```json5 { tools: { codeMode: { - enabled: true, + enabled: false, }, }, } @@ -104,13 +110,12 @@ The shorthand is also accepted: ```json5 { - tools: { codeMode: true }, + tools: { codeMode: false }, } ``` -`enabled` also accepts `"auto"`, which engages code mode only for models whose -catalog entry flags `compat.codeMode: "preferred"`. See -[Code Mode - automatic per-model activation](/tools/code-mode#automatic-per-model-activation). +`enabled: true` forces code mode on for every tool-capable run, regardless of +model. MCP declarations are exposed through the read-only virtual API file surface in code mode. Guest code can call `API.list("mcp")` and diff --git a/docs/tools/code-mode.md b/docs/tools/code-mode.md index b5faa803afd0..a2eb6c18e876 100644 --- a/docs/tools/code-mode.md +++ b/docs/tools/code-mode.md @@ -9,8 +9,10 @@ read_when: - You are reviewing the MCP namespace bridge or virtual API declarations --- -Code mode is an experimental, opt-in OpenClaw agent-runtime feature. When -enabled, the model no longer sees every enabled tool schema; instead, it sees +Code mode is an experimental OpenClaw agent-runtime feature. It defaults to the +`"auto"` tier, which engages only models whose catalog marks them as preferred +code-mode performers; every other model keeps normal tool exposure. When +engaged, the model no longer sees every enabled tool schema; instead, it sees `exec`, `wait`, and any direct-only tool whose structured result cannot cross the JSON-only guest bridge. The model writes a small JavaScript or TypeScript program that searches, describes, and calls the hidden tool catalog. @@ -23,8 +25,8 @@ separate implementations: freeform-grammar tool: the model writes raw JavaScript source (optionally prefixed by a `// @exec: {...}` pragma line for execution options), executed in Codex's in-process V8 Code Mode runtime. -- OpenClaw code mode runs in the generic OpenClaw agent runtime and is - disabled unless `tools.codeMode.enabled` is `true` or `"auto"`. Its `exec` +- OpenClaw code mode runs in the generic OpenClaw agent runtime, gated by + `tools.codeMode.enabled` (default `"auto"`, per-model activation). Its `exec` tool takes a JSON `{ code, language }` payload, executed in a QuickJS-WASI worker. @@ -89,19 +91,25 @@ the QuickJS-WASI guest. ## Quickstart -### Enable Code Mode +### Defaults and overrides + +Code mode ships enabled in the `"auto"` tier: it engages only when the run's +model is flagged as a preferred code-mode performer in its provider catalog, +and every other model keeps normal tool exposure. No configuration is needed. +See [Automatic per-model activation](#automatic-per-model-activation) for the +exact semantics and the shipped model list. + +To opt out for every run: ```json5 { tools: { - codeMode: { - enabled: true, - }, + codeMode: false, }, } ``` -Shorthand: +To force code mode on for every tool-capable run, regardless of model: ```json5 { @@ -111,22 +119,9 @@ Shorthand: } ``` -Code mode stays off when `tools.codeMode` is omitted, `false`, or an object -without `enabled: true` or `enabled: "auto"`. - -To engage code mode only for models whose catalog marks them as strong code-mode -performers, use the `"auto"` tier instead of `true`: - -```json5 -{ - tools: { - codeMode: "auto", - }, -} -``` - -See [Automatic per-model activation](#automatic-per-model-activation) for the -exact semantics and the shipped model list. +Object form works too: `tools.codeMode.enabled` accepts the same `false`, +`true`, and `"auto"` values. An object without `enabled` keeps the `"auto"` +default. If you use sandboxed agents with configured MCP servers, also allow the bundled MCP plugin in the sandbox tool policy, for example @@ -205,7 +200,7 @@ validating high-risk deployments. | | | | ------------------- | ------------------------------------------------------------------------------------------- | | Runtime | [`quickjs-wasi`](https://github.com/vercel-labs/quickjs-wasi) | -| Default state | disabled | +| Default state | `"auto"` (engages only catalog-preferred models) | | Stability | experimental OpenClaw surface (Codex Code Mode is a separate, stable Codex harness surface) | | Target surface | generic OpenClaw agent runs | | Security posture | model code is hostile | @@ -252,7 +247,7 @@ enable the feature on its own. | Field | Default | Clamp | | --------------------- | ------------------------------ | ----------------------------------------------- | -| `enabled` | `false` | `false`, `true`, or `"auto"` (per-model) | +| `enabled` | `"auto"` | `false`, `true`, or `"auto"` (per-model) | | `runtime` | `"quickjs-wasi"` | only supported value | | `mode` | `"only"` | exposes control/direct tools, catalogs the rest | | `languages` | `["javascript", "typescript"]` | any subset of the two | @@ -274,13 +269,13 @@ an engaged run never silently falls back to broad direct tool exposure. `tools.codeMode.enabled` accepts three values: -- `false` (default): code mode is off for every run. +- `"auto"` (default): code mode engages only when the run's model is flagged + as a preferred code-mode performer in its provider catalog. +- `false`: code mode is off for every run. - `true`: code mode engages for every tool-capable run, regardless of model. -- `"auto"`: code mode engages only when the run's model is flagged as a - preferred code-mode performer in its provider catalog. -`false` and `true` behave exactly as before the `"auto"` tier existed; `"auto"` -is purely additive. +`false` and `true` are absolute overrides and behave exactly as before the +`"auto"` tier existed. ### The `compat.codeMode` catalog flag @@ -327,8 +322,9 @@ replacing many full tool schemas and per-tool round trips with one compact program surface. Models below the preferred tier showed no consistent win and sometimes regressed, which is why `"auto"` leaves them on direct tools. -Use `"auto"` when agents switch between models: strong models get the compact -surface, weaker or local ones keep the exposure they handle best. Use `true` +The default `"auto"` fits agents that switch between models: strong models get +the compact surface, weaker or local ones keep the exposure they handle best. +Use `true` when you have verified a specific unflagged model performs well with code mode; global force-on is most predictable for single-model deployments. For open-weight or uncached serving where every prompt token is billed or diff --git a/src/agents/code-mode-runtime.test.ts b/src/agents/code-mode-runtime.test.ts index f1e1b7ee1d8d..f0155b0f055e 100644 --- a/src/agents/code-mode-runtime.test.ts +++ b/src/agents/code-mode-runtime.test.ts @@ -13,8 +13,8 @@ describe("Code Mode master switch resolution", () => { { name: "boolean shorthand false", codeMode: false, enabled: false }, { name: "auto shorthand", codeMode: "auto", enabled: "auto" }, { name: "object enabled auto", codeMode: { enabled: "auto" }, enabled: "auto" }, - { name: "object without enabled", codeMode: { timeoutMs: 5000 }, enabled: false }, - { name: "omitted", codeMode: undefined, enabled: false }, + { name: "object without enabled", codeMode: { timeoutMs: 5000 }, enabled: "auto" }, + { name: "omitted", codeMode: undefined, enabled: "auto" }, ])("resolves enabled for $name", ({ codeMode, enabled }) => { expect(resolveCodeModeConfig({ tools: { codeMode } } as never).enabled).toBe(enabled); }); diff --git a/src/agents/code-mode-runtime.ts b/src/agents/code-mode-runtime.ts index adaae26e1664..64517904419a 100644 --- a/src/agents/code-mode-runtime.ts +++ b/src/agents/code-mode-runtime.ts @@ -119,7 +119,9 @@ function readCodeModeRawConfig(config?: OpenClawConfig, agentId?: string): Recor } function readEnabled(value: unknown): boolean | "auto" { - return typeof value === "boolean" || value === "auto" ? value : false; + // Shipped default is "auto": code mode engages only for catalog-preferred + // models, so unevaluated models keep normal tool exposure by construction. + return typeof value === "boolean" || value === "auto" ? value : "auto"; } export function readPositiveInteger(value: unknown, fallback: number): number { diff --git a/src/agents/code-mode.test.ts b/src/agents/code-mode.test.ts index da0368c03848..1b0c25b75e2e 100644 --- a/src/agents/code-mode.test.ts +++ b/src/agents/code-mode.test.ts @@ -203,7 +203,7 @@ describe("Code Mode", () => { }, }, } as never); - expect(resolved.enabled).toBe(false); + expect(resolved.enabled).toBe("auto"); expect(resolveCodeModeConfig({ tools: { codeMode: { enabled: true } } } as never).enabled).toBe( true, ); diff --git a/src/config/schema.help.runtime.ts b/src/config/schema.help.runtime.ts index c1dfabf1944b..bd1229f12051 100644 --- a/src/config/schema.help.runtime.ts +++ b/src/config/schema.help.runtime.ts @@ -112,7 +112,7 @@ export const RUNTIME_FIELD_HELP: Record = { "tools.codeMode": "Generic OpenClaw code mode. When enabled, agent runs expose only `exec` and `wait` to the model and hide normal tools behind a QuickJS-WASI catalog bridge.", "tools.codeMode.enabled": - 'Enables generic code mode. Default is off. `true` engages every tool-capable run and fails closed if the runtime is unavailable instead of exposing the full tool list. `"auto"` engages only models whose catalog flags `compat.codeMode: "preferred"`.', + 'Enables generic code mode. Default is `"auto"`, which engages only models whose catalog flags `compat.codeMode: "preferred"`. `true` engages every tool-capable run and fails closed if the runtime is unavailable instead of exposing the full tool list. `false` turns code mode off for every run.', "tools.codeMode.runtime": 'Guest JavaScript runtime. Only "quickjs-wasi" is supported.', "tools.codeMode.mode": 'Model-facing surface. Only "only" is supported: expose code-mode `exec` and `wait` and hide normal tools.', diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index f5e5c9340a1c..4586e01170c6 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -181,7 +181,7 @@ export type CodeModeConfig = | boolean | "auto" | { - /** Enable generic OpenClaw code mode. Default: false. "auto" engages it only for models whose catalog compat flags `codeMode: "preferred"`. */ + /** Enable generic OpenClaw code mode. Default: "auto", which engages it only for models whose catalog compat flags `codeMode: "preferred"`. */ enabled?: boolean | "auto"; /** Guest runtime. Only quickjs-wasi is supported. */ runtime?: "quickjs-wasi"; diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index c14b46f0dfa9..a5233cc0b98b 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -2406,7 +2406,8 @@ export const en: TranslationMap = { saveFailed: "The feature setting could not be saved.", codeMode: { title: "Code Mode", - description: "Let agents combine tools in compact, sandboxed JavaScript workflows.", + description: + "Let agents combine tools in compact, sandboxed JavaScript workflows. Auto engages code mode only for models evaluated as strong code-mode performers.", }, swarm: { title: "Swarm", diff --git a/ui/src/pages/labs/labs-page.test.ts b/ui/src/pages/labs/labs-page.test.ts index c2b4335d1dd3..0f1d28317f9d 100644 --- a/ui/src/pages/labs/labs-page.test.ts +++ b/ui/src/pages/labs/labs-page.test.ts @@ -130,10 +130,12 @@ describe("LabsPage", () => { it.each([ { + // The on position restores the shipped "auto" tier, never `true`: Labs + // offers Auto/Off, and force-on stays a config-only power-user state. label: "Code Mode", index: 0, sourceConfig: { tools: { codeMode: { enabled: false } } }, - expectedPatch: { tools: { codeMode: { enabled: true } } }, + expectedPatch: { tools: { codeMode: { enabled: "auto" } } }, note: "labs: update codeMode", }, { @@ -169,7 +171,7 @@ describe("LabsPage", () => { expectedPatch: { logging: { audit: { messages: "direct" } } }, note: "labs: update auditMessages", }, - ])("writes true at the registered config path when enabling $label", async (testCase) => { + ])("writes the on value at the registered config path when enabling $label", async (testCase) => { const { page, runtimeConfig } = await mountPage(testCase.sourceConfig); const toggle = labToggle(page, testCase.index, testCase.label); @@ -228,6 +230,47 @@ describe("LabsPage", () => { }); }); +describe("LabsPage code mode enablement", () => { + // Mirrors resolveCodeModeConfig: the shipped default is "auto", so the row + // reads as on until an explicit `false` opts out. `true` stays a valid + // config-only force-on and must also read as on. + it.each([ + { label: "unset", config: {}, expected: true }, + { + label: "object without enabled", + config: { tools: { codeMode: { timeoutMs: 5000 } } }, + expected: true, + }, + { label: "explicit true", config: { tools: { codeMode: { enabled: true } } }, expected: true }, + { + label: "explicit disabled", + config: { tools: { codeMode: { enabled: false } } }, + expected: false, + }, + { label: "boolean shorthand false", config: { tools: { codeMode: false } }, expected: false }, + { label: "auto shorthand", config: { tools: { codeMode: "auto" } }, expected: true }, + ])("reads $label as $expected", async ({ config, expected }) => { + const { page, provider } = await mountPage(config); + + expect(codeModeToggle(page).checked).toBe(expected); + provider.remove(); + }); + + it("writes an explicit false when disabling the shipped default", async () => { + const { page, runtimeConfig } = await mountPage({}); + const toggle = codeModeToggle(page); + + 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: { codeMode: { enabled: false } } }, + note: "labs: update codeMode", + }); + }); +}); + describe("LabsPage tool search enablement", () => { const toolSearchIndex = LAB_FEATURES.findIndex((feature) => feature.id === "toolSearch"); diff --git a/ui/src/pages/labs/labs-registry.ts b/ui/src/pages/labs/labs-registry.ts index a0526024cad6..d202b4f855bc 100644 --- a/ui/src/pages/labs/labs-registry.ts +++ b/ui/src/pages/labs/labs-registry.ts @@ -47,12 +47,23 @@ export const LAB_FEATURES = [ description: () => t("labsPage.codeMode.description"), docsUrl: "https://docs.openclaw.ai/tools/code-mode", configPath: ["tools", "codeMode", "enabled"], - onValue: true, + // The on position writes the shipped "auto" tier, never `true`: Labs offers + // Auto/Off, and force-on for unevaluated models stays a config-only choice. + onValue: "auto", offValue: false, - // "auto" engages code mode per model catalog flag; it must read as on so - // the toggle does not silently narrow an operator's deliberate auto tier. activeValues: [true, "auto"], - readEnabled: null, + // Mirrors resolveCodeModeConfig: the shipped default is "auto", so an unset + // gate reads as on; only an explicit `false` (shorthand or leaf) reads off. + // `true` remains a valid config-only force-on and must also read as on. + readEnabled: (raw) => { + if (typeof raw === "boolean") { + return raw; + } + if (raw && typeof raw === "object" && !Array.isArray(raw)) { + return (raw as Record).enabled !== false; + } + return true; + }, enableAlso: null, restartHint: null, },