AUTH.md — agentauth/v1 (agentic server authentication manifest)
Defines the auth-provider doctype that lets CLI tools and agents authenticate to API servers via a standardized discovery chain and pluggable flow engines, aligned to the WorkOS auth.md open standard.
| Field | Value |
|---|---|
| AIP | 50 |
| Title | AUTH.md — agentauth/v1 (agentic server authentication manifest) |
| Author | Jeremy André <jeremy@agentik.net> |
| Status | Draft |
| Type | Schema |
| Requires | AIP-19, AIP-43 |
| Resources | ./resources/aip-50 — AUTH.schema.json, ADAPTER.md, EXAMPLES.md |
Abstract
AUTH.md is a markdown-with-frontmatter manifest that declares how a CLI
tool or agent authenticates to a remote API server. It is the counterpart to
the provision-recipe doctype (AIP-19): together, an auth:
section (this spec) and an install: section (AIP-19) form a complete vendor
connection manifest. Clients discover server endpoints from the standard
/.well-known/ chain at runtime; a TS-literal builtin serves as offline
fallback. Flow engines are registered as plain objects keyed by flow id —
no if/switch branching at dispatch sites.
The service-auth flow aligns to the WorkOS auth.md open standard
claim ceremony, which itself composes RFC 8628 (device authorization), RFC
7523 (JWT-bearer grant), and RFC 8414 (authorization server metadata). The
device-code flow aligns directly to plain RFC 8628 device authorization,
without the claim-ceremony indirection — auth.md governs one flow in this
spec, not the whole of it.
Motivation
Provision recipes (AIP-19) tell a CLI tool what credential to
install and where it lives locally. They say nothing about how to obtain a
valid bearer token to make the install call in the first place. Today this is
solved ad-hoc: bureaus and CLI tools require users to paste personal API keys
(gld_*, sk-*, …) fetched manually from a web UI.
The WorkOS auth.md protocol offers the right abstraction here: a server hosts
an auth.md file describing its supported registration flows, and clients
follow a discoverable, standard ceremony — the same UX as gh auth login or
vercel login. By making agentproto-native CLI tools (bureau login <vendor>)
auth.md-aware, any server that publishes auth.md becomes auto-configurable
with no hardcoded client-side metadata.
The AUTH.md doctype (this AIP) extends the standard with:
- A typed
install:section that pairs auth with provision (AIP-19 target URLs), so one vendor manifest covers the full connection lifecycle. - A TS-literal authoring path (
defineAuthProvider) for builtins and tests, matching the pattern every other agentproto doctype uses. - A pluggable flow-engine registry so new auth protocols register as plain objects, not if/switch branches.
Specification
File location
An AUTH.md manifest may live in one of two places:
- Server-hosted (primary):
https://<server>/auth.md— the resource server publishes its own spec, discovered by clients at runtime. - Client-bundled builtin: authored as a TS literal via
defineAuthProviderand registered at module load. Used when the server does not publishauth.md, or as an offline fallback.
Frontmatter
YAML frontmatter delimited by ---.
---
id: <string> # required — vendor id, 2–80 chars, ^[a-z0-9][a-z0-9._-]*$
description: <string> # required — ≤2000 chars
apiBase: <url> # required — canonical API base URL (no trailing slash)
audience: <string> # optional — credential plane hint (e.g. tunnel | api | mcp); see Audience binding
auth: # required — authentication configuration
flow: pat | service-auth | device-code
# … flow-specific fields (see below)
install: # optional — provision target (AIP-19 companion)
sealKey: <path> # path template for the seal-key endpoint
secretBacked: <path> # path template, may contain {guildId}
---auth block — pat flow
auth:
flow: pat
tokenStore:
keychain: <service-name> # macOS Keychain service
account: <literal | "{server}"> # Keychain account; {server} = resolved server URLThe PAT flow reads an existing token from the local Keychain or prompts the user interactively. It does not open a browser. Use this for providers that issue long-lived personal API keys.
auth block — service-auth flow
auth:
flow: service-auth
clientId: <string> # optional; defaults to "agentproto-cli"
loginHint: <email> # optional; passed as login_hint to /agent/identity
tokenStore:
keychain: <service-name>
account: <literal | "{server}">The service-auth flow implements the auth.md claim ceremony:
- Discovery: fetch
{apiBase}/.well-known/oauth-protected-resource→ authorization server URL →/.well-known/oauth-authorization-server→agent_auth.identity_endpoint+token_endpoint. - Registration:
POST {identity_endpoint} { type: "service_auth", login_hint? }→{ claim_token, claim: { user_code, verification_uri, expires_in, interval } }. - Hand-off: open
verification_uriin the default browser; printuser_code. - Poll:
POST {token_endpoint} grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=<clm_…>everyintervalseconds until success,authorization_pending,slow_down(back off), orexpired_token. - On success: the response carries
{ access_token, identity_assertion, assertion_expires }. Store theidentity_assertionJWT in the Keychain. MUST NOT store theaccess_token. - Refresh: exchange the stored
identity_assertionat the token endpoint withgrant_type=urn:ietf:params:oauth:grant-type:jwt-bearerto obtain a freshaccess_token. Restart at step 1 when the assertion itself expires orinvalid_grantis returned.
auth block — device-code flow
auth:
flow: device-code
clientId: <string> # optional; defaults to "agentproto-cli"
scope: <string> # optional; space-delimited scopes requested
deviceLabel: <string> # optional; human label shown during the authorization prompt (e.g. "MacBook Pro — bureau-cli")
tokenStore:
keychain: <service-name>
account: <literal | "{server}">The device-code flow performs RFC 8628 device authorization directly against
the token endpoint — there is no claim ceremony and no identity_assertion:
- Discovery: resolve
device_authorization_endpointandtoken_endpointper the Discovery algorithm below. - Registration:
POST {device_authorization_endpoint} { client_id, scope? }→{ device_code, user_code, verification_uri, verification_uri_complete?, expires_in, interval }. - Hand-off: open
verification_uri(orverification_uri_complete) in the default browser; printuser_code. - Poll:
POST {token_endpoint} grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=<device_code>&client_id=<client_id>everyintervalseconds until success,authorization_pending,slow_down(back off), orexpired_token. - On success: the response carries
{ access_token, refresh_token?, expires_in, scope? }. Theaccess_tokenissued here is a durable credential — a daemon/machine credential suitable for unattended use — unlike the short-lived token exchanged in theservice-authflow. Store bothaccess_tokenand, if present,refresh_tokenin the Keychain. - Refresh (if a
refresh_tokenwas issued): exchange it at the token endpoint withgrant_type=refresh_tokento rotate theaccess_tokenwithout repeating the device-authorization ceremony.
This flow targets the same token_endpoint used by service-auth — the
server dispatches by grant_type, so device_code, claim_token, and
jwt-bearer grants share one endpoint (see Rationale).
auth block — id-jag flow (reserved)
auth:
flow: id-jag
# … (unspecified in v1; requires agentproto-as-IdP)Reserved. Implementations MUST NOT parse this block in v1. When agentproto
functions as an identity provider, id-jag enables the agent to register
with a third-party service entirely without user interaction: the agent mints
a JWT assertion audience-bound to the target, the service validates it against
a trusted-issuer list, and an access token is issued without any claim ceremony.
install block (optional)
install:
sealKey: /path/to/connectors/seal-key
secretBacked: /path/to/guilds/{guildId}/connectors/secret-backedURL path templates for the AIP-19 provision flow. The variable {guildId} is
substituted by the caller at provision time. These fields mirror the httpTarget
port from @agentproto/secrets — having both in one manifest eliminates the need
to store endpoint URLs separately.
Discovery algorithm (normative)
When a client resolves an AUTH.md handle for a given server URL:
- Attempt:
GET {server}/.well-known/oauth-protected-resource. - On success (200): parse JSON; extract
authorization_servers[0]asauthServerBase. - Fetch:
GET {authServerBase}/.well-known/oauth-authorization-server. - On success: parse JSON; extract
token_endpoint,revocation_endpoint,agent_auth.identity_endpoint,agent_auth.claim_endpoint,agent_auth.identity_types_supported,device_authorization_endpoint(if present). - Alternate metadata source: if step 1 or step 3 fails (non-200, network
error, or a missing required field), attempt
GET {server}/.well-known/agentproto-host.jsonbefore giving up. On success, parse JSON and extractdevice_authorization_endpointandtoken_endpointdirectly. This alternate source is tried strictly AFTER the OAuth protected-resource / authorization-server chain (steps 1–4) — it is a fallback metadata document, not a replacement for RFC 8414 discovery. - Return
DiscoveredEndpoints. On any fetch error or missing required field from both sources: throwDiscoveryErrorand let the caller fall back to the static manifest config.
Clients MUST NOT fail permanently on discovery errors — static config MUST always work as a fallback. Discovery errors SHOULD be surfaced as debug output, not user-facing errors.
Token storage model (normative)
Storage semantics are a property of the flow, not the storage layer: the
Keychain (or platform equivalent, see TokenStoreSpec below) is a single
mechanism, but what each flow persists into it differs by design, driven by
that flow's own trust model.
| Flow | Persisted | Memory-only (never persisted) |
|---|---|---|
pat | raw access_token (durable) | — |
service-auth | identity_assertion JWT ONLY | access_token, claim_token |
device-code | access_token (durable) + refresh_token (if issued) | device_code |
pat: stores a rawaccess_tokenin the Keychain. The token is durable and opaque; there is no ceremony artifact to distinguish it from.service-auth: stores theidentity_assertionJWT ONLY. Theaccess_tokenMUST NOT be persisted — it is re-exchanged at the token endpoint (jwt-bearergrant) to obtain a fresh, short-livedaccess_tokenon demand.claim_tokenvalues MUST be held in memory only for the duration of the ceremony and MUST NOT be persisted to disk or Keychain. (Unchanged from the originalservice-authdesign.)device-code: stores the durableaccess_tokenreturned by the device-authorization grant, plus therefresh_tokenif the server issued one.device_codeMUST be held in memory only for the duration of the polling loop and MUST NOT be persisted.
Implementations MUST NOT generalize one flow's persistence rule to another —
e.g. a host MUST NOT infer from pat that device-code should also skip
storing a refresh_token, nor infer from service-auth that device-code
should withhold its access_token. Each flow's row above is authoritative
for that flow alone.
TokenStoreSpec (normative)
interface TokenStoreSpec {
keychain: string // macOS Keychain service name
account?: string // literal, or "{server}" → resolved server URL
}Hosts on non-macOS platforms MUST fall back to a per-platform secure store
equivalent (e.g. libsecret on Linux, Windows Credential Store on Windows).
The keychain field names the service; the platform adapter is host-resolved.
Audience binding
Manifests MAY declare a top-level audience field (recommended values:
tunnel, api, mcp; free-form strings are allowed for vendor-specific
planes). It hints which credential plane a manifest's issued token belongs
to, for vendors that expose more than one plane behind a single apiBase.
Clients SHOULD embed audience in the credential store key (e.g. as a
suffix on the Keychain account), and a broker resolving a stored credential
MUST NOT return a credential across mismatched audiences.
Normative enforcement is server-side. Client-side audience embedding is
defense-in-depth against local credential mix-ups — it is NOT the security
boundary. Servers MUST scope issued tokens per plane and MUST validate
scope/aud claims (in addition to grant_type dispatch, see Rationale) on
every request. A client that omits or mislabels audience MUST NOT thereby
gain the ability to use a token outside its intended plane; implementations
MUST NOT treat client-side audience embedding as sufficient isolation on its
own.
FlowEngine interface (normative)
interface FlowEngine {
readonly id: FlowId
run(
provider: AuthProviderHandle,
discovered: DiscoveredEndpoints | null,
opts: FlowRunOptions,
): Promise<FlowResult>
}
interface FlowRunOptions {
server: string // resolved server URL (may differ from provider.apiBase)
force?: boolean // force re-auth even if a stored credential is found
signal?: AbortSignal
}
interface FlowResult {
identityAssertion?: string // service-signed JWT (service-auth flow)
assertionExpires?: string // ISO 8601 datetime
accessToken?: string // short-lived token (pat/service-auth); durable token (device-code)
refreshToken?: string // durable refresh credential (device-code flow, if issued)
scope?: string // space-delimited granted scopes, if returned by the server
subject?: string // resolved subject/account identifier, if returned by the server
revocationId?: string // opaque id for server-side revocation, if returned by the server
tokenKind: "pat" | "assertion" | "oat" | "daemon"
}Hosts MUST dispatch by provider.auth.flow against FLOW_ENGINES[id]. Hosts
MUST NOT branch on flow id with if/switch chains — use the registry.
defineAuthProvider standard signature (normative)
defineAuthProvider(definition: AuthProviderDefinition): AuthProviderHandle
interface AuthProviderDefinition {
id: string
description: string
apiBase: string
audience?: string
auth: PATAuthConfig | ServiceAuthConfig | DeviceCodeAuthConfig
install?: InstallConfig
}
type AuthProviderHandle = Readonly<AuthProviderDefinition>Conformance rules:
- The export MUST be named
defineAuthProvider. - The returned handle MUST be frozen (
Object.freeze). - The
idMUST match/^[a-z0-9][a-z0-9._-]{1,79}$/. - The
descriptionMUST be 1–2000 chars.
Registry contract (normative)
function registerAuthProvider(handle: AuthProviderHandle): void
function getAuthProvider(id: string): AuthProviderHandle | undefined
function listAuthProviderIds(): string[]The module-level registry is pre-seeded with builtin providers. Last write wins so a host can shadow a builtin. The registry MUST be synchronously readable (no async init).
Body
Markdown body following the frontmatter. Hosts MUST function with parsers that read only the frontmatter. Recommended sections:
## Overview— human summary of the server and its auth flows.## Scopes— what capabilities the issued token grants.## Procurement— how to get credentials if the automated flow fails.
Rationale
Alignment with WorkOS auth.md is scoped to service-auth: auth.md
governs one flow in this spec, not the whole of it. The service-auth claim
ceremony maps one-to-one to the auth.md service_auth flow; using the same
grant URN (urn:workos:agent-auth:grant-type:claim) at the token endpoint
means any agentproto client works out-of-the-box against any server that
implements auth.md — including future third-party servers — without custom
integration. This interop is a retained asset of the spec, not something
device-code replaces: device-code is for vendors that want plain RFC 8628
device authorization without adopting the auth.md claim ceremony at all.
identity_assertion over long-lived token: The two-step model (store
assertion → exchange for access_token on demand) produces tokens that are
short-lived (≤1h), revocable at the registration layer, and stateless on the
server side. The assertion acts as a capability certificate; the access_token
is ephemeral. This is strictly better than storing a gld_* PAT forever.
service-auth grant URN, not RFC 8628 device_code: Using a
profile-specific URN prevents collision now that the same token endpoint
also dispatches standard RFC 8628 device authorization for the device-code
flow — the server routes by grant_type, never confusing a claim_token
with a device_code. This is exactly the one-endpoint, multi-grant dispatch
this bullet anticipated: device_code, claim_token-based claim, and
jwt-bearer refresh all resolve against the same token_endpoint.
Separate from AIP-19: AIP-19 provision recipes answer "what credential to install, where does it live locally?" This AIP answers "how does the CLI authenticate to call the install endpoint?" The concerns are orthogonal: one recipe can be installed with multiple auth configs; one auth config can provision multiple recipes. Combining them would conflate the credential-lifecycle with the authentication-lifecycle.
TS-literal + .md authoring duality: All agentproto doctypes support both
paths. TS literals bundle into the tsup output (zero fs reads); .md parsing
is the seam for hosts that ship their own manifests alongside their server.
Reference Implementation
@agentproto/auth —
implements defineAuthProvider, parseAuthProviderManifest,
discoverEndpoints, FLOW_ENGINES (pat, service-auth, device-code), and
the module-level registry. See also ./resources/aip-50/draft/ADAPTER.md.
Backwards Compatibility
Existing credential formats and already-issued durable tokens remain valid.
Adding the device-code flow and the optional audience field is purely
additive: manifests that omit audience, or that use flow: pat or
flow: service-auth, are unaffected. Existing bureau login --token flows
continue to work unchanged (the PAT flow is the TS-literal builtin for all
providers that have not migrated to service-auth or device-code).
Security Considerations
- Assertion in Keychain, not plaintext token: the
identity_assertionJWT is short-lived; an attacker who steals it from the Keychain can exchange it for anaccess_tokenonly while the assertion is valid (≤1h). A stolen PAT is valid indefinitely until the user revokes it. claim_tokennever persisted: Persisting aclaim_tokento disk would allow an attacker who can read the filesystem to complete a ceremony initiated by the victim. Holding it in memory only (and discarding on process exit) limits the window to the ceremony duration.- No echo, no log: The
accessTokenreturned byFlowResultMUST NOT be printed to stdout or logged. Implementations MUST treat it as opaque bytes bound to the in-process use. - Discovery TOCTOU: An attacker who can intercept the
/.well-known/responses can redirect the client to a rogue token endpoint. Implementations MUST use HTTPS for all discovery and auth requests; MUST validate TLS certificates; MUST NOT follow redirects for the discovery endpoints. - Plaintext argv risk (macOS Keychain write): The
securityCLI receives the token as an argument. On multi-user systems, other processes can readargv. Implementations SHOULD use the Keychain API directly (via native bindings) when available rather than shelling out tosecurity -w.
Resources
Supporting artifacts for AIP-50. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →
AIP-49: WALLET — agentwallet/v1 (principal-owned multi-asset wallet)
A principal-owned multi-asset wallet primitive. Assets are declared ERC-20-style (ref/symbol/decimals/standard/peg/ruleSet) and partitioned into ERC-1410 tranches; value lives in an append-only event journal whose fold is the balance. A restriction lattice makes laundering structurally impossible and conversions no-arbitrage; an authorize policy engine separates passive (cost-of-existing) from active (chosen) spend and bounds delegation by lineage-min. All settlement rails (Stripe/PawaPay/x402/on-chain) are adapters behind a per-asset SettlementPort.
AIP-51: PROCESSOR — agentprocessor/v1 (I/O transform primitive)
A runtime primitive for pure input/output transforms — (value, ctx) → value — mounted at declared seams (codec-import/export, agent-input/output, egress), chainable, tagged with a capability in the AIP-43 namespace. Redaction is one built-in capability, not a privileged one; a separate redaction profile layers the boundary-seam safety invariant on top.