This is the safety-critical page. The handoff that drove this audit specifically demanded depth here. Permission parity between Claude SDK and Pi is not complete — and the multi-CLI dispatch path bypasses OS sandboxing entirely. Read carefully.

The three layers

Every Orion tool call passes through up to three independent controls:
LayerWhat it gatesWhere it lives
L1 — SDK SandboxFilesystem paths Bash can touch (OS-level)SDK options + sandbox-exec/@carderne/sandbox-runtime
L2 — canUseTool / Pi middlewareWhether the tool runs at all (allow/deny/prompt)canUseTool.mjs closure (Claude); pi-permission-middleware.mjs (Pi)
L3 — Capability preferencesUser-configured feature toggles (Settings → Capabilities)applyCapabilityPreferences() in handler.mjs
The two engines diverge at L2. Read on.

MCP architecture — Claude SDK vs Pi at a glance

MCP architecture

5 internal MCP servers, all natively wired via createSdkMcpServer() and wrapped as trackingCreateServer() to add to internalServers:
ServerNamespaceSource
orionmcp__orion__*memory/tools.mjs
orion-toolsmcp__orion-tools__*ccw/orion-tools-mcp-server.mjs
canvasmcp__canvas__*canvas/canvas-tools.mjs
terminalmcp__terminal__*created in handler.mjs per query
codexlensmcp__codexlens__*optional, enabled when .codexlens index exists
External MCP servers (from .mcp.json) handled separately by ExternalMcpManager. ✓ VERIFIED at handler.mjs:1647-1652.

Permission flow — Claude SDK path

Three modes the closure runs in:
  • default — every tool call routes through user approval (with auto-trust for internalServers).
  • bypassPermissionscreateBypassCanUseTool(boundCanUseTool) auto-approves everything except AskUserQuestion. Cron uses this.
  • plan — read-only mode (SDK restricts to read-only tools).
The closure is wrapped in try/catch; any unexpected error returns { behavior: 'deny' } for safety. ✓ VERIFIED at canUseTool.mjs:222-257.

Permission flow — Pi path (the gap)

GAP-B1: mcp_tool_permissions DB lookup does not run for Pi.When you configure tool-level permissions in Settings (block/allow/ask), they are stored in the mcp_tool_permissions table and enforced by lookupToolPermission() in canUseTool.mjs:81-100. That call is only made from the Claude SDK path at canUseTool.mjs:238-257.The Pi permission middleware (pi-permission-middleware.mjs:58-78) makes its own classification with NO DB lookup. Every mcp__* tool is treated as {destructive:false, isWrite:false, dangerLevel:'low'} regardless of user configuration.Result: A user who blocks mcp__orion-tools__delegate_to_cli in Settings will see it blocked in Claude sessions but not in Pi sessions (Gemini, Codex, etc.). See Trust → GAP-B1.
canUseTool IS called for Pi sessions (handler.mjs:2091-2097) — but only the Pi sandbox’s promptBridge uses it (to emit synthetic pi-sandbox-* permission requests when path policy misses). The full DB-aware lookupToolPermission codepath never runs in Pi sessions. ✓ VERIFIED

Sandbox layer stack

Layer 1 — cwd + additionalDirectories allowlist
  cwd = effectiveCwd (PARA project or vault root)
  additionalDirectories = [orionDir, appDataDir,
    ~/Library, ~/.local, ~/.cache, ~/Orion, ~/.agent-browser]

Layer 2 — sandbox-exec / bwrap via SDK
  enabled: true
  autoAllowBashIfSandboxed: true
  allowUnsandboxedCommands: true
  excludedCommands: orion:*, ccw:*, gemini:*, codex:*,
                   claude:*, qwen:*, opencode:*

Layer 3 — env isolation for Bash subprocesses
  createEnvIsolationHook() strips ANTHROPIC_*, LANGFUSE_*,
    ORION_*, FIREBASE_*, etc.

