Skip to content

Week 8 second session: Security

The second session this week is a guest session or a studio. Contribution 2 is due this week.

The fixture files are course-made and synthetic. Every command below uses ordinary python from the standard library; nothing is installed, no model is called and nothing leaves your machine. Work in a dedicated directory as the tools and model access practices describe.

If there is a guest

A practitioner speaks for at most 45 minutes on agent and application security. Write one question beforehand. Afterwards, write one sentence comparing one claim the guest made with evidence from your own triage record. A guest describes their own organization's practice; the course endorses no product.

The rest of the session is part 3 and part 4 of the studio. Do parts 1 and 2 before class in that case: they take about 50 minutes and need only the course checkout.

If there is no guest: studio sequence

Individual work, with a written comparison in place of discussion if you prefer.

Minutes Activity Evidence to keep
0–25 1. Reproduce the inert chain, repair it, verify both directions failing output, your diff, passing output
25–50 2. Scan and triage drill your classes, the verifier output before and after, one wrong agent claim
50–80 3. Your own repository: scan, triage, reproduce one disposition per finding; one reproduction and its regression
80–90 4. Decide one decision line with its evidence

The times are a plan, not a limit. If part 1 is not running after ten minutes, ask for help and start part 2, which does not depend on it.

1. Reproduce the chain and repair the boundary

Read the contract in the notes and candidate.py. Write down what a fresh boundary returns for {"tool": "export", "record": "copied_log"} and what effects holds afterwards. Then, from the course checkout:

COURSE="$PWD"
WORK="$(mktemp -d)"
cp -r lectures/week-08/fixture/. "$WORK"/
cd "$WORK"
export PYTHONDONTWRITEBYTECODE=1
python chain.py > chain-before.txt; echo "exit $?"
python exercise.py > starter-checks.json; echo "exit $?"
cp candidate.py candidate.starter.py
exit 1
exit 1

The export line stops Python from caching compiled modules, so every run reads the file you have just edited. Both commands fail on the starter. Open chain-before.txt and compare it with your prediction: which field shows that the refusal came too late?

Now get a repair. Give your coding agent only the contract and candidate.py, and ask for the smallest change that meets the contract. Edit only candidate.py; leave chain.py, exercise.py and the expectations alone. If you have no working agent today, write the repair yourself and say so in your record. Then run the block below in the same shell. The output shown is for the repair from the notes, which moves the allowlist check before the effect:

diff candidate.starter.py candidate.py
python chain.py > chain-after.txt; echo "exit $?"
python exercise.py > repaired-checks.json; echo "exit $?"
tail -n 1 chain-before.txt chain-after.txt
31d30
<         self.effects.append(request["tool"])
33a33
>         self.effects.append(request["tool"])
exit 0
exit 0
==> chain-before.txt <==
{"kind": "synthetic_injection_chain", "task_completed": true, "unauthorized_effects": 3}

==> chain-after.txt <==
{"kind": "synthetic_injection_chain", "task_completed": true, "unauthorized_effects": 0}

Your diff may differ. What must match is the two exit 0 lines with the checks unchanged.

Before you move on, try to break your own green result:

  • If chain.py exits 0 but exercise.py exits 1, read which case failed. A repair that refuses only export passes the chain and fails denied_unlisted.
  • If every denied_ case passes and an allowed_ case fails, the repair denies too much.
  • Ask a partner, or your agent in a fresh session, for one request the contract forbids that your repair might still let through. Run it; do not argue about it.

2. Scan and triage drill

Stay in the same directory.

  1. Run the course's offline scanners on your copy, then the triage verifier:

sh python "$COURSE"/lectures/week-08/fixture/scan/run_scans.py --target . > scan-before.json; echo "exit $?" python verify_triage.py > triage-before.json; echo "exit $?"

text exit 0 exit 1

