Skip to content

Agent discovery

An agent that hits a 401 on a protected resource can work out how to authenticate on its own. Two hops of standard metadata get it to the endpoints; a Markdown document at /auth.md tells it, in prose it can act on, which of those endpoints apply to it and in what order.

The same file is a short integration note for a human. That is the point of publishing the recipe as Markdown rather than as another JSON document: one artifact, two readers.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://acme.oauth.work/.well-known/oauth-protected-resource",
error="invalid_token"
  1. Fetch the resource_metadata URL — protected resource metadata (RFC 9728). It names the resource and its authorization servers.
  2. Fetch the authorization server’s own metadata at /.well-known/oauth-authorization-server (RFC 8414).
  3. Read the agent_auth block in that document, and fetch the skill URL it names.

Steps 1 and 2 are what any OAuth client does; see MCP authorization for the same walk from a resource server’s perspective. Step 3 is what makes the rest of it legible to an agent with no prior knowledge of this platform.

{
"issuer": "https://acme.oauth.work",
"agent_auth": {
"skill": "https://acme.oauth.work/auth.md",
"identity_types_supported": []
}
}

skill is the URL of the skill document on this issuer. Each tenant subdomain is its own issuer with its own keys and its own copy, so the URL always points back at the host whose metadata you just read — never the apex. See multi-tenancy.

identity_types_supported lists the identity types a self-registration endpoint would accept. It is empty, deliberately. This platform has no agent identity endpoint: an agent authenticates as a client, and it gets that client identity one of the three ways below. An agent that finds its assertion type absent from this list is meant to stop looking for a registration endpoint and read skill instead.

No identity_endpoint, claim_endpoint, or events_endpoint key appears in the block, because there are no such endpoints. Advertising a URL an agent cannot use is worse than advertising nothing: the agent follows it, gets a 404 mid-flow, and has no human in the loop to notice. If those keys ever appear here, they will appear together with the endpoints that answer them.

GET /auth.md
Content-Type: text/markdown; charset=utf-8
Cache-Control: public, max-age=300, stale-while-revalidate=3600
Access-Control-Allow-Origin: *

Served at the root of every issuer, unauthenticated, and readable cross-origin — a browser-hosted agent reaches it by following skill out of metadata it just fetched, so a document only some clients can read would be no use. Every URL in it is absolute and already resolved for the host you fetched it from:

Terminal window
curl -s https://acme.oauth.work/auth.md | head -20

It contains a capability table (which RFCs are implemented), an explicit list of what is not implemented, and six numbered steps: discover, get a client identity, get an access token, call the API, refresh, revoke.

The steps map onto the guides here:

StepWhere it goes
Get a client identityDynamic registration, URL client IDs, or a provisioned agent
Get a tokenClient credentials, code + PKCE, or token exchange
Call the APIAudience binding and per-tool scopes
Refresh and revokeToken lifecycle
Read an errorErrors and rate limits

Only a provisioned agent can hold tool scopes or be bound to an organization, and the document says so — an agent that self-registers and then requests tool:search would otherwise be left guessing why the scope came back empty.

Three surfaces an agent might reasonably probe for are named as absent, so it stops instead:

  • Self-registration by identity assertion. An assertion cannot be exchanged for a client identity, so it does not replace getting a client. It can be exchanged for an access token — see identity chaining — but only by a client already registered here, and only from an issuer the organization has explicitly trusted.
  • A user-code claim ceremony, or device authorization. There is no user code and no polling grant. A human joins the flow at /authorize, where they sign in and consent directly. Freshness is controlled there too, with prompt=login or max_age — see authorization code + PKCE.
  • Anonymous credentials. Every token is bound to a client identity.

Stating an absence is load-bearing for a reader with no human to escalate to. Without it, an agent probes, reads the 404s as a transport fault, and retries.

Where the document and the metadata disagree, the metadata wins — it is generated from the running configuration, and the document says as much. A test asserts that every grant type and every endpoint URL the metadata advertises also appears in the document, so the two cannot drift apart without the suite failing.