| --- |
| 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:** `~/.openclaw/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:** `~/.openclaw/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: `~/.openclaw/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": "~/.openclaw/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 `~/.openclaw/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>, id=<runId>)` |
| - `Exec finished (node=<id>, id=<runId>, code=<code>)` + optional output tail |
| - `Exec denied (node=<id>, id=<runId>, <reason>)` |
|
|
| ### 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=<sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` |
| - Per-agent, per-session overrides; non-persistent unless saved via config. |
| - `/elevated on|off|ask|full` remains a shortcut for `host=gateway security=full` (with `full` skipping approvals). |
|
|
| ## 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) |
|
|