The question always arrives during an audit: “which agents do we have in production, who owns them, and why does that one have write access to payments?”
Nobody can answer it, because agents accumulate exactly the way cron jobs and service accounts accumulate — one at a time, each obviously justified at the time, none with an owner or an end date. Somebody gave the support agent a refund tool “temporarily” for an incident in March. It is July.
The standard response is a spreadsheet. A spreadsheet is documentation, and documentation drifts from reality the day after it is written.
TL;DR
Make the profile enforced by the harness rather than filed in a wiki:
- No owner, retired, or past its review date → the run does not start. Not a warning; a halt, before a single token is spent.
- A tool outside the declared set → denied, and recorded as privilege drift. The declared tool list stops being a description and becomes the boundary, which means the diff that widens it is the diff that gets reviewed.
- Environment and data classes are declared, so “this agent was never approved for production” and “never approved to touch PII” are answerable in code.
A declaration the runtime honours cannot drift from reality. That is the whole idea.
OWASP: ASI04, ASI10, ASI03.
Runnable code:
patterns/governance/lifecycle_profile/
Enforced, not filed
uv run python -m patterns.governance.lifecycle_profile.demo
=== WITH the pattern: privilege drift is denied and recorded ===
tool 'issue_refund' is not in the declared capability set for agent
'support-agent'. Declaring it is a reviewed change owned by payments-team@corp.
=== the profile is enforced at run start, not filed in a wiki ===
retired six months ago -> HALTED agent is retired (on 2026-01-05)
review 45 days overdue -> HALTED review overdue by 45 days (was due 2026-06-15)
nobody owns it -> HALTED no owner: an unowned agent is a standing credential
never approved for prod -> HALTED not approved for environment 'prod' (approved: dev)
=== the registry: answering the audit questions in code ===
4 registered agent(s):
doc-summarizer pilot owner=— envs=dev <- UNOWNED
old-triage retired owner=support@corp envs=prod
payments-agent production owner=treasury@corp envs=prod <- REVIEW OVERDUE
support-agent production owner=payments-team@corp envs=dev,prod
can move money: ['payments-agent']
unowned: ['doc-summarizer']
review overdue: ['payments-agent']
The pattern
SUPPORT = AgentProfile(
id="support-agent",
purpose="Answer refund status questions from ticket text",
owner="payments-team@corp", # a team that can be paged
stage=Stage.PRODUCTION,
declared_tools=frozenset({"read_ticket"}),
environments=frozenset({"dev", "prod"}),
data_classes=frozenset({"pii"}),
review_by=date(2026, 9, 28), # every agent expires
)
harness = Harness(model, tools, hooks=[LifecycleGuard(profile=SUPPORT)])
Every agent expires. review_by is not optional in spirit — an agent that outlives
its review is an unowned agent with a plausible explanation attached.
Owner means a team that can be paged. Not “the AI team.” The halt message names them, so whoever hits the wall knows who to ask.
Retirement is a state, not a deleted directory. RETIRED keeps the record — who
owned it, what it could do, when it stopped — which is exactly what you need six months
later. DEPRECATED still runs (no new consumers, migration underway); RETIRED does not.
Audit mode exists for adoption. enforce_declared_tools=False records drift without
denying, so you can run a real fleet for a week and discover what your agents actually
use before turning enforcement on. Enforcement is the default, because opt-in
enforcement is how this becomes a spreadsheet again.
When to use it
- The moment you have more than about three agents, or any agent in production that outlived the person who built it.
- Any regulated environment. “Which automated systems can touch financial data” is a question you will be asked in writing.
- Before granting an agent a new tool. The profile diff is the change-review artefact.
- When adopting agents across teams — the registry is how a platform team keeps an inventory without becoming a bottleneck on every deploy.
When NOT to use it
- A single agent, one team, one environment, in a startup that will rewrite it next quarter. The answer to “who owns this?” is the person reading the sentence.
- Do not use it as a substitute for actual identity and permissions.
declared_toolsis defence in depth above the privilege broker and identity propagation. It records intent and catches drift. It authenticates nothing. - Do not set
review_bydates you will not honour. A fleet of agents all “overdue” trains everyone to extend the date reflexively, which consumes attention and produces nothing. Start with the agents that touch money or PII. - Do not build the registry before the enforcement. An inventory nobody’s runtime checks is the spreadsheet with extra steps — the exact failure this pattern exists to fix.
Trade-offs and failure modes
- A halt is a production outage. An expired review date will take down a working agent
at an inconvenient moment. That is the intended trade — but you need a documented
break-glass path with an owner and an audit record, plus alerting ahead of expiry
(
review_due(today, within_days=30)) so it never surprises anyone. - Profiles drift toward permissiveness. Each individual “just add this tool” is
reasonable, and after twelve of them
declared_toolsis the full registry and the boundary is gone. Trackdeclared_toolsgrowth per agent over time — that number is the real privilege-drift metric. data_classesis a declaration nothing verifies. Saying an agent does not touch PII does not prevent it. Pair with redaction and RAG access control, which enforce where data actually moves.- Stage semantics need a written convention. The code only enforces that
DEVELOPMENTcannot run in prod. Write the rest down or teams will use them inconsistently. todayis injected, which means it can be wrong. A misconfigured clock either runs a retired agent or halts a healthy one. Source it from one place.- One profile per agent does not model multi-tenant reality. The same agent serving two customers with different data-class approvals needs two profiles or a per-tenant overlay, and this implementation has neither.
Frequently asked questions
Is this just a service catalogue for agents?
It is a service catalogue that the runtime reads. Catalogues that are only read by humans go stale within a quarter; catalogues that gate execution stay accurate because inaccuracy causes an immediate, visible failure.
How do I retrofit this onto agents already in production?
Audit mode. Turn on drift recording with enforcement off, run for a week, and the drift
log becomes your first draft of declared_tools for every agent. That is far more
accurate than asking teams what their agents use.
Who should own an agent — the platform team or the product team?
The team that owns the consequences. If the payments agent misfires, treasury gets the call, so treasury owns it. Platform teams own the harness and the patterns; product teams own the profiles. Getting this backwards produces a platform team that is accountable for behaviour it cannot see.
What about kill switches for an agent already running?
Genuinely not covered by this pattern, and I want to be clear about it. The profile refuses to start a retired agent; stopping runs already in flight, and propagating credential revocation to them, is a different mechanism. It is one of the open gaps in the series.
References
- OWASP GenAI Security Project, Top 10 for Agentic Applications 2026 — ASI04, ASI10, ASI03
- Microsoft, Agent Governance Toolkit — agent identity, marketplace trust tiers, lifecycle controls as runtime concerns
- Cloud Security Alliance, AI Agents: Architecture and Control Plane — agents as first-class governed assets
- Saviynt, Building Trust for AI Agents: From Accountability to Audit — ownership as the prerequisite for audit
- Runnable code and tests:
patterns/governance/lifecycle_profile/
Part of the agent harness and governance series. Next: do these patterns actually compose? — twelve of them on one agent, and the gap that only appeared when they ran together.