feat(codemode): code mode on by default for MCP composition, code_mode=false escape hatch

#6562 founder decision: code mode for MCP is on by default. [features]
code_mode now defaults to true, so execute_tools is eager from the first
request of a session; direct tools and tool_search stay available (nothing
is stripped). `code_mode = false` (or --disable code_mode) defers
execute_tools behind tool_search again. The flag is session config, so the
pinned prefix stays stable within a session; the definition text is
static. The flag stays listed as experimental so /config shows the switch.

Docs: TOOL_SURFACE.md gains a "Code mode (execute_tools)" section (one
gate, approval suspension, receipts, describe-only discovery, what stays
direct, the escape hatch); CONFIGURATION.md lists code_mode under
[features].

Tests: scripts/dev-cargo.sh test -p codewhale-tui --locked --lib --
codemode execute_tools code_mode_flag tool_preparation computer_use_bundle
computer_use_duplicate requested_tool_mode catalog_advertises -> 35 passed,
0 failed; ... -- core::engine::approval core::engine::tool_catalog
core::engine::turn_loop tool_surface features:: -> 104 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-24 23:37:50 -07:00
1 parent fda77b64ad
commit 18241e3e83
4 files changed
+66 -13

No files matched your search

@@ -380,13 +380,15 @@ fn catalog_build_does_not_append_registry_guidance_to_the_shell_tool() {
fn requested_tool_mode_prefers_model_hint_then_flag_then_direct() {
use crate::features::{Feature, Features};
let off = Features::with_defaults();
assert_eq!(requested_tool_mode(None, &off), ToolMode::Direct);
let mut on = Features::with_defaults();
on.enable(Feature::CodeMode);
// #6562: code mode is the default; `[features] code_mode = false` is the
// escape hatch back to Direct.
let on = Features::with_defaults();
assert_eq!(requested_tool_mode(None, &on), ToolMode::CodeMode);
let mut off = Features::with_defaults();
off.disable(Feature::CodeMode);
assert_eq!(requested_tool_mode(None, &off), ToolMode::Direct);
// Model metadata wins over config, in both directions (Codex parity).
assert_eq!(
requested_tool_mode(Some(ToolMode::Direct), &on),
+10 -3
View File
@@ -231,8 +231,12 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::CodeMode,
key: "code_mode",
// Stays listed as experimental so `/config` shows the escape hatch.
stage: Stage::Experimental,
default_enabled: false,
// #6562: code mode is the default way MCP and plugin tools compose.
// `[features] code_mode = false` is the escape hatch: execute_tools
// goes back to deferred (reachable through tool_search).
default_enabled: true,
},
];
@@ -257,9 +261,12 @@ mod tests {
}
#[test]
fn code_mode_flag_parses_and_defaults_off() {
fn code_mode_flag_parses_and_defaults_on_with_an_off_switch() {
assert_eq!(feature_from_key("code_mode"), Some(Feature::CodeMode));
assert!(!Features::with_defaults().enabled(Feature::CodeMode));
let mut features = Features::with_defaults();
assert!(features.enabled(Feature::CodeMode));
features.apply_map(&BTreeMap::from([("code_mode".to_string(), false)]));
assert!(!features.enabled(Feature::CodeMode));
}
#[test]
+6
View File
@@ -2960,8 +2960,14 @@ web_search = true # enables deferred Web; the flag name is retained for config c
apply_patch = true
mcp = true
exec_policy = true
code_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search
```
`code_mode` is on by default: `execute_tools` is advertised from the first
request and nested calls go through the same permission gate as direct calls
(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set
`code_mode = false` to defer it behind `tool_search` again.
You can also override features for a single run:
- `codewhale --enable web_search`
+43 -5
View File
@@ -82,11 +82,49 @@ MCP tools are dynamic. Successfully connected servers register names such as
must not be presented as available. MCP and plugin tools are deferred unless a
user explicitly names them in `[tools].always_load`.
`execute_tools` is deferred and engine-injected, alongside the synthetic
interpreter tools. It runs a JavaScript program whose only host surface is
`tools.call(name, args)`; nested calls must be read-only and auto-approved,
and anything else aborts the program with a host-owned receipt. It is hidden
from Plan mode and refused under a worker authority envelope.
### Code mode (`execute_tools`)
`execute_tools` is engine-injected, alongside the synthetic interpreter tools.
It runs a JavaScript program whose only host surface is
`await tools.call(name, args)`, and it is the default way to compose several
tool calls — MCP and plugin tools included — without round-tripping every
intermediate result through the conversation. It is hidden from Plan mode and
refused under a worker authority envelope.
- **One gate.** In a session turn every nested call is sent back to the turn
loop and planned exactly like a direct call: deny/allow lists, preparation
(MCP `readOnlyHint`/`destructiveHint`), `tool_call_before` hooks, ask-rules,
Auto-Review, repository law, and the Computer Use consent refusal. MCP calls
run through the session MCP pool. Approving the program grants nothing, so
`execute_tools` itself is auto-approved.
- **Approvals suspend the program.** A nested call that needs approval raises
the normal approval card (named `execute_tools program call: ...`) and the
program waits; allow resumes it, deny fails only that nested call as an
exception the program can catch. Time spent waiting on a decision does not
count against the program's run deadline, which is the turn's remaining
wall clock.
- **Receipts.** The result lists every nested call with its decision (`auto`,
`approved`, `denied`, `refused`) and status (`ok`, `failed`, `refused`,
`in_flight`). A program that hits its deadline still returns the receipt;
`in_flight` calls were cancelled and may have partially run. Each nested
result is `{content, metadata, truncated}`; an oversized result keeps that
shape, and `truncated` names the original size and the spillover file with
the full output.
- **Discovery without re-pinning.** Inside a program,
`tools.call('tool_search', {query})` returns matching deferred tools with
their input schemas and does not activate them, so the request's tool array
and the session-pinned prefix do not change.
- **Stays direct:** `agent`, `workflow`, `request_user_input`, nested
`execute_tools`, interactive shells, sandbox escalation, Computer Use
consent and scripts, and MCP sign-in (`mcp_<server>_authenticate`).
Code mode is on by default (`[features] code_mode = true`), which makes
`execute_tools` eager from the first request; direct tools and `tool_search`
stay available either way. Set `code_mode = false` (or run with
`--disable code_mode`) to go back to deferring `execute_tools` behind
`tool_search`. The flag is session configuration, so the prompt prefix stays
stable within a session. Without an engine turn (sub-agents), a program keeps
the conservative profile: read-only, auto-approved native calls only, no MCP.
### Conversation toolbox cache