Hard-blocks: handled by sandbox-exec policy
GAP-B3: Pi sandbox does NOT wrap MCP-bridged tool handlers.pi-sandbox/fs-wrappers.mjs wraps Pi’s native Read/Write/Edit/Grep/Find/Ls tools — but not MCP-bridged tools. mcp__orion-tools__write_file calls the Zod handler directly via mcp-bridge.mjs:134-146, bypassing assertSandboxPath() and the denyWrite floor.The orion-tools-mcp-server.mjs handler uses validatePath() from shell-utils.mjs which checks control characters and path traversal — but not the denyWrite pattern list. A Pi agent could in principle write to .env files through mcp__orion-tools__write_file if the handler doesn’t otherwise gate it.The risk is structural rather than active (the handlers are Orion-controlled code), but a future tool addition could inadvertently allow credential file writes without triggering the hard-block. See Trust → GAP-B3.

MCP OAuth — three lifetimes

External MCP servers requiring OAuth are managed by a process-scoped singleton in engine/mcp-oauth/oauth-singleton.mjs: Hard rules from mcp-oauth-singleton-ownership.md:
  1. ExternalMcpManager.closeAll() closes transports only. Never calls oauthSingleton.reset() or shutdown().
  2. reset() clears providers/flows/states but keeps the callback server listening.
  3. shutdown() only on sidecar exit. Does NOT delete persistent tokens at {vault}/.orion/mcp-oauth/<server>/tokens.json.
  4. Browser launch is Rust-owned via tauri-plugin-shell — the sidecar emits mcpOAuthRedirect and Rust opens the browser. Sidecar MUST NOT spawn open/xdg-open/cmd /c start.
✓ VERIFIED at oauth-singleton.mjs:485-540.

What’s protected, what isn’t

✓ Protected

  • Credential file writes (Pi sandbox denyWrite floor) — .env, *.pem, **/.ssh/**, **/.aws/**, **/orion.db, **/.orion/mcp-oauth/**
  • OAuth tokens survive shutdown() (so users don’t re-authorize on every launch)
  • Env credential leakage to spawned CCW CLIs (28-key strip in buildSpawnEnv)
  • Claude CLI auth isolation (deliberate CLAUDE_CONFIG_DIR strip so spawned Claude CLI uses user’s own ~/.claude/)
  • Internal MCP servers (internalServers Set + BAKED_IN_TRUSTED_SERVERS) auto-trusted on Claude path

✗ NOT protected

  • mcp_tool_permissions user settings on Pi path (GAP-B1)
  • MCP-bridged file tools wrt denyWrite floor (GAP-B3)
  • CCW external CLI spawned subprocesses’ filesystem write scope (GAP-D1) — see Background
  • Network policy enforcement for @carderne/sandbox-runtime (open question — allowedDomains/deniedDomains passed but enforcement is platform-dependent)

Key files

buildPiCustomTools (L340-473), bridgeMcpTools (L116-160), extractMcpToolDefs (L193-278). The Pi MCP integration. Proxy-vs-legacy gate at L372-377.
createCanUseTool factory (L117-311). lookupToolPermission (L81-100). Internal MCP auto-trust at L222-234. mcp_tool_permissions DB lookup at L238-257.
classifyTool (L58-78) — the mcp__ blanket-trust branch*. wrapWithPermission (L103-178).
index.mjs (factory), path-policy.mjs (assertSandboxPath, denyWrite enforcement), policy-loader.mjs (BUILTIN_DEFAULTS floor L82-127), bash-wrapper.mjs, fs-wrappers.mjs, env-hardening.mjs, prompt-bridge.mjs.
Process-scoped OAuth singleton. Three-lifetime model. Callback server lifecycle.

Next

Frontend Surfaces

How tool calls drive the canvas — and where the UI-level AI-WITH-YOU gate lives.

Trust

The full safety appendix with severity, evidence, and fix paths for every gap.