fix(microvm): --agents tools is a JSON array; the verifier's restriction was never applied
deploy / test (push) Successful in 4m56s
deploy / build (push) Successful in 6m17s

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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-09-18 20:36:33 -05:00
co-authored by Claude Opus 5
parent 794f2124bc
commit cbc9c2d353
+32 -3
View File
@@ -232,7 +232,13 @@ fn agent_definitions() -> serde_json::Value {
// so `acceptEdits` reaches it either way. It is also why the image is // 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 // 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. // 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 // Foreground, against the default since 2.1.198. A background verifier
// lets the lead carry on and write its report before the check has // lets the lead carry on and write its report before the check has
// finished — the finding would arrive after the conclusion. The whole // 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 \ "description": "Reads and searches the codebase to answer a specific \
question. Use when finding something out would otherwise \ question. Use when finding something out would otherwise \
fill the main context with files and search output.", fill the main context with files and search output.",
"tools": "Read, Grep, Glob", "tools": ["Read", "Grep", "Glob"],
"prompt": format!( "prompt": format!(
"You answer one question about this codebase by reading it. Return the \ "You answer one question about this codebase by reading it. Return the \
answer and the paths that support it — not a transcript of your \ 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 /// 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. /// report success, and the report is about a tree nobody reviewed.
#[test] #[test]
fn the_verifier_cannot_modify_what_it_checks() { fn the_verifier_cannot_modify_what_it_checks() {
let defs = agent_definitions(); 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"] { for forbidden in ["Edit", "Write"] {
assert!( assert!(
!tools.contains(forbidden), !tools.contains(forbidden),