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.mdfor the chat / SMB-tier framing that consumes this taxonomy.
1. The thesis
Section titled “1. The thesis”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.
2. The five enforcement positions
Section titled “2. The five enforcement positions”Three in-process rings
Section titled “Three in-process rings”| Ring | Where it runs | What it intercepts | Backed by today |
|---|---|---|---|
| Ring 1 — Harness | Inside the agent CLI / framework | Tool-call attempts: which tools, with which args (semantic) | settings.json-style files, plus per-CLI providers (claude-code, codex, cursor, …) |
| Ring 2 — Kernel | OS-enforced (Linux LSM, macOS sandbox-exec, Windows AppContainer) | Syscalls, FS access, network, exec, credentials | tappass/sandbox/ (OpenShell + nono) — Landlock + seccomp + L7 net via inference.local |
| Ring 3 — Interpreter | Language-runtime sandbox | Code the agent writes (codemode): imports, network, allocation | New — Monty + V8 isolates + Wasmtime + ephemeral containers |
Two cross-cutting layers (between processes)
Section titled “Two cross-cutting layers (between processes)”| Cross-cutting layer | Where it runs | What it intercepts | Backed by today |
|---|---|---|---|
| LLM Gateway | HTTP proxy at network boundary | Every prompt + response — PII/secret/exfil/no-train, capability tokens, budget, rate | tappass/gateway/ (built) — Anthropic-native, OpenAI-compat, LiteLLM 100+ providers |
| MCP Broker | Application-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.
3. Why rings vs cross-cutting (not flat)
Section titled “3. Why rings vs cross-cutting (not flat)”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 type | Positions involved |
|---|---|
| LLM call from a wrapped agent | Kernel ring (allows network) → Harness ring (env setup, allow-list check) → LLM Gateway (intercepts on egress) |
| Tool call from a wrapped agent | Kernel ring → Harness ring → MCP Broker |
| LLM-generated code execution | Kernel 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 consumer | MCP Broker only (or MCP Broker + the rings if also wrapped) |
Customer self-selects depth
Section titled “Customer self-selects depth”| Tier | Positions used | Pitch |
|---|---|---|
| SMB day one | LLM Gateway only | Paste two env vars; every model call governed |
| Mid-market | + MCP Broker | Add per-call tool governance with capability tokens |
| Enterprise / regulated | + all three rings | Full defense in depth at every position |
Same progression as the commercial tiers in gateway.md, expressed at the architecture level.
5. Code references
Section titled “5. Code references”| Position | Where it lives in the codebase |
|---|---|
| Harness ring | New — per-CLI providers under tappass/providers/harness/ (planned). Today: settings.json is generated ad hoc. |
| Kernel ring | tappass/sandbox/ (OpenShell, nono backends) + tappass exec -- invocation wrapper |
| Interpreter ring | New — adapts existing sandbox primitives. Monty integration planned. |
| LLM Gateway | tappass/gateway/ (built; Anthropic-native, OpenAI-compat, LiteLLM, MCP, capability tokens, JWKS) |
| MCP Broker | tappass/gateway/mcp_server.py plus broker forwarding mode (broker mode is new) |
6. Open items
Section titled “6. Open items”- 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.
7. Where this is used
Section titled “7. Where this is used”- Strategy Memo v3 §06 — Pillar 1: the Enforcement Plane.
governed-agents.md§9 — defense-in-depth bypass matrix.gateway.md§5 / §11 — the SMB / mid-market / enterprise tier story (consumes the same taxonomy).- Architecture diagrams (FigJam / Figma) — same ordering.
- Component cards under
build/components/q09-rings-and-cross-cutting/— one component per ring + one pointer per cross-cutting layer.
8. Related ADRs
Section titled “8. Related ADRs”- ADR 0001 — Rings, not layers: why we organize as 3 + 2 instead of flat 5.
- ADR 0002 — Provider per target, not Adapter per ecosystem: how each ring is filled by per-target Providers selected into a Runtime recipe.
- ADR 0003 — Compiled Policy organized by aspect, not by ring: why the IR is content-by-aspect; the same aspect can land at multiple positions.