mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 01:49:49 -10:00
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:
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),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user