The scanner exits 0 whether or not it finds anything; open scan-before.json to read the one static finding and the line that says no dependency was audited. 2. Read triage-scan.json and preferences.py. The pickle.loads call in it is the planted finding: never pass it anything except the inputs the verifier sends, and do not craft or load pickle data. Write your own class for each of the three findings: true positive, false positive or needs more evidence. 3. Read agent-draft.json, a course-written draft with typical errors. Mark each claim you would accept as written. 4. Open triage-before.json. For each row compare your class, the draft's class and the raw observation. For the deserialization finding, confirm that the recorder saw a loader call; no payload is executed. For the dependency, write the missing fact and the check that would supply it. 5. Repair parse_config so that it calls the data-only JSON decoder the module already provides, and delete the import pickle line: the verifier rejects a file that still imports pickle, and does not run a file that uses an import, attribute or name outside the short lists in the notes. Do not keep a loader as a fallback for text that is not JSON, and do not change verify_triage.py or the draft. Then:

sh python verify_triage.py > triage-after.json; echo "exit $?" grep '"repaired"' triage-before.json triage-after.json

text exit 0 triage-before.json: "repaired": false triage-after.json: "repaired": true

  1. Record one draft claim that was wrong, what was wrong about it and which check caught it.

3. Your own repository: scan, triage, reproduce

Work in your project repository or in your contribution checkout, not in the course checkout. Use the change you are preparing for Contribution 2 if it touches input handling, a dependency or anything near a credential.

  1. Name the surfaces. List three places where your agent reads text you did not write: an issue, a dependency's README, a tool result, a web page, a log. Next to each, write which tools the agent can use after reading it. Mark every row where untrusted text and a tool that writes, sends or installs meet in one session.
  2. Scan. Run a static analyser, a dependency audit and a secret scan: the ones the repository's own checks already use, or the course tools named in the syllabus. The offline kit from part 2 takes any directory as --target, but its scope is narrow: the static rules read Python files only, and the dependency audit reads only a uv.lock and compares it with a fixed snapshot dated 2026-09-05 that was collected for seven packages. On your own repository it can therefore print "dependency": 0 with nothing marked as unchecked although most of your dependencies were never looked up. That is the false "clean" this lesson warns about. If you use the kit, record its dependency row as "not audited beyond the course snapshot" and run a real dependency audit for the decision. For each scan, record the exact command, the tool version, the date and what the tool covered. A scan that did not run, or that audited nothing, is recorded as "not scanned", never as clean.
  3. Triage every finding in a table before you ask an agent:
Finding Class Evidence Decision Regression or rerun
rule and location true positive, false positive or needs more evidence reproduction, the condition that stops it, or the missing fact fix, narrow exception, or wait for the named check the test or command you will rerun
  1. Ask your agent to triage the same findings, with the scan output and the flagged files in context. Compare its classes with yours row by row. Where you disagree, one of you needs an observation; a second opinion from the same agent is not one.
  2. Reproduce one true positive. Write the smallest input that shows the problem, then a test that fails before your repair and passes after it, and keep one test of the permitted behavior beside it. If you have no true positive, do this for the riskiest row from step 1 instead: write the check that would show the agent's tool refusing the action.

Keep every reproduction inert:

  • its input stays in memory, or goes only to a mock you started on 127.0.0.1;
  • it reads no real credential and touches no file outside its working directory;
  • it runs only against code on your own machine, never against a deployed service or someone else's system;
  • if you save it as a file, the first line says # INERT PAYLOAD — course material.

In a contribution repository, follow the project's security policy. If it asks for private reports, do not put a reproducer in a public issue or pull request.

4. Decide

Write one line for the work you scanned: accept, narrow, get one more observation, or stop. Under it, name the evidence you used and one thing that evidence does not cover, such as the layer your check ran at, the history a secret scan did not read, or a dependency that was not audited. Say what result would change the decision.

The disposition table from part 3 is the start of the security-scan disposition your final project needs; keep it with the project.

If execution is unavailable

Trace candidate.py and preferences.py by hand, write the expected and actual outcome for one denied and one permitted request, and give a reasoned repair. Label the result as text analysis: it is not a run, and it cannot be recorded as a passed check.

Participation and accessibility

All material is plain text and every step can be done with a keyboard. An approved written or asynchronous exchange can replace live discussion and keeps the same outputs. Keep one work record and one AI-use note; do not copy private chat, personal data or credentials into it.