Identity
Namespace registration, JWKS publication, token format, human linkage and key rotation. Specification v0.1.
Status v0.1 · Default broker id.interagentic.dev
An identity is a namespace held at a broker, backed by a keypair the holder generated. A service verifies a caller by fetching the broker's published key set. Nothing is registered on the service's side, and no secret is shared.
1. Identity URI
interagentic://<broker>/<namespace><broker> is a DNS hostname. <namespace> matches:
^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$Namespaces beginning user_ are reserved for human accounts.
The URI maps to HTTPS by substituting the scheme, so
interagentic://id.interagentic.dev/brave-fox-a3f2 resolves against
https://id.interagentic.dev/brave-fox-a3f2. A verifier MUST derive the broker
from the token's iss claim and MUST NOT accept a broker supplied out of band.
Comparison is exact and case-sensitive. A verifier MUST NOT normalise, follow redirects across hosts, or treat two brokers as equivalent because they resolve to the same address.
2. Broker endpoints
A conforming broker serves these under the namespace path.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /<ns>/register | none | Submit a public key, receive a challenge |
POST | /<ns>/register/verify | none | Return the signed challenge |
GET | /<ns>/jwks.json | none | The namespace's public key set |
GET | /<ns>/metadata.json | domain JWT | Per-service view of the linked human |
POST | /<ns>/keys/rotate | namespace JWT | Rotate to a new key |
GET | /<ns>/link | browser | Human approval page |
POST | /<ns>/token | namespace JWT | Optional: audience-scoped broker token |
3. Registration
Two requests. The challenge proves the caller holds the private key for the public key it submitted.
POST /acme-worker/register
Content-Type: application/json
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA…\n-----END PUBLIC KEY-----",
"algorithm": "EdDSA"
}{ "challenge": "3f9c1e…", "expiresIn": 300 }The challenge is single-use and expires in five minutes. The agent signs the raw challenge bytes and returns the signature:
POST /acme-worker/register/verify
{ "challenge": "3f9c1e…", "signature": "MEUCIQ…" }{
"namespace": "acme-worker",
"keyId": "key_V1StGXR8",
"algorithm": "EdDSA",
"rotatedAt": "2026-09-17T08:14:02Z"
}Status codes
| Code | Meaning |
|---|---|
201 | Registered |
400 | Malformed namespace, key, or algorithm |
401 | Signature did not verify |
409 | Namespace already held by a different key |
410 | Challenge expired or already used |
4. Token format
Agents authenticate with a JWT signed by their private key.
{ "alg": "EdDSA", "typ": "JWT", "kid": "key_V1StGXR8" }{
"iss": "interagentic://id.interagentic.dev/brave-fox-a3f2",
"sub": "interagentic://id.interagentic.dev/brave-fox-a3f2",
"aud": "api.example.com",
"iat": 1774051200,
"exp": 1774051500,
"jti": "01JQ8F3K…"
}| Claim | Required | Notes |
|---|---|---|
iss | yes | Signer's identity URI. The broker is read from here. |
sub | yes | Acting identity. Equals iss unless acting for a group. |
aud | yes | Target service, as a bare hostname. |
iat | yes | Issued-at, seconds. |
exp | yes | At most 300 seconds after iat. |
jti | yes | Unique per token. |
Supported algorithms are EdDSA (Ed25519, default), ES256 and RS256.
none MUST be rejected.
Verifying
import { createRemoteJWKSet, jwtVerify } from "jose";
const { host, pathname } = new URL(iss.replace("interagentic:", "https:"));
const jwks = createRemoteJWKSet(new URL(`https://${host}${pathname}/jwks.json`));
const { payload } = await jwtVerify(token, jwks, {
audience: "api.example.com",
maxTokenAge: "5m",
});A verifier MUST check aud against its own hostname. Skipping it means a token
minted for another service is accepted here.
5. Human linkage
metadata.json answers one question: is a person attached to this agent, and
what have they allowed you to do?
The request is authenticated with a JWT signed by the calling service's own domain key, expiring within five minutes:
GET /brave-fox-a3f2/metadata.json
Authorization: Bearer <jwt signed by api.example.com>{
"namespace": "brave-fox-a3f2",
"linked": true,
"humanId": "h_9c3a1f7e5b2d8046",
"permissions": ["orders:read", "orders:write"],
"keyRotatedAt": "2026-09-17T08:14:02Z"
}humanId is derived from the caller's registrable root domain combined with the
broker's internal user identifier. The derivation MUST be stable for a given
pair and MUST NOT be reversible or correlatable across services: the same person
appears as an unrelated identifier at every service they use.
permissions contains only grants made to the calling service.
Requesting a human
When linked is false, or a needed permission is absent, the service refuses
and supplies a link. The URL is deterministic, so an agent can also construct it
without a round trip:
https://id.interagentic.dev/brave-fox-a3f2/link
?service=api.example.com
&permissions=orders:read,orders:write
&state=<opaque>403 Forbidden
Link: <https://id.interagentic.dev/brave-fox-a3f2/link?…>; rel="approve"After approval the agent repeats its original request unchanged.
6. Broker-issued tokens (optional)
To avoid two round trips per call, a broker MAY issue an audience-scoped token carrying the human identifier and active permissions:
{
"iss": "https://id.interagentic.dev",
"sub": "interagentic://id.interagentic.dev/brave-fox-a3f2",
"aud": "api.example.com",
"human": "h_9c3a1f7e5b2d8046",
"permissions": ["orders:read", "orders:write"],
"exp": 1774051500
}Because the broker signs it, the service does not have to trust the agent's
account of its own permissions. A broker that does not implement this is still
conforming; services SHOULD treat it as a fast path and keep metadata.json as
the fallback.
7. Key rotation
Rotation is driven by the service, not by a global schedule.
Declaring a maximum key age
401 Unauthorized
WWW-Authenticate: Interagentic realm="api.example.com",
error="key_too_old", max_key_age=900max_key_age is in seconds. On receiving it the agent SHOULD rotate and retry
once. Using the standard WWW-Authenticate header means no new status code is
needed and existing HTTP tooling still behaves.
Rotating
POST /brave-fox-a3f2/keys/rotate
Authorization: Bearer <jwt signed by the key being rotated>
{ "newPublicKey": "-----BEGIN PUBLIC KEY-----…" }Rotation is idempotent: submitting a key that is already current returns the current state rather than chaining again, so a retry after a timeout is safe.
Each key records the fingerprint it replaced, forming a chain:
{
"keyId": "key_8Hq2Lm4P",
"fingerprint": "SHA256:xK9c…",
"previous": "SHA256:aB3f…",
"rotatedAt": "2026-09-17T08:14:02Z"
}Fingerprints are SHA256: followed by the base64url SHA-256 digest of the key's
DER bytes.
Theft detection
If a token arrives signed by a fingerprint already present earlier in the chain,
two copies of that key exist. The broker MUST lock the namespace immediately.
A locked namespace rejects all authentication with 423 and can only be
unlocked by the linked human.
| Code | Condition |
|---|---|
401 | Key is stale — rotate and retry |
409 | Rotation already applied (idempotent replay) |
423 | Namespace locked |
429 | Rotation in progress; retry after the given delay |
The dangerous move during recovery is retrying with the old key: a rotated-out fingerprint is exactly the signal that triggers a lock, so a naive retry loop locks the namespace it was trying to save. Clients MUST write the pending key before calling the broker and MUST try the pending key first on restart.
Client requirements
A conforming client MUST:
- hold an exclusive lock while rotating, so concurrent processes sharing a keypair cannot chain twice from the same parent;
- collapse concurrent rotation requests into a single one;
- write the new private key before calling the broker, and keep it until the broker confirms;
- back off and retry on
429rather than starting a competing rotation.
The reference SDK does all four. This is the main reason to use it rather than signing tokens by hand.