---
title: "Agent identity and delegated access"
canonical: https://guavaindex.com/specs/agent-identity
modified: 2026-11-10T08:36:22Z
lang: en
---

# Agent identity and delegated access

Status: draft 0.1 (`agent-grant/0.1`). Covers friction #3 (agents borrow the user's full session or password), #8 (sites can't tell which agent acts for whom), #9 (bot detection blocks legitimate agents) and #16 (OAuth scopes are coarse and long-lived). Reference implementation: `agent_index/identity.py`.

## The problem

Today an agent either logs in as the person, with every permission the person has and no record that an agent did it, or it looks like an anonymous scraper and gets blocked. Sites can't tell a person's own agent from abuse, so they block both or trust both.

## The model

Three keys, two signatures:

1. **The person** has a key, registered with the site once (the way a passkey is).
2. **The agent** has its own key. Its id is the public key, so anyone can verify it without a lookup.
3. The person signs a **grant**: this agent, this site, these scopes, these limits, this purpose, until this time.
4. The agent signs **every request** with its own key and attaches the grant.

The site checks both signatures and learns who is acting (the agent), for whom (the person), with what authority (scopes and limits), and why (purpose). It never sees the person's password or session.

## Grant

A compact token: base64url JSON payload, a dot, base64url Ed25519 signature by the person's key.

```json
{
  "typ": "agent-grant/0.1",
  "iss": "ed25519:<person public key>",
  "iss_name": "Maxam",
  "sub": "ed25519:<agent public key>",
  "aud": "shop.example",
  "scopes": ["orders:read"],
  "constraints": {},
  "purpose": "Check order status",
  "iat": 1794200000,
  "exp": 1794203600,
  "jti": "9c1e0f4b2a7d3e11"
}
```

Rules:
- `aud` is one site. A grant for one site is useless on another.
- `exp` defaults to one hour. Task-length grants are the point; long-lived ones defeat it.
- `scopes` are the site's own names, published in its [action manifest](/specs/agent-actions).
- `constraints` carries limits the site enforces, such as spending caps (see [payments](/specs/agent-payments)).
- The person can revoke a grant by `jti` at any time; sites keep a revocation list until `exp`.

## Signed requests

Requests are signed with HTTP Message Signatures (RFC 9421), Ed25519, in the Web Bot Auth profile (`agent_index/httpsig.py`):

```
Agent-Grant: <grant token>
Content-Digest: sha-256=:<base64>:
Signature-Agent: "https://agents.example"
Signature-Input: sig1=("@method" "@authority" "@path" "@query" "content-digest" "agent-grant" "signature-agent");created=1794200123;expires=1794200183;keyid="<JWK thumbprint>";alg="ed25519";nonce="<random>";tag="web-bot-auth"
Signature: sig1=:<base64>:
```

- `keyid` is the RFC 7638 JWK thumbprint of the agent's key. For a delegated request it must match the grant's `sub`.
- A delegated request must cover `@method`, `@authority`, `@path`, `agent-grant`, `@query` when there is one, and `content-digest` when there is a body. Payment retries also cover `agent-payment`.
- Sites reject signatures more than 5 minutes old, expired ones, and any nonce seen before.
- The implementation matches RFC 9421's Ed25519 test vector (appendix B.2.6) byte for byte; the tests check it.

## Signed agents without a grant (#9)

An agent that only reads public pages doesn't need a grant, but it shouldn't look like an anonymous scraper either. With `AGENT_INDEX_SIGNATURE_AGENT` set to the URL where its operator hosts a key directory (`agent-index key-directory` prints it, to serve at `/.well-known/http-message-signatures-directory`), every request agent-index sends is signed with Web Bot Auth. Bot-management services that verify Web Bot Auth can then let it through. A site using the middleware passes `bot_keys=httpsig.DirectoryResolver(fetch=...)`: it fetches and caches the directory, verifies the signature, and hands the app `scope["signed_agent"]`. The site's policy can require it with `requires_signature`.

## Issuing, holding and revoking grants

- The person: `agent-index person-key` creates their key (it belongs on the person's own device), `agent-index grant <site> --scope ... --purpose ... --ttl 3600` signs a grant for the agent key `whoami` shows, and `--max-total` makes it a spending mandate.
- The agent keeps grants in its wallet (`wallet_add`, `agent-index wallet add`). For each request it picks the narrowest live grant for that site and scope, and only uses one when the site asks (`401` with `get_grant`) or an action needs the scope. Pages fetched under a grant are private: they never enter the shared cache or index.
- `agent-index revoke <site> <grant id>` signs a revocation and posts it to the site's `/.well-known/agent-revocations`. The site accepts it only from the grant's issuer (or another trusted principal) and refuses the grant from then on, across restarts (`sitestate.SiteState`).

## What a site does with it

- Let identified agents through bot defenses instead of serving CAPTCHAs (#9).
- Apply per-agent rate limits and per-person accountability.
- Show the person an activity log: which agent did what, under which grant, for what purpose.
- Fall back to normal login for anything without a grant.

## Errors

`not_delegated`, `bad_token`, `bad_signature`, `bad_key`, `unknown_principal`, `unknown_agent`, `wrong_audience`, `grant_expired`, `grant_revoked`, `wrong_agent`, `stale_request`, `replayed_request`, `scope_missing`. Each is a stable code, never prose (see [errors](/specs/agent-errors)).

## Relation to existing work

Signatures are RFC 9421 in the Web Bot Auth profile, which bot-management services already verify. The grant could also be carried as an OAuth token-exchange result, so existing OAuth servers can issue grants; that is the remaining step for sites that already run OAuth.
