Private beta

Request beta access

Interagentic is in private beta. Leave your email and we'll send you an access key when there's room.

Interagentic
Protocols

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.

MethodPathAuthPurpose
POST/<ns>/registernoneSubmit a public key, receive a challenge
POST/<ns>/register/verifynoneReturn the signed challenge
GET/<ns>/jwks.jsonnoneThe namespace's public key set
GET/<ns>/metadata.jsondomain JWTPer-service view of the linked human
POST/<ns>/keys/rotatenamespace JWTRotate to a new key
GET/<ns>/linkbrowserHuman approval page
POST/<ns>/tokennamespace JWTOptional: 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

CodeMeaning
201Registered
400Malformed namespace, key, or algorithm
401Signature did not verify
409Namespace already held by a different key
410Challenge 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…"
}
ClaimRequiredNotes
issyesSigner's identity URI. The broker is read from here.
subyesActing identity. Equals iss unless acting for a group.
audyesTarget service, as a bare hostname.
iatyesIssued-at, seconds.
expyesAt most 300 seconds after iat.
jtiyesUnique 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=900

max_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.

CodeCondition
401Key is stale — rotate and retry
409Rotation already applied (idempotent replay)
423Namespace locked
429Rotation 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 429 rather 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.

On this page