# GATE-MAP — Mission Agent Tool Call Gate, End to End > Research phase finding. No source files were modified. > Traced from code; every claim cites a file and function. --- ## Overview There are **two distinct gating paths** in this repository, depending on whether the agent is a *chat agent* (ZeroClaw, tool-free, MCP door) or a *mission agent* (`claude -p` inside a container or microVM). This document traces the mission path exclusively, as scoped by the task. The mission path has three layers that execute in order: ``` [GUEST] PreToolUse hook (tool-gate.sh) — blocks inside the container [HOST] Phase runner drain — reads gate records + tap after phase ends [HOST] mission_events INSERT — writes to the `mission_events` DB table ``` --- ## Hop 1 — The PreToolUse Hook Inside the Guest **File:** `crates/cm-api/src/vm_tool_gate.rs` **Executes in:** the guest (container or microVM) ### Installation The host generates a shell script (`hook_script_with`, line 562) and installs it via `install_command_with` (line 819). For container-tier missions the installer is run by `container_tool_hooks::install_with` (`crates/cm-api/src/container_tool_hooks.rs`, line 53), which calls `container_exec::exec_as_root` to run the install script inside the container. The installed file is `/root/toolhooks/tool-gate.sh` (constant `HOOK_DIR = "/root/toolhooks"`). For microVM missions the same script is placed at `/root/toolgate/tool-gate.sh` (constant `GUEST_DIR = "/root/toolgate"`). The settings document written to `/root/toolhooks/settings.json` (or `/root/guest-settings.json` for the microVM tier) registers the hook via: ```json [{ "hooks": [{ "type": "command", "command": "/tool-gate.sh" }] }] ``` (`settings_hook` function, line 809.) ### What the hook does When `claude -p` is about to execute a tool it fires the `PreToolUse` event. The generated shell script: 1. Reads the hook JSON payload from stdin via `node` (the `NODE_EXTRACT` constant, line 789) — extracts `tool_name`, `tool_input.command`, `tool_input.file_path`, and `agent_type` (the subagent role). 2. Optionally checks the **task-permission policy** (`TaskPolicy`, line 297): if `enforce = true`, any tool not in `allowed` is denied; in shadow mode the denial is written to `would-deny.jsonl` and the call is allowed. 3. Checks **role policies** (`ROLE_POLICIES`, line 261): the `verifier` role is denied write tools (`Write`, `Edit`, `MultiEdit`, `NotebookEdit`). 4. Checks **write-tool path rules**: write tools targeting protected paths (`PROTECTED_PATHS`, line 225 — `/root/toolgate`, `/root/toolhooks`, `/root/tap/`, `/root/guest-settings.json`, `/root/.claude/settings`, `.git/hooks/`) are denied. 5. For `Bash` only, iterates over command segments split on `;|&\n` and applies the `RULES` deny list (line 127): `rm -rf /`, force-push, hard-reset onto remote, outbound POST (`curl`/`wget` with body flags), `--dangerously-skip-permissions`, and reads/writes of the hook files themselves. **On denial:** the script writes one JSONL line `{"rule":"","payload":}` to `denied.jsonl` (or `would-deny.jsonl` for shadow task-permission), prints the human reason to stderr, and **exits 2**. Exit 2 is the Claude Code contract for "block this tool call and return the stderr text to the model." **On allow:** exits 0 — the tool call proceeds. **Inert gate:** if `node` is absent, the script writes to `inert` and exits 0 (never fail-closed), making every call allowed while leaving a marker the host can check. --- ## Hop 2 — PostToolUse Tap (Guest, Telemetry Only) **File:** `crates/cm-api/src/vm_tool_tap.rs` **Executes in:** the guest (container or microVM) A second hook (`PostToolUse`) appends one JSON record per completed tool call to `/root/tap/tools.jsonl` (container tier: `/root/toolhooks/tap/`). It **always exits 0** (line 13) — it is an observer, not a participant. A non-zero exit here would feed its stderr back to the model. The tap records tool name, path, session id, bounded response (for command tools only), and the subagent's `agent_type`/`agent_id`. --- ## Hop 3 — Host Drains the Gate and Tap Records **File:** `crates/cm-api/src/phase_runner.rs`, function `drain_finished_container_phases` (line ~601) **Executes in:** the host (server process) After a container-tier phase reaches `completed` or `failed`, the host's sweep (run on a tick) connects to Docker and reads the guest files: 1. **Inert marker** (`drain_inert`): if the file exists the host logs the count and records a `gate.inert` `MissionEvent`. 2. **Denials** (`drain_denied`, line ~657): each `denied.jsonl` line becomes a `gate.denied` `MissionEvent`. The detail is parsed by `vm_tool_gate::denial_detail` (line 386) so the `rule` field is alongside the hook payload. 3. **Would-deny shadow** (`drain_would_deny`, line ~672): each `would-deny.jsonl` line becomes a `gate.would_deny` `MissionEvent`. 4. **Tool tap** (`drain`, line ~707): the tap file is read-then-cleared. Each parsed `Observed` record is passed to `record_vm_tools`. For microVM phases the tap and gate records are read from inside the VM over SSH before the VM is destroyed (`phase_runner.rs`, line ~1852 and ~1855). --- ## Hop 4 — DB Write: `mission_events` Table **File:** `crates/cm-api/src/mission_events.rs`, function `record` / `record_all` (line ~126) **Executes in:** the host `record_vm_tools` (phase_runner.rs line 2423) builds `MissionEvent` structs and calls `mission_events::record_all`. Each event is inserted into the **`mission_events`** Postgres table: ```sql INSERT INTO mission_events (mission_id, phase_id, run_id, agent_id, kind, target, detail) VALUES ($1, $2, $3, $4, $5, $6, $7) -- subject to a per-phase cap of 400 events for TOOL_CALL / FILE_TOUCH kinds ``` Event kinds recorded for tool calls: - `tool.call` — one per tool invocation (constant `TOOL_CALL`, line 23) - `file.touch` — one per tool that touched a path (constant `FILE_TOUCH`, line 24) - `gate.denied` — one per gate denial - `gate.would_deny` — one per shadow-mode would-have-denied - `gate.inert` — when the gate ran without `node` --- ## Summary Table | Step | Where | File | Function | What happens | |------|-------|------|----------|-------------| | 1 | **Guest** | `vm_tool_gate.rs` | `hook_script_with` → `tool-gate.sh` | PreToolUse hook evaluates deny rules; exits 2 (block) or 0 (allow) | | 2 | **Guest** | `vm_tool_tap.rs` | PostToolUse tap script | Appends tool call record to `tools.jsonl`; always exits 0 | | 3a | **Host** | `container_tool_hooks.rs` | `drain_denied` / `drain_would_deny` / `drain_inert` / `drain` | Reads gate denial + tap files out of the container via Docker exec | | 3b | **Host** | `phase_runner.rs` | `drain_finished_container_phases` | Calls drain functions; calls `record_vm_tools` with parsed `Observed` | | 4 | **Host** | `mission_events.rs` | `record_all` | INSERTs `tool.call`, `file.touch`, `gate.denied`, … into `mission_events` | --- ## Key Design Notes - **Pre-execution gate is not the §15 human-approval gate.** The comment at the top of `vm_tool_gate.rs` is explicit: the §15 `GatePolicy` (chat path, keys on `session_id`/`message_id`) has no mission-shaped form, and a hook that blocked the agent's process while waiting for a human would wedge the turn. The gate is a deterministic deny list only. - **The hook is installed by the host, baked at install time.** The task policy is compiled into the script at install time, not re-read from a file at call time, so the guest cannot persuade itself to use a different policy file (`hook_script_with`, line 562 comment). - **Container and microVM tiers share the same gate/tap code** but differ in how the host drains: containers via `container_exec::connect` (DOCKER_HOST-aware); microVMs by SSH before the VM is destroyed. - **DB table is `mission_events`, not `run_events` / `audit_log`.** Chat-path tool calls land in `run_events` (written by `Runtime::emit`, `cm-runtime/src/runtime.rs:624`). Mission-path tool calls land in `mission_events`. The `audit_log` table is only for the MCP door path (chat agents using the ZeroClaw MCP door), not for mission agents. --- ## Follow-up Items TASK: INT-01 — Verify that `record_vm_tools` attribution logic correctly maps tap session IDs to agent IDs under multi-agent phases TASK: INT-02 — Document the MCP door path (chat agents) in a parallel DOOR-MAP.md for comparison TASK: INT-03 — Confirm `CLAWMATES_TASK_PERMISSION=enforce` flag is set (or note it is still shadow) in production config