# 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= # (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 " } } } } # 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 /.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 (Google) — built-in `gemini` API family (no image rebuild). Key via env # ZEROCLAW_providers__models__gemini__default__api_key=; model in # config. gemini-2.5-flash = stable + high free-tier RPM (throttle-friendly); # gemini-3-pro-preview is the flagship. # [providers.models.gemini.default] # model = "gemini-2.5-flash" # [agents.worker_gemini] # model_provider = "gemini.default" # risk_profile = "toolfree" # # 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_gemini,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"] # ── 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/" # # the cm-api EDGE, never this daemon # [agents..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=/hour. # NOTE: `claude_cli` is a TEXT-ONLY provider — `claude -p` doesn't surface # tool-calls back to ZeroClaw, so claude_cli agents reason but can't invoke the # door. 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"]