Quick start
AutoPIL is a retrieval-layer governance engine for autonomous AI agents. Install the Python package, point it at a policy directory, and wrap your retrieval functions with @guard.protect().
Installation
# Base install (SQLite backend) pip install autopil # With Postgres support pip install autopil[postgres] # With OpenTelemetry (gRPC exporter) pip install autopil[otel] # With OpenTelemetry (HTTP exporter — Grafana Cloud) pip install autopil[otel-http] # All dev dependencies (tests, linting) pip install autopil[dev]
Protect your first retrieval function
from autopil import ContextGuard, SensitivityLevel # Point at a policy file or directory guard = ContextGuard( policy_path="policies/", audit_db="autopil.db", ) @guard.protect( agent_role="loan_underwriter", user_id="user_001", source_id="credit_scores", sensitivity_level=SensitivityLevel.HIGH, session_id=session_id, ) def get_credit_score(customer_id: str) -> dict: return credit_db.query(customer_id) # ALLOW — policy matched, audit event logged, OTEL span emitted score = get_credit_score("cust_abc") # DENY — raises PermissionError, denial logged, alert rules checked # source='executive_comms' is on the denylist for this role
First-start bootstrap
Run the API server pointing at your policy directory:
autopil-serve --policy policies/
On first start, if no tenants exist, the server creates a default tenant and prints a superadmin key once:
⚠ First start — created 'default' tenant and superadmin key:
apl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Save this — it will not be shown again.
X-API-Key for all subsequent API calls, including creating tenants and provisioning additional keys.
If you missed the key, regenerate it by setting AUTOPIL_RESET_ADMIN_KEY=1 and restarting the server — a fresh key will be printed to stdout. For Docker, retrieve it with:
docker compose logs | grep "AUTOPIL ADMIN KEY"
Once running, create a scoped evaluate key from the dashboard under Settings → API Keys for use in agent code. Keep the superadmin key out of agent processes.
Architecture
AutoPIL fills the governance gap between your data sources and your agents. Existing tools govern upstream (data catalogs, access management) or downstream (output filters). AutoPIL governs retrieval — the moment an agent queries a vector store, database, or API to ground its response.
Enforcement flow
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.
Multi-tenancy
AutoPIL uses row-level tenant isolation. Every table carries a tenant_id column. All queries are filtered to the calling tenant's ID — audit events, policies, alert rules, API keys, and sessions are fully separated across tenants.
Superadmin keys (created at first start) can access /v1/admin/* routes to provision tenants and keys. Regular tenant keys are blocked with HTTP 403.
# Create a tenant curl -X POST http://localhost:8000/v1/admin/tenants \ -H "X-API-Key: apl_superadmin_key" \ -H "Content-Type: application/json" \ -d '{"name": "my_app"}' # → { "tenant_id": "ten_abc123", "admin_key": "apl_..." } # Create an agent-specific key curl -X POST http://localhost:8000/v1/keys \ -H "X-API-Key: apl_tenant_admin_key" \ -H "Content-Type: application/json" \ -d '{"name": "production_agent"}' # → { "key": "apl_...", "key_id": "key_..." } (shown once)
Agent identity
By default, AutoPIL trusts the agent_role supplied at decoration time — a structural guarantee that is sufficient for most single-agent pipelines. For multi-agent systems where agents are autonomous or long-running, you can add cryptographic identity binding so that only a specific registered, approved agent instance can make a given evaluation call.
How it works
Agent identity has three independent layers, each building on the previous:
| Layer | Where it's configured | What it enforces |
|---|---|---|
| Registry approval | Agent registry (dashboard or API) | Only agents with status=approved may evaluate. Draft and deprecated agents are denied with policy_name="agent_not_approved". |
| Role restriction | permitted_roles on the registry entry | An agent can only claim roles listed in its own permitted_roles. Prevents role escalation across agent types. |
| Key binding | bound_agent_id / permitted_roles on the API key | A key bound to an agent rejects any request from a different agent. A key with permitted_roles rejects claims for roles not in the list. |
| Policy-level agent IDs | permitted_agent_ids in YAML policy | A policy can require that only named agents (exact match or wildcard) may evaluate against it. Useful for locking a high-sensitivity role to a specific production agent. |
agent_id are completely unaffected — the binding checks only run when agent_id is present in the request or when the calling key has bound_agent_id set.Tenant-level enforcement: require agent_id on all calls
For tenants where every AI agent must be registered and identified, you can make agent_id mandatory at the tenant level. Anonymous evaluate calls (no agent_id) are denied immediately with policy_name="agent_id_required" before any other checks run.
Two ways to enable this:
- Global (all tenants): Set
AUTOPIL_REQUIRE_AGENT_ID=1as a server environment variable. Takes effect on restart. - Per tenant: Call
PATCH /v1/admin/tenants/{tenant_id}/settingswith{"require_agent_id": true}. Takes effect immediately, no restart needed.
curl -X PATCH https://api.autopil.ai/v1/admin/tenants/{tenant_id}/settings \
-H "X-API-Key: $SUPERADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"require_agent_id": true}'
Once enabled, the effective rule is: AUTOPIL_REQUIRE_AGENT_ID=1 OR tenant.require_agent_id=true. The SDK path (Guard(require_agent_id=True)) enforces the same rule without an API call, useful for local development or single-tenant deployments.
Typical approval workflow
Register — Developer registers the agent via POST /v1/agents. Status starts as draft.
Approve — Data governance team transitions to approved via PATCH /v1/agents/{agent_id}/status. Only approved agents may evaluate.
Bind key — Create or bind an evaluate-scoped API key to this agent via PATCH /v1/keys/{key_id}/binding. Now only this key can present this agent's ID.
Deploy — The agent runtime passes agent_id in every evaluate call. Audit events are stamped with the agent ID — full traceability from retrieval back to the specific agent instance.
Deprecate — When an agent is retired, transition to deprecated. All subsequent evaluate calls from that agent ID are denied with no code changes required.
Policy file format
Policies are YAML files with a single top-level policies key containing a list of role definitions. Multiple roles live in the same file.
policies: - name: loan_underwriter_policy agent_role: loan_underwriter allowed_sources: - credit_scores - loan_history - property_valuations - income_verification denied_sources: - other_customer_data - internal_risk_models - executive_communications allowed_tasks: - credit_decision - collateral_check - risk_assessment denied_tasks: - account_freeze - fraud_flag max_sensitivity: high session_ttl_minutes: 240 # optional: overrides global server TTL sensitivity_decay: # optional: tightens ceiling as session ages - after_minutes: 60 max_sensitivity: medium - after_minutes: 120 max_sensitivity: low - name: fraud_analyst_policy agent_role: fraud_analyst allowed_sources: - transaction_history - device_signals - watchlist denied_sources: - executive_communications - internal_risk_models max_sensitivity: high
Directory structure
Scale beyond a single file with an industry-organized directory. AutoPIL recursively loads all *.yaml files and injects industry and category metadata automatically from the path — no additional YAML fields required.
policies/
financial_services/
consumer_banking.yaml # 4 roles
wealth.yaml # 4 roles
risk_compliance.yaml # 4 roles
operations.yaml # 4 roles
healthcare/
clinical_operations.yaml # 4 roles
compliance_privacy.yaml # 3 roles
revenue_cycle.yaml # 4 roles
technology/
saas_platform.yaml # 5 roles
telecom/
network_operations.yaml # 3 roles
fraud_assurance.yaml # 3 roles
customer_experience.yaml # 3 roles
logistics/
supply_chain.yaml # 3 roles
fleet_operations.yaml # 3 roles
customs_compliance.yaml # 3 roles
industry is set from the first path component and category from the YAML filename stem. Explicit values in the YAML always take precedence.
Fields reference
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Unique identifier. Appears in the audit log as policy_name. |
agent_role | string | yes | Must match the agent_role passed to guard.protect() exactly. |
allowed_sources | list | yes | Data sources this role may access. Empty list [] = allow all non-denied sources. |
denied_sources | list | yes | Always blocked, regardless of the allowed list. Evaluated first. |
max_sensitivity | string | yes | Sensitivity ceiling: low, medium, high, or critical. |
allowed_tasks | list | no | Task types this role may perform. Empty [] = allow all non-denied tasks. |
denied_tasks | list | no | Task types always blocked for this role. |
session_ttl_minutes | integer | no | Session expiry in minutes for this role. Overrides the global AUTOPIL_SESSION_TTL_MINUTES env var. Omit for no per-role TTL. |
sensitivity_decay | list | no | Age-based sensitivity ceiling schedule. Each entry has after_minutes and max_sensitivity. Evaluated in ascending order — highest exceeded threshold applies. Cannot loosen the static ceiling. |
permitted_agent_ids | list | no | If set, only agents whose agent_id matches one of these patterns may evaluate against this policy. Supports exact match and fnmatch wildcards (e.g. reporting-agent-*). Requests that omit agent_id are denied when this list is non-empty. |
industry | string | no | Auto-injected from directory path (e.g. financial_services). |
category | string | no | Auto-injected from filename stem (e.g. consumer_banking). |
Evaluation order
Evaluation stops at the first matching condition:
- Agent ID — if
permitted_agent_idsis set on the policy: deny ifagent_idis absent; deny ifagent_iddoes not match any pattern in the list. - Denied sources — if
source_idis indenied_sources, immediately deny. - Denied tasks — if
task_typeis indenied_tasks, deny. - Allowed tasks — if
allowed_tasksis non-empty andtask_typeis not in it, deny. - Allowed sources — if
allowed_sourcesis non-empty andsource_idis not in it, deny. - Sensitivity ceiling — the effective ceiling is the more restrictive of
max_sensitivityand any applicablesensitivity_decayrule for the session's current age. Ifsensitivity_levelexceeds the effective ceiling, deny. - Allow — all checks passed.
- Default deny — no policy found for this
agent_role.
task_type and agent_id are both optional in requests. Task-level and agent-ID checks are skipped when the corresponding request field and policy field are both absent.Sensitivity levels
Ordered from least to most sensitive: low < medium < high < critical
| Level | Typical data |
|---|---|
low | Public product catalogs, FAQ knowledge bases, public filings |
medium | Account summaries, transaction history, standard reports |
high | Credit scores, identity records, loan history, PII |
critical | Regulatory filings, internal risk models, audit logs, board communications |
Task scoping
Task scoping restricts a role to specific categories of work. Pass task_type in evaluate requests and define allowed_tasks / denied_tasks in the policy:
@guard.protect( agent_role="loan_underwriter", user_id="user_001", source_id="credit_scores", sensitivity_level=SensitivityLevel.HIGH, task_type="credit_decision", # checked against allowed_tasks / denied_tasks session_id=session_id, ) def get_credit_score(customer_id: str) -> dict: ...
Hot reload
Reload all policies from disk without restarting the server or interrupting agents in flight:
curl -X POST http://localhost:8000/v1/policies/reload \ -H "X-API-Key: apl_yourkey" # → { "loaded": 45, "policies_count": 45 }
You can also manage policies via the REST API — create, update, and delete individual policies without touching YAML files. DB-managed and file-loaded policies coexist.
Agent ID binding in policies
Add permitted_agent_ids to a policy to lock it to specific registered agent instances. This is a policy-level complement to registry approval — useful when you want a single high-sensitivity role usable only by a named production agent, even if other agents share the same role name.
# policies/financial_services/fraud_investigation.yaml policies: - name: transaction_analyst_policy agent_role: transaction_analyst permitted_agent_ids: # only these agent IDs may use this policy - fraud-analyst-prod # exact match - fraud-analyst-* # fnmatch wildcard — matches fraud-analyst-v2, etc. allowed_sources: - transaction_history - velocity_signals max_sensitivity: critical session_ttl_minutes: 60
permitted_agent_ids is set. Any evaluate call that omits agent_id is denied with policy_name="transaction_analyst_policy" and reason "anonymous calls are not permitted". This is intentional — once you lock a policy to specific agents, all callers must identify themselves.REST API
All /v1/* routes require an API key in the X-API-Key header. The server returns HTTP 401 if missing and 403 if invalid or revoked.
Authentication
curl https://api.autopil.ai/v1/... \
-H "X-API-Key: apl_yourkey"
Context evaluate
# Request body { "agent_role": "loan_underwriter", "user_id": "user_001", "source_id": "credit_scores", "sensitivity_level": "high", "session_id": "sess_abc", "task_type": "credit_decision", # optional "agent_id": "loan-agent-prod-01" # optional — enables identity binding } # Response (ALLOW) { "decision": "ALLOW", "policy_name": "loan_underwriter_policy", "reason": "all checks passed", "event_id": "evt_abc123" } # Response (DENY) { "decision": "DENY", "policy_name": "loan_underwriter_policy", "reason": "source 'executive_comms' is on denylist", "event_id": "evt_def456" }
Audit log
{
"total": 1247,
"allowed": 1118,
"denied": 129,
"by_source_type": {
"sdk": { "total": 847, "allowed": 790, "denied": 57 },
"api": { "total": 203, "allowed": 178, "denied": 25 },
"langchain": { "total": 87, "allowed": 80, "denied": 7 },
"bedrock": { "total": 64, "allowed": 58, "denied": 6 },
"rest": { "total": 46, "allowed": 12, "denied": 34 }
}
}
Log retention
Retention is configured per tenant from Settings → Retention in the dashboard. Defaults are 365 days for audit events and 90 days for session records. The purge workers run once at server startup then every 24 hours.
{
"retention_days": 365,
"session_retention_days": 90,
"audit_policy": "Audit events older than 365 days are automatically purged daily.",
"session_policy": "Expired and revoked sessions older than 90 days are purged daily."
}
{
"retention_days": 180,
"session_retention_days": 60
}
Either field is optional — omit one to leave it unchanged. Set to 0 to disable automatic purge for that data type. Reducing retention permanently deletes records older than the new window on the next nightly cycle.
Call GET /v1/audit/retention to inspect the last purge anchor for chain integrity:
{
"retention_days": 365,
"policy": "Audit events older than 365 days are automatically purged daily.",
"anchor": {
"anchor_hash": "a3f8c1d2...",
"purged_before": "2025-03-30T00:00:00",
"purged_count": 142
}
}
anchor is null if no purge has run yet. Once set, auditors can confirm the chain is intact from the anchor forward by calling GET /v1/audit/verify.
Session retention
Session records are purged separately from audit events. Only expired or revoked sessions older than session_retention_days are deleted — active sessions are never purged. The default is 90 days. Configure from Settings → Retention alongside audit retention.
PII masking
When AUTOPIL_LOG_PII_MASK=true, three fields are masked before each new audit event is written. Existing events are not modified.
| Field | Stored as | Notes |
|---|---|---|
query | sha256:<16-char hex> | Tenant-salted SHA-256 of the raw query |
user_id | uid:<16-char hex> | Tenant-salted hash — consistent within a tenant, so per-user filtering still works |
reason | Preserved, query=<value> substrings replaced with query=sha256:<hash> | Policy decision context is kept intact; embedded query content is removed |
Events written with masking enabled carry "pii_masked": true. The dashboard Event Detail drawer shows a PII masked badge on these events. The context_hash is unaffected — chain integrity verification continues to work normally.
{
"query": "sha256:a3f8c1d2e4b7f091",
"user_id": "uid:7c2a9f3d11e04b82",
"reason": "policy consumer_banking denied: sensitivity=high exceeds max_allowed=medium, query=sha256:a3f8c1d2e4b7f091",
"pii_masked": true
}
Policies
Alerts
Rule types: denial_spike, new_source_access, isolation_violation, high_deny_rate.
curl -X POST http://localhost:8000/v1/alerts/rules \ -H "X-API-Key: apl_yourkey" \ -H "Content-Type: application/json" \ -d '{ "name": "denial spike", "rule_type": "denial_spike", "threshold": 10, "window_minutes": 15, "cooldown_minutes": 60, "webhook_url": "https://hooks.slack.com/services/..." }'
Sessions
API keys
Keys support three permission scopes:
| Scope | Access | Use case |
|---|---|---|
admin | All endpoints | Dashboard, policy management, key management |
read | GET + evaluate | Monitoring, audit review, read-only integrations |
evaluate | POST /v1/context/evaluate only | Agent runtime — least privilege for production agents |
curl -X POST http://localhost:8000/v1/keys \ -H "X-API-Key: apl_yourkey" \ -H "Content-Type: application/json" \ -d '{ "name": "agent-runtime", "scope": "evaluate", "expires_days": 90 }' # Response — store the key now, it will not be shown again { "key_id": "key_abc123", "name": "agent-runtime", "key": "apl_...", "scope": "evaluate", "expires_at": "2026-07-05T00:00:00", "created_at": "2026-04-06T00:00:00" }
Key binding
Bind a key to a specific registered agent and/or restrict it to a set of roles. Both fields are optional and independent — you can set one or both.
| Field | Type | Effect |
|---|---|---|
bound_agent_id | string | The key will only accept evaluate calls that present this exact agent_id. Requests with a different or missing agent_id are denied with policy_name="key_agent_binding". |
permitted_roles | list | The key will only accept evaluate calls that claim a role in this list. Empty list or omitted = no role restriction. |
curl -X PATCH https://api.autopil.ai/v1/keys/key_abc123/binding \ -H "X-API-Key: apl_yourkey" \ -H "Content-Type: application/json" \ -d '{ "bound_agent_id": "fraud-analyst-prod-01", "permitted_roles": ["transaction_analyst", "account_profiler"] }' # → { "status": "ok" }
Agent registry
The agent registry is the authoritative catalog of AI agent instances in your tenant. Agents must be registered and approved before they can present an agent_id in evaluate calls.
draftdraft → pending_approval → approved → deprecated| Field | Type | Description |
|---|---|---|
agent_role | string | The role this agent will claim in evaluate calls. Must match a policy. |
display_name | string | Human-readable name shown in the dashboard. |
description | string | Purpose and data access rationale. Used during approval review. |
owner | string | Team or individual responsible for this agent. |
framework | string | Runtime framework: langgraph, openai_agents, bedrock, etc. |
permitted_roles | list | Roles this agent is allowed to claim. If omitted, defaults to [agent_role]. |
credential_type | string | Authentication method: api_key (default), jwt_oidc, mtls, conjur. |
bound_key_id | string | If set, only the key with this ID may present this agent's identity. Prevents key sharing across agent instances. |
# 1. Register curl -X POST https://api.autopil.ai/v1/agents \ -H "X-API-Key: apl_yourkey" \ -H "Content-Type: application/json" \ -d '{ "agent_role": "transaction_analyst", "display_name": "Fraud Transaction Analyst v2", "description": "Analyzes transaction patterns for fraud signals", "owner": "fraud-eng-team", "framework": "langgraph" }' # → { "agent_id": "agt_abc123", "status": "draft", ... } # 2. Approve (governance review complete) curl -X PATCH https://api.autopil.ai/v1/agents/agt_abc123/status \ -H "X-API-Key: apl_yourkey" \ -H "Content-Type: application/json" \ -d '{"status": "approved"}' # 3. Evaluate with agent_id — identity is now verified curl -X POST https://api.autopil.ai/v1/context/evaluate \ -H "X-API-Key: apl_yourkey" \ -H "Content-Type: application/json" \ -d '{ "agent_role": "transaction_analyst", "agent_id": "agt_abc123", "user_id": "user_001", "source_id": "transaction_history", "sensitivity_level": "critical", "session_id": "sess_xyz" }'
Tenant enforcement settings
| Field | Type | Description |
|---|---|---|
require_agent_id | bool | When true, every evaluate call for this tenant must supply an agent_id. Anonymous callers are denied with policy_name="agent_id_required". Takes effect immediately — no restart needed. The global server flag AUTOPIL_REQUIRE_AGENT_ID=1 applies this to all tenants at once. |
Superadmin key provisioning
Allows a superadmin to create additional keys for any tenant without needing that tenant's credentials. Accepts the same name, scope, and expires_days fields as POST /v1/keys. Returns 404 if the tenant does not exist and 409 if the tenant is deactivated.
Choosing an integration pattern
AutoPIL has two primary integration paths for Python — the embedded decorator and the REST API. They enforce the same policies and write to the same audit log, but the right choice depends on how your stack is deployed and how user identity flows through your system.
Decision guide
| Scenario | Use | Why |
|---|---|---|
| Local development or single-process self-hosted deployment | Embedded decorator (@guard.protect) | No network hop; policy files load directly from disk in the same process |
| AutoPIL deployed on Render or any remote host | REST API (POST /v1/context/evaluate) | The embedded ContextGuard would open its own SQLite/Postgres — separate from the server's. All agents must call the shared server |
| Service-account style access — one known identity per retrieval function | Embedded decorator | agent_role and user_id are fixed at decoration time; all calls to that function share the same audit identity |
| Multi-user app — different end users hit the same retrieval path | REST API | user_id comes in the request payload per call, so each request gets a distinct identity in the audit log |
| Polyglot stack (Go, Java, TypeScript, Ruby) | REST API | The embedded SDK is Python-only; any language can call /v1/context/evaluate |
| Already using LangChain, LlamaIndex, Bedrock, or OpenAI Agents SDK | Framework guard for that library | Wraps at the tool or query-engine level without changing your retrieval code |
| Enforcing governance without touching agent code | AutoPILMiddleware (ASGI) | Intercepts every request at the HTTP layer; agents need no changes |
The user_id distinction
user_id identifies who triggered a retrieval in the audit log — it does not affect allow/deny decisions. How it flows differs between the two patterns:
| Embedded decorator | REST API | |
|---|---|---|
| Set at | Decoration time — fixed per function | Request time — varies per call |
| Good for | Service accounts, internal agents with a single identity | Multi-user apps where attribution matters per request |
| Tradeoff | All calls to the function share one user_id in the audit trail | Each call carries its own user_id — more granular audit trail |
agent_role is developer-supplied — it declares the identity of the access path, not a runtime claim by the agent. The agent cannot change it. In the decorator, it is fixed at decoration time. In the REST API, it is a declared field in the request payload trusted because the API key is already authenticated.
Side-by-side example
The same governance check — analyst accessing financial reports at high sensitivity — written both ways:
from autopil import ContextGuard guard = ContextGuard(policy_path="policies/", audit_db="autopil.db") # agent_role and user_id are fixed here — the agent cannot change them @guard.protect( agent_role="analyst", user_id="svc-reporting-agent", # same for every call source_id="financial_reports", sensitivity_level="high", ) def retrieve_report(query: str): return vectorstore.search(query)
import httpx def retrieve_report(query: str, user_id: str, session_id: str): r = httpx.post( "https://your-app.onrender.com/v1/context/evaluate", headers={"X-API-Key": AUTOPIL_API_KEY}, json={ "agent_role": "analyst", "user_id": user_id, # varies per caller "source_id": "financial_reports", "sensitivity_level": "high", "query": query, "session_id": session_id, }, timeout=10, ) r.raise_for_status() result = r.json() if result["decision"] == "DENY": raise PermissionError(result["reason"]) return vectorstore.search(query)
ContextGuard(audit_db="...") locally opens a separate database — your events will not appear in the shared audit log and policies will not be in sync. Use the REST pattern for any hosted deployment.
Python SDK
The Python package embeds directly in your agent process. The ContextGuard decorator is the primary interface.
Connecting to a hosted instance (Render / production)
If your AutoPIL instance is deployed on Render or any other host, call the REST API directly — do not use the embedded ContextGuard. Follow these steps in order.
Verify your instance is runningcurl https://your-app.onrender.com/health should return {"status": "ok"}. If it fails, check your Render service logs before continuing.
Create a policy in the dashboard
Go to Policies → New Policy. Set the agent role (e.g. analyst), max sensitivity, and allowed sources. Without a matching policy every call is denied.
Create an API key
Go to Settings → API Keys → Create Key. Choose evaluate scope. Copy the key immediately — it is shown only once.
Set environment variables — never hardcode keys in source code.
AUTOPIL_BASE_URL=https://your-app.onrender.com AUTOPIL_API_KEY=apl_xxxxxxxxxxxxxxxxxxxx
Install the HTTP clientpip install httpx
Wrap your retrieval function
import os, uuid, httpx AUTOPIL_BASE_URL = os.environ["AUTOPIL_BASE_URL"] AUTOPIL_API_KEY = os.environ["AUTOPIL_API_KEY"] def evaluate_context(agent_role, user_id, source_id, query, sensitivity_level="high", session_id=None): try: r = httpx.post( f"{AUTOPIL_BASE_URL}/v1/context/evaluate", headers={"X-API-Key": AUTOPIL_API_KEY}, json={"query": query, "agent_role": agent_role, "user_id": user_id, "source_id": source_id, "sensitivity_level": sensitivity_level, "session_id": session_id}, timeout=10, ) r.raise_for_status() except (httpx.TimeoutException, httpx.ConnectError): raise PermissionError("[AutoPIL] Governance check unreachable — access blocked") result = r.json() if result["decision"] == "DENY": raise PermissionError(result["reason"]) return result # result["event_id"] available for lineage # Generate session_id once per conversation/workflow — not per call SESSION_ID = str(uuid.uuid4()) result = evaluate_context( agent_role="analyst", user_id="user_123", source_id="financial_reports", query="Q3 revenue", sensitivity_level="high", session_id=SESSION_ID, ) # ALLOW — proceed with retrieval context = your_vectorstore.search(query)
Verify it is working
Open the dashboard → Audit Events. Your event should appear with the agent role, source, and decision. If nothing shows up: confirm the agent_role and source_id match your policy exactly, and that the API key belongs to the correct tenant.
ContextGuard (embedded mode)
Use this for local development or self-hosted single-process deployments where AutoPIL runs in the same process as your agent. For production agents on Render, use the REST pattern above.
from autopil import ContextGuard guard = ContextGuard( policy_path="policies/", # file or directory audit_db="autopil.db", # SQLite path database_url="postgresql://...", # use Postgres instead tenant_id="ten_abc", # optional — uses default tenant )
@guard.protect() decorator parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_role | str | yes | Must match a policy's agent_role exactly. |
user_id | str | yes | The user on whose behalf the agent is acting. |
source_id | str | yes | The data source being accessed. |
sensitivity_level | SensitivityLevel | yes | LOW, MEDIUM, HIGH, or CRITICAL. |
session_id | str | yes | Groups related retrievals. Enforces cross-agent isolation. |
agent_id | str | no | Registered agent instance ID. When supplied, triggers identity binding checks — registry approval, role permissions, and key binding. Stamped on every audit event for full traceability. |
task_type | str | no | Checked against allowed_tasks / denied_tasks if provided. |
tenant_id | str | no | Override the tenant set at construction time. |
Action lineage
After a successful retrieval, record what the agent did with the context:
result = get_credit_score("cust_abc") guard.record_action( event_id=guard.last_event_id, # thread-local, set by protect() action_type="loan_decision", detail={"outcome": "approved", "amount": 250000}, outcome="approved", )
guard.last_event_id is stored in threading.local() for sync code. For async agents using protect_async, it uses contextvars.ContextVar — safe under asyncio.gather() and concurrent async tasks.
Async decorator — protect_async
For async agent frameworks (FastAPI, asyncio, aioboto3), use protect_async instead of protect. It uses contextvars.ContextVar instead of threading.local(), making last_event_id safe under concurrent async tasks.
from autopil import ContextGuard, SensitivityLevel guard = ContextGuard(policy_path="policies/") @guard.protect_async( agent_role="analyst", user_id="user_002", source_id="reports", sensitivity_level=SensitivityLevel.MEDIUM, session_id=session_id, ) async def fetch_report(query: str) -> list: return await vector_db.asearch(query) # Safe to run concurrently — each task gets its own ContextVar slot results = await asyncio.gather( fetch_report("Q1 revenue"), fetch_report("Q2 margins"), )
protect_async uses the same _source_type="sdk" as the sync decorator. All audit events are stamped consistently regardless of sync vs async path.
TypeScript SDK
A thin REST API wrapper published as @autopil/sdk. Zero runtime dependencies — uses native fetch (Node 18+).
npm install @autopil/sdk
Set your Render URL and API key as environment variables, then pass them to the client. Get your key from the dashboard under Settings → API Keys (evaluate scope for agent code).
import { AutoPilClient } from "@autopil/sdk" const client = new AutoPilClient({ baseUrl: process.env.AUTOPIL_BASE_URL!, // https://your-app.onrender.com apiKey: process.env.AUTOPIL_API_KEY!, }) // Evaluate a retrieval request const result = await client.context.evaluate({ agent_role: "loan_underwriter", source_id: "credit_scores", user_id: "user_001", sensitivity_level: "high", session_id: "sess_abc", }) if (result.decision === "DENY") { throw new Error(result.reason) } // Query audit events const events = await client.audit.listEvents({ agent_role: "loan_underwriter", decision: "DENY", limit: 50, })
Client resources
| Resource | Methods |
|---|---|
client.context | evaluate() |
client.sessions | list(), get(), revoke() |
client.audit | getSession(), listEvents(), getStats() |
client.lineage | record(), get() |
client.policies | list(), create(), update(), delete(), reload(), getHistory() |
client.alerts | createRule(), listRules(), updateRule(), deleteRule(), listFired() |
client.keys | create(), list(), revoke() |
client.admin | createTenant(), listTenants(), deactivateTenant() |
client.health() | — no auth required |
AutoPilError is thrown for non-2xx responses and carries statusCode and detail from the server.
MCP Server
AutoPIL ships an MCP (Model Context Protocol) server that exposes context governance as native tools for any MCP-compatible AI agent — Claude, GPT-4, Gemini, or any custom agent that speaks MCP. No code changes in the agent. The governance layer sits between the model and your data.
Install
pip install autopil[mcp]
Start the server
autopil-mcp --policy policies/ --db autopil.db
# or with Postgres:
export DATABASE_URL=postgresql://user:pass@host:5432/autopil
autopil-mcp --policy policies/
Claude Desktop configuration
{
"mcpServers": {
"autopil": {
"command": "autopil-mcp",
"args": ["--policy", "/path/to/policies/", "--db", "/path/to/autopil.db"],
"env": {}
}
}
}
Restart Claude Desktop. AutoPIL's tools will appear in the tool list automatically.
Available tools
| Tool | Description |
|---|---|
evaluate_context | Policy check before retrieval — returns ALLOW or DENY with reason and event_id |
record_action | Link a downstream decision to the audit event that authorized it (lineage) |
query_audit_log | Query recent audit events, filterable by role, decision, or session |
get_audit_stats | Aggregate stats — totals, deny rate, top roles |
list_policies | List active policies, filterable by industry or agent_role |
reload_policies | Hot-reload policies from disk without restarting |
get_session_status | Session summary — owner, event count, sources accessed |
System prompt pattern
Add this to your agent's system prompt so governance is enforced automatically:
Before accessing any data source, you MUST call evaluate_context with:
- agent_role: your role in this conversation
- user_id: the current user's ID
- source_id: the data source you want to access
- sensitivity_level: the sensitivity of that data
- session_id: a consistent ID for this conversation
Only proceed if decision is ALLOW. If DENY, inform the user and provide the reason.
After a successful retrieval, call record_action with the event_id.
evaluate_context response
✅ ALLOW — loan_underwriter may access 'credit_scores'.
Policy: loan_underwriter_policy
Event ID: evt_abc123 (use this in record_action to log what you did with the data)
🚫 DENY — loan_underwriter is not permitted to access 'executive_communications'.
Reason: source 'executive_communications' is on denylist for role 'loan_underwriter'
Policy: loan_underwriter_policy
Do not proceed with this retrieval. Inform the user that access is not permitted.
Cross-agent isolation
Session isolation is enforced automatically. If a fraud_analyst agent tries to access a session owned by loan_underwriter:
🚫 DENY — fraud_analyst is not permitted to access session 'sess_abc12…'.
Reason: Session is owned by 'loan_underwriter'. Agent 'fraud_analyst' cannot access another agent's session context.
Policy: cross_agent_isolation
This happens without any configuration — it's built into the evaluate_context tool.
ASGI Middleware
AutoPILMiddleware enforces access policy at the HTTP layer for FastAPI and Starlette apps — before your route handler runs. Each RouteRule maps a URL pattern to a policy check.
from fastapi import FastAPI from autopil.middleware import AutoPILMiddleware, RouteRule from autopil import ContextGuard, SensitivityLevel guard = ContextGuard(policy_path="policies/") app = FastAPI() app.add_middleware( AutoPILMiddleware, guard=guard, rules=[ RouteRule( path_pattern=r"^/api/credit/.*", agent_role="loan_underwriter", user_id_header="X-User-ID", source_id="credit_scores", sensitivity_level=SensitivityLevel.HIGH, on_deny="reject", # or "log" for shadow mode ), RouteRule( path_pattern=r"^/api/reports/.*", agent_role="analyst", user_id_header="X-User-ID", source_id="reports", sensitivity_level=SensitivityLevel.MEDIUM, on_deny="log", # shadow mode — log but allow through ), ], )
RouteRule parameters
| Parameter | Type | Description |
|---|---|---|
path_pattern | str (regex) | Regex matched against the request path. First matching rule wins. |
agent_role | str | Role used for policy evaluation. |
user_id_header | str | HTTP header name from which to extract user_id (e.g. X-User-ID). |
source_id | str | Data source label evaluated against the policy. |
sensitivity_level | SensitivityLevel | Sensitivity of the data behind this route. |
on_deny | str | "reject" (default) returns HTTP 403. "log" logs the denial but allows the request through (shadow mode). |
source_type="api", regardless of which guard subclass is in use. This lets the dashboard distinguish HTTP-layer enforcement from SDK-level enforcement.
Framework integrations
AutoPIL ships native guard subclasses for the five major agent frameworks. Each sets a distinct source_type so the dashboard can break down access by framework.
LangChain
from autopil.langchain_guard import LangChainGuard from langchain_core.tools import tool guard = LangChainGuard(policy_path="policies/") @tool @guard.protect( agent_role="research_analyst", user_id="u1", source_id="market_data", sensitivity_level=SensitivityLevel.MEDIUM, session_id=session_id, ) def get_market_data(ticker: str) -> dict: return data_api.fetch(ticker) # source_type="langchain" — works with LCEL and LangChain agents
LlamaIndex
from autopil.llamaindex_guard import LlamaIndexGuard guard = LlamaIndexGuard(policy_path="policies/") @guard.protect( agent_role="document_analyst", user_id="u1", source_id="legal_contracts", sensitivity_level=SensitivityLevel.HIGH, session_id=session_id, ) def retrieve_contract(clause: str) -> str: return index.as_query_engine().query(clause) # source_type="llamaindex" — wraps query engines and retrievers
Gemini
from autopil.gemini_guard import GeminiGuard guard = GeminiGuard(policy_path="policies/") @guard.protect( agent_role="content_reviewer", user_id="u1", source_id="internal_docs", sensitivity_level=SensitivityLevel.MEDIUM, session_id=session_id, ) def fetch_document(doc_id: str) -> str: return docs_api.get(doc_id) # source_type="gemini" — wraps functions called from Gemini function-calling agents
OpenAI Agents SDK
from autopil.openai_agents_guard import OpenAIAgentsGuard from agents import function_tool guard = OpenAIAgentsGuard(policy_path="policies/") @function_tool @guard.protect( agent_role="compliance_checker", user_id="u1", source_id="regulatory_filings", sensitivity_level=SensitivityLevel.RESTRICTED, session_id=session_id, ) def get_filing(filing_id: str) -> dict: return filings_db.fetch(filing_id) # source_type="openai_agents"
AWS Bedrock Agents
import boto3 from autopil.bedrock_guard import BedrockGuard guard = BedrockGuard(policy_path="policies/") boto_client = boto3.client("bedrock-agent-runtime") # Wrap the client — guard extracts inputText as the policy query client = guard.wrap_invoke_agent( boto_client, agent_role="compliance_agent", user_id="u1", source_id="regulatory_data", sensitivity_level=SensitivityLevel.HIGH, session_id=session_id, ) response = client.invoke_agent( agentId="ABCDEF123", agentAliasId="TSTALIASID", sessionId=session_id, inputText="Summarize Q1 compliance filings", ) # source_type="bedrock" — ALLOW runs the call; DENY raises PermissionError
import aioboto3 from autopil.bedrock_guard import BedrockGuard guard = BedrockGuard(policy_path="policies/") async with aioboto3.Session().client("bedrock-agent-runtime") as boto_client: client = guard.wrap_invoke_agent_async( boto_client, agent_role="compliance_agent", user_id="u1", source_id="regulatory_data", sensitivity_level=SensitivityLevel.HIGH, session_id=session_id, ) response = await client.invoke_agent( agentId="ABCDEF123", agentAliasId="TSTALIASID", sessionId=session_id, inputText="Summarize Q1 compliance filings", )
Integration channel reference
| Guard class | source_type | Use case |
|---|---|---|
ContextGuard | sdk | Python microservices, notebooks, sync agents |
ContextGuard.protect_async | sdk | Async Python agents (asyncio, FastAPI) |
AutoPILMiddleware | api | FastAPI / Starlette HTTP-layer enforcement |
| MCP server | mcp | Claude Desktop, any MCP-compatible agent |
| REST API directly | rest | Go, Java, Ruby, PHP, .NET clients |
LangChainGuard | langchain | LangChain agents, tools, LCEL pipelines |
LlamaIndexGuard | llamaindex | LlamaIndex query engines and retrievers |
GeminiGuard | gemini | Google Gemini function-calling agents |
OpenAIAgentsGuard | openai_agents | OpenAI Agents SDK function tools |
BedrockGuard | bedrock | AWS Bedrock Agents via boto3 / aioboto3 |
Go SDK
A stdlib-only REST client published at github.com/vibrantcapital/autopil-go. No external dependencies — uses only net/http and encoding/json. Requires Go 1.21+.
go get github.com/vibrantcapital/autopil-go
Set AUTOPIL_BASE_URL and AUTOPIL_API_KEY in your environment. Get your key from the dashboard under Settings → API Keys (evaluate scope for agent code).
import "github.com/vibrantcapital/autopil-go/autopil" client := autopil.New( os.Getenv("AUTOPIL_BASE_URL"), // https://your-app.onrender.com os.Getenv("AUTOPIL_API_KEY"), ) // Evaluate a retrieval request result, err := client.Context.Evaluate(ctx, autopil.EvaluateRequest{ AgentRole: "loan_underwriter", UserID: "user_001", SourceID: "credit_scores", SensitivityLevel: "high", SessionID: sessionID, }) if err != nil { return err } if result.Decision == "DENY" { return fmt.Errorf("access denied: %s", result.Reason) } // Query audit events events, err := client.Audit.ListEvents(ctx, autopil.ListEventsParams{ AgentRole: "loan_underwriter", Decision: "DENY", Limit: 50, })
Client resources
| Resource | Methods |
|---|---|
client.Context | Evaluate() |
client.Audit | ListEvents(), GetStats(), GetSession(), RecordLineage(), GetLineage() |
client.Sessions | List(), Get(), Revoke() |
client.Policies | List(), Create(), Update(), Delete(), Reload() |
client.Health() | — no auth required |
AutoPILError implements error and carries StatusCode int and Detail string from the server response. Options: autopil.WithTimeout(d), autopil.WithHTTPClient(c).
Java SDK
A Jackson-only REST client published as ai.vibrantcapital:autopil-java:0.1.0. Uses java.net.http.HttpClient (no external HTTP library). Requires Java 17+.
Set AUTOPIL_BASE_URL and AUTOPIL_API_KEY as environment variables. Get your key from the dashboard under Settings → API Keys (evaluate scope for agent code).
<dependency> <groupId>ai.vibrantcapital</groupId> <artifactId>autopil-java</artifactId> <version>0.1.0</version> </dependency>
AutoPILClient client = AutoPILClient.builder() .baseUrl(System.getenv("AUTOPIL_BASE_URL")) // https://your-app.onrender.com .apiKey(System.getenv("AUTOPIL_API_KEY")) .build(); // Evaluate a retrieval request EvaluateResponse resp = client.context().evaluate( EvaluateRequest.builder() .agentRole("loan_underwriter") .userId("user_001") .sourceId("credit_scores") .sensitivityLevel("high") .sessionId(sessionId) .build() ); if ("DENY".equals(resp.getDecision())) { throw new SecurityException(resp.getReason()); } // Query audit events AuditTrail trail = client.audit().getSession(sessionId); List<AuditEvent> events = client.audit().listEvents("loan_underwriter", "DENY", 50);
Client resources
| Resource | Methods |
|---|---|
client.context() | evaluate(EvaluateRequest) |
client.audit() | getSession(), listEvents(), getStats(), recordLineage() |
client.sessions() | list(), get(), revoke() |
client.policies() | list(), create(), update(), delete(), reload() |
client.health() | — no auth required |
AutoPILException (unchecked) carries getStatusCode() and getDetail(). All request types use builder pattern. JUnit 5 test suite included.
Local development
The repo is a monorepo. The backend lives in packages/core and the dashboard in packages/dashboard. Run them in two terminals:
python3 -m venv .venv && source .venv/bin/activate pip install -e "packages/core[server,postgres]" pip install -e "packages/saas[catalog]" # Build the dashboard cd packages/dashboard && npm install && npm run build cp -r dist/* ../core/autopil/api/static/ && cd ../.. # Start (full stack) AUTOPIL_DB=./autopil_audit.db \ AUTOPIL_POLICY=./policies \ AUTOPIL_SECRET_KEY=local-dev-secret \ AUTOPIL_RESET_ADMIN_KEY=1 \ autopil-saas-serve
Server flags (core edition)
| Flag | Default | Description |
|---|---|---|
--policy | policies/ | Path to policy file or directory |
--db | autopil_audit.db | SQLite audit DB path |
--host | 0.0.0.0 | Bind host |
--port | 8000 | Listen port |
Docker
# Build docker build -t autopil:latest . # Run with SQLite (minimal) docker run -p 8000:8000 -e AUTOPIL_SECRET_KEY=your-secret autopil:latest # Run with persistent data and custom policies docker run -d \ --name autopil \ -p 8000:8000 \ -v $(pwd)/policies:/app/policies:ro \ -v autopil-data:/data \ -e AUTOPIL_SECRET_KEY=your-secret \ autopil:latest # Docker Compose (Postgres + persistent data) docker compose up --build
Postgres
# Full Stack edition pip install "autopil-saas[catalog]" export DATABASE_URL=postgresql://user:pass@host:5432/autopil export AUTOPIL_SECRET_KEY=your-secret export AUTOPIL_POLICY=./policies autopil-saas-serve # Core edition pip install "autopil[server,postgres]" export DATABASE_URL=postgresql://user:pass@host:5432/autopil autopil-serve --policy policies/
Schema is created automatically on first start. Subsequent starts run ALTER TABLE … ADD COLUMN IF NOT EXISTS migration guards — safe to run against existing databases. The docker-compose.yml starts postgres:16-alpine and wires DATABASE_URL automatically.
OpenTelemetry
Activated by setting OTEL_EXPORTER_OTLP_ENDPOINT. When unset, the telemetry module is a complete no-op — no spans, no metrics, no overhead.
# Datadog export OTEL_SERVICE_NAME=autopil export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com/api/intake/otlp/v1 export OTEL_EXPORTER_OTLP_HEADERS="DD-API-KEY=your_key" # Grafana Cloud export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-east-0.grafana.net/otlp export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64>" # Local console (no collector needed) export OTEL_TRACES_EXPORTER=console
Alerting (SMTP)
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | — | SMTP hostname. Required for email delivery. |
SMTP_PORT | 587 | SMTP port |
SMTP_USER | — | SMTP username |
SMTP_PASSWORD | — | SMTP password |
SMTP_FROM | alerts@autopil.ai | From address for alert emails |
Email delivery is only attempted on rules with a configured email field. If SMTP_HOST is not set, delivery is recorded with status no_destination.
Retention
Retention is configured per tenant from the dashboard under Settings → Retention — no environment variable changes required. Defaults are 365 days for audit events and 90 days for session records. Both purge workers start automatically and run every 24 hours.
| Data type | Default | Notes |
|---|---|---|
| Audit events | 365 days | All events older than the configured window are deleted. A chain anchor is stored before each purge. |
| Session records | 90 days | Only expired or revoked sessions older than the configured window are deleted. Active sessions are never purged. |
To change retention, open the Settings tab in the dashboard, update either value, and click Save. A confirmation prompt appears when reducing either window — the deletion is permanent and runs on the next nightly cycle.
The AUDIT_RETENTION_DAYS environment variable sets a system-wide fallback but is superseded by per-tenant settings once configured. Check current retention state and last purge anchor via GET /v1/audit/retention. See Log retention in the API reference for the full response schema.
PII masking
For deployments subject to data privacy requirements, AutoPIL can mask sensitive content in audit events before writing to the database. Enable with a single environment variable:
| Variable | Default | Description |
|---|---|---|
AUTOPIL_LOG_PII_MASK | — | Set to true to mask query, user_id, and embedded query content in reason before writing each new audit event. Existing events are not modified. |
AUTOPIL_LOG_PII_MASK=true
Salt is derived from AUTOPIL_SECRET_KEY + tenant_id — the same user always hashes to the same value within a tenant, so per-user filtering still works. See PII masking in the API reference for field-by-field detail.
Enforcing agent identity globally
To require that every evaluate call across all tenants must supply a registered agent_id, set the server-wide flag:
| Variable | Default | Description |
|---|---|---|
AUTOPIL_REQUIRE_AGENT_ID | — | Set to 1, true, or yes to deny any evaluate call that omits agent_id. Denial is recorded with policy_name="agent_id_required". Per-tenant enforcement can also be toggled at runtime via PATCH /v1/admin/tenants/{id}/settings without a restart. |