Skip to content

Enforcement Positions — 3 Rings + 2 Cross-Cutting Layers

Enforcement Positions — 3 Rings + 2 Cross-Cutting Layers

Section titled “Enforcement Positions — 3 Rings + 2 Cross-Cutting Layers”

Status: Concept. Canonical taxonomy of enforcement positions per ADR 0001 — Rings, not layers. Date: 2026-05-05 (aligned to Strategy Memo v3 framing 2026-05-07). Strategic frame: TapPass Strategy Memo v3 §06. The enforcement plane has five enforcement positions, organized as 3 + 2: three in-process rings, two between-process cross-cutting layers. Rings depend on the surrounding sandbox to enforce; cross-cutting layers are always compulsory.

Naming policy. Rings are inside the agent's process. Cross-cutting layers sit between processes. Together they are the enforcement plane — five distinct interception points for different threat classes. The earlier "5 flat layers" framing conflated these structurally different positions; this doc is the canonical successor. See gateway.md for the chat / SMB-tier framing that consumes this taxonomy.


Five enforcement positions, defense in depth. Three rings sit inside the agent's process; two cross-cutting layers sit between processes. Each catches a different egress path. Rings depend on the surrounding sandbox cooperating; cross-cutting layers are always compulsory because they live where the agent's bytes have to cross a boundary.

Ordering principle. Rings group by what they govern inside the agent (harness / kernel / interpreter). Cross-cutting layers group by what they govern between agent and the world (model API, tool API). Different axes; not a single linear stack.


RingWhere it runsWhat it interceptsBacked by today
Ring 1 — HarnessInside the agent CLI / frameworkTool-call attempts: which tools, with which args (semantic)settings.json-style files, plus per-CLI providers (claude-code, codex, cursor, …)
Ring 2 — KernelOS-enforced (Linux LSM, macOS sandbox-exec, Windows AppContainer)Syscalls, FS access, network, exec, credentialstappass/sandbox/ (OpenShell + nono) — Landlock + seccomp + L7 net via inference.local
Ring 3 — InterpreterLanguage-runtime sandboxCode the agent writes (codemode): imports, network, allocationNew — Monty + V8 isolates + Wasmtime + ephemeral containers

Two cross-cutting layers (between processes)

Section titled “Two cross-cutting layers (between processes)”
Cross-cutting layerWhere it runsWhat it interceptsBacked by today
LLM GatewayHTTP proxy at network boundaryEvery prompt + response — PII/secret/exfil/no-train, capability tokens, budget, ratetappass/gateway/ (built) — Anthropic-native, OpenAI-compat, LiteLLM 100+ providers
MCP BrokerApplication-protocol server (MCP wire format)Every tool call — outbound (agent → tool) and inbound (tool → agent)tappass/gateway/mcp_server.py + per-org MCP registry; schema_acl + loop_guard pipeline steps

Each row catches a different egress path. They compose; a serious deployment uses all five.


The structural difference per ADR 0001:

  • Rings sit inside the agent's process. Each ring's enforcement depends on what the agent's runtime supports — cooperative for harness (the runtime checks itself), compulsory for kernel (the OS rejects regardless), narrow for interpreter (only protects code the agent writes).
  • Cross-cutting layers sit between processes. Always compulsory, always reachable, no runtime-specific cooperation needed. They reach agents we cannot otherwise instrument — code we don't own, third-party MCP servers, BYOK clients, custom integrations.

The flat-5-layer framing hid this distinction. Calling rings and cross-cutting layers "the same thing in different positions" obscures the fact that one layer cooperates with the agent's process while the other is structurally above it.


4. Composition — which positions fire for which call type

Section titled “4. Composition — which positions fire for which call type”
Call typePositions involved
LLM call from a wrapped agentKernel ring (allows network) → Harness ring (env setup, allow-list check) → LLM Gateway (intercepts on egress)
Tool call from a wrapped agentKernel ring → Harness ring → MCP Broker
LLM-generated code executionKernel ring → Harness ring → Interpreter ring
LLM call from an unwrapped consumer (paste-a-base_url path)LLM Gateway only
Tool call from any MCP-aware consumerMCP Broker only (or MCP Broker + the rings if also wrapped)
TierPositions usedPitch
SMB day oneLLM Gateway onlyPaste two env vars; every model call governed
Mid-market+ MCP BrokerAdd per-call tool governance with capability tokens
Enterprise / regulated+ all three ringsFull defense in depth at every position

Same progression as the commercial tiers in gateway.md, expressed at the architecture level.


PositionWhere it lives in the codebase
Harness ringNew — per-CLI providers under tappass/providers/harness/ (planned). Today: settings.json is generated ad hoc.
Kernel ringtappass/sandbox/ (OpenShell, nono backends) + tappass exec -- invocation wrapper
Interpreter ringNew — adapts existing sandbox primitives. Monty integration planned.
LLM Gatewaytappass/gateway/ (built; Anthropic-native, OpenAI-compat, LiteLLM, MCP, capability tokens, JWKS)
MCP Brokertappass/gateway/mcp_server.py plus broker forwarding mode (broker mode is new)

  • Interpreter-ring runtime choice. V8 isolate (Cloudflare Workers style), Pyodide (browser-WASM Python), Monty (Rust + microsecond startup), ephemeral container (E2B), or Modal/Daytona-style ephemeral VMs. Trade-off: latency vs language coverage vs isolation strength. Decision needed before the interpreter ring ships in production.
  • Harness-ring vs. kernel-ring boundary in the dashboard. Today OpenShell mixes kernel rules and harness invocation in one "OpenShell profile." Surface them as separate concepts in the UI (a Sandbox-spec selects one Provider per ring), or keep them coupled? Lean: surface as separate Providers per ADR 0002.
  • Naming for external materials. Internally we say "3 rings + 2 cross-cutting layers" or just "5 enforcement positions." Buyer-facing: "five sandbox positions — harness, kernel, interpreter, plus LLM gateway and MCP broker." Avoid the older "L1–L5" or "5 layers" framing in external decks.