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]>
274 lines
14 KiB
TOML
274 lines
14 KiB
TOML
# Phase-1 runtime config for a tenant: one provider model + a multi-agent
|
|
# role-cast. ONE container hosts the whole cast; Clawmates' cm-orchestrator owns
|
|
# the topology graph and drives a specific role by alias over /ws/chat?agent=...
|
|
#
|
|
# Secrets are NOT committed — the API key is injected at runtime via env
|
|
# ZEROCLAW_providers__models__groq__default__api_key=<key>
|
|
# (double-underscore = config nesting). Swap groq→anthropic for prod.
|
|
|
|
# Mark onboarding complete (headless equivalent of the browser Quickstart) so
|
|
# the agents will answer; otherwise /ws/chat returns NEEDS_ONBOARDING.
|
|
[onboard_state]
|
|
quickstart_completed = true
|
|
|
|
# Reasoning backend = the local Claude Code CLI (native `claude_cli` provider).
|
|
# The daemon spawns `claude -p --output-format json` per turn, authed by
|
|
# CLAUDE_CODE_OAUTH_TOKEN (injected at runtime) — i.e. the Claude subscription,
|
|
# no per-minute API TPM ceiling. Leave model unset to use the subscription
|
|
# default; set e.g. model = "haiku" to force a model.
|
|
[providers.models.claude_cli.default]
|
|
model = "haiku"
|
|
|
|
# claude_cli ACTING through the §15 door (subscription agent that ACTS, not just
|
|
# reasons). claude -p has no tool API ZeroClaw can drive, so instead we let
|
|
# Claude Code's OWN MCP client reach the door: `--mcp-config` points at a JSON
|
|
# file describing the door as an http MCP server; `tools` (--allowedTools)
|
|
# auto-approves the door tool; `disallowed_tools` (--disallowedTools) locks out
|
|
# claude's native tools so the gated door is the ONLY actuator.
|
|
# Create /zeroclaw-data/clawmates-mcp.json (NOT committed; token injected):
|
|
# { "mcpServers": { "clawmates": { "type": "http",
|
|
# "url": "http://clawmates_server_1:8080/mcp",
|
|
# "headers": { "Authorization": "Bearer <per-workspace door token>" } } } }
|
|
# NOTE: model MUST be set via env (ZEROCLAW_providers__models__claude_cli__door__model)
|
|
# — the TOML sub-table parses empty (resolve_default_model gotcha).
|
|
[providers.models.claude_cli.door]
|
|
tools = ["mcp__clawmates__email_send"]
|
|
disallowed_tools = ["Bash", "BashOutput", "KillShell", "Write", "Edit", "NotebookEdit", "Read", "Glob", "Grep", "WebFetch", "WebSearch", "Task"]
|
|
mcp_config = "/zeroclaw-data/clawmates-mcp.json"
|
|
|
|
# An agent that ACTS through the door (uncomment; it needs NO ZeroClaw
|
|
# mcp_bundles — the door comes via claude's own MCP client):
|
|
# [agents.subdoor]
|
|
# model_provider = "claude_cli.door"
|
|
# risk_profile = "toolfree"
|
|
|
|
# --- Extra worker LLMs: Kimi Code (native CLI) and GLM (Zhipu) ---
|
|
# Run a topology WORKER on another model.
|
|
#
|
|
# Kimi Code: subscription-billed. Its coding endpoint (api.kimi.com/coding) is
|
|
# coding-AGENT gated (User-Agent enforced) — a raw OpenAI-compat client gets 403,
|
|
# so we drive Kimi through its OWN CLI via the `kimi_cli` provider (mirrors
|
|
# `claude_cli`): it spawns `kimi -p` headless on the Kimi membership, with Kimi's
|
|
# own agent loop + sub-agents. Auth is the CLI's own ~/.kimi-code/config.toml
|
|
# (default_model + [providers."managed:kimi-code"] type="kimi" with the key) or
|
|
# `kimi login` — no key is passed by ZeroClaw. (HOME=/zeroclaw-data, so the file
|
|
# lives at /zeroclaw-data/.kimi-code/config.toml in the runtime volume.)
|
|
# [providers.models.kimi_cli.default]
|
|
# # binary_path = "kimi" # default; resolved on PATH
|
|
# # yolo = true # -y: auto-approve tool calls (needed for a door agent
|
|
# # # that acts via Kimi's own MCP client, mcp.json)
|
|
# [agents.worker_kimi]
|
|
# model_provider = "kimi_cli.default"
|
|
# risk_profile = "toolfree"
|
|
#
|
|
# GLM (Zhipu) — like Kimi, the GLM Coding Plan is subscription-billed and its
|
|
# raw OpenAI/SDK endpoint is ToS-THROTTLED ("1113 Insufficient Balance"); it is
|
|
# only served inside OFFICIALLY SUPPORTED tools, and Claude Code is the #1 one.
|
|
# So GLM rides our existing `claude_cli` provider via a z.ai env override — no
|
|
# new provider, no image rebuild. Needs a fresh HOME + CLAUDE_CODE_OAUTH_TOKEN=""
|
|
# so the container's cached Claude subscription doesn't override the routing
|
|
# (pre-create <HOME>/.claude.json = {"hasCompletedOnboarding":true}). The z.ai
|
|
# key is read from the daemon env via the `$NAME` indirection (keep it out of
|
|
# config). Models: glm-5.2 (Opus-class), glm-4.7 (Sonnet-class), glm-4.5-air.
|
|
# [providers.models.claude_cli.glm]
|
|
# model = "glm-4.7"
|
|
# env = { HOME = "/zeroclaw-data/glm-home", CLAUDE_CODE_OAUTH_TOKEN = "", ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic", ANTHROPIC_AUTH_TOKEN = "$ZAI_API_KEY", API_TIMEOUT_MS = "3000000" }
|
|
# [agents.worker_glm]
|
|
# model_provider = "claude_cli.glm"
|
|
# risk_profile = "toolfree"
|
|
# Flagship tier (Opus-class): a second alias on glm-5.2 (same z.ai env). Costs
|
|
# 2-3x quota vs glm-4.7 — reserve for hard reasoning roles, not routine ones.
|
|
# [providers.models.claude_cli.glm5]
|
|
# model = "glm-5.2"
|
|
# env = { HOME = "/zeroclaw-data/glm-home", CLAUDE_CODE_OAUTH_TOKEN = "", ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic", ANTHROPIC_AUTH_TOKEN = "$ZAI_API_KEY", API_TIMEOUT_MS = "3000000" }
|
|
# [agents.worker_glm5]
|
|
# model_provider = "claude_cli.glm5"
|
|
# risk_profile = "toolfree"
|
|
#
|
|
# Gemini is NOT wired. The platform stripped it: no provider alias, no key
|
|
# forwarded to agent containers, no selector entry. ZeroClaw still has a built-in
|
|
# `gemini` family, so an operator could add it back here — but `provider_alias_for`
|
|
# no longer resolves `gemini*` to it, and it would log as an unrecognised model.
|
|
#
|
|
# Groq — built-in `groq` family (key via ZEROCLAW_providers__models__groq__default__api_key).
|
|
# Fast, but LOW free-tier TPM: each turn carries a ~9.5k-tok system prompt, so 2
|
|
# turns/min can exceed 12k TPM → 429. Keep Groq to light/sequential roles.
|
|
# [providers.models.groq.default]
|
|
# # model via ZEROCLAW_providers__models__groq__default__model=llama-3.3-70b-versatile
|
|
# [agents.worker_groq]
|
|
# model_provider = "groq.default"
|
|
# risk_profile = "toolfree"
|
|
|
|
# --- Phase C: heterogeneous cross-model topologies ---
|
|
# The orchestrator maps a topology node's `role` → an agent alias via the SERVER
|
|
# env ZEROCLAW_AGENT_MAP="role=alias,role=alias" (+ ZEROCLAW_DEFAULT_AGENT for
|
|
# unmapped roles). Point each semantic role at a different model to run ONE
|
|
# topology across vendors, e.g.:
|
|
# ZEROCLAW_AGENT_MAP="coordinator=coordinator,researcher=worker_glm,analyst=worker_kimi,writer=worker_glm5,actor=worker_groq"
|
|
# Then POST /api/topologies/run {task, graph} with those role names. QUOTA CARE:
|
|
# GLM/Kimi plans have 5h/weekly caps + low concurrency (GLM Lite ~1 project at a
|
|
# time) — prefer pipeline (sequential) over swarm/mesh, keep tasks short, and ask
|
|
# the model for brief replies to cap output tokens.
|
|
|
|
# Tool-free profile — agents can only reason + call the MCP door (tools inject
|
|
# AFTER this allowlist filter). `level = "full"` removes ZeroClaw's OWN approval
|
|
# prompts so the agent is autonomous; governance happens at the Clawmates door
|
|
# (auto-policy, no human — "agents control their destiny"). Use "supervised" if
|
|
# you want ZeroClaw to also gate sensitive calls with an in-channel approval.
|
|
[risk_profiles.toolfree]
|
|
level = "full"
|
|
allowed_tools = []
|
|
excluded_tools = ["shell", "file_read", "file_write", "http_request", "browser", "composio"]
|
|
|
|
# Writable coding-loop profile (coder/tester/committer/engineer roles). The
|
|
# `allowed_tools` list is a STRICT allowlist — a tool must be named here to be
|
|
# callable. These MUST be the current ZeroClaw 0.8+ tool names:
|
|
# file_edit — create/overwrite/patch (the real write tool; `file_write`
|
|
# was renamed and now REFUSES on ephemeral workspaces, so a
|
|
# stale `file_write` entry silently leaves agents read-only)
|
|
# content_search — grep across the workspace
|
|
# glob_search — find files by glob
|
|
# git_operations — git status/add/commit/diff/log
|
|
# Regression guard: if you ever see an agent report "I only have file_read" and
|
|
# burn tokens dumping code inline, this list drifted back to pre-0.8 names.
|
|
[risk_profiles.coding_readwrite]
|
|
level = "full"
|
|
# `file_write` creates and overwrites; `file_edit` only replaces an exact
|
|
# existing string and rejects an empty `old_string`, so without file_write an
|
|
# agent literally cannot create a new file. Observed on mission 019fc372: the
|
|
# agent burned its turn reasoning about how to make file_edit create a file
|
|
# ("the tool rejected empty old_string... the shell is restricted") before
|
|
# working around it through `shell`. The comment below has claimed file_write
|
|
# was here since the profile was written; the list never had it.
|
|
allowed_tools = ["file_read", "file_write", "file_edit", "content_search", "glob_search", "git_operations", "shell"]
|
|
excluded_tools = ["http_request", "browser", "composio"]
|
|
|
|
# Read-only research profile (scout/researcher/reviewer/planner roles).
|
|
[risk_profiles.research_readonly]
|
|
level = "full"
|
|
allowed_tools = ["file_read", "content_search", "glob_search"]
|
|
excluded_tools = ["shell", "file_write", "http_request", "browser", "composio"]
|
|
|
|
# Read-only research + public web (papers, docs). Still no shell / no write.
|
|
[risk_profiles.research_web_readonly]
|
|
level = "full"
|
|
allowed_tools = ["file_read", "content_search", "glob_search", "web_search", "web_fetch"]
|
|
excluded_tools = ["shell", "file_write", "http_request", "browser", "composio"]
|
|
|
|
# The role-cast. node.role → agent alias is configured Clawmates-side via
|
|
# ZEROCLAW_AGENT_MAP (e.g. "analyst=researcher"); `scout` is the default
|
|
# fallback (ZEROCLAW_DEFAULT_AGENT) for any unmapped role.
|
|
[agents.coordinator]
|
|
model_provider = "claude_cli.default"
|
|
risk_profile = "toolfree"
|
|
mcp_bundles = ["clawmates_door"]
|
|
|
|
[agents.researcher]
|
|
model_provider = "claude_cli.default"
|
|
risk_profile = "toolfree"
|
|
mcp_bundles = ["clawmates_door"]
|
|
|
|
[agents.writer]
|
|
model_provider = "claude_cli.default"
|
|
risk_profile = "toolfree"
|
|
mcp_bundles = ["clawmates_door"]
|
|
|
|
[agents.worker]
|
|
model_provider = "claude_cli.default"
|
|
risk_profile = "toolfree"
|
|
mcp_bundles = ["clawmates_door"]
|
|
|
|
# Default fallback alias for roles not present above.
|
|
[agents.scout]
|
|
model_provider = "claude_cli.default"
|
|
risk_profile = "toolfree"
|
|
mcp_bundles = ["clawmates_door"]
|
|
|
|
# Step 1B — the Clawmates §15 MCP door. Tool-free agents reach it as their ONLY
|
|
# actuator; MCP tools inject AFTER the empty allowlist above. The door enforces
|
|
# §15 (audit, broker credential custody) but auto-decides via policy (no human;
|
|
# default allow-all → agents are autonomous). The Authorization bearer is a
|
|
# per-workspace door token (a Clawmates session token) — injected on gw-04, NOT
|
|
# committed; the placeholder below is replaced at deploy time.
|
|
[mcp]
|
|
enabled = true
|
|
deferred_loading = false
|
|
|
|
[[mcp.servers]]
|
|
name = "clawmates"
|
|
transport = "http"
|
|
url = "http://clawmates_server_1:8080/mcp"
|
|
tool_timeout_secs = 120
|
|
headers = { Authorization = "Bearer REPLACE_WITH_DOOR_TOKEN" }
|
|
|
|
[mcp_bundles.clawmates_door]
|
|
servers = ["clawmates"]
|
|
|
|
# The skills catalogue (cm-api::mcp_skills). Templates name this bundle and
|
|
# `provision_claw` now honours what they name — but a bundle an agent is
|
|
# assigned and this file does not define resolves to nothing, so the entry
|
|
# below is what makes the assignment mean something.
|
|
#
|
|
# NOTE, and it is the important one: this channel only works for a provider
|
|
# that can surface tool calls. Mission claws run on `claude_cli`, which is
|
|
# TEXT-ONLY (see the note further down this file), so they cannot call an MCP
|
|
# server at all. Their skills arrive as text in the turn prompt instead
|
|
# (`topology_exec::pinned_skills_text`). This entry is for tool-capable agents
|
|
# — and so that an agent assigned the bundle is not assigned a name that
|
|
# resolves to nothing.
|
|
[[mcp.servers]]
|
|
name = "clawmates_skills"
|
|
transport = "http"
|
|
url = "http://clawmates_server_1:8080/mcp/skills"
|
|
tool_timeout_secs = 60
|
|
headers = { Authorization = "Bearer REPLACE_WITH_DOOR_TOKEN" }
|
|
|
|
[mcp_bundles.clawmates_skills]
|
|
servers = ["clawmates_skills"]
|
|
|
|
# NOT DEFINED: `gitea_forge`. Six team templates name it and it resolves to
|
|
# nothing here. Left undefined rather than invented — a plausible-looking
|
|
# definition pointing at the wrong URL would turn a name that resolves to
|
|
# nothing into a server that fails at call time, which is harder to notice, not
|
|
# easier. Agents reach the forge through `git` over HTTPS with the ambient
|
|
# GITEA_TOKEN (see cm-api::mission_workspace), which is why nothing broke.
|
|
|
|
# ── A2A ingress (Phase 2) ───────────────────────────────────────────────────
|
|
# ZeroClaw's Agent2Agent server. These props are set at RUNTIME by cm-api
|
|
# (runtime_provision::enable_a2a_server / publish_claw) when a workspace opts in
|
|
# via POST /api/a2a/settings — they are NOT committed here. Shown for reference:
|
|
#
|
|
# [a2a.server]
|
|
# enabled = true
|
|
# bind = "0.0.0.0" # internal interface only
|
|
# port = 42617
|
|
# public_base_url = "https://api.clawmates.work/api/a2a/<workspace-id>"
|
|
# # the cm-api EDGE, never this daemon
|
|
# [agents.<alias>.a2a]
|
|
# published = true
|
|
# exposed_skills = ["search", "summarize"]
|
|
#
|
|
# SECURITY (must hold at deploy): the daemon's :42617 is NEVER host-published or
|
|
# Traefik-routed. Only cm-api is internet-facing; Traefik routes /api/a2a/** (and
|
|
# the rest of /api/**) to the cm-api service, which authenticates the external
|
|
# caller per-workspace (a2a_tokens), then injects the internal ZEROCLAW_TOKEN
|
|
# when proxying to this daemon's /a2a/{alias}. A leaked global door bearer must
|
|
# never reach an internet-exposed :42617. Cards advertise only the edge URL via
|
|
# public_base_url. Ingress throttle: CLAWMATES_A2A_POLICY=deny (kill switch),
|
|
# CLAWMATES_A2A_RATE_LIMIT=<n>/hour.
|
|
|
|
# NOTE: `claude_cli` agents DO call tools — Claude Code's own — and they can
|
|
# invoke the door too, via `mcp_config` (see [providers.models.claude_cli.door]
|
|
# above). What `--output-format json` does not do is surface those calls BACK to
|
|
# ZeroClaw: it returns one final result object, so the provider reports no tool
|
|
# calls even when the agent made several. `--output-format stream-json` emits
|
|
# `tool_use`/`tool_result` blocks (verified on Claude Code 2.1.228).
|
|
# See docs/TOOL-CALL-ARCHITECTURE.md. An agent that ACTS through the door needs a tool-capable provider
|
|
# (groq/anthropic/openai). Example actor agent (uncomment + provide groq creds
|
|
# via env ZEROCLAW_providers__models__groq__default__{model,api_key}):
|
|
# [providers.models.groq.default]
|
|
# [agents.actor]
|
|
# model_provider = "groq.default"
|
|
# risk_profile = "toolfree"
|
|
# mcp_bundles = ["clawmates_door"]
|