Start Free Trial
Getting started

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

terminal
# 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

retrieval.py
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:

terminal
autopil-serve --policy policies/

On first start, if no tenants exist, the server creates a default tenant and prints a superadmin key once:

stdout
⚠  First start — created 'default' tenant and superadmin key:
   apl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
   Save this — it will not be shown again.
Save this key. It is shown exactly once. Use it as 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:

terminal
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.

Core concepts

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:

1

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.

2

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".

3

Session lifecycle check — Sessions that are expired or revoked are denied before policy evaluation runs. Status values: active, expired, revoked.

4

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.

5

OTEL span emitted — An autopil.evaluate span is started. Decision, latency, and policy name are recorded. DENY sets StatusCode.ERROR on the span.

6

Retrieval executes (ALLOW only) — The wrapped function runs. On DENY, the data source is never queried.

7

Context hashing (ALLOW only) — A SHA-256 hash of the returned context is computed (first 16 hex chars stored) for provenance verification.

8

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.

9

Alert rules evaluated — The AlertEngine checks threshold rules against the audit log. Webhook and email delivery is dispatched in a background thread.

10

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.

Provision a tenant and key
# 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:

LayerWhere it's configuredWhat it enforces
Registry approvalAgent registry (dashboard or API)Only agents with status=approved may evaluate. Draft and deprecated agents are denied with policy_name="agent_not_approved".
Role restrictionpermitted_roles on the registry entryAn agent can only claim roles listed in its own permitted_roles. Prevents role escalation across agent types.
Key bindingbound_agent_id / permitted_roles on the API keyA 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 IDspermitted_agent_ids in YAML policyA 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.
Backward compatible. All identity fields are optional. Existing agents that omit 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=1 as a server environment variable. Takes effect on restart.
  • Per tenant: Call PATCH /v1/admin/tenants/{tenant_id}/settings with {"require_agent_id": true}. Takes effect immediately, no restart needed.
enable per-tenant agent_id enforcement
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

1

Register — Developer registers the agent via POST /v1/agents. Status starts as draft.

2

Approve — Data governance team transitions to approved via PATCH /v1/agents/{agent_id}/status. Only approved agents may evaluate.

3

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.

4

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.

5

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 guide

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/financial_services/consumer_banking.yaml
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/
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
Path-based metadata injection: When loading from a directory, 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

FieldTypeRequiredDescription
namestringyesUnique identifier. Appears in the audit log as policy_name.
agent_rolestringyesMust match the agent_role passed to guard.protect() exactly.
allowed_sourceslistyesData sources this role may access. Empty list [] = allow all non-denied sources.
denied_sourceslistyesAlways blocked, regardless of the allowed list. Evaluated first.
max_sensitivitystringyesSensitivity ceiling: low, medium, high, or critical.
allowed_taskslistnoTask types this role may perform. Empty [] = allow all non-denied tasks.
denied_taskslistnoTask types always blocked for this role.
session_ttl_minutesintegernoSession expiry in minutes for this role. Overrides the global AUTOPIL_SESSION_TTL_MINUTES env var. Omit for no per-role TTL.
sensitivity_decaylistnoAge-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_idslistnoIf 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.
industrystringnoAuto-injected from directory path (e.g. financial_services).
categorystringnoAuto-injected from filename stem (e.g. consumer_banking).

Evaluation order

Evaluation stops at the first matching condition:

  1. Agent ID — if permitted_agent_ids is set on the policy: deny if agent_id is absent; deny if agent_id does not match any pattern in the list.
  2. Denied sources — if source_id is in denied_sources, immediately deny.
  3. Denied tasks — if task_type is in denied_tasks, deny.
  4. Allowed tasks — if allowed_tasks is non-empty and task_type is not in it, deny.
  5. Allowed sources — if allowed_sources is non-empty and source_id is not in it, deny.
  6. Sensitivity ceiling — the effective ceiling is the more restrictive of max_sensitivity and any applicable sensitivity_decay rule for the session's current age. If sensitivity_level exceeds the effective ceiling, deny.
  7. Allow — all checks passed.
  8. Default deny — no policy found for this agent_role.
Note: 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

LevelTypical data
lowPublic product catalogs, FAQ knowledge bases, public filings
mediumAccount summaries, transaction history, standard reports
highCredit scores, identity records, loan history, PII
criticalRegulatory 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 call with task_type
@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:

terminal
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.

Restrict a policy to specific agent IDs
# 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
Anonymous calls are denied when 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.
API reference

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

All authenticated requests
curl https://api.autopil.ai/v1/... \
  -H "X-API-Key: apl_yourkey"

Context evaluate

POST/v1/context/evaluateEvaluate a retrieval request against policy
Request / Response
# 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

GET/v1/audit/eventsList audit events (query: limit, agent_role, decision, source_id)
GET/v1/audit/sessions/{session_id}Full audit trail for a session — all events in order
GET/v1/audit/statsAggregate stats — totals, deny rate, top roles, by_source_type breakdown
GET/v1/audit/verifyVerify SHA-256 hash chain integrity for this tenant
GET/v1/audit/retentionActive retention policy and last purge anchor for this tenant
POST/v1/audit/lineageRecord a downstream action linked to an audit event
GET/v1/audit/lineage/{event_id}Event + all linked downstream actions
GET /v1/audit/stats — by_source_type breakdown
{
  "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.

GET/v1/settings/retentionReturn this tenant's current audit and session retention settings
PUT/v1/settings/retentionUpdate retention settings (admin-scoped key required)
GET /v1/settings/retention — response
{
  "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."
}
PUT /v1/settings/retention — request
{
  "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:

GET /v1/audit/retention — response
{
  "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.

FieldStored asNotes
querysha256:<16-char hex>Tenant-salted SHA-256 of the raw query
user_iduid:<16-char hex>Tenant-salted hash — consistent within a tenant, so per-user filtering still works
reasonPreserved, 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.

Example — masked audit event
{
  "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

GET/v1/policiesList active policies (filter: industry, category, agent_role)
POST/v1/policiesCreate a policy
PUT/v1/policies/{policy_id}Update a policy (saves version history, sets user_modified)
DELETE/v1/policies/{policy_id}Soft-delete a policy
GET/v1/policies/{policy_id}/historyFull version history for a policy
POST/v1/policies/{policy_id}/restoreRestore a policy to its original YAML definition (clears user_modified)
POST/v1/policies/reloadReload all policies from disk without restart

Alerts

POST/v1/alerts/rulesCreate an alert rule
GET/v1/alerts/rulesList alert rules
PUT/v1/alerts/rules/{rule_id}Update a rule (enable/disable, thresholds)
DELETE/v1/alerts/rules/{rule_id}Delete a rule
GET/v1/alerts/firedList fired alert deliveries with trigger details

Rule types: denial_spike, new_source_access, isolation_violation, high_deny_rate.

Create a denial spike alert
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

GET/v1/sessionsList sessions (filter: agent_role, status)
GET/v1/sessions/{session_id}Session detail and status
DELETE/v1/sessions/{session_id}Revoke a session immediately

API keys

POST/v1/keysCreate an API key — plaintext returned once, store immediately
GET/v1/keysList keys for the tenant (no plaintext)
DELETE/v1/keys/{key_id}Revoke a key immediately
POST/v1/keys/{key_id}/rotateRotate a key — creates replacement with same scope/expiry, revokes original
PATCH/v1/keys/{key_id}/bindingBind a key to a registered agent or restrict it to specific roles

Keys support three permission scopes:

ScopeAccessUse case
adminAll endpointsDashboard, policy management, key management
readGET + evaluateMonitoring, audit review, read-only integrations
evaluatePOST /v1/context/evaluate onlyAgent runtime — least privilege for production agents
Create a scoped key with expiry
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.

FieldTypeEffect
bound_agent_idstringThe 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_roleslistThe key will only accept evaluate calls that claim a role in this list. Empty list or omitted = no role restriction.
Bind a key to an agent and restrict its roles
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.

POST/v1/agentsRegister a new agent — status starts as draft
GET/v1/agentsList all registered agents for this tenant
GET/v1/agents/{agent_id}Get a single agent by ID — includes live activity stats
PUT/v1/agents/{agent_id}Update agent metadata — display name, description, framework, owner
PATCH/v1/agents/{agent_id}/statusTransition approval status: draft → pending_approval → approved → deprecated
GET/v1/agents/stats/summaryRegistry-wide counts — total, approved, pending, deny rates
FieldTypeDescription
agent_rolestringThe role this agent will claim in evaluate calls. Must match a policy.
display_namestringHuman-readable name shown in the dashboard.
descriptionstringPurpose and data access rationale. Used during approval review.
ownerstringTeam or individual responsible for this agent.
frameworkstringRuntime framework: langgraph, openai_agents, bedrock, etc.
permitted_roleslistRoles this agent is allowed to claim. If omitted, defaults to [agent_role].
credential_typestringAuthentication method: api_key (default), jwt_oidc, mtls, conjur.
bound_key_idstringIf set, only the key with this ID may present this agent's identity. Prevents key sharing across agent instances.
Register → Approve → Use
# 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

PATCH/v1/admin/tenants/{tenant_id}/settingsUpdate per-tenant enforcement flags — superadmin only
FieldTypeDescription
require_agent_idboolWhen 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

POST/v1/admin/tenants/{tenant_id}/keysProvision a key for any tenant — superadmin only

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.

SDKs & integrations

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

ScenarioUseWhy
Local development or single-process self-hosted deploymentEmbedded decorator (@guard.protect)No network hop; policy files load directly from disk in the same process
AutoPIL deployed on Render or any remote hostREST 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 functionEmbedded decoratoragent_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 pathREST APIuser_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 APIThe embedded SDK is Python-only; any language can call /v1/context/evaluate
Already using LangChain, LlamaIndex, Bedrock, or OpenAI Agents SDKFramework guard for that libraryWraps at the tool or query-engine level without changing your retrieval code
Enforcing governance without touching agent codeAutoPILMiddleware (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 decoratorREST API
Set atDecoration time — fixed per functionRequest time — varies per call
Good forService accounts, internal agents with a single identityMulti-user apps where attribution matters per request
TradeoffAll calls to the function share one user_id in the audit trailEach call carries its own user_id — more granular audit trail
agent_role works the same way. In both patterns, 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:

Embedded decorator — fixed identity, same process
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)
REST API — per-request identity, any language
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)
Never use the embedded decorator against a remote AutoPIL instance. If your AutoPIL server is on Render, calling 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.
SDKs

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.

1

Verify your instance is running
curl https://your-app.onrender.com/health should return {"status": "ok"}. If it fails, check your Render service logs before continuing.

2

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.

3

Create an API key
Go to Settings → API Keys → Create Key. Choose evaluate scope. Copy the key immediately — it is shown only once.

4

Set environment variables — never hardcode keys in source code.

.env
AUTOPIL_BASE_URL=https://your-app.onrender.com
AUTOPIL_API_KEY=apl_xxxxxxxxxxxxxxxxxxxx
5

Install the HTTP client
pip install httpx

6

Wrap your retrieval function

agent.py
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)
7

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.

Fail closed on network errors. If AutoPIL is unreachable, block access rather than silently bypassing governance. The example above does this — never swap the except block for a silent pass.

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.

ContextGuard constructor
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

ParameterTypeRequiredDescription
agent_rolestryesMust match a policy's agent_role exactly.
user_idstryesThe user on whose behalf the agent is acting.
source_idstryesThe data source being accessed.
sensitivity_levelSensitivityLevelyesLOW, MEDIUM, HIGH, or CRITICAL.
session_idstryesGroups related retrievals. Enforces cross-agent isolation.
agent_idstrnoRegistered 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_typestrnoChecked against allowed_tasks / denied_tasks if provided.
tenant_idstrnoOverride the tenant set at construction time.

Action lineage

After a successful retrieval, record what the agent did with the context:

Record downstream action
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.

async_retrieval.py
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"),
)
Source type: 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+).

