Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B1.15.mp4The approval gate is recorded as a control and operates as a click. At volume it approves everything, including the one request that mattered.
Approval reserved for irreversible actions, with everything else bounded by policy (B3.6).
Push approval volume up and measure the point at which review quality collapses.
Approval is a genuine control at four requests a day. At four hundred it is a person clicking approve, and the control has quietly become a log of things somebody scrolled past.
What it covers. Push approval volume up and measure the point at which review quality collapses.
Why a security engineer needs it. The approval gate is recorded as a control and operates as a click. At volume it approves everything, including the one request that mattered. The control it builds is: approval reserved for irreversible actions, with everything else bounded by policy (B3.6).
This is a risk lesson: it shows the failure happening before anything tries to stop it, so the control that follows is answering something you have already watched go wrong.
The approval gate is recorded as a control and operates as a click. At volume it approves everything, including the one that mattered.
Reserve approval for irreversible actions and bound everything else by policy.
Queue depth and median decision time per reviewer. A control that degrades with volume has to be measured against volume.
Measure this
| what to measure | denominator or target |
|---|---|
| queue depth per reviewer | count |
| median decision time per reviewer | minutes, measured against volume |
requests/hour 4 40 400 read carefully yes some no approval is control friction a log the control does not fail loudly. it degrades into a click.
OWASP T10 — Overwhelming Human-in-the-Loop.
Human approval is the control everyone reaches for first. It is placed at the tool call — the right place — and it is genuinely strong for rare, consequential decisions.
Then the system scales, and the arithmetic turns on it.
An agent generates approval requests at machine speed. A human reads them at human speed. When the queue exceeds what a person can actually consider, the behaviour does not degrade gracefully into "slower but careful". It degrades into approving without reading, because the alternative is being the reason nothing shipped.
The failure is invisible from inside the system. Every approval is recorded. The audit trail shows a human decision on every action. The control appears to be operating at 100%, and the thing being measured — that a human clicked — is not the thing anyone cared about.
There is a second-order effect worth naming: an attacker who wants one approval can manufacture the volume that makes it likely. Generate two hundred benign requests, put the one that matters at position 173, and the control has been defeated by arithmetic rather than by cleverness.
Approval is a control for irreversible actions. Used as a general-purpose gate it becomes a click, and the risk register still counts it.
Where this lands on the reference architecture. `` ingress -> orchestrator -> agent_runtime -> model | | messaging tools / mcp | | knowledge / memory egress identity + policy wrap every call · observability records it ``
Approval quality against volume, and the position an attacker chooses.
Approval coverage at CyberTravels reads 100% at every volume, because coverage measures whether a human was asked. The skill models what reading does instead, and finds the volume at which the gate stops being one.
The skill — skills/threats/approval-queue-saturation-model/SKILL.md
name: approval-queue-saturation-model
description: >-
Model what happens to an approval gate as volume rises — coverage staying at
100% while actual review collapses — and find the volume at which the control
stops working. Use when reviewing human-in-the-loop design, approval fatigue,
or a gate that has never been measured.
allowed-tools: Read, Grep, GlobAn approval gate reports the same number at every volume, because coverage measures whether a human was asked. What degrades is whether they read it, and an attacker who can choose position only has to generate enough requests to sit behind.
Any control whose enforcement is a person: approvals, exception reviews, alert triage sign-off, change advisory.
1 — Measure current volume, per reviewer per day. Not the design volume — the observed one, at peak rather than mean.
2 — Establish the reading budget. How many items can one reviewer consider properly in a shift? Ask them; the number is usually between 20 and 30 and it is always far below the queue.
3 — Model detection against position. Place a malicious item at various depths and compute the probability it is actually read. The curve falls off a cliff at the reading budget, not gradually.
4 — Note who controls position. If a requester can generate the items in front of theirs, depth is attacker-chosen and the average case is irrelevant.
5 — Report the two numbers that change the design. Items per reviewer per day, and the reading budget. The gap between them is the finding, and it points at routing by reversibility rather than at hiring.
Input — the fixture committed at the top of scripts/approval_queue_saturation_model.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:
daily volume considered stamped caught missed
10 10 0 1 0
25 25 0 1 0
100 25 75 0 1
500 25 475 0 1
At every volume the audit trail shows a human approval on 100% of
actions. The control reports full coverage in all four rows.
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.
{
"volume": {"per_reviewer_per_day": 0, "measured_at": "peak|mean"},
"reading_budget": 0,
"coverage_reported": 1.0,
"detection_by_depth": [{"depth": 0, "read_probability": 0.0}],
"position_attacker_controlled": true,
"gap": 0
}
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 B1.15Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b1-15-overwhelming-the-human-in-the-loop:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B1.15 | type /b1-15-overwhelming-the-human-in-the-loop |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B1.15 | type /b1-15-overwhelming-the-human-in-the-loop in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B1.15 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B1.15 | type $b1-15-overwhelming-the-human-in-the-loop, 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 B1.15Neither 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 B1.15 --out work/cybertravels
python3 scripts/checkpoint.py --at B1.15 --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.
Approval coverage reads 100% at every volume while the malicious request is caught only when the queue is small enough to be read — and an attacker choosing the position needs only to generate the requests in front of it.
Count how many approval requests one of your agents generates per day and ask the person approving them how many they read in full. The gap between those two numbers is the control's real coverage.
Skill source: skills/threats/approval-queue-saturation-model