--- summary: "Refactor plan: exec host routing, node approvals, and headless runner" read_when: - Designing exec host routing or exec approvals - Implementing node runner + UI IPC - Adding exec host security modes and slash commands --- # Exec host refactor plan ## Goals - Add `exec.host` + `exec.security` to route execution across **sandbox**, **gateway**, and **node**. - Keep defaults **safe**: no cross-host execution unless explicitly enabled. - Split execution into a **headless runner service** with optional UI (macOS app) via local IPC. - Provide **per-agent** policy, allowlist, ask mode, and node binding. - Support **ask modes** that work *with* or *without* allowlists. - Cross-platform: Unix socket + token auth (macOS/Linux/Windows parity). ## Non-goals - No legacy allowlist migration or legacy schema support. - No PTY/streaming for node exec (aggregated output only). - No new network layer beyond the existing Bridge + Gateway. ## Decisions (locked) - **Config keys:** `exec.host` + `exec.security` (per-agent override allowed). - **Elevation:** keep `/elevated` as an alias for gateway full access. - **Ask default:** `on-miss`. - **Approvals store:** `~/.clawdbot/exec-approvals.json` (JSON, no legacy migration). - **Runner:** headless system service; UI app hosts a Unix socket for approvals. - **Node identity:** use existing `nodeId`. - **Socket auth:** Unix socket + token (cross-platform); split later if needed. - **Node host state:** `~/.clawdbot/node.json` (node id + pairing token). - **macOS exec host:** run `system.run` inside the macOS app; node host service forwards requests over local IPC. - **No XPC helper:** stick to Unix socket + token + peer checks. ## Key concepts ### Host - `sandbox`: Docker exec (current behavior). - `gateway`: exec on gateway host. - `node`: exec on node runner via Bridge (`system.run`). ### Security mode - `deny`: always block. - `allowlist`: allow only matches. - `full`: allow everything (equivalent to elevated). ### Ask mode - `off`: never ask. - `on-miss`: ask only when allowlist does not match. - `always`: ask every time. Ask is **independent** of allowlist; allowlist can be used with `always` or `on-miss`. ### Policy resolution (per exec) 1) Resolve `exec.host` (tool param → agent override → global default). 2) Resolve `exec.security` and `exec.ask` (same precedence). 3) If host is `sandbox`, proceed with local sandbox exec. 4) If host is `gateway` or `node`, apply security + ask policy on that host. ## Default safety - Default `exec.host = sandbox`. - Default `exec.security = deny` for `gateway` and `node`. - Default `exec.ask = on-miss` (only relevant if security allows). - If no node binding is set, **agent may target any node**, but only if policy allows it. ## Config surface ### Tool parameters - `exec.host` (optional): `sandbox | gateway | node`. - `exec.security` (optional): `deny | allowlist | full`. - `exec.ask` (optional): `off | on-miss | always`. - `exec.node` (optional): node id/name to use when `host=node`. ### Config keys (global) - `tools.exec.host` - `tools.exec.security` - `tools.exec.ask` - `tools.exec.node` (default node binding) ### Config keys (per agent) - `agents.list[].tools.exec.host` - `agents.list[].tools.exec.security` - `agents.list[].tools.exec.ask` - `agents.list[].tools.exec.node` ### Alias - `/elevated on` = set `tools.exec.host=gateway`, `tools.exec.security=full` for the agent session. - `/elevated off` = restore previous exec settings for the agent session. ## Approvals store (JSON) Path: `~/.clawdbot/exec-approvals.json` Purpose: - Local policy + allowlists for the **execution host** (gateway or node runner). - Ask fallback when no UI is available. - IPC credentials for UI clients. Proposed schema (v1): ```json { "version": 1, "socket": { "path": "~/.clawdbot/exec-approvals.sock", "token": "base64-opaque-token" }, "defaults": { "security": "deny", "ask": "on-miss", "askFallback": "deny" }, "agents": { "agent-id-1": { "security": "allowlist", "ask": "on-miss", "allowlist": [ { "pattern": "~/Projects/**/bin/rg", "lastUsedAt": 0, "lastUsedCommand": "rg -n TODO", "lastResolvedPath": "/Users/user/Projects/.../bin/rg" } ] } } } ``` Notes: - No legacy allowlist formats. - `askFallback` applies only when `ask` is required and no UI is reachable. - File permissions: `0600`. ## Runner service (headless) ### Role - Enforce `exec.security` + `exec.ask` locally. - Execute system commands and return output. - Emit Bridge events for exec lifecycle (optional but recommended). ### Service lifecycle - Launchd/daemon on macOS; system service on Linux/Windows. - Approvals JSON is local to the execution host. - UI hosts a local Unix socket; runners connect on demand. ## UI integration (macOS app) ### IPC - Unix socket at `~/.clawdbot/exec-approvals.sock` (0600). - Token stored in `exec-approvals.json` (0600). - Peer checks: same-UID only. - Challenge/response: nonce + HMAC(token, request-hash) to prevent replay. - Short TTL (e.g., 10s) + max payload + rate limit. ### Ask flow (macOS app exec host) 1) Node service receives `system.run` from gateway. 2) Node service connects to the local socket and sends the prompt/exec request. 3) App validates peer + token + HMAC + TTL, then shows dialog if needed. 4) App executes the command in UI context and returns output. 5) Node service returns output to gateway. If UI missing: - Apply `askFallback` (`deny|allowlist|full`). ### Diagram (SCI) ``` Agent -> Gateway -> Bridge -> Node Service (TS) | IPC (UDS + token + HMAC + TTL) v Mac App (UI + TCC + system.run) ``` ## Node identity + binding - Use existing `nodeId` from Bridge pairing. - Binding model: - `tools.exec.node` restricts the agent to a specific node. - If unset, agent can pick any node (policy still enforces defaults). - Node selection resolution: - `nodeId` exact match - `displayName` (normalized) - `remoteIp` - `nodeId` prefix (>= 6 chars) ## Eventing ### Who sees events - System events are **per session** and shown to the agent on the next prompt. - Stored in the gateway in-memory queue (`enqueueSystemEvent`). ### Event text - `Exec started (node=, id=)` - `Exec finished (node=, id=, code=)` + optional output tail - `Exec denied (node=, id=, )` ### Transport Option A (recommended): - Runner sends Bridge `event` frames `exec.started` / `exec.finished`. - Gateway `handleBridgeEvent` maps these into `enqueueSystemEvent`. Option B: - Gateway `exec` tool handles lifecycle directly (synchronous only). ## Exec flows ### Sandbox host - Existing `exec` behavior (Docker or host when unsandboxed). - PTY supported in non-sandbox mode only. ### Gateway host - Gateway process executes on its own machine. - Enforces local `exec-approvals.json` (security/ask/allowlist). ### Node host - Gateway calls `node.invoke` with `system.run`. - Runner enforces local approvals. - Runner returns aggregated stdout/stderr. - Optional Bridge events for start/finish/deny. ## Output caps - Cap combined stdout+stderr at **200k**; keep **tail 20k** for events. - Truncate with a clear suffix (e.g., `"… (truncated)"`). ## Slash commands - `/exec host= security= ask= node=` - Per-agent, per-session overrides; non-persistent unless saved via config. - `/elevated on|off` remains a shortcut for `host=gateway security=full`. ## Cross-platform story - The runner service is the portable execution target. - UI is optional; if missing, `askFallback` applies. - Windows/Linux support the same approvals JSON + socket protocol. ## Implementation phases ### Phase 1: config + exec routing - Add config schema for `exec.host`, `exec.security`, `exec.ask`, `exec.node`. - Update tool plumbing to respect `exec.host`. - Add `/exec` slash command and keep `/elevated` alias. ### Phase 2: approvals store + gateway enforcement - Implement `exec-approvals.json` reader/writer. - Enforce allowlist + ask modes for `gateway` host. - Add output caps. ### Phase 3: node runner enforcement - Update node runner to enforce allowlist + ask. - Add Unix socket prompt bridge to macOS app UI. - Wire `askFallback`. ### Phase 4: events - Add node → gateway Bridge events for exec lifecycle. - Map to `enqueueSystemEvent` for agent prompts. ### Phase 5: UI polish - Mac app: allowlist editor, per-agent switcher, ask policy UI. - Node binding controls (optional). ## Testing plan - Unit tests: allowlist matching (glob + case-insensitive). - Unit tests: policy resolution precedence (tool param → agent override → global). - Integration tests: node runner deny/allow/ask flows. - Bridge event tests: node event → system event routing. ## Open risks - UI unavailability: ensure `askFallback` is respected. - Long-running commands: rely on timeout + output caps. - Multi-node ambiguity: error unless node binding or explicit node param. ## Related docs - [Exec tool](/tools/exec) - [Exec approvals](/tools/exec-approvals) - [Nodes](/nodes) - [Elevated mode](/tools/elevated)