diff --git a/docs/building/by-layer/L1/request-signing.mdx b/docs/building/by-layer/L1/request-signing.mdx index 3f7db28e38f..ad118527cb8 100644 --- a/docs/building/by-layer/L1/request-signing.mdx +++ b/docs/building/by-layer/L1/request-signing.mdx @@ -59,16 +59,20 @@ For blast-radius isolation, publish a second `request-signing` key under a separ ### Discovery chain -Verifiers find your public key through a three-step chain: +Verifiers find your public key starting from your agent URL: ``` -Your domain (e.g., agent.example.com) - -> /.well-known/brand.json # brand manifest with agent declarations - -> agents[].jwks_uri # pointer to your key store - -> /.well-known/jwks.json # JSON Web Key Set with public keys +Your agent URL (e.g., https://agent.example.com/mcp) + -> get_adcp_capabilities # protocol call to your agent + -> identity.brand_json_url # pointer to your operator brand.json + -> agents[] entry matching your URL # operator attests the agent + -> jwks_uri # pointer to your key store + -> JWKS # public keys, matched by keyid ``` -The `@adcp/client` SDK provides `BrandJsonJwksResolver` which handles this chain automatically with caching and refresh. +The verifier calls your agent, not your domain's `/.well-known/brand.json`. Many brand.json files can list the same agent URL, so only the one your agent points to counts. The domain-derived `/.well-known/brand.json` lookup is a 3.x fallback for webhooks from older sellers, never for request signatures. The full algorithm and its error codes are in [Discovering an agent's signing keys via `brand_json_url`](/docs/building/by-layer/L1/security#discovering-an-agents-signing-keys-via-brand_json_url). + +The TypeScript SDK's `BrandJsonJwksResolver` (see [JWKS resolver options](#jwks-resolver-options)) runs this chain with caching and refresh. It calls `get_adcp_capabilities` on the agent URL, including when the seller already holds an onboarding record for the agent. ## Step 1: Generate a signing key @@ -184,7 +188,7 @@ Only public keys go here — no `d` field. Set `Cache-Control: max-age=3600` or ### brand.json -Serve at `/.well-known/brand.json` on your brand domain. The `jwks_uri` is how verifiers find your keys: +Serve at `/.well-known/brand.json` on your brand domain. The `agents[].url` must be your exact agent URL, and the `jwks_uri` is how verifiers find your keys: ```json { @@ -192,15 +196,33 @@ Serve at `/.well-known/brand.json` on your brand domain. The `jwks_uri` is how v "domain": "example.com", "agents": [ { + "type": "buying", + "id": "media_buyer", "url": "https://agent.example.com", - "jwks_uri": "https://agent.example.com/.well-known/jwks.json", - "capabilities": ["media-buy"], - "adcp_use": ["request-signing"] + "jwks_uri": "https://agent.example.com/.well-known/jwks.json" } ] } ``` +### Capabilities pointer + +Your agent's `get_adcp_capabilities` response carries `identity.brand_json_url`, the HTTPS URL of the brand.json above. The host of that URL must share a registrable domain (eTLD+1) with your agent URL, unless the brand.json delegates your agent's domain through `authorized_operators[]`. + +```json +{ + "identity": { + "brand_json_url": "https://example.com/.well-known/brand.json" + } +} +``` + + +**Buyers that only call sellers still answer `get_adcp_capabilities`.** A verifier starts from your agent URL, so a buyer that signs requests must answer `get_adcp_capabilities` there, over MCP or A2A, even if it hosts no other tools. A seller's onboarding record doesn't remove this. The record must be re-confirmed against your `identity.brand_json_url` within the brand.json cache lifetime (see [Agent resolution](/docs/building/by-layer/L1/security#agent-resolution)). A buyer that serves nothing at its agent URL fails verification with `request_signature_capabilities_unreachable`. + +Verifiers read only `identity` from this response. The response schema still requires `adcp` and a non-empty `supported_protocols`, whose values commit the agent to the matching seller-side storyboards, and there is no buyer-only value yet. A path for signers with no capabilities endpoint, and a response shape for buyers that serve nothing else, are under discussion in [#6105](https://github.com/adcontextprotocol/adcp/issues/6105). + + ## Step 3: Sign outbound requests (buyer / orchestrator) ### Wrapping fetch / HTTP client diff --git a/docs/building/by-layer/L1/security.mdx b/docs/building/by-layer/L1/security.mdx index 9e4da57bc57..e93e6b9b133 100644 --- a/docs/building/by-layer/L1/security.mdx +++ b/docs/building/by-layer/L1/security.mdx @@ -1353,6 +1353,8 @@ Every surface then applies the same steps to the selected record: **Shortcuts must be bound both ways.** Onboarding records, registry caches, and crawler indexes MAY stand in for steps 1–4 of the discovery chain. Such a mapping from `A` to an operator record MUST have been established by, or re-confirmed against, `A`'s `identity.brand_json_url`, and re-confirmed at least as often as the brand.json cache lifetime in step 4 of the discovery chain. If several brand.json files list `A`, only the one `A` points to counts. +Agent resolution starts with `get_adcp_capabilities` at `A`, even when the agent hosts no other tools. This includes a buyer agent that only calls sellers. Request signing and non-legacy webhook discovery bind the operator record to `A`'s `identity.brand_json_url`. The 3.x legacy webhook fallback may instead use the agent-origin well-known brand.json when that pointer is absent, but still starts with the capabilities call and is never used for request signatures. An onboarding record does not replace the capabilities endpoint, because its mapping must be established or re-confirmed through discovery. A signer that serves nothing at `A` fails discovery step 1 with `request_signature_capabilities_unreachable`. A bootstrap for signers with no capabilities endpoint is under discussion in [#6105](https://github.com/adcontextprotocol/adcp/issues/6105). + #### Discovering an agent's signing keys via `brand_json_url` The `identity.brand_json_url` field on `get_adcp_capabilities` (added in 3.x, see schema `static/schemas/source/protocol/get-adcp-capabilities-response.json`) is the on-wire bootstrap for the agent → operator → keys chain. The field name reflects the artifact it points at (the operator's `brand.json` file), independent of whether the operator structure is a single brand, a house with sub-brands, an agency, or a pure operator record. In this section (apart from the `authorized_operators[]` field), *operator* means the agent's operator: whoever publishes the brand.json that lists the agent and its keys. It isn't the `operator` in an account's natural key. For buyer-declared accounts, that value comes from `accounts[].operator` on `sync_accounts` and `account.operator` on later calls, and one agent can act for many account operators (see [Seller walkthrough: buyer-declared accounts](/docs/building/by-layer/L2/accounts-and-agents#seller-walkthrough-buyer-declared-accounts)). Steps 1–4 below select the operator record for [Agent resolution](#agent-resolution); steps 5–8 apply that section's shared steps with the request-signing error codes. Given only an agent URL `A`, a verifier resolves the agent's signing keys via: