# AutoPIL — Platform Intelligence Layer

Context governance middleware for autonomous AI agents.

AutoPIL sits between your agents and your data sources, enforcing who can retrieve what, under what conditions — and logging every access decision with an immutable audit trail.

```
Agent → @guard.protect(...) → Policy Engine → Data Source
                    ↓
              Audit Log (SQLite / Postgres)
```

---

## Why AutoPIL

As enterprises deploy multi-agent AI systems, the gap that emerges isn't in the LLMs — it's in what context they're allowed to retrieve. Most governance tools operate at the output layer (content filtering, hallucination detection). AutoPIL operates at the **retrieval layer**, before sensitive data ever enters the agent's context window.

Key problems it solves:

- An underwriting agent accessing another customer's credit data
- A customer service bot retrieving internal risk models it has no business seeing
- One agent in a pipeline hijacking another agent's session context
- No audit trail of what data was retrieved and when

---

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│                       Your Agent                        │
└────────────────────────┬────────────────────────────────┘
                         │  @guard.protect(...)
┌────────────────────────▼────────────────────────────────┐
│                    ContextGuard                         │
│  1. Cross-agent session isolation check                 │
│  2. Policy evaluation (allow / deny)                    │
│  3. Execute retrieval (if allowed)                      │
│  4. Hash context for provenance                         │
│  5. Record immutable audit event                        │
└──────────────┬────────────────────────┬─────────────────┘
               │                        │
┌──────────────▼──────┐   ┌─────────────▼───────────────┐
│   PolicyEngine      │   │     Storage Backend          │
│   YAML-based rules  │   │   SQLite (default)           │
│   Deny by default   │   │   Postgres (production)      │
└─────────────────────┘   └─────────────────────────────┘
```

**Five enforcement mechanisms:**

| Mechanism | Description |
|---|---|
| Source allowlist | Agent can only access explicitly permitted data sources |
| Source denylist | Certain sources are always blocked, regardless of allowlist |
| Sensitivity ceiling | Each role has a maximum data sensitivity level it can access |
| Cross-agent isolation | A session belongs to the first agent that used it — others are denied |
| Default deny | No matching policy = access denied |

---

## Quickstart

### Install

```bash
pip install -e .
```

For Postgres support:

```bash
pip install -e ".[postgres]"
```

### Define a policy

```yaml
# policies/my_policy.yaml
policies:
  - name: analyst_policy
    agent_role: analyst
    allowed_sources:
      - reports
      - market_data
    denied_sources:
      - executive_comms
    max_sensitivity: high
```

### Wrap your retrieval functions

```python
from autopil import ContextGuard, SensitivityLevel

guard = ContextGuard(
    policy_path="policies/my_policy.yaml",
    audit_db="autopil.db",
)

@guard.protect(
    agent_role="analyst",
    user_id="user_123",
    source_id="reports",
    sensitivity_level=SensitivityLevel.HIGH,
    session_id="session_abc",
)
def retrieve_reports(query: str) -> list:
    return vectorstore.search(query)

# Authorized — returns results
results = retrieve_reports("Q3 performance")

# Unauthorized — raises PermissionError, logs the denial
results = retrieve_reports_from_exec_comms("board notes")
```

### View the audit trail

```python
events = guard.get_audit_trail("session_abc")
for e in events:
    print(e.decision, e.source_id, e.context_hash)
```

---

## Running the API Server

### First start

On first start, AutoPIL creates a `default` tenant and prints a one-time superadmin API key:

```
  AutoPIL API Server
  Policy   : policies/financial_services.yaml
  Database : sqlite @ autopil_audit.db

  ⚠  First start — created 'default' tenant and superadmin key:
     apl_<your-key-here>
     Save this — it will not be shown again.

  Docs     : http://localhost:8000/docs
```

Use this key in the `X-API-Key` header for all API requests.

### Local

```bash
python serve.py --policy policies/financial_services.yaml
```

### With Postgres

```bash
export DATABASE_URL=postgresql://user:pass@localhost:5432/autopil
python serve.py --policy policies/financial_services.yaml
```

### Docker

```bash
docker compose up --build
```

The server starts on `http://localhost:8000`.

- **Dashboard**: http://localhost:8000
- **API docs**: http://localhost:8000/docs

---

## API Authentication

All `/v1/*` endpoints require an API key in the `X-API-Key` header:

```bash
curl -H "X-API-Key: apl_yourkey" http://localhost:8000/v1/policies
```

### Key management

```bash
# Create a new key (scoped to your tenant)
curl -X POST http://localhost:8000/v1/keys \
  -H "X-API-Key: apl_yourkey" \
  -H "Content-Type: application/json" \
  -d '{"name": "production_agent"}'

# List keys for your tenant
curl http://localhost:8000/v1/keys \
  -H "X-API-Key: apl_yourkey"

# Revoke a key
curl -X DELETE http://localhost:8000/v1/keys/{key_id} \
  -H "X-API-Key: apl_yourkey"
```

