Skip to main content

Cryptographic identity and trust for AI agents

Project description

agentid

Cryptographic identity for AI agents.

agentid gives each agent a keypair, a signed identity document, and a way to prove on every request that the request actually came from that agent. Services verify agent requests before processing them. Stolen tokens are useless without the private key.

Built on Ed25519, OAuth 2.0 Dynamic Client Registration (RFC 7591), and DPoP (RFC 9449).


The problem

AI agents call APIs, read databases, write files, and send emails. Most do this with a hardcoded service account token or a borrowed user credential. There is no standard way for a service to know:

  • which agent made a request
  • who owns and is accountable for that agent
  • what the agent is actually allowed to do
  • whether the request was replayed from a stolen token

agentid solves this with a small set of primitives that compose together.


Install

pip install agentid

Quick start

Give your agent an identity:

from agentid import generate_keypair, save_keypair, create_software_statement

key = generate_keypair()
save_keypair(key)

statement = create_software_statement(
    private_key=key,
    operator_domain="acme.com",
    agent_name="billing-agent",
    agent_version="1.0.0",
    scopes=["invoices:read", "payments:write"],
)

Sign every request:

from agentid import create_dpop_proof

proof = create_dpop_proof(key, method="GET", uri="https://api.acme.com/invoices")

response = httpx.get(
    "https://api.acme.com/invoices",
    headers={
        "X-Agent-Statement": statement,
        "X-Agent-DPoP": proof,
    }
)

Verify on the server:

from fastapi import FastAPI, Request
from agentid.middleware import AgentIDMiddleware, verify_agent

app = FastAPI()

app.add_middleware(AgentIDMiddleware, get_public_key=my_key_resolver)

@app.get("/invoices")
def list_invoices(request: Request):
    agent = verify_agent(request, required_scope="invoices:read")
    return {"agent": agent.agent_name, "invoices": [...]}

How it works

Agent identity

Every agent gets an Ed25519 keypair. The private key never leaves the agent. The public key is published at a well-known URL so any service can verify requests without calling home.

The agent's identity document is a signed JWT called a software statement. It declares who owns the agent, what the agent is allowed to do, and where to find the public key.

statement = create_software_statement(
    private_key=key,
    operator_domain="acme.com",       # who is accountable for this agent
    agent_name="billing-agent",
    agent_version="1.0.0",
    scopes=["invoices:read"],
    model="claude-sonnet-4-6",        # optional
    prompt_hash="sha256:abc123",      # optional, for version tracking
)

Request signing with DPoP

Bearer tokens can be stolen and replayed. DPoP (RFC 9449) binds each token to the agent's private key. Every request includes a fresh proof signed by the key, covering the HTTP method and URI. A stolen token is useless without the private key.

# Create a fresh proof for each request
proof = create_dpop_proof(
    private_key=key,
    method="GET",
    uri="https://api.acme.com/invoices",
)

Verification

The service checks four things on every request:

  1. The software statement signature is valid
  2. The software statement has not expired
  3. The agent has the required scope
  4. The DPoP proof is fresh, matches this request, and has not been used before
from agentid import verify_agent_request, VerifiedAgent, VerificationError

result = verify_agent_request(
    software_statement=statement,
    dpop_proof=proof,
    method="GET",
    uri="https://api.acme.com/invoices",
    operator_public_key=public_key,
    required_scope="invoices:read",
)

if isinstance(result, VerifiedAgent):
    print(result.agent_name)     # billing-agent
    print(result.operator_domain) # acme.com
    print(result.scopes)         # ['invoices:read']

FastAPI middleware

The middleware handles verification automatically on every route. Verified agent details are attached to request.state.agent.

from agentid.middleware import AgentIDMiddleware, verify_agent

def get_public_key(operator_domain: str):
    # Return the Ed25519PublicKey for this operator
    # Fetch from your database, config, or key registry
    return your_key_store.get(operator_domain)

app.add_middleware(
    AgentIDMiddleware,
    get_public_key=get_public_key,
    exclude_paths=["/health", "/docs"],  # skip verification on these
)

@app.get("/invoices")
def list_invoices(request: Request):
    agent = verify_agent(request, required_scope="invoices:read")
    # agent.agent_name, agent.scopes, agent.operator_domain, etc.
    return {"invoices": [...]}

