Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B3.7.mp4Per-agent controls diverge as the fleet grows, and legacy downstreams force a static credential back into agent code.
A single enforcement point holding identity, policy, egress, budget and audit — with the credential for legacy systems held there rather than by the agent.
Route every call through one gateway and show the same policy holding for agents that never implemented it.
At one agent the controls live in the agent. At fifty, each team implements them slightly differently, none of them is audited, and the only honest answer to "is default-deny on?" is "in some of them".
cybertravels/gateway.py. Policy, budget, egress and return validation each live where it was convenient to write them, which is four places a second agent's author can skip. They move behind one entry point, and coverage() is the number that finds the agent that still has a direct route — every control passes its own tests either way.Moving controls that each live where they were convenient to write behind a single entry point, and the number that finds the agent still holding a direct route. Plus the cost, stated rather than hidden: one choke point is a single point of failure and a queue.
Per-agent controls diverge as the fleet grows, and legacy downstreams push a static credential back into agent code.
One enforcement point holding identity, policy, egress, budget and audit together.
Share of agent traffic that transits the gateway. Whatever does not is unenforced, whatever the policy says.
Measure this
| what to measure | denominator or target |
|---|---|
| agent traffic that transits the gateway | share; whatever does not is unenforced whatever the policy says |
one agent fifty agents
+--------------+ +-----+ +-----+ +-----+ +-----+
| controls in | | a1 | | a2 | | ... | | a50 |
| the agent | +--+--+ +--+--+ +--+--+ +--+--+
+--------------+ | | | |
+-------+---+---+-------+
v
+--------------+
| gateway | identity, policy,
+------+-------+ budget, egress, log
v
tools
one place to enforce, one place to audit, one place to turn offMitigates: every threat in this chapter, at one enforcement point.
Everything in Chapters B2 and B3 works. The problem is where it lives.
At one agent, the controls sit in the agent, and that is fine. At fifty, it stops being fine for reasons that have nothing to do with security engineering:
The gateway is the same controls, moved to a point every call must pass through. It holds identity (B2.1–B2.3), policy (B3.1), egress (B3.3), budgets (B3.4) and audit (B2.7). An agent that implements none of them still gets all of them, because the enforcement is no longer the agent's responsibility.
It also solves a problem nothing else does: downstream systems that cannot consume a delegated identity. A legacy database or a vendor API that only understands a static credential forces that credential back into agent code — undoing B2.3 completely. The gateway holds it instead, authorises the user before the call, and presents the static credential onward. The agent never sees it.
The honest cost: the gateway is now a single point of failure and a very attractive target. It has to be operated accordingly.
What this control closes. Not a new control. The same controls, at a point every call passes through — and the only answer to a downstream that cannot consume delegated identity.
A gateway is only a choke point if nothing routes around it, and "nothing routes around it" is not a fact you can read off a route table. The procedure tests reachability from inside the deployment, covers the paths people forget — tool-initiated calls, background jobs, retries — and records each guardrail's action, because a filter set to observe is a filter that is switched on and stopping nothing. This is the file in this repository:
The skill — skills/attestation/llm-gateway-guardrail-verifier/SKILL.md
name: llm-gateway-guardrail-verifier
description: >-
Prove all model traffic leaves through the sanctioned gateway, including
tool-initiated and background calls, and that guardrail policies are
attached and enforced. Use to attest gateway routing, to check whether a
provider endpoint is directly reachable, or to evidence which guardrail
policies are switched on.
allowed-tools: Bash, ReadControls: Control 4 — gateway routing and guardrails
An application-layer gateway configuration is a routing preference. An agent that can open a socket can bypass it by calling the provider directly. If the allowlist is not enforced at the network layer, downgrade this control to PARTIAL and say why.
When a deployment routes model traffic through a gateway and the attestation wants to say so. Check first whether egress is enforced below the application: if it is not, an agent opens a socket and the gateway is advisory, and this control's confidence drops with it.
Input — the fixture committed at the top of scripts/llm_gateway_guardrail_verifier.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
the intended call ALLOWED
unregistered agent denied at identity
verb not permitted denied at policy
exfiltration destination denied at egress
over the per-target ceiling denied at budget
audit entries written: 1
credential held by the agent: never - attached at the gateway
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"deployment_id": "str",
"gateway_enforced": true,
"enforcement_layer": "network|application",
"reachable_provider_findings": [{"endpoint": "str", "path": "direct|tool|background"}],
"guardrail": {"id": "str", "version": "str",
"policies": [{"name": "str", "applied_to": "input|output|both",
"action": "block|anonymize|observe"}]},
"verdict": "PASS|PARTIAL|FAIL"
}
Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons B3.7Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b3-7-the-agent-gateway:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B3.7 | type /b3-7-the-agent-gateway |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B3.7 | type /b3-7-the-agent-gateway in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B3.7 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B3.7 | type $b3-7-the-agent-gateway, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py B3.7Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at B3.7 --out work/cybertravels
python3 scripts/checkpoint.py --at B3.7 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
Five calls hit one gateway. The intended call is allowed and audited with the human principal attached; the unregistered agent, the unpermitted verb, the exfiltration destination and the over-budget call are each denied at the first check that catches them — and the legacy credential is attached at the gateway, never held by the agent. The skill also reports its shape. Its confidence is HIGH only where egress is enforced below the application — the gateway is a choke point because the network makes it one, not because the SDK was configured to point at it, and an application-level base URL is a default, not a control.
Count your agents. If it is more than five, work out how you would currently answer 'is egress control on for all of them' — and how long that would take.
Tools used
Skill source: skills/attestation/llm-gateway-guardrail-verifier