HTTP request authentication and access control for agents.
The Python package provides an ASGI middleware for FastAPI and Starlette, an Aithos Registry resolver, and an HTTP request signer. Authentication uses P-256 HTTP Message Signatures. Access rules select agent identifiers and verified domains. The protocol is defined in SPEC.md.
The protocol is shared across language packages. Each package has its own dependencies, tests, version and release. Python is the first implementation.
Python 3.11 or newer, from the repository root:
python -m pip install ./pythonfrom aithos_auth import AithosAuthMiddleware
app.add_middleware(
AithosAuthMiddleware,
allowed_agent_ids="*",
required_domains=["1234.io", "abcd.com"],
)This configuration accepts an authenticated agent with either verified domain.
domain_operator="or" is the default. Set domain_operator="and" to require
both domains on the same agent.
allowed_agent_ids is required: use "*" for any authenticated agent or a list
of accepted identifiers. Omit required_domains to apply only the agent rule.
The agent rule and domain rule must both pass.
resolver defaults to AithosResolver, using https://registry.aithos.world.
It resolves the agent's active keys and verified domains. The server chooses
the resolver and accepts its identity and domain assertions.
A valid request proves possession of a key authorized for the resolved agent.
For A2A, apply the middleware to operation endpoints and serve public Agent Card discovery separately. Declare the extension described in SPEC.md.
from dataclasses import dataclass
from datetime import datetime
from typing import Protocol
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
@dataclass(frozen=True)
class VerifiedDomain:
domain: str
certified_at: datetime
last_checked_at: datetime
@dataclass(frozen=True)
class ResolvedAgent:
agent_id: str
public_key: EllipticCurvePublicKey
verified_domains: tuple[VerifiedDomain, ...] = ()
class AgentResolver(Protocol):
async def __call__(self, key_id: str) -> ResolvedAgent | None:
...The resolver returns a key authorized for agent_id, with any verified domains,
or None to reject the identifier. Failures and timeouts stop authentication.
The middleware validates the key as a P-256 public key and verifies the request.
Pass a custom resolver through resolver=resolve_agent. Its implementation may
use local keys, a JWKS, a directory, or any developer-selected source satisfying
the interface.
sign_request receives the method, absolute URL, header pairs, final body bytes,
key identifier and P-256 private key. It returns a new list of header pairs
containing the digest and signature, preserving unrelated headers.
from aithos_auth import sign_request
headers = sign_request(
method="POST",
url=url,
headers=[("Content-Type", "application/json")],
body=body,
key_id=f"{agent_id}:{key_thumbprint}",
private_key=private_key,
)For Aithos, agent_id identifies the registry entry and key_thumbprint identifies
its current signing key; key_thumbprint(public_key) computes that thumbprint.
Send the returned headers with the same method, URL and body bytes. An A2A
client includes A2A-Extensions before signing.
additional_covered_headers=("x-tenant",) configures extra required fields on
both the signer and middleware.
The default nonce store is SQLite at .aithos-auth/nonces.sqlite3, relative to
the process working directory. Keep this file on persistent storage. Instances
accepting the same requests share their nonce store. A custom nonce_store
implements the atomic operation defined in SPEC.md.
The middleware uses the original encoded path and query from ASGI. URLs omit an
empty trailing ?: ASGI exposes the query bytes, but loses that delimiter.
sign_request rejects this ambiguous form.
For TLS termination, set external_origin="https://your-api.example".
external_path_prefix="/api" supplies a prefix removed by the proxy. Configure
these values on the server and preserve the original encoded path and query.
Default limits are 16 KiB of headers, 1 MiB of content, 10 seconds to read content,
and 5 seconds each for resolution and nonce storage. Configure them with
max_header_bytes, max_body_bytes, body_timeout, resolver_timeout and
nonce_timeout. SQLite retains up to 100,000 live nonces by default;
SQLiteNonceStore(max_entries=...) configures capacity.
cd python
uv sync --frozen --extra demo
uv run --frozen pytest
uv run --frozen ruff check .
uv run --frozen ruff format --check .
uv buildStock demo: public Agent Card, signed A2A client, mock inventory, local TLS setup and registry-backed configuration. PLAN.md tracks development and release readiness.