Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B2.5.mp4Agents accumulate with no owner and no expiry, and an unregistered agent joins a topology as a peer.
A registry with a named owner, an expiry, and admission bound to a registered identity.
Admit agents against a registry and show an unregistered one refused at the door.
Non-human identities already outnumber humans in most estates, and they sit outside joiner-mover-leaver entirely. Nobody ever leaves, so nothing is ever revoked, and last year's proof-of-concept still holds production write. The protocol that fixes it is one you already run for humans.
orphans() to the registry. Revoking the File System Agent stops its next token exchange, not its next restart. orphans() answers both directions — registered identities nothing runs, and running workloads nobody registered.The non-human identity lifecycle: rotation, revocation that takes effect at the next call rather than the next restart, and finding orphans in both directions.
Agents accumulate with no owner and no expiry, and an unregistered one joins the topology as a peer.
A registry with a named owner, an expiry, and admission bound to a registered identity.
Agents with an owner and a future expiry, as a share of those actually observed running.
Measure this
| what to measure | denominator or target |
|---|---|
| agents with an owner and a future expiry | share of those actually observed running |
humans non-human identities
joiner -> mover -> leaver created -> ... -> ?
| |
HR system drives it nothing drives it
revocation is automatic nobody ever leaves
the fix is to run ONE protocol over both:
HR/IdP --SCIM--> POST /Users a person joins
POST /Agents an agent is deployed
PATCH active:false a leaver, or a retirement
owner is a $ref to a User, not a string, so the leaver
event that already exists is the one that retires the agentMitigates: T9 Identity Spoofing · T13 Rogue Agents · T3 Privilege Compromise.
Human identities have a lifecycle: someone joins, moves team, leaves, and an HR event drives the change. Non-human identities have none of that. They are created by whoever needed one, owned by nobody in particular, and removed never.
They also outnumber humans, often by a large multiple.
B1.11's rogue agent was admitted because the orchestrator had no notion of an approved agent. The control is a registry, and a registry is only useful if it carries three fields:
A named owner. A person, not a team alias. An identity with no owner cannot be renewed, questioned or revoked, because nobody is accountable for answering.
An expiry. Not for the credential — for the registration. It forces a recurring decision about whether this agent should still exist, which is the only mechanism that removes the ones nobody uses.
An admission binding. The registry entry names the workload identity from B2.2. Admission then checks the presented identity against the registry rather than checking a name the caller supplied.
That last point is what makes it a control rather than a spreadsheet. A registry consulted by name is documentation; a registry consulted by attested identity is an authorization decision.
A registry nobody updates decays into the spreadsheet it replaced. Humans do not have this problem, because their lifecycle is already automated by a protocol: SCIM — System for Cross-domain Identity Management, RFC 7643 for the schema and RFC 7644 for the protocol. The HR system creates a user, the identity provider POSTs it to /Users, a leaver event PATCHes {"active": false}, and every downstream application finds out without anybody filing a ticket.
Point the same protocol at agents and three things become true at once:
The joiner-mover-leaver machinery you already run is the machinery that governs agents. No parallel process, no second system of record. Deploying an agent means creating a SCIM resource; retiring it means one PATCH.
The owner is a reference, not a string. owner.$ref points at the SCIM User — so when Sam leaves, Sam's own leaver event is enough to answer "which agents just lost their owner", automatically, on the day it happens rather than at the next audit.
Orphans become a query. GET /Agents?filter=active eq true and owner pr false is a one-line answer to a question that is otherwise a quarter of someone's life.
What is and is not standard here. SCIM's protocol — the endpoints, the filter syntax,PATCHwithactive: false, themetablock — is RFC 7644 and your IdP already speaks it. A resource type for agents is not in RFC 7643; you declare it as a schema extension under your own URN, exactly as the enterprise extension does formanager. The protocol is standard, the schema is yours, and that split is the whole reason this is cheap to adopt.
What this control closes. Turns 'which agents are allowed here' from a convention into a check, and turns 'is this one still wanted' from an audit question into a leaver event. Closes B1.11 at the door.
PATCH, and what it does not reachThis is the part a registry without a protocol behind it gets wrong. Sam's leaver event fires correctly — his own account is deactivated the same afternoon. The agent he deployed keeps running.
cybertravels-svc was created for a proof of concept in March and is still authenticating. The skill provisions agents as SCIM resources with an owner reference, then runs Sam's leaver event and re-runs admission — because a reference can be followed and a name in a text field cannot.
The skill — skills/identity/nhi-lifecycle-audit/SKILL.md
name: nhi-lifecycle-audit
description: >-
Audit non-human identities as provisioned resources with a human owner
reference — checking admission, orphaning, lapse, and whether a leaver event
on the owner actually removes the agent's access. Use when agents outlive the
people who created them, or when asked who owns a service account.
allowed-tools: Read, Grep, GlobNon-human identities are created for a proof of concept and are still authenticating two years later. The lifecycle question is not "does it exist" but "what event removes it", and the answer that works is an owner reference resolved at admission — so the leaver process you already run does the work.
Any estate with service accounts, agent identities or machine credentials — particularly where agents are created programmatically, or where a proof-of-concept identity was never retired.
1 — Enumerate the identities as resources. Name, created-at, last-used, and the schema they are provisioned under. SCIM (RFC 7643/7644) is the interface most estates already have; use it rather than inventing a registry.
2 — Require an owner reference, not an owner string. owner.$ref pointing at a User resource resolves; a name in a text field does not. The difference is whether a leaver event can be followed.
3 — Classify every identity against admission. Four outcomes and all four must be exercised: admitted, unregistered (exists, nobody provisioned it), orphaned (owner reference does not resolve), lapsed (unused past its window).
4 — Run a leaver event. A single PATCH {active: false} on the owner. Then re-run admission. Any agent whose owner is now inactive must be refused on the next presentation — not swept overnight, refused.
5 — Report the sweep as a control, not a report. A list of orphaned identities that nobody acts on is a recurring finding. Name the automatic consequence.
Input — the fixture committed at the top of scripts/nhi_lifecycle_audit.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:
POST /scim/v2/Agents
{
"active": true,
"displayName": "pricing-agent",
"externalId": "spiffe://cybertravels.com/ns/prod/sa/pricing-agent",
"id": "b7f3a1c2",
"meta": {
"created": "2026-01-14T09:02:00Z",
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": [{"name": "str", "owner_ref": "str|null", "last_used": 0,
"verdict": "admitted|unregistered|orphaned|lapsed"}],
"owner_resolution": {"kind": "reference|string", "resolvable": false},
"leaver": {"event": "str", "agents_refused_after": ["str"], "on_next_presentation": true},
"sweep": {"window_days": 0, "automatic_consequence": "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 B2.5Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b2-5-the-non-human-identity-lifecycle:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B2.5 | type /b2-5-the-non-human-identity-lifecycle |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B2.5 | type /b2-5-the-non-human-identity-lifecycle in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B2.5 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B2.5 | type $b2-5-the-non-human-identity-lifecycle, 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 B2.5Neither 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.5 --out work/cybertravels
python3 scripts/checkpoint.py --at B2.5 --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.
An agent is provisioned as a SCIM resource whose owner is a $ref to a User. Four agents present identities and one is admitted — unregistered, orphaned and lapsed are all refused. Sam's leaver event is a single PATCH {active: false} on /Users, and because admission resolves the owner reference, his agent is refused on the next call. The orphan query returns one result.
Count your non-human identities and how many have a named human owner that resolves to a live account. The difference is the set nobody can revoke during an incident, because nobody can be asked whether it is still needed. Then check whether your IdP's SCIM connector can carry a custom resource type — most can, and it is usually a configuration rather than a project.
Tools used
Skill source: skills/identity/nhi-lifecycle-audit