terminal
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).

client usage
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

ResourceMethods
client.contextevaluate()
client.sessionslist(), get(), revoke()
client.auditgetSession(), listEvents(), getStats()
client.lineagerecord(), get()
client.policieslist(), create(), update(), delete(), reload(), getHistory()
client.alertscreateRule(), listRules(), updateRule(), deleteRule(), listFired()
client.keyscreate(), list(), revoke()
client.admincreateTenant(), listTenants(), deactivateTenant()
client.health()— no auth required

AutoPilError is thrown for non-2xx responses and carries statusCode and detail from the server.

SDKs

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

bash
pip install autopil[mcp]

Start the server

bash
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

~/Library/Application Support/Claude/claude_desktop_config.json
{
  "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

ToolDescription
evaluate_contextPolicy check before retrieval — returns ALLOW or DENY with reason and event_id
record_actionLink a downstream decision to the audit event that authorized it (lineage)
query_audit_logQuery recent audit events, filterable by role, decision, or session
get_audit_statsAggregate stats — totals, deny rate, top roles
list_policiesList active policies, filterable by industry or agent_role
reload_policiesHot-reload policies from disk without restarting
get_session_statusSession summary — owner, event count, sources accessed

System prompt pattern

Add this to your agent's system prompt so governance is enforced automatically:

system prompt
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 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 response
🚫 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:

cross-agent isolation DENY
🚫 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.

SDKs & integrations

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.

main.py
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

ParameterTypeDescription
path_patternstr (regex)Regex matched against the request path. First matching rule wins.
agent_rolestrRole used for policy evaluation.
user_id_headerstrHTTP header name from which to extract user_id (e.g. X-User-ID).
source_idstrData source label evaluated against the policy.
sensitivity_levelSensitivityLevelSensitivity of the data behind this route.
on_denystr"reject" (default) returns HTTP 403. "log" logs the denial but allows the request through (shadow mode).
source_type: All events from the middleware are stamped source_type="api", regardless of which guard subclass is in use. This lets the dashboard distinguish HTTP-layer enforcement from SDK-level enforcement.
SDKs & integrations

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

langchain_agent.py
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

llamaindex_agent.py
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

gemini_agent.py
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

openai_agent.py
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

bedrock_agent.py — sync (boto3)
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
bedrock_agent_async.py — async (aioboto3)
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 classsource_typeUse case
ContextGuardsdkPython microservices, notebooks, sync agents
ContextGuard.protect_asyncsdkAsync Python agents (asyncio, FastAPI)
AutoPILMiddlewareapiFastAPI / Starlette HTTP-layer enforcement
MCP servermcpClaude Desktop, any MCP-compatible agent
REST API directlyrestGo, Java, Ruby, PHP, .NET clients
LangChainGuardlangchainLangChain agents, tools, LCEL pipelines
LlamaIndexGuardllamaindexLlamaIndex query engines and retrievers
GeminiGuardgeminiGoogle Gemini function-calling agents
OpenAIAgentsGuardopenai_agentsOpenAI Agents SDK function tools
BedrockGuardbedrockAWS Bedrock Agents via boto3 / aioboto3
SDKs & integrations

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+.

terminal
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).

