All lessons › Function B — Securing AI Architectures › Securing the Architecture — Identity and Ingress
B2.7

Attribution: an audit trail that answers "who"

B2
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/B2.7.mp4
Risk

Without the motivating input, root cause cannot be established at all; without the principal, nothing can be attributed.

Control

Per-hop attribution written to an append-only store outside the agent's reach.

The lab

Answer 'which user caused this deletion' from the trace, then try the same on a trace missing one field.

◉Use case relevance

A trace that records tool calls is not evidence. Evidence answers who asked, which agent acted, what authority it held, and what input made it act — and an auditor will ask all four in that order.

At CyberTravels. The fourth question, which A2.2 left open. db.audit takes the provenance.Span that B2.6 made it possible to have, and records its origin plus a digest — not the text, because a refund request quoted in full puts traveller prose in a long-lived store.

◴What this lesson is — Day 0, Day 1, Day 2

Recording what motivated an action, not only who took it — the fourth investigation question, answerable only once ingress marks where text came from, and recorded as a digest rather than the text.

Day 0 — why

Without the motivating input, root cause cannot be established at all; without the principal, nothing can be attributed.

Day 1 — how

Per-hop attribution written to an append-only store outside the agent's reach.

Day 2 — measure

Actions with a complete chain. The share below 1.00 is precisely the part of the estate you cannot audit.

Measure this

what to measuredenominator or target
actions with a complete delegation chainshare; the shortfall below 1.00 is the part of the estate you cannot audit

▦The framework, and how it works

   the four fields an auditor asks for, in order

   1. principal      dana@corp          who asked
   2. agent          patch-agent#7      who acted
   3. authority      repo:write, exp+90s   under what
   4. motivating input   PR #412 body, [data]   why

   a trace with 1-3 and not 4 cannot distinguish authorised from injected

Mitigates: T8 Repudiation & Untraceability · T13 Rogue Agents.

B1.14 showed a log that was complete for debugging and empty for investigation. This is the record that is not.

Four fields, each answering a question the tool-call log could not:

The human principal — who caused this. From B2.1.

The agent identity and instance — what performed it, and which run. From B2.2, so it is attested rather than claimed.

The delegation chain — how authority got from the human to this action. From B2.3, which is also what makes B1.17's laundering path visible.

The motivating input, with its origin — what made the agent decide. From B2.6. This is the field that establishes root cause, and the one most often missing, because logging tool calls feels like logging decisions.

Then the structural property, which is not a field: the store must be outside the agent's reach. An agent with broad credentials can usually touch the logging stack, and a record the actor can edit is not evidence. Append-only, different credential, ideally different trust domain.

The test is not whether the log looks thorough. It is whether you can answer "which user caused this, and what made it happen" without asking anyone.

What this control closes. Makes the incident answerable. Also the control an auditor asks for first, because a system that cannot attribute an action cannot be defended even on a quiet day.

2 · The control

3 · The check, as a skill

The four questions an auditor asks about that $5,000 refund have to be answerable from one entry. The skill puts them to a single ledger record, and then tries to amend the record as the agent — because a complete log its subject can edit records what the agent wanted you to see.

▶Real time execution as skill

The skill — skills/identity/attribution-ledger-check/SKILL.md

name: attribution-ledger-check
description: >-
  Check that one record answers all four investigation questions — human
  principal, attested workload and run, delegation chain, and the motivating
  input with its origin — and that the agent cannot amend it. Use when
  designing agent audit records rather than reading them.
allowed-tools: Read, Grep, Glob

One entry, four questions

An audit trail is not "we log tool calls". It is a record that answers, from a single entry, the four questions an investigation asks — and that the subject of the record cannot edit. Both halves are required: a complete record the agent can rewrite is a record of what the agent wanted you to see.

When to use this

While designing the record. Retrofitting attribution after an incident means reconstructing it from four services' timestamps, which does not happen at 2am.

Procedure

1 — Write the four questions down first. Which human. Which workload and which run. Through what delegation chain. On what motivating input, from what origin. Design the entry to answer them; do not collect fields and hope.

2 — Map each question to a field. Principal from the delegated token, not from a header the agent set. Workload and instance from the attestation. Chain from the token's act nesting. Motivating input from the ingress record that caused this step, with its origin tag.

3 — Test each question against one entry. Alone. If answering needs a join across services, that question is unanswered for practical purposes and should be recorded as such.

4 — Attempt the amendment as the agent. Append, overwrite, delete, reorder. Every one must be refused by something the agent does not control — an append-only store, a separate writer identity, a downstream sink it cannot reach.

5 — Check the write path's own identity. If the agent's role can write to the log destination, the ledger is advisory whatever the API says.

Example

Input — the fixture committed at the top of scripts/attribution_ledger_check.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — from a real run, the fixture summary and the fourth question's verdict:

the fixture this run is derived from
   LEDGER                       3 item(s)
   STORE                        6 item(s)

   "answered_from_single_entry": true,
   "question": "On what motivating input, from what origin, did the agent
                decide? ('wiki/473: retire invoice 8812 when the customer
                disputes a duplicate charge' from origin 'knowledge', a
                trust-0 source, not the user's request about a double charge)"

That last clause is the whole lesson: the traveller asked about a double charge, and a retrieved document is what produced the DELETE. A record without question 4 closes this incident as "the agent misbehaved".

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.

Output contract

{
  "entry": {"principal": "str", "workload": "str", "instance": "str",
            "chain": ["str"], "motivating_input": {"ref": "str", "origin": "str"}},
  "questions": [{"question": "str", "answered_from_single_entry": true}],
  "amendment_attempts": [{"operation": "append|overwrite|delete|reorder", "refused_by": "str"}],
  "writer_identity": {"agent_can_write_destination": false}
}

Failure modes

  • A principal field the agent populates. It is a claim, not attribution.
  • Answering a question by correlation. Correlation is a plan, not a record.
  • Testing amendment through the API only. Try the storage layer the agent's role can reach.

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.

1In your AI agent — you are taught

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 B2.7

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick b2-7-attribution-an-audit-trail-that-answers:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons B2.7type /b2-7-attribution-an-audit-trail-that-answers
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons B2.7type /b2-7-attribution-an-audit-trail-that-answers in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons B2.7type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons B2.7type $b2-7-attribution-an-audit-trail-that-answers, or run /skills

2As one Python command — you just run it

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 B2.7

3Read the code, and the difference — nothing runs

Neither 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 B2.7 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.7 --diff

On 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.

✓What you just proved

One ledger entry answers all four investigation questions — the human principal, the attested workload and run, the delegation chain, and the motivating input with its origin — and the agent's attempt to amend the record is refused.

✎Your turn

Take the last significant action one of your agents performed and try to fill in these four fields from what you actually logged. The missing one is almost always the motivating input.

Tools used

OpenTelemetrySigstore

Skill source: skills/identity/attribution-ledger-check

← PreviousB2.6 · Ingress: marking untrusted content at the door