Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/A1.3.mp4A shared service-account key makes every action look identical in the log, and no investigation can name who caused one.
A workload identity per agent, and a session token for the human that grants nothing downstream.
Mint both tokens and read the claims that make them different.
One API key in an environment variable, shared by four agents. Every action in the log reads service-account-prod. Nine weeks later somebody asks which agent issued a particular refund and on whose behalf, and the honest answer is that the system cannot tell — not slowly, not with effort. It never recorded it.
cybertravels/config.py. Dana is a traveller, Alex runs agent operations, Priya is in finance — and the difference between them is the whole of the next lesson.Three principals in every agent action: the human who asked, the workload that acted, and the individual call. Each gets a name, and the human's token is shown to grant nothing downstream.
One shared key makes every action in the log identical, and 'which agent, on whose behalf' has no answer at all.
Give each agent a workload identity, and the human a session token that grants nothing downstream.
Audit rows that name both the human and the specific agent, as a fraction of all rows. Anything below 100% is a row you cannot investigate.
Measure this
| what to measure | denominator or target |
|---|---|
| audit rows naming both the human and the specific agent | share of all rows; target 1.00 |
THREE PRINCIPALS, NOT ONE the human "who wanted this" dana the workload "what acted" spiffe://…/agent/workflow the call "under what authority" aud=mcp:internal scope=bookings:read collapse these into one key and every log row is identical
Before the abstractions, the cast — they are real rows in cybertravels/config.py and they do not change again:
| who | role | and so |
|---|---|---|
| Dana | traveller | books and cancels her own trips. Cannot refund anything. |
| Alex | agent operations | runs the platform. Can write bookings, cannot refund. |
| Priya | finance | the only one of the three who may approve a refund. |
Dana owns booking CT-4417. Every example from here to the end of A2 is one of those three asking for something, and the interesting cases are all Dana asking for something only Priya may have.
A system that models only one of the three principals cannot answer any question an incident asks.
| principal | what it is | in this request | what it answers |
|---|---|---|---|
| the human | the person who asked | Dana | who wanted this |
| the workload | the agent process | the workflow agent | what acted |
| the call | this one tool invocation | refund(CT-4417) | under what authority |
Most systems collapse these into a service-account API key. Every action then looks identical in the log, and "which agent did this, on whose behalf" has no answer at all — not a slow answer, no answer.
CyberTravels gives each agent a SPIFFE-style name:
spiffe://cybertravels.local/agent/workflow
spiffe://cybertravels.local/agent/rag-advisor
It is an identity, not a credential — short-lived, attested, and registered. The registration is what lets a resource server refuse an actor it does not recognise, which is the difference between an allow-list and a hope.
This is the part that is easy to get wrong. The traveller's session token is addressed to the orchestrator and carries one scope: agent:invoke. It does not open a booking. It buys the right to ask an agent to act, and the authority for the action itself is minted separately — which is A1.4.
The file is cybertravels/identity.py.
1. Start Python in the folder you cloned. 2. Make one of each kind of token and print what is inside them:
from cybertravels import identity
import jwt
person = identity.mint_user_token("dana")
machine = identity.mint_agent_token("workflow")
for name, token in (("Dana", person), ("the agent", machine)):
claims = jwt.decode(token, options={"verify_signature": False})
print(name, "->", claims)
3. Compare the two lines it prints. One describes a person. The other describes a piece of software. They are not the same kind of thing, and the system stores them separately — which is the whole idea of this lesson.
4. Look at what Dana's token lets her do. It carries one permission: agent:invoke. That means she may ask an agent to do something. It does not let her book, cancel or refund anything herself. Those permissions get created later, one at a time, for one action each — and that is the next lesson.
If import jwt fails, run pip install PyJWT and try again.
The skill below is the one B2.3 uses to audit a delegation design. Run it on yours now, while changing the design is still easy.
The skill — skills/identity/agent-identity-review/SKILL.md
name: agent-identity-review
description: >-
Review how an agent authenticates and how a user's authority is delegated to
it, including token exchange, on-behalf-of chains, service accounts and
non-human identity. Use when asked who an agent is calling as, whether a
delegation is auditable, why a downstream system sees the wrong principal, or
how to scope an agent's credentials.
allowed-tools: Read, Grep, GlobThree identities are in play whenever an agent acts, and most incidents come from collapsing them:
A service account shared by every agent answers "what ran" and destroys "for whom". That is the gap non-human identity exists to close.
Reviewing an agent's auth design, an OBO/token-exchange implementation, a gateway that fronts an agent, or any log where you cannot tell which user caused an action.
1 — Name the three identities. For the flow under review, write down the user principal, the workload identity, and where each is asserted. If the workload identity is a long-lived shared secret, that is finding one.
2 — Check the delegation narrows. In RFC 8693 token exchange the issued token must satisfy both rules:
Either rule alone is insufficient. Subset-only lets a highly privileged user hand an agent authority the agent should never hold; ceiling-only lets an agent exceed the user who asked. Test both directions explicitly.
3 — Check the chain is preserved and not duplicated. The act chain should read user → agent, once. A chain that repeats the principal (dana → dana → agent) usually means the head was appended twice, and it breaks any audit query that counts hops.
4 — Find where OBO stops. Some downstream systems cannot consume a delegated token — legacy databases, vendor APIs, anything with a static credential. Identify each, and require a choke point: a gateway that holds the credential, enforces per-user authorisation before the call, and logs the original principal. The credential must not be reachable by the agent directly.
5 — Check expiry and revocation. How long is the delegated token valid, and what stops it being replayed after the user's session ends? Just-in-time authority that outlives the task is standing authority with extra steps.
6 — Check the log answers the audit question. Pick a real question — "which user caused this row to be deleted?" — and try to answer it from the logs alone. If you cannot, the delegation is not auditable regardless of how it is built.
Input — the fixture committed at the top of scripts/agent_identity_review.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:
request authorized? attributed to
dana@corp -> reports:read True dana@corp via reports-agent
dana@corp -> db:admin False dana@corp via reports-agent
memory keys - the same workspace, two users:
dana -> acme:dana@corp
priya -> acme:priya@corp
shared? False
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.
{
"identities": {"user": "str", "workload": "str", "assertion": "str"},
"delegation": {"mechanism": "obo|impersonation|shared_service_account|none",
"subset_of_presented": true, "within_actor_ceiling": true,
"chain": ["principal", "..."], "chain_wellformed": true,
"ttl_seconds": 0, "revocable": true},
"chokepoints": [{"downstream": "str", "reason": "str",
"enforced_at": "gateway|service|none",
"credential_reachable_by_agent": false}],
"audit": {"question": "str", "answerable_from_logs": true},
"findings": [{"issue": "str", "severity": "critical|high|medium|low", "fix": "str"}]
}
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.3Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick a1-3-identity:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons A1.3 | type /a1-3-identity |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons A1.3 | type /a1-3-identity in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons A1.3 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons A1.3 | type $a1-3-identity, 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.3Neither 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.3 --out work/cybertravels
python3 scripts/checkpoint.py --at A1.3 --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.
Three principals named, the agent's identity separated from any credential it holds, and the human's token shown to grant nothing beyond invoking an agent.
Give two agents the same workload identity. Everything still runs. Then read the audit log and try to say which one acted.
Tools used
Skill source: skills/identity/agent-identity-review