main.go
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

ResourceMethods
client.ContextEvaluate()
client.AuditListEvents(), GetStats(), GetSession(), RecordLineage(), GetLineage()
client.SessionsList(), Get(), Revoke()
client.PoliciesList(), 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).

pom.xml
<dependency>
  <groupId>ai.vibrantcapital</groupId>
  <artifactId>autopil-java</artifactId>
  <version>0.1.0</version>
</dependency>
Usage.java
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

ResourceMethods
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.

Deployment

Local development

The repo is a monorepo. The backend lives in packages/core and the dashboard in packages/dashboard. Run them in two terminals:

Terminal 1 — backend (Python 3.11+)
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)

FlagDefaultDescription
--policypolicies/Path to policy file or directory
--dbautopil_audit.dbSQLite audit DB path
--host0.0.0.0Bind host
--port8000Listen port

Docker

Build and run
# 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

Postgres setup
# 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.

Environment variables
# 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)

VariableDefaultDescription
SMTP_HOSTSMTP hostname. Required for email delivery.
SMTP_PORT587SMTP port
SMTP_USERSMTP username
SMTP_PASSWORDSMTP password
SMTP_FROMalerts@autopil.aiFrom 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 typeDefaultNotes
Audit events365 daysAll events older than the configured window are deleted. A chain anchor is stored before each purge.
Session records90 daysOnly 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:

VariableDefaultDescription
AUTOPIL_LOG_PII_MASKSet to true to mask query, user_id, and embedded query content in reason before writing each new audit event. Existing events are not modified.
Render environment variable
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:

VariableDefaultDescription
AUTOPIL_REQUIRE_AGENT_IDSet 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.