All lessons › Function A — Getting Started — Building Agentic AI › Build the Agent — From a Loop to a Running Platform
A1.3

Identity — the human, the workload, and the call

A1
▶
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/A1.3.mp4
Risk

A shared service-account key makes every action look identical in the log, and no investigation can name who caused one.

Control

A workload identity per agent, and a session token for the human that grants nothing downstream.

The lab

Mint both tokens and read the claims that make them different.

◉Use case relevance

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.

At CyberTravels. Four agents, four workload identities, in 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.

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

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.

Day 0 — why

One shared key makes every action in the log identical, and 'which agent, on whose behalf' has no answer at all.

Day 1 — how

Give each agent a workload identity, and the human a session token that grants nothing downstream.

Day 2 — measure

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 measuredenominator or target
audit rows naming both the human and the specific agentshare of all rows; target 1.00

▦The framework, and how it works

        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

Three people, for the rest of Function A

Before the abstractions, the cast — they are real rows in cybertravels/config.py and they do not change again:

whoroleand so
Danatravellerbooks and cancels her own trips. Cannot refund anything.
Alexagent operationsruns the platform. Can write bookings, cannot refund.
Priyafinancethe 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.

And three principals in every action

A system that models only one of the three principals cannot answer any question an incident asks.

principalwhat it isin this requestwhat it answers
the humanthe person who askedDanawho wanted this
the workloadthe agent processthe workflow agentwhat acted
the callthis one tool invocationrefund(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.

A workload identity is not a secret

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.

The human's token grants nothing downstream

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.

2 · Mint both, and read what makes them different

The file is cybertravels/identity.py.

Do this

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

▶Real time execution as skill

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, Glob

Agent identity and delegation review

Three identities are in play whenever an agent acts, and most incidents come from collapsing them:

  • the user whose request started the work
  • the agent doing the work — a workload identity, not a person
  • the actor chain connecting them, which is what an auditor needs

A service account shared by every agent answers "what ran" and destroys "for whom". That is the gap non-human identity exists to close.

When to use this

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.

Procedure

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:

  • subset of presented — never more scope than the incoming token carried
  • within the actor's ceiling — never more than the agent is itself allowed

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.

Example

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.

Output contract

{
  "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"}]
}

Failure modes

  • Checking only one narrowing rule. Both, every time.
  • Accepting a shared service account because it is "internal". Internal is a network property; it says nothing about attribution.
  • Treating the gateway as optional where the downstream cannot do OBO. It is the only place authorisation can happen.
  • Measuring delegation by whether the call succeeded. A call that succeeds with too much scope is the failure being looked for.

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 A1.3

Then 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:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons A1.3type /a1-3-identity
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons A1.3type /a1-3-identity in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons A1.3type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons A1.3type $a1-3-identity, 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 A1.3

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 A1.3 --out work/cybertravels
python3 scripts/checkpoint.py --at A1.3 --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

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.

✎Your turn

Give two agents the same workload identity. Everything still runs. Then read the audit log and try to say which one acted.

Tools used

SPIFFE/SPIREPyJWT

Skill source: skills/identity/agent-identity-review

← PreviousA1.2 · Tools over MCP — a resource server, and why it is a separate process