Delegation chains

Orchestrator agents can delegate a subset of their permissions to sub-agents. The chain is cryptographically linked. Scopes can only shrink as they pass down the chain.

from agentid.delegation import create_root_mandate, create_delegation, verify_delegation_chain

# Human authorises orchestrator
root = create_root_mandate(
    private_key=operator_key,
    operator_domain="acme.com",
    human_principal="alice@acme.com",
    scopes=["invoices:read", "payments:write"],
    agent_id="acme.com/orchestrator",
)

# Orchestrator delegates a subset to sub-agent
delegation = create_delegation(
    delegator_key=orchestrator_key,
    delegator_statement=orchestrator_statement,
    delegate_agent_id="acme.com/summariser",
    delegate_public_key_b64=summariser_pub_key,
    scopes=["invoices:read"],    # subset of parent scopes only
    parent_token=root,
    human_principal="alice@acme.com",
)

# Tool verifies the full chain
result = verify_delegation_chain(
    token=delegation,
    required_scope="invoices:read",
    get_public_key=key_resolver,
)

print(result.human_principal)    # alice@acme.com
print(result.delegate_agent_id)  # acme.com/summariser
print(result.depth)              # 1

Scope escalation is rejected immediately:

# This returns a DelegationError, not a token
create_delegation(..., scopes=["invoices:read", "admin:delete"])
# DelegationError: Cannot delegate scopes not present in parent token: {'admin:delete'}

Replay cache

The default replay cache is in-memory. It works for single-process deployments but will not survive a restart or work across multiple processes.

For production, use Redis:

from agentid.cache import RedisCache
from agentid.middleware import AgentIDMiddleware

app.add_middleware(
    AgentIDMiddleware,
    get_public_key=get_public_key,
    cache=RedisCache(url="redis://localhost:6379"),
)

Or pass a cache directly to verify_agent_request:

from agentid.cache import RedisCache

cache = RedisCache(url="redis://localhost:6379")

result = verify_agent_request(
    ...,
    cache=cache,
)

What gets verified on every request

Check What it catches
Software statement signature Forged or tampered identity documents
Statement expiry Stale tokens
Scope enforcement Agents claiming permissions they were not granted
DPoP proof signature Requests not made by the key holder
DPoP method and URI binding Proofs reused on a different endpoint
DPoP freshness Old proofs being replayed
DPoP jti uniqueness Exact replay of a captured request

Running the examples

# Terminal 1: start the server
uvicorn examples.server:app --reload

# Terminal 2: run the client
python examples/client.py

Running the tests

pytest tests/ -v

Design decisions

Ed25519 only. No algorithm negotiation. Ed25519 is fast, has small keys, and has no known weaknesses. Supporting multiple algorithms adds complexity and attack surface.

No blockchain, no DID infrastructure. DNS is the trust anchor. Operators publish their public key at a well-known URL on their domain. Every developer already knows how DNS works.

Errors as values, not exceptions. verify_agent_request returns a VerifiedAgent or a VerificationError. No try/except needed in normal usage. The error always includes a human-readable reason.

Replay cache is pluggable. The default in-memory cache works for development. Redis works for production. Any backend that implements ReplayCache works.


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

proveyouragent-0.1.0.tar.gz (21.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

proveyouragent-0.1.0-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

Details for the file proveyouragent-0.1.0.tar.gz.

File metadata

  • Download URL: proveyouragent-0.1.0.tar.gz
  • Upload date:
  • Size: 21.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for proveyouragent-0.1.0.tar.gz
Algorithm Hash digest
SHA256 846122d93240d5c2ebdaacadae0395c27e89c16e97f0b347a9348f67d94f645e
MD5 0ecf3d2116e5912801310bea5bb43581
BLAKE2b-256 ab9e5c8a9f803fcd9825d3afbeea36cdab41c13098a84526b74b6ffab4f475fc

See more details on using hashes here.

File details

Details for the file proveyouragent-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: proveyouragent-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for proveyouragent-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2ea727d9c81d226d055e118b04d262ed554d84e1f66d6ff573313ac3430298fd
MD5 fb629e32f47df80365d0237ec830b2c1
BLAKE2b-256 572bf9c94a024b81a3ad5283b0859ea5ac807a1faa0c1ed1d4b4c7f2a392876b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page