How lifecycle & policy work
The state machine that governs what an agent may become, the precedence ladder that decides what it may call, the YAML those rules live in, and the cadence that keeps humans in the loop.
The state machine
draft ──approve──▶ approved ──activate──▶ active ⇄ suspended
│ │ (suspend / reactivate)
│ │
└── production agents start here ▼
dev & staging start active decommissioned (terminal)Transitions are validated by pure functions before any database write — an illegal move never half-happens. Five rules carry the compliance story:
| Rule | What the API does |
|---|---|
| Segregation of duties | The approver must differ from the agent's creator — self-approval returns 403 segregation_of_duties, no exceptions, no role high enough to bypass it. |
| No skipping approval | Activating a draft agent directly returns 409 approval_required. The only path to active runs through a second person. |
| Scope frozen while suspended | Suspension requires a reason string (400 reason_required without one), and while suspended, edits to allowed_mcp/denied_mcp return 409 scope_frozen — nobody quietly widens a quarantined agent. |
| 30-day stale suspension | A suspension under 30 days old is undone with reactivate. Over 30 days, reactivate returns 409 reapproval_required — the agent must go back through approve, with a fresh second person on record. |
| Terminal decommission | Any state can be decommissioned. It revokes every active token at that instant, snapshots the final agent row into the audit log, and after it every transition returns 409 terminal_state. |
The starting state is environment-aware: production agents are created as draft; dev and staging agents self-activate at registration, so the approval chain never slows a developer's inner loop.
Policy evaluation precedence
Every decision — advisory via POST /policy/check or live in layer 3 — walks the same ladder, top to bottom, first match wins:
1. agent status ≠ active → deny ("agent status is 'suspended', not active")
2. server in denied_mcp → deny (policy_rule: denied_mcp)
3. server not in allowed_mcp → deny (policy_rule: default_deny)
4. no tool policy for the agent → allow (policy_rule: allowed_mcp)
5. tool in the server's deny → deny (policy_rule: <server>.deny)
6. tool in the server's allow → allow (policy_rule: <server>.allow)
7. deny_unlisted (default true) → deny (policy_rule: deny_unlisted)
deny_unlisted switched off → allow (policy_rule: allow_unlisted)The verdict is a Decision with three fields: permitted (boolean), reason (a human-readable sentence), and policy_rule (the name of the rule that won). Every response and log row carries all three, so a decision is always explainable after the fact. Note rule 1: lifecycle state is policy input — a suspended or decommissioned agent fails every check before any list is consulted.
The YAML policy
Your organization edits its own policy from the Policy page (security role): validate, save — live within ~5 seconds — or revert to the platform default. Your org's policy overlays the platform default below and is isolated from every other tenant. Self-hosted deployments can still manage the default as files on disk.
policies:
defaults:
deny_unlisted: true # tools not listed are denied (shipped default)
high_risk_requires_review: true
prod-remediation: # key = agent id OR agent name
github-mcp: # key = registered MCP server name
allow: [read_file, list_repos, create_issue]
deny: [delete_repo, force_push]
blast_radius: {delete_repo: 0.9, force_push: 0.7, read_file: 0.1}
aws-prod:
allow: [ec2.describe, cloudwatch.read]
deny: [ec2.terminate, iam.modify]
blast_radius: {ec2.terminate: 0.9, iam.modify: 0.8}Under policies:, each key except defaults is an agent — matched by id first, then name. Under an agent, each key is an MCP server, holding up to three things: an allow list of tool names, a deny list, and a blast_radius map scoring each tool 0.0–1.0 for "how bad if this goes wrong?". An agent with no entry at all keeps its server-level decision — tool-level policy is adopted one agent at a time.
Loading and hot reload
| Behavior | Detail |
|---|---|
| Merge | Every *.yaml/*.yml in the policies directory is loaded and merged, sorted by filename — later files win on collisions. |
| Hot reload | Files are re-read whenever any file's mtime changes. An edit lands on the next evaluation — no API restart, no deploy. |
| Strict validation | A malformed file fails loudly at load, naming the file and the offending key. There is never a silently-empty policy. |
| Version control | Keep the YAML in git. A policy change is a diff, a review, and a merge — the same discipline as any other code. |
Review cadence
Every agent carries a next_review deadline derived from its risk level. A daily sweep flags what is overdue — as audit events, not status changes; reviews are advisory pressure, not an outage mechanism.
| Risk level | Review cadence | When overdue |
|---|---|---|
high | every 30 days | One review_overdue audit event per missed deadline; surfaced on the dashboard and governance report |
medium | every 90 days | Same — flagged once per deadline, not once per sweep run |
low | every 180 days | Same; agents past their expires_at are additionally flagged expired |
POST /agents/:id/review) sets last_review to now and advances next_review by the cadence. It is a record of human attention, not a state transition.