AAuth
draftThe spec is a draft; the support is real. AAuth is an evolving specification, and agentd implements the agent (client) side of it. The
aauthfeature is in the release binary and the published container image — its crypto (ring) is already linked by the defaulttls(rustls) transport, so enabling it costs zero marginal dependency. It is a compile-time feature, so a--no-default-features/ trimmed build can omit it.
Some MCP servers replace the shared API key with AAuth: your agent holds an Ed25519 key, gets a short-lived agent token from an Agent Provider, and signs every MCP request (RFC 9421). The server verifies the signature and knows exactly which agent is calling — no shared secret, and no human on each request.
Turn it on
The release binary and the container image carry the feature, so just pass the flags:
$ agentd \
--instruction "…" --intelligence https://gw.example/v1 \
--mcp secure=https://mcp.secure.example/mcp \
--aauth-provider https://apd.example \
--aauth-key-file /var/lib/agentd/agent.key \
--aauth-enroll-token '{{secret:AAUTH_ENROLL}}'
(A trimmed build that dropped the feature rebuilds it with
cargo build -p agentd-cli --release --features aauth.)
At startup agentd loads (or creates) the Ed25519 key and installs the identity process-wide; enrollment and the first agent-token fetch happen on the first signed request. From then on every MCP request — and, when configured, the intelligence dial — is signed; a non-AAuth server simply ignores the extra headers.
Each flag is also a config path under security.aauth (so --aauth-provider is
security.aauth.provider, and AGENTD_SECURITY_AAUTH_PROVIDER works alongside
the shorter env name below).
| Flag | Env | Meaning |
|---|---|---|
--aauth-provider <url> | AGENT_AAUTH_PROVIDER | The Agent Provider — this turns AAuth on. |
--aauth-key-file <path> | AGENT_AAUTH_KEY_FILE | Durable Ed25519 key (created 0600 if absent; default /var/lib/agentd/aauth-key). Put it on shared storage so subagents resolve the same identity. |
--aauth-enroll-token <T> | AGENT_AAUTH_ENROLL_TOKEN | One-time enrollment token (a {{secret:…}} reference), if the provider is in token mode. |
--aauth-enroll-assertion-file <path> | AGENT_AAUTH_ENROLL_ASSERTION_FILE | Federated enrollment: a file holding an enrollment assertion — e.g. a Kubernetes projected ServiceAccount token whose audience is the provider. Re-read fresh on every enroll (so a rotated token is always current); the assertion never touches config or logs. |
--aauth-person-server <url> | AGENT_AAUTH_PERSON_SERVER | Person Server for user-scoped identity (Case C — the resource-token → user auth-token exchange). |
A binary built without --features aauth accepts the settings and ignores them —
nothing is signed. With the feature, a key file that cannot be read or created,
or an enrollment-token reference that cannot be resolved, exits 2 at startup,
before any network I/O.
Enrollment modes. open (nothing), token (--aauth-enroll-token, a
one-time secret), and federated (--aauth-enroll-assertion-file, a
platform-issued assertion). Federated is the secret-free fleet path: each pod
presents its own projected identity token, so there is no shared enrollment
secret and no operator-custodied key — the provider verifies the assertion and
binds it to the agent's public key.
What agentd does on the wire
Each MCP POST carries three RFC 9421 headers:
Signature-Input: sig=("@method" "@authority" "@path" "signature-key");created=<now>
Signature: sig=:<base64 ed25519 over the signature base>:
Signature-Key: sig=jwt;jwt="<your agent token>"
The agent token is fetched and cached automatically, refreshed shortly before it
expires. There is nothing to rotate by hand — losing a token just fetches a new
one. The whole agent process tree (root + every subagent) signs under one
identity, inherited via the spawn payload like --tls-ca.
The intelligence dial is signed too. When an identity is installed, agentd
signs its requests to the --intelligence endpoint with the same RFC 9421
headers. This lets a model gateway attest the individual agent by signature
instead of source IP — the inbound side of the identity story. A plain LLM
endpoint ignores the headers, and the endpoint's bearer token (if any) rides
alongside — signing is additive.
agentd reacts to what a server asks for. If, at connect, discovery
(/.well-known/aauth-resource.json) says the server requires body integrity,
the signature additionally covers a Content-Digest (SHA-256 of the body). If a
response carries an AAuth-Access token (Case B, resource-managed), agentd
adopts it and presents Authorization: AAuth … on the retry and later calls. If
a response is 401 requirement=auth-token (Case C, user-scoped), agentd runs the
Person-Server exchange and presents the resulting user auth-token — all inside
the same request, bounded so a mis-satisfied requirement can't spin.
agentd validates its provider and its token. It fetches the Agent-Provider
metadata document (/.well-known/aauth-agent.json) and enforces the AAuth
protocol's anti-host-poisoning rule: a document whose issuer isn't the
configured provider aborts enrollment. A provider that publishes no document
still works (best-effort). The agent token itself is then acted on, not treated
as opaque: agentd refreshes against the token's own exp, and fails fast if the
token's iss isn't the configured provider, its ps isn't the configured
Person Server, or its cnf.jwk isn't the signing key — each a misconfiguration
that would otherwise surface as a silent wall of downstream 401s.
Where the human sits
You (or your operator) act at setup only for the common case (identity-based servers, "Case A"): enable the agent and, if the provider requires it, hand over a one-time enrollment token. After that the agent operates autonomously — it signs every call; no per-request consent.
Servers that want the human's identity (user-scoped, "Case C") route through a
Person Server where the user approves new authority. On a
401 requirement=auth-token, agentd exchanges the server's resource token at the
configured --aauth-person-server (carrying a justification the human sees),
receives the user-scoped auth-token, and presents it on the retry — the human
consents at their PS, not in agentd. agentd never renders a consent screen and
never caches a decision on the human's behalf: the approval lives with the
Person Server, and agentd only carries the token it hands back.
Embedding
An embedder building on agentd-core can drive AAuth directly:
agentd::aauth::{AgentKey, ApdConfig, AAuthClient} — construct a client, install
it (agentd::aauth::install), and every MCP connection agentd makes signs. The
signer is the agentd::aauth::RequestSigner trait; verify_ed25519 is exposed
for the server side of a test. See embedding.md.
What's covered (and what isn't)
All three access modes run end to end:
- Case A (identity-based) — sign every request; the common case.
- Case B (resource-managed) — adopt a returned
AAuth-Accesstoken and present it on the retry + later calls. - Case C (user-scoped) — run the Person-Server exchange on
401 requirement=auth-tokenand present the user auth-token.
Plus resource discovery (/.well-known/aauth-resource.json) and
provider discovery + issuer validation (/.well-known/aauth-agent.json),
agent-token claim validation (refresh off the token's exp; fail fast on an
iss / ps / cnf mismatch), content-digest covering when a server
requires body integrity, per-server opt-out (aauth: false on an
mcp.servers[] entry), federated enrollment
(--aauth-enroll-assertion-file), and signing the intelligence dial.
Two parts of the draft are not implemented: a server's own
202 requirement=interaction (HITL elicitation) and AAuth Events (/inbox) for
async results. A server that needs either will not work with agentd today.