Keys are hashed (SHA-256) at rest. The plaintext is returned once at creation and never stored.

---

## Multi-Tenancy

AutoPIL supports multiple tenants with row-level data isolation. Each tenant's audit events, API keys, and stats are fully isolated.

### Create a tenant (superadmin required)

```bash
curl -X POST http://localhost:8000/v1/admin/tenants \
  -H "X-API-Key: apl_superadminkey" \
  -H "Content-Type: application/json" \
  -d '{"name": "acme_corp"}'
```

Response includes `tenant_id` and `admin_key` — the initial key for that tenant. Save it; it won't be shown again.

### Tenant admin routes

| Method | Path | Description |
|---|---|---|
| `POST` | `/v1/admin/tenants` | Create tenant + initial admin key |
| `GET` | `/v1/admin/tenants` | List all tenants |
| `DELETE` | `/v1/admin/tenants/{id}` | Deactivate a tenant |

All admin routes require a superadmin key (`is_superadmin=true`).

---

## Project Structure

```
autopil/
├── autopil/
│   ├── __init__.py          # Public API: ContextGuard, Decision, SensitivityLevel
│   ├── models.py            # Dataclasses: ContextRequest, AuditEvent, enums
│   ├── policy_engine.py     # YAML policy loader and evaluator
│   ├── audit_log.py         # Shim → SQLiteAuditLog
│   ├── guard.py             # ContextGuard decorator — core enforcement
│   ├── db/
│   │   ├── __init__.py      # create_stores() factory
│   │   ├── base.py          # AuditLogBase, KeyStoreBase, TenantStoreBase ABCs
│   │   ├── sqlite.py        # SQLite backend (default)
│   │   └── postgres.py      # Postgres backend (production)
│   └── api/
│       ├── app.py           # FastAPI application factory
│       ├── key_store.py     # Shim → SQLiteKeyStore
│       ├── schemas.py       # Pydantic request/response models
│       └── static/
│           └── index.html   # Control plane dashboard UI
├── policies/
│   ├── financial_services.yaml   # Loan underwriting roles
│   └── wealth_onboarding.yaml    # 4-agent onboarding pipeline roles
├── examples/
│   ├── langgraph_demo.py         # Single agent with LangGraph
│   └── multi_agent_demo.py       # Cross-agent isolation in action
├── tests/
│   ├── conftest.py               # Shared fixtures
│   ├── test_policy_engine.py     # 14 policy evaluation tests
│   ├── test_audit_log.py         # 14 audit persistence tests
│   ├── test_guard.py             # 16 ContextGuard enforcement tests
│   ├── test_api.py               # 20 REST API endpoint tests
│   ├── test_auth.py              # 15 API key authentication tests
│   ├── test_tenants.py           # 13 multi-tenancy and isolation tests
│   └── test_postgres.py          # 11 Postgres backend tests (skipped unless DATABASE_URL set)
├── Dockerfile
├── docker-compose.yml
└── serve.py
```

---

## Policy Reference

A policy file is a YAML document with a top-level `policies` list. Each entry maps to one agent role.

```yaml
policies:
  - name: my_policy          # Unique identifier, appears in audit log
    agent_role: analyst      # Must match agent_role passed to guard.protect()
    allowed_sources:         # Empty list [] = allow all non-denied sources (admin wildcard)
      - reports
      - market_data
    denied_sources:          # Always blocked, takes priority over allowed_sources
      - executive_comms
    max_sensitivity: high    # Ceiling: low | medium | high | restricted
```

**Evaluation order** (first match wins):

1. Is `source_id` in `denied_sources`? → **DENY**
2. Is `allowed_sources` non-empty and `source_id` not in it? → **DENY**
3. Does `sensitivity_level` exceed `max_sensitivity`? → **DENY**
4. No issues found → **ALLOW**
5. No policy matched for this `agent_role` → **DENY** (default deny)

---

## REST API Reference

Base URL: `http://localhost:8000`

All `/v1/*` routes require `X-API-Key` header. `/health` is exempt.

### Evaluate a context request

```
POST /v1/context/evaluate
X-API-Key: apl_yourkey
```

```json
{
  "query": "Q3 loss rates",
  "agent_role": "loan_underwriter",
  "user_id": "user_001",
  "source_id": "credit_scores",
  "sensitivity_level": "high",
  "session_id": "optional-uuid"
}
```

Response:

```json
{
  "decision": "ALLOW",
  "policy_name": "loan_underwriter_policy",
  "reason": "Access granted",
  "event_id": "uuid",
  "session_id": "uuid",
  "timestamp": "2026-03-25T06:00:00"
}
```

