From cbc9c2d3530a6c704bab5f05d21356e8f3ad5c37 Mon Sep 17 00:00:00 2001 From: Omar Sobh Date: Fri, 18 Sep 2026 20:36:33 -0500 Subject: [PATCH] fix(microvm): --agents tools is a JSON array; the verifier's restriction was never applied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2.1.276 canary (mission 01a0b747) exited before its first API call: Error: Invalid --agents configuration: explorer.tools: Invalid input verifier.tools: Invalid input We sent `"tools": "Read, Grep, Glob, Bash"` — frontmatter syntax, where the `--agents` JSON schema takes an array. Claude Code 2.1.243 changed invalid agent definitions from silently ignored to a hard error, which is how the canary caught it. The uncomfortable half of that: every CLI before 2.1.243 DROPPED the definition, so the verifier's whole guarantee — a tool allowlist with no Edit and no Write — has plausibly never been in force on any VM run; the lead's `Agent` calls would have fallen through to a general-purpose subagent. The test that guarded it read the field with `as_str` and would have kept passing on the exact string the CLI was discarding. Both roles now send arrays; the test reads an array; a new test asserts the shape for every role. Server-side only — no rootfs changes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz --- crates/cm-api/src/microvm_executor.rs | 35 ++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/cm-api/src/microvm_executor.rs b/crates/cm-api/src/microvm_executor.rs index 610b9f7..ec6b2e3 100644 --- a/crates/cm-api/src/microvm_executor.rs +++ b/crates/cm-api/src/microvm_executor.rs @@ -232,7 +232,13 @@ fn agent_definitions() -> serde_json::Value { // so `acceptEdits` reaches it either way. It is also why the image is // pinned to 2.1.223: 2.1.222 fixed background subagents being able to // bypass tool restrictions, and this is the tool restriction in question. - "tools": "Read, Grep, Glob, Bash", + // A JSON ARRAY. The comma-separated string is frontmatter syntax, + // not `--agents` syntax, and every CLI before 2.1.243 dropped an + // invalid definition SILENTLY — so from the day this shipped until + // the 2.1.276 canary on 2026-09-18 refused it ("verifier.tools: + // Invalid input"), this restriction may never have been applied. + // The guarantee below is only as real as this line's shape. + "tools": ["Read", "Grep", "Glob", "Bash"], // Foreground, against the default since 2.1.198. A background verifier // lets the lead carry on and write its report before the check has // finished — the finding would arrive after the conclusion. The whole @@ -256,7 +262,7 @@ fn agent_definitions() -> serde_json::Value { "description": "Reads and searches the codebase to answer a specific \ question. Use when finding something out would otherwise \ fill the main context with files and search output.", - "tools": "Read, Grep, Glob", + "tools": ["Read", "Grep", "Glob"], "prompt": format!( "You answer one question about this codebase by reading it. Return the \ answer and the paths that support it — not a transcript of your \ @@ -1115,12 +1121,35 @@ mod tests { } } + /// The CLI's `--agents` schema takes `tools` as an array. 2.1.276 refused + /// the string form with "verifier.tools: Invalid input" and exited before + /// a single API call; every earlier CLI ignored the definition silently. + #[test] + fn every_tools_field_is_an_array_the_cli_accepts() { + for (name, d) in agent_definitions().as_object().expect("object") { + let tools = d.get("tools").unwrap_or_else(|| panic!("{name} has no tools")); + assert!(tools.is_array(), "{name}.tools must be a JSON array, got {tools}"); + assert!( + tools.as_array().unwrap().iter().all(|t| t.is_string()), + "{name}.tools entries must be strings" + ); + } + } + /// A verifier that can edit will fix what it was asked to check and then /// report success, and the report is about a tree nobody reviewed. #[test] fn the_verifier_cannot_modify_what_it_checks() { let defs = agent_definitions(); - let tools = defs["verifier"]["tools"].as_str().expect("a tools allowlist"); + // An ARRAY, as `--agents` requires. `as_str` here would have kept + // passing on the string form the CLI was silently discarding. + let tools: Vec<&str> = defs["verifier"]["tools"] + .as_array() + .expect("a tools allowlist, as a JSON array") + .iter() + .map(|t| t.as_str().expect("tool names are strings")) + .collect(); + let tools = tools.join(", "); for forbidden in ["Edit", "Write"] { assert!( !tools.contains(forbidden),