Where the Policy Is Enforced
Last reviewed · content updated
IntermediateWhat you'll learn
~18 min- Compare harness, gateway, admission, and organization-policy enforcement points, including their path coverage and override behavior
- State the two enforcement invariants - deny beats allow, and a scope can only narrow - and the one documented exception to each
- Read a tool's own annotations as an untrustworthy hint, and read a raw request as proof that an unenforced boundary was never a boundary
Before the detail — Decision: decide where a rule about an agent’s tools actually lives before it is tested by an attempt, not after. Outcome: a rule that survives a compacted transcript, a manipulated agent, and a request that never goes near the agent’s own reasoning. Artifact: an enforcement-point map for MU-AI-011’s gateway, with one denied-call log line proving each entry. Status of what follows: reusable guidance — every enforcement point named here ships today, dated where it matters. Demonstrated with NON-PRODUCTION classroom artifacts.
Prompt first: audit where your own rules live
Read-only: list every place a rule about this agent's tools iscurrently written down - a system-prompt line, a CLAUDE.mdinstruction, a config file, a gateway policy, an admission check, anorg-wide policy.
For each one, answer: if the agent's own reasoning changed, or itscontext got compacted, or a request reached the target by a path thatskips the agent entirely, would this rule still hold? Mark each YESor NO. For every NO, name where an equivalent deny rule would have tolive instead.The prompt asks for a location, not a restatement of the rule, because a rule and its enforcement point are two different facts. A system-prompt line can say the same words as a gateway policy and be worth nothing next to it.
A rule is worth what its enforcement point is worth
AI Assurance 4.3 (a separate training in this series) asks for a control that lives outside the prompt and can be evidenced there; it left row 3 NEEDS-OWNER and the deployment answer “not yet.” This lesson owns only the enforcement-point map behind that unresolved row.
The harness — the program running the agent’s own loop — is one of them. Claude Code’s own permissions documentation states it plainly: “Boundaries are not stored as rules. The classifier re-reads them from the transcript on each check, so a boundary can be lost if context compaction removes the message that stated it. For a hard guarantee, add a deny rule instead.” A boundary the agent has to remember is a fact about the transcript, not a fact about what the harness will allow — and the harness’s own documentation says so about itself, which is the strongest version of this lesson available. A gateway is another: a service on an enforced path between caller and target, intercepting every request routed through it. Its rule becomes the target’s envelope only when alternative paths are closed or the target requires a gateway-held credential — this training’s own gateway is one such case, closed exactly that way by the fixture later in this lesson. An admission check — a control plane’s own approve-or-reject step before a change lands — and an org-wide policy that a whole account or tenant inherits are the remaining two. None of the four is a sentence anywhere near the model.
A rule that lives only in the harness travels with every agent that runs inside it; a rule that lives at the gateway travels with every path that reaches the target, including one the harness never sees.
Deny beats allow, and a scope can only narrow
Two invariants hold across the harnesses and gateways that publish how they decide. Claude Code’s settings resolve in a fixed order — managed policy, then --settings, then local, then project, then user — and “nothing you set overrides them” at the top of that order; allow rules merge across levels, but “deny and ask rules apply right away” while an allow rule waits for folder trust. Copilot CLI states the same shape for its own flags: “Deny rules always take precedence over allow rules, even when --allow-all is set or a matching approval has been saved in permissions-config.json.” Azure SRE Agent — Microsoft’s agent product — resolves Deny, then Allow, then Ask, then no-match across three scopes, but its precedence is narrower than Claude Code’s or Copilot CLI’s: “Only the global scope can deny tools… can never weaken a global deny.” Azure SRE Agent differs: only global scope can deny; custom-agent and thread scopes may allow but cannot deny or weaken a global denial.
Amazon Web Services (AWS) AgentCore’s own gateway states the reason this matters at all: “Every agent action through Amazon Bedrock AgentCore Gateway is intercepted and evaluated at the boundary outside of agent’s code — ensuring consistent, deterministic enforcement regardless of how the agent is implemented,” specifically “reducing the risk of policy bypass through agent manipulation.” A rule a manipulated agent cannot talk its way around is a rule that never asked the agent’s opinion in the first place.
A scope that could loosen a global deny would turn every project-level config into its own review point; a scope that can only narrow turns the review into one gateway policy, read once.
The hook that overrides deny
The one documented break in “deny always wins” is deliberate, and it is logged. Azure SRE Agent’s own documentation states: “A user-defined hook returning allow overrides policy rules, including global deny… Every override is audit-logged.” Somebody can still write code that lets a specific call through a global deny — but only by writing a named, callable thing that runs on that path, and every time it fires, it leaves a row behind. That is a different shape of risk than an unenforced boundary: this is a named override with an audit-log entry, not an action receipt and not a silent gap. Inventory the hooks and review every override row.
The gateway’s own hints are wrong on purpose
The Model Context Protocol — MCP, the request format most of this training’s gateway traffic speaks — lets a tool server describe its own tools with annotations: readOnlyHint, destructiveHint, idempotentHint, openWorldHint. This training’s gateway speaks the 2026-07-28 revision, the one that retired the earlier handshake and made every request stateless and self-contained; annotations are unchanged by that revision, and the specification’s own rule for a client reading them is unambiguous: “clients MUST consider tool annotations to be untrusted unless they come from trusted servers,” with a default posture of readOnlyHint false, destructiveHint true, idempotentHint false, openWorldHint true — an unannotated tool is presumed destructive until told otherwise. A tool server describes itself; it does not authorize itself.
This training’s own gateway makes the point concrete rather than theoretical. Its tool listing declares delete_backup — a standing deny in the envelope — with "destructiveHint": false, "idempotentHint": true, and send_dispatch_notice — irreversible, two-person, no inverse at any price — with the identical pair. Both are wrong on purpose, and the gateway’s own header says so: the guard ignores every annotation and reads the envelope instead. A gateway that trusted the hint over the policy would have handed both actions a green light straight from the tool that most needs a red one.
That gap is not abstract either. A separate NON-PRODUCTION fixture makes the path failure concrete: the FieldDesk control plane, called here by a different agent, meridian-ops-agent, rather than MU-AI-011 — the lesson carries across both targets. curl-bypass.txt records the guard refusing delete_backup for that agent with exit 4, then a raw call reaching the same endpoint directly:
$ curl -s -X POST http://127.0.0.1:PORT/control \ -d '{"action":"delete_backup","params":{"backup_id":"bk-2026-08-18"}}'{ "dry_run": false, "note": "backup bk-2026-08-18 destroyed (2 remain)", "ok": true, "post_state_hash": "703bad4ed9e913f216b161e7cd8045b19b949f9fb28f550d2a0a4c7a36083367", "pre_state_hash": "dbf3098bde64c1db76c0b91eb9fe3a50cab612656353b4ef9e0e5ec1d047a17e"}The guard’s own receipt log for that hour shows exactly one row, and it is the refusal — the deletion that actually happened left no row at all, because nothing on that path was asked to write one. Once the endpoint required a signed request, curl-bypass-closed.txt records the identical raw call answered with HTTP 403 (Hypertext Transfer Protocol status code for a refused request) and "error": "no valid X-Meridian-Gateway signature: this control plane answers only calls that came through the guard" — the target did not get safer; the second path into it closed. The envelope is a property of the path, not of the tool’s own description of itself, and this fixture is not a MU-AI-011 storm-night call.
Stop and escalate when a target can be reached by any path that does not pass through the enforcement point you are relying on — a raw endpoint behind the same firewall, a second credential, a debug route nobody closed. Whether that alternate path gets closed or the target moves behind the gateway entirely is a call for whoever owns the target’s own network boundary, not for the agent’s operator to make alone.
This training's gateway labels delete_backup with destructiveHint: false and idempotentHint: true. policy/envelope.json declares delete_backup a standing deny. Which one does the guard obey?
Key takeaway
The value of an enforcement point is the set of request paths it can actually block. Harness, gateway, admission, and organization policy have different coverage and precedence; the logged hook is an explicit override, tool annotations are hints, and the FieldDesk BEFORE/AFTER fixture proves path closure. Lesson 2.2 turns MU-AI-011’s limits into numeric bounds.
LEADERSHIP DECISION name the one enforcement point every path to the target must cross, and fund closing every other path into itPRACTITIONER ACTION write the deny as a rule at that enforcement point, not as an instruction in a prompt or a hint on a tool's own descriptionSUCCESS MEASURE zero standing tools reachable by a path that skips the enforcement point - an audit finding avoided