docs(research): missions DO call tools — the claim was wrong, and the truth is worse

Deep research into "missions can't call tools at all", which I wrote and
which is false. docs/TOOL-CALL-ARCHITECTURE.md has the full findings.

WHAT IS ACTUALLY TRUE

Three of the four mission paths end in `claude -p` with Claude Code's own
toolset and permissions PRE-ACCEPTED:

  solo microVM      Read Edit Write Bash Agent   --permission-mode acceptEdits
  composed microVM  same, per node               same
  direct session    Read Edit Write Bash         acceptEdits

So the position is not "no tools". It is: mission agents run Bash and Write
with permissions pre-accepted, and nothing in this platform can gate them.
That is a stronger finding than the one it replaces — "can't call tools"
sounds like a missing feature; "calls tools freely, ungated, and mostly
unobserved" is a security posture, and it is ours.

Observe and gate are different and both are partial. vm_tool_tap is a
PostToolUse hook: it fires AFTER the tool ran and exit-0s unconditionally,
so it is telemetry and structurally cannot gate. The direct-session tier has
no tap at all. GatePolicy has exactly one enforcement site — the chat loop —
and its approvals key on (session_id, message_id), which no mission phase
can produce.

WHY THE CONTAINER TIER LOOKED TOOL-FREE

`claude_cli` runs `claude -p --output-format json`, which returns a single
final result object, and the provider hardcodes `tool_calls: Vec::new()`.
The calls happen; the transport discards them. The comment reading that
emptiness as "§15 by construction: agents are provisioned tool-free" was
inferring a design property from a serialization choice.

Verified against the deployed Claude Code 2.1.228 rather than assumed:
`--output-format stream-json --verbose` emits `tool_use` blocks with the
tool name and `tool_result` blocks. The calls are fully observable; we ask
for the wrong format.

THE DOOR WE ALREADY BUILT AND NEVER PLUGGED IN

claude_cli.rs is OURS — upstream zeroclaw-labs/zeroclaw has no such file —
and so is 88eef99d4 "claude_cli --mcp-config + allow/disallow tools (act via
door)". The provider already accepts mcp_config (claude's own MCP client
reaches our door), tools, and disallowed_tools (lock out the natives so the
gated door is the ONLY actuator). agent.config.example.toml documents the
whole shape.

In the live runtime: clawmates-mcp.json does not exist, there is no
[providers.*] block, and every mission claw binds to claude_cli.default
which sets none of it. My earlier "claude_cli cannot reach MCP, therefore
the skills server is unreachable" was wrong in its reasoning — the
capability is built, documented by us, and never deployed.

Related: we set `agents.<alias>.mcp_bundles`, which configures ZeroClaw's
OWN MCP client for its native loop. A claude_cli agent's actuator is the
claude subprocess, which reads `mcp_config` on the PROVIDER. We were turning
a knob wired to a loop that does not run.

UPSTREAM

218 commits behind. No upstream work on claude_cli (the file is ours). ACP
already exists in the fork; the three new commits are workspace-default and
localization fixes, not new capability. The one item worth pulling is
"feat(plugins): add shared egress policy foundation (#9137)" — a network
guard with DNS pinning and metadata-address blocking, defence for the egress
problem we have not solved.

Stale claims corrected in place, in topology_exec.rs and the runtime config,
so the codebase stops asserting the thing that is false.

Recommended order, cheapest first: stream-json for observability; the
PreToolUse hook for a real gate (it FIRES under claude -p per vm_stop_gate,
and has zero call sites); then deploy the door. The executor swap is NOT
recommended — the blockers are structural, not wiring, and the cheap fixes
deliver what it was wanted for.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-19 13:06:59 -07:00
co-authored by Claude Opus 5
parent 771092b165
commit ea0b989b3f
3 changed files with 199 additions and 11 deletions
+21 -8
View File
@@ -7,11 +7,22 @@
//! gateway, opens `/ws/chat?agent=<alias>`, sends the role+task+context prompt,
//! and streams the turn's events back into a [`TurnOutcome`].
//!
//! **§15 by construction:** the agents are provisioned tool-free (every
//! sensitive capability is a gated Clawmates MCP tool — the "door"), so a turn
//! takes no sandbox-leaving action here. If the gateway nonetheless emits an
//! `approval_request`, we record it as a **blocked** `GatedAction` and end the
//! turn — we never auto-approve.
//! **These agents are NOT tool-free.** That claim stood here for months and is
//! false — see `docs/TOOL-CALL-ARCHITECTURE.md`. It was inferred from a frame
//! stream that carried no tool events, and the emptiness has a different cause:
//! `claude_cli` runs `claude -p --output-format json`, which returns a single
//! final result object, and the provider hardcodes `tool_calls: Vec::new()`.
//! The agent calls Claude Code's own tools; the transport discards them.
//! `--output-format stream-json` emits `tool_use`/`tool_result` blocks —
//! verified against the deployed Claude Code 2.1.228.
//!
//! The door-shaped provider that WOULD make this true (`--mcp-config` +
//! `--disallowedTools` on the natives) is built and documented in
//! `agent.config.example.toml`, and is not deployed: mission claws bind to
//! `claude_cli.default`, which sets none of it.
//!
//! If the gateway emits an `approval_request` we still record it as a
//! **blocked** `GatedAction` and end the turn — we never auto-approve.
use std::collections::HashMap;
use std::sync::Arc;
@@ -136,9 +147,11 @@ pub struct ToolTrace {
/// `chunk`, `done` and `session_start` and no tool frames at all. That is
/// not a protocol mismatch — `tool_call` is in the deployed binary
/// (`zeroclaw-gateway/src/ws.rs` emits `{"type":"tool_call","id","name",
/// "args"}`) — it is §15: these agents are provisioned tool-free behind the
/// MCP door, so they call nothing. The histogram is what let us tell those
/// two apart, which was its whole purpose.
/// "args"}`) — and it is NOT that the agents are tool-free, which is what
/// this comment used to say. `claude_cli` asks for `--output-format json`,
/// so the subprocess's tool calls never reach the gateway to be framed.
/// The histogram still does its job: it distinguishes "no frames" from
/// "frames we do not recognise", and the answer was the former.
pub unmatched: std::collections::BTreeMap<String, u32>,
}