The three layers
Every Orion tool call passes through up to three independent controls:| Layer | What it gates | Where it lives |
|---|---|---|
| L1 — SDK Sandbox | Filesystem paths Bash can touch (OS-level) | SDK options + sandbox-exec/@carderne/sandbox-runtime |
| L2 — canUseTool / Pi middleware | Whether the tool runs at all (allow/deny/prompt) | canUseTool.mjs closure (Claude); pi-permission-middleware.mjs (Pi) |
| L3 — Capability preferences | User-configured feature toggles (Settings → Capabilities) | applyCapabilityPreferences() in handler.mjs |
MCP architecture — Claude SDK vs Pi at a glance
MCP architecture
- Claude SDK side
- Pi side
5 internal MCP servers, all natively wired via
External MCP servers (from
createSdkMcpServer() and wrapped as trackingCreateServer() to add to internalServers:| Server | Namespace | Source |
|---|---|---|
orion | mcp__orion__* | memory/tools.mjs |
orion-tools | mcp__orion-tools__* | ccw/orion-tools-mcp-server.mjs |
canvas | mcp__canvas__* | canvas/canvas-tools.mjs |
terminal | mcp__terminal__* | created in handler.mjs per query |
codexlens | mcp__codexlens__* | optional, enabled when .codexlens index exists |
.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). - bypassPermissions —
createBypassCanUseTool(boundCanUseTool)auto-approves everything exceptAskUserQuestion. Cron uses this. - plan — read-only mode (SDK restricts to read-only tools).
{ behavior: 'deny' } for safety. ✓ VERIFIED at canUseTool.mjs:222-257.
Permission flow — Pi path (the gap)
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
- Claude SDK
- Pi
MCP OAuth — three lifetimes
External MCP servers requiring OAuth are managed by a process-scoped singleton inengine/mcp-oauth/oauth-singleton.mjs:
Hard rules from mcp-oauth-singleton-ownership.md:
ExternalMcpManager.closeAll()closes transports only. Never callsoauthSingleton.reset()orshutdown().reset()clears providers/flows/states but keeps the callback server listening.shutdown()only on sidecar exit. Does NOT delete persistent tokens at{vault}/.orion/mcp-oauth/<server>/tokens.json.- Browser launch is Rust-owned via
tauri-plugin-shell— the sidecar emitsmcpOAuthRedirectand Rust opens the browser. Sidecar MUST NOT spawnopen/xdg-open/cmd /c start.
oauth-singleton.mjs:485-540.
What’s protected, what isn’t
✓ Protected
- Credential file writes (Pi sandbox
denyWritefloor) —.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_DIRstrip so spawned Claude CLI uses user’s own~/.claude/) - Internal MCP servers (
internalServersSet +BAKED_IN_TRUSTED_SERVERS) auto-trusted on Claude path
✗ NOT protected
mcp_tool_permissionsuser settings on Pi path (GAP-B1)- MCP-bridged file tools wrt
denyWritefloor (GAP-B3) - CCW external CLI spawned subprocesses’ filesystem write scope (GAP-D1) — see Background
- Network policy enforcement for
@carderne/sandbox-runtime(open question —allowedDomains/deniedDomainspassed but enforcement is platform-dependent)
Key files
src-tauri/sidecar/engine/mcp-bridge.mjs
src-tauri/sidecar/engine/mcp-bridge.mjs
buildPiCustomTools (L340-473), bridgeMcpTools (L116-160), extractMcpToolDefs (L193-278). The Pi MCP integration. Proxy-vs-legacy gate at L372-377.src-tauri/sidecar/permissions/canUseTool.mjs
src-tauri/sidecar/permissions/canUseTool.mjs
createCanUseTool factory (L117-311). lookupToolPermission (L81-100). Internal MCP auto-trust at L222-234. mcp_tool_permissions DB lookup at L238-257.src-tauri/sidecar/engine/pi-permission-middleware.mjs
src-tauri/sidecar/engine/pi-permission-middleware.mjs
classifyTool (L58-78) — the mcp__ blanket-trust branch*. wrapWithPermission (L103-178).src-tauri/sidecar/engine/pi-sandbox/
src-tauri/sidecar/engine/pi-sandbox/
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.src-tauri/sidecar/engine/mcp-oauth/oauth-singleton.mjs
src-tauri/sidecar/engine/mcp-oauth/oauth-singleton.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.