Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/A1.1.mp4A loop with no verifier accepts whatever the model says it did, and a demo that 'works' has never been checked.
Plan, act, verify — with the verifier independent of the thing it checks.
Build the loop and add the independent verifier that decides what it may accept.
The difference between a demo and a system is one line. In the demo, the model decides to call a tool and the tool is called. In the system, the model proposes and separate code decides. Every control in the rest of this commons lives in the gap between those two sentences, and a loop written without the gap has nowhere to put any of them.
cybertravels/runtime.py. execute_tool is the line where the model stops proposing and CyberTravels' own code starts deciding — every control in Function B attaches to it.The reasoning loop in three stages: the model proposes, your code disposes, and something independent of the model verifies. Plus the exit condition, which cannot be the model agreeing that it is done.
A loop that accepts whatever the model says it did has no controls in it, because there is nowhere to put one.
Write the loop in three stages — plan, act, verify — with the verifier independent of the model.
Runs the verifier rejected. Zero means either the loop is perfect or the verifier is not independent, and it is the second one.
Measure this
| what to measure | denominator or target |
|---|---|
| runs the verifier rejected | count per batch; zero means the verifier is not independent, not that the loop is perfect |
PLAN, ACT, VERIFY
model proposes ──► YOUR CODE DECIDES ──► tool
▲ │
│ ▼
└──── verifier ◄── result
(not the model)
a loop with two stages is a demo · the third makes it a harnessThe loop is four lines long and everything else in this commons is a consequence of it.
while not done:
step = model(context) # the model proposes
if step.wants_a_tool:
result = you_decide(step) # YOUR code disposes
context += result
The third line is the whole subject. The model proposes and your code disposes — and a system where those two are the same line has no controls in it, because there is nowhere to put one.
A loop with two stages plans and acts. A loop with three stages also checks, and the check is what separates a harness from a demo:
The independence in the third stage is not a detail. A model asked to grade its own output will grade it generously, and a loop that terminates on the model saying "done" terminates on an opinion. C2.1 measures exactly this and finds a 1.5B model with an independent verifier beating a much larger one without.
Every loop needs one that does not depend on the model agreeing. Yours will have three: the task completed, the step budget ran out, or a control refused. The second and third are the ones that matter, and A1.7 builds them.
The file is cybertravels/runtime.py. Three things live in it, and they are the three stages from the section above:
| stage | the code | what it does |
|---|---|---|
| plan | run() — the while budget.step() loop | asks the model what to do next |
| act | execute_tool() | checks the rules, gets a token, makes the call |
| verify | _verify_result(), at the end of execute_tool | decides whether the answer that came back is acceptable |
1. Open cybertravels/runtime.py and find _verify_result. It is about twenty lines long.
2. Read what goes into it. Three things: the name of the tool, the arguments it was called with, and the result that came back.
3. Notice what is missing from that list: the model. The function is never given one, so it cannot ask one anything. That is the whole idea — the thing that checks the work is not the thing that did the work.
4. Now make it reject something. You do not need a server running for this. Start Python in your checkout and type:
from cybertravels import runtime
# we asked for a refund of 140 and the server says it paid 1400
print(runtime._verify_result("issue_refund", {"amount": 140},
{"amount": 1400, "ok": True}))
It prints the reason it refused:
asked to refund 140, resource server refunded 1400
5. Now give it a correct result and watch it stay quiet:
print(runtime._verify_result("issue_refund", {"amount": 140},
{"amount": 140, "ok": True}))
This prints None. In this function, None means nothing wrong here — there is no reason to give, so it gives none.
What you have just seen. A refund that came back ten times too large was caught by nine lines of arithmetic, with no AI involved. When that happens inside a real run, the rejection is recorded with the label at="verifier", which is how A2.1 tells it apart from the other three places a request can be refused.
The skill below is the one C2.1 uses to review a harness. Run it against the loop you are writing, not after it ships.
The skill — skills/appsec/agentic-harness-loop/SKILL.md
name: agentic-harness-loop
description: >-
Build and check the plan-act-verify loop that turns a model into a harness —
including the exit condition, the parser, and an independent verifier that
decides what the loop is allowed to accept. Use when writing an agentic
pipeline, or when a loop reports success on work nobody checked.
allowed-tools: Read, Grep, GlobA harness is four moves — plan, act, verify, stop — and the security engineer owns three of them. What the loop accepts is decided by the verifier, not by the model, and a loop with no verifier accepts whatever came back and reports success.
Whenever you are about to put a model in a for-loop: triage, remediation, detection authoring, anything that iterates until it is satisfied.
1 — Write the exit condition before the prompt. A loop whose exit is easier than the work will take the exit. Do not offer the model a way to declare itself done; decide that from the outside, on the artefact.
2 — Parse defensively, because parsing is the harness's job. Asked for one line, a small model returns the whole function and a larger one returns a fenced block. Both are reasonable readings. Take what you need from what arrives rather than requiring the model to do you a favour.
3 — Write the verifier as an independent check. Independent means it does not ask the model whether the answer is good. It examines the artefact: does the line use a placeholder, does the test pass, does the exploit stop working.
4 — Test the verifier against a correct answer you did not expect. This is the step that gets skipped. A verifier that accepts only one spelling of correct rejects real work and burns the budget doing it — check %s, :name and $1 before shipping a check for ?.
5 — Test it against a plausible wrong answer. Something that reads like a fix and is not — an escape function wrapped around concatenation. If the verifier accepts it, the loop ships it, reports success, and the trace looks clean.
Input — the fixture committed at the top of scripts/agentic_harness_loop.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 loop, wired: ['plan', 'act', 'verify', 'stop']
backend : replay
steps : 1
accepted : return DB.execute("SELECT * FROM bookings WHERE ref=?", (ref,))
verified : None <- nothing checked it
The loop stopped because the model produced something, which is not the
same as producing something correct. Whatever came back was accepted.
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.
{
"loop": {"moves": ["plan", "act", "verify", "stop"], "max_steps": 0, "exit_owned_by": "harness|model"},
"parse": {"expected": "str", "shapes_seen": ["str"], "strategy": "str"},
"verifier": {"independent": true, "accepts": ["str"], "rejects": ["str"]},
"runs": [{"verifier": "none|present", "steps": 0, "accepted": "str", "verified": null}]
}
Report the unverified run too. The pair is the lesson: the same model and the same prompt, with and without something checking the answer.
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 A1.1Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick a1-1-the-loop:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons A1.1 | type /a1-1-the-loop |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons A1.1 | type /a1-1-the-loop in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons A1.1 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons A1.1 | type $a1-1-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 A1.1Neither 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 A1.1 --out work/cybertravels
python3 scripts/checkpoint.py --at A1.1 --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.
The loop's three stages named, the exit condition stated explicitly, and the verifier identified as independent of the model or flagged as not being so.
Delete the verifier — in cybertravels/runtime.py, replace the body of _verify_result with a single return None, which is the two-stage loop most systems actually ship. Then run the snippet above again: the 1400-for-140 refund now comes back None, meaning acceptable, and the run reports success.
That is the failure mode, and the point is that it does not look like one. Nothing errored. No span says anything is wrong. The trace is shorter and tidier than before, because there is no denied at="verifier" row in it. A reviewer reading that trace sees a clean run.
Then put it back — or keep going, and remember that scripts/lesson.py for A1.2 will tell you the tree has changed and offer you --force. That is the harness protecting your edit, not an error.
Tools used
Skill source: skills/appsec/agentic-harness-loop