Metadata-Version: 2.4
Name: agentis-verify
Version: 0.1.0
Summary: FastAPI middleware for verifying agent requests using the Agentis identity platform
Author: Lujain Khalil
License: MIT
Requires-Python: >=3.11
Requires-Dist: cryptography>=41.0
Requires-Dist: fastapi>=0.100
Requires-Dist: httpx>=0.24
Requires-Dist: proveyouragent>=0.2.0
Provides-Extra: dev
Requires-Dist: anyio[trio]; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# agentis-verify

`agentis-verify` is a Python middleware package for FastAPI that verifies incoming agent requests against the [Agentis](https://agentis-id.vercel.app) identity platform. When an agent makes an HTTP request signed with [proveyouragent](https://pypi.org/project/proveyouragent), this library fetches the agent's public key and verification metadata from Agentis, validates the DPoP signature on the request, and either passes the request through with verified agent details attached, or rejects it with a clear error response. It handles caching of agent lookups, configurable verification tier requirements, and per-capability access control.

## Install

```bash
pip install agentis-verify
```

## Quickstart

### a. Middleware for all routes

Apply `AgentisMiddleware` once and every route is protected automatically. The verified agent is available at `request.state.agent`.

```python
from fastapi import FastAPI, Request
from agentis_verify import AgentisMiddleware, AgentisConfig

app = FastAPI()

app.add_middleware(
    AgentisMiddleware,
    config=AgentisConfig(min_verification_tier=2),
    exclude_paths=["/health", "/docs", "/openapi.json"],
)

@app.get("/invoices")
async def list_invoices(request: Request):
    agent = request.state.agent
    return {"agent": agent.verified_name, "invoices": []}
```

### b. `verify_agent()` per route

Use `verify_agent()` when you need per-route control or want to check a specific capability inline.

```python
from fastapi import FastAPI, Request
from agentis_verify import verify_agent

app = FastAPI()

@app.get("/invoices")
async def list_invoices(request: Request):
    agent = await verify_agent(request)
    return {"agent": agent.verified_name, "invoices": []}

@app.post("/payments")
async def process_payment(request: Request):
    agent = await verify_agent(request, required_capability="payments:write")
    return {"processed_by": agent.did}
```

### c. `@require_capability()` decorator

Protect a route with a single decorator. Returns 403 if the agent lacks the capability, 401 if verification fails.

```python
from fastapi import FastAPI, Request
from agentis_verify import require_capability

app = FastAPI()

@app.get("/payments")
@require_capability("payments:write")
async def process_payment(request: Request):
    agent = request.state.agent
    return {"processed_by": agent.did}
```

## Capabilities

Capabilities are returned as strings for agents registered directly, and as structured objects for agents registered with an A2A Agent Card. agentis-verify handles both formats transparently.

```python
# plain string format (direct registration)
agent.capabilities  # ["invoices:read", "payments:write"]

# structured object format (A2A Agent Card registration)
agent.capabilities  # [{"id": "invoices:read", "description": "Read and list invoices"}]

# use has_capability() to check either format
agent.has_capability("invoices:read")  # True in both cases
```

## Configuration

`AgentisConfig` controls how the middleware behaves:

| Option | Default | Description |
|---|---|---|
| `base_url` | `"https://agentis-id.vercel.app"` | Base URL of the Agentis API |
| `cache_ttl_seconds` | `60` | How long to cache agent lookups |
| `timeout_seconds` | `10` | HTTP timeout for Agentis API calls |
| `min_verification_tier` | `1` | Minimum verification tier required |

```python
from agentis_verify import AgentisConfig

config = AgentisConfig(
    base_url="https://agentis-id.vercel.app",
    cache_ttl_seconds=120,
    timeout_seconds=5,
    min_verification_tier=2,
)
```

## Error handling

| Exception | HTTP status | Meaning |
|---|---|---|
| `AgentisVerificationError` | 401 (or 403 for capability) | Verification failed. The `reason` attribute describes what went wrong. |
| `AgentisUnavailableError` | 503 | The Agentis API could not be reached. |

When using the middleware, these are caught automatically and returned as JSON:

```json
{"detail": "Agent is not active (status: suspended)"}
```

When calling `verify_agent()` directly, catch them yourself:

```python
from agentis_verify import AgentisVerificationError, AgentisUnavailableError

try:
    agent = await verify_agent(request)
except AgentisVerificationError as e:
    # e.reason has the message, e.status_code is 401 or 403
    raise HTTPException(status_code=e.status_code, detail=e.reason)
except AgentisUnavailableError:
    raise HTTPException(status_code=503, detail="Identity service unavailable")
```

## How it connects to proveyouragent and Agentis

Agents register on [Agentis](https://agentis-id.vercel.app) and receive an Ed25519 keypair and a DID (`did:web:agentis.dev:agents:abc123`). When making HTTP requests, they use [proveyouragent](https://pypi.org/project/proveyouragent) to attach two headers:

- `X-Agent-Statement`: a JWT software statement containing the agent's DID
- `X-Agent-DPoP`: a DPoP proof binding the request to the agent's key

`agentis-verify` extracts the DID from the statement, fetches the agent's public key from the Agentis API at `GET /api/agents/[did]`, checks that the agent is active and meets the minimum verification tier, then calls `proveyouragent.verify_agent_request()` to validate the DPoP proof against the public key. If everything passes, the verified agent is attached to `request.state.agent` and the request proceeds.

## Links

- Agentis platform: https://agentis-id.vercel.app
- Agentis source: https://github.com/lujainkhalil/Agentis
- proveyouragent on PyPI: https://pypi.org/project/proveyouragent
