9.1 KiB
9.1 KiB
summary, read_when
| summary | read_when | |||
|---|---|---|---|---|
| Refactor plan: exec host routing, node approvals, and headless runner |
|
Exec host refactor plan
Goals
- Add
exec.host+exec.securityto 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
/elevatedas 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.runinside the macOS app; node 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)
- Resolve
exec.host(tool param → agent override → global default). - Resolve
exec.securityandexec.ask(same precedence). - If host is
sandbox, proceed with local sandbox exec. - If host is
gatewayornode, apply security + ask policy on that host.
Default safety
- Default
exec.host = sandbox. - Default
exec.security = denyforgatewayandnode. - 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 whenhost=node.
Config keys (global)
tools.exec.hosttools.exec.securitytools.exec.asktools.exec.node(default node binding)
Config keys (per agent)
agents.list[].tools.exec.hostagents.list[].tools.exec.securityagents.list[].tools.exec.askagents.list[].tools.exec.node
Alias
/elevated on= settools.exec.host=gateway,tools.exec.security=fullfor 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):
{
"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.
askFallbackapplies only whenaskis required and no UI is reachable.- File permissions:
0600.
Runner service (headless)
Role
- Enforce
exec.security+exec.asklocally. - 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)
- Node service receives
system.runfrom gateway. - Node service connects to the local socket and sends the prompt/exec request.
- App validates peer + token + HMAC + TTL, then shows dialog if needed.
- App executes the command in UI context and returns output.
- 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
nodeIdfrom Bridge pairing. - Binding model:
tools.exec.noderestricts the agent to a specific node.- If unset, agent can pick any node (policy still enforces defaults).
- Node selection resolution:
nodeIdexact matchdisplayName(normalized)remoteIpnodeIdprefix (>= 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 tailExec denied (node=<id>, id=<runId>, <reason>)
Transport
Option A (recommended):
- Runner sends Bridge
eventframesexec.started/exec.finished. - Gateway
handleBridgeEventmaps these intoenqueueSystemEvent.
Option B:
- Gateway
exectool handles lifecycle directly (synchronous only).
Exec flows
Sandbox host
- Existing
execbehavior (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.invokewithsystem.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|offremains a shortcut forhost=gateway security=full.
Cross-platform story
- The runner service is the portable execution target.
- UI is optional; if missing,
askFallbackapplies. - 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
/execslash command and keep/elevatedalias.
Phase 2: approvals store + gateway enforcement
- Implement
exec-approvals.jsonreader/writer. - Enforce allowlist + ask modes for
gatewayhost. - 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
enqueueSystemEventfor 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
askFallbackis respected. - Long-running commands: rely on timeout + output caps.
- Multi-node ambiguity: error unless node binding or explicit node param.