### Key management

```
POST   /v1/keys              — create key (scoped to calling tenant)
GET    /v1/keys              — list keys for calling tenant
DELETE /v1/keys/{key_id}     — revoke a key
```

### Tenant management (superadmin)

```
POST   /v1/admin/tenants         — create tenant + initial admin key
GET    /v1/admin/tenants         — list all tenants
DELETE /v1/admin/tenants/{id}    — deactivate tenant
```

### Audit

```
GET /v1/audit/sessions/{session_id}
GET /v1/audit/events?decision=DENY&agent_role=analyst&limit=100
GET /v1/audit/stats
```

### Policies

```
GET  /v1/policies
POST /v1/policies/reload
```

---

## Storage Backends

### SQLite (default)

Zero configuration. Used for development and single-instance deployments.

```bash
python serve.py --policy policies/financial_services.yaml --db /data/autopil.db
```

### Postgres (production)

```bash
pip install autopil[postgres]
export DATABASE_URL=postgresql://user:pass@host:5432/autopil
python serve.py
```

Schema is created automatically on first start. Supports horizontal scaling — multiple instances can share one Postgres database.

### Docker Compose (Postgres)

```bash
docker compose up --build
```

The default `docker-compose.yml` starts AutoPIL with a `postgres:16-alpine` service wired via `DATABASE_URL`.

---

## Docker Reference

### Build

```bash
docker build -t autopil:0.1.0 .
```

### Run with environment overrides

```bash
docker run -d \
  -p 8000:8000 \
  -v $(pwd)/policies:/app/policies:ro \
  -v autopil-data:/data \
  -e AUTOPIL_POLICY=/app/policies/wealth_onboarding.yaml \
  autopil:0.1.0
```

### Environment variables

| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | _(unset)_ | `postgresql://...` — uses Postgres if set, otherwise SQLite |
| `AUTOPIL_POLICY` | `/app/policies/financial_services.yaml` | Policy file path inside container |
| `AUTOPIL_DB` | `/data/autopil_audit.db` | SQLite audit DB path (ignored if `DATABASE_URL` is set) |
| `AUTOPIL_HOST` | `0.0.0.0` | Bind host |
| `AUTOPIL_PORT` | `8000` | Listen port |

---

## Running the Examples

### LangGraph demo — loan underwriting agent

```bash
python examples/langgraph_demo.py
```

Runs a 3-node LangGraph pipeline that makes 4 retrieval attempts (2 allowed, 2 denied) and prints the full audit trail.

### Multi-agent demo — cross-agent isolation

```bash
python examples/multi_agent_demo.py
```

Runs a 4-agent wealth onboarding pipeline (intake → KYC → risk → compliance) and demonstrates 4 isolation violations being blocked while the legitimate pipeline still reaches an APPROVED decision.

---

## Running the Test Suite

```bash
pip install -e ".[dev]"
pytest tests/ -v
```

92 tests, all passing. Coverage:

| File | Tests | What it covers |
|---|---|---|
| `test_policy_engine.py` | 14 | Allow/deny paths, sensitivity ceiling, admin wildcard, policy reload |
| `test_audit_log.py` | 14 | Persistence, session isolation, session owner, context hash |
| `test_guard.py` | 16 | Decorator enforcement, audit creation, context hash, cross-agent isolation |
| `test_api.py` | 20 | All REST endpoints, filtering, stats, policy reload |
| `test_auth.py` | 15 | API key creation, listing, revocation, auth enforcement |
| `test_tenants.py` | 13 | Tenant lifecycle, data isolation, superadmin enforcement |

Postgres tests (11) run automatically when `DATABASE_URL` is set.

---

## Sensitivity Levels

Ordered from least to most sensitive:

| Level | Value | Example data |
|---|---|---|
| Low | `low` | Public product catalogs, FAQ knowledge bases |
| Medium | `medium` | Account summaries, transaction history |
| High | `high` | Credit scores, identity records, loan history |
| Restricted | `restricted` | Regulatory filings, internal risk models, audit logs |

---

## License

AutoPIL is licensed under the [Business Source License 1.1](LICENSE).

**What this means in practice:**

- **Free to use** for development, research, evaluation, and any non-commercial purpose
- **Free for internal production use** — deploy it inside your own organization without restriction
- **Commercial restriction** — you may not offer AutoPIL or a substantially similar product as a managed service or SaaS to third parties without a commercial license
- **Converts to MIT on March 25, 2030** — the full codebase becomes open source automatically on that date

BSL is not an OSI-approved open source license. It is a source-available license designed to allow broad community use while protecting against cloud vendors and competitors commoditizing the work before the project can sustain itself commercially.

For commercial licensing, partnership inquiries, or enterprise deployments: **anil@vibrantcapital.ai**

---

## Version

`0.1.0` — initial release
