Enforcement flow
The ten ordered checks every guard.protect() call runs, from agent identity binding to policy evaluation.
Every @guard.protect() call executes ten steps in order:
Agent identity binding check
If agent_id is supplied, it is validated against the agent registry. Draft and deprecated agents are denied. The agent's permitted_roles must include the claimed role. If the calling key has a bound_agent_id, the agent_id in the request must match. Requests without agent_id skip this step entirely.
Cross-agent session isolation
The first agent role to use a session_id owns it. Any other role attempting the same session is immediately denied and logged with policy_name="cross_agent_isolation".
Session lifecycle check
Sessions that are expired or revoked are denied before policy evaluation runs. Status values: active, expired, revoked.
Policy evaluation
The PolicyEngine checks permitted_agent_ids, denied sources, denied tasks, allowed tasks, allowed sources, and sensitivity ceiling in order. No matching policy defaults to deny.
OTEL span emitted
An autopil.evaluate span is started. Decision, latency, and policy name are recorded. DENY sets StatusCode.ERROR on the span.
Retrieval executes (ALLOW only)
The wrapped function runs. On DENY, the data source is never queried.
Context hashing (ALLOW only)
A SHA-256 hash of the returned context is computed (first 16 hex chars stored) for provenance verification.
Audit event recorded
Every decision — allow or deny — is written with policy name, reason, context hash, agent_id, and timestamp. Append-only; no delete API.
Alert rules evaluated
The AlertEngine checks threshold rules against the audit log. Webhook and email delivery is dispatched in a background thread.
PermissionError raised (DENY only)
The caller receives a PermissionError with the policy name and denial reason.