---
title: "agent-identity-v1 specification"
description: "The normative definition of the agent-identity-v1 discovery document and brokered signup handshake."
image: "https://docs.agentregistrar.dev/og/docs-default.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.agentregistrar.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# agent-identity-v1 specification

> Generated from [`agentregistrar/spec`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/agent-identity-v1.md) at `v1.2.0` (tag `v1.2.0`).
> Do not edit this page; edit the spec and re-run `pnpm sync:spec`.

# Agent Identity Relying Party Discovery v1 (`agent-signup.json`)

- **Status:** Normative specification — `v1.2.0`. Maintained in [`agentregistrar/spec`](https://github.com/agentregistrar/spec).
- **Protocol identifier:** `agent-identity-v1`
- **Well-known URI:** `GET /.well-known/agent-signup.json`
- **Version:** `v1.2.0` (see §1.3)
- **Companion document:** [`scope-boundaries.md`](https://docs.agentregistrar.dev/agent-identity/v1/scope-boundaries/) — the identity-vs-vendor boundary and the future-change review checklist.

This document is the single normative definition of the `agent-signup.json` discovery contract and the
brokered signup handshake. The historical reference implementation's documentation carries a short
summary that points here. Where any other document disagrees with this specification, this
specification wins for identity-only v1.

---

## 1. Status, Version, Changelog & Normative Language

### 1.1 Scope of this document

This specification defines:

- the discovery document served at `GET /.well-known/agent-signup.json` (Contract D.1);
- the ToS binding model advertised by the document and enforced at signup;
- the `POST endpoints.agent_signup` handshake, including stable error codes;
- the identity facts of the signup receipt (Contract E) and the optional post-signup `context` pointer;
- the optional broker-issued RP attestation (Contract G), the provider registration/status API that
  issues it (Contract H), and the broker metadata + attestation status surface (Contract I);
- authenticated abuse reporting (Contract D.2) and authenticated receipt ingestion (Contract F);
- the trust model, assurance levels, verification rules, and the status/error taxonomy;
- the rules agents and Relying Parties MUST follow to be conformant.

The Agent Identity JWT assertion format itself is **Contract A** and is not redefined here. This
specification only defines how an assertion is *discovered* and *presented* during brokered signup.
MCP OAuth discovery (RFC 9728) and the Broker MCP API are also out of scope except where referenced
for context. Contract F is defined here as a broker endpoint profile; MCP transport remains out of
scope.

### 1.2 Changelog

| Version | Date | Change |
| :--- | :--- | :--- |
| `v1.2.0` | 2026-10-24 | Adds Contracts G/H/I, optional `rp_attestation`, authenticated D.2, RP-signed E, authenticated F, the trust model, assurance levels, verification rules, and the status/error taxonomy. **The wire protocol id stays `agent-identity-v1`; `rp_attestation` is OPTIONAL and `v1.0.0`/`v1.1.0` documents remain conformant.** |
| `v1.1.0` | 2026-10-18 | Additive clarifications: replay vs resolve (idempotent resolution, `assertion_replayed` reserved for conflicting reuse), the optional `resolution: "created" \| "resolved"` success field, acceptance lifetime, a knowledge-of-text restatement, recommended account identity keys (§9.1), and credential handoff & recovery (§12.5). **No field removals or renames; the wire protocol id stays `agent-identity-v1`; `v1.0.0` implementors remain conformant.** |
| `v1.0.0` | 2026-10-17 | Initial identity-only v1 specification. Discovery is identity-only; vendor catalogs, provisioning endpoints, legacy OTP endpoints, and broker receipt callbacks are removed from the protocol and are forbidden. Adds the optional signed post-signup `context` pointer. |

**Release record.** `v1.0.0` was recorded on **2026-10-17**, once the identity-only
`AgentSignupDiscoverySchema`, the checked-in [`agent-signup.schema.json`](https://docs.agentregistrar.dev/agent-identity/v1/agent-signup.schema.json),
the conformance validator (`src/validate-discovery.ts`, `validatorVersion` `1.0.0`), the
VaporCloud reference implementation, and this documentation were confirmed to agree. Release notes,
including the release evidence and the change process, are in
[`release-notes-v1.0.0.md`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/release-notes-v1.0.0.md). The published adoption surface is the
implementation registry in [`registry/README.md`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/registry/README.md) (machine-readable:
[`registry/implementations.json`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/registry/implementations.json)).

**Release record (`v1.1.0`).** `v1.1.0` was recorded on **2026-10-18** as a **text-only, additive**
revision: it changes no schema/Zod/JSON-Schema, no validator, and no wire-protocol behaviour — it adds
the optional `resolution` success field and clarifies replay vs resolve, acceptance lifetime,
account identity keys, and credential handoff & recovery. Release notes, including the
conformance-validator evidence and the change process, are in
[`release-notes-v1.1.0.md`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/release-notes-v1.1.0.md). `v1.0.0` implementors remain conformant.

**Release record (`v1.2.0`).** `v1.2.0` is the **2026-10-24** revision. It adds the Contracts
G/H/I text in §14–§19, the optional `rp_attestation` field (§5.2, §5.4), the authenticated abuse
contract (§11.5) and receipt ingestion contract (§11.6), the v1.2 receipt rules (§11.1, §11.4), and
renumbers the former §14–§17 to §20–§23. The release ships in the same tag with the JSON Schemas
`rp-attestation.schema.json`, `broker-metadata.schema.json`, `attestation-status.schema.json`,
`signup-receipt.schema.json`, `abuse-report.schema.json`, and the updated `agent-signup.schema.json`;
the fixture matrix in [`fixtures/`](https://github.com/agentregistrar/spec/tree/35659ce9febc13f78c7fcc69ee1908dca584805c/fixtures) with `fixtures/expected.json`; the validator
(`validatorVersion` and package version `1.2.0`) with `pnpm validate:discovery`,
`pnpm validate:fixtures`, and `pnpm validate:registry`; and
[`release-notes-v1.2.0.md`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/release-notes-v1.2.0.md). This branch is the reviewed artifact, not the
publication: the tag, npm release, and GitHub release follow the merge (release notes §6). Document
conformance never requires a live RP broker: only `--verify-attestation` contacts one.

### 1.3 Version identity

- The protocol version is carried **major-only** in the discovery document's `protocol` field:
  `agent-identity-v1`.
- This specification revision is `v1.2.0`. Minor/patch revisions are additive and do not change
  the `protocol` value; breaking changes require a new `protocol` value and a new discovery path (§13).
  Additive v1 changes are recorded in the changelog above and must pass the
  [`scope-boundaries.md`](https://docs.agentregistrar.dev/agent-identity/v1/scope-boundaries/) review checklist; the process is written down in
  [`release-notes-v1.0.0.md`](https://github.com/agentregistrar/spec/blob/35659ce9febc13f78c7fcc69ee1908dca584805c/release-notes-v1.0.0.md) §3.
- `rp_attestation` (§5.2) is **OPTIONAL**. A `v1.0.0` or `v1.1.0` discovery document that omits it
  remains conformant; absence means **unverified**, never **invalid**. An attestation that is present
  but expired, ill-signed, or issued by an untrusted broker is a *verification* result, not a
  conformance failure.
- This repository is the **canonical location** of this specification (until a rendered copy exists in
  the documentation site). The optional `spec` field points at a versioned copy and **stays OPTIONAL**;
  implementations may omit it, and the placeholder `https://<canonical-spec-host>/…` is illustrative
  only.

### 1.4 Normative language

The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**,
**SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are
to be interpreted as described in **RFC 2119** and **RFC 8174** when, and only when, they appear in
all capitals.

---

## 2. Scope & Non-Goals

The protocol is an **identity and brokerage** protocol. Its responsibility ends once an agent is
verified, delegated, has accepted the vendor's ToS, and the signup event has been recorded.

### 2.1 In scope

- Relying Party discovery of identity-only signup parameters.
- Delegated acceptance of the Relying Party's ToS, with an unambiguous content hash.
- Presentation and verification of an Agent Identity assertion during brokered signup.
- Identity/attribution traceability: which agent signed up with which provider, on whose behalf,
  under which ToS, when, and with what cryptographic proof.
- An opaque, hash-pinned pointer to the vendor's post-signup integration guide (`context`).
- Identity assertions, issuer trust (`issuer_allowlist` + issuer-hosted JWKS), and revocation
  (enforced by the issuer at assertion-minting time).
- Relying Party identity: provider registration, origin/audience verification, RP signing keys, and
  the broker-issued RP attestation (§14, §15).
- Local attestation verification, the trusted-RP-broker trust list, assurance levels, and the
  `allow`/`warn`/`require`/`block` policy (§17, §19).
- RP-signed receipts and authenticated receipt ingestion (§11.4, §11.6).
- Authenticated abuse reporting and broker metadata discovery (§11.5, §16).

### 2.2 Non-goals (explicitly excluded)

The following are **vendor responsibilities** and MUST NOT be represented as protocol surface in the
discovery document or the signed receipt. They belong to the vendor's post-signup context guide
(for example a `SKILL.md`):

- product catalogs, SKUs, pricing tiers, free-tier quotas, or product specs;
- resource provisioning APIs, instance lifecycle, resource endpoints, or resource credentials;
- vendor API/SDK usage, operational runbooks, and vendor error catalogs;
- legacy email/OTP signup endpoints implemented by the vendor;
- vendor admin dashboards and instance lifecycle actions;
- free-form vendor resource metadata of any kind.

Provisioning is a **separate vendor API call** the agent makes *after* signup by reading the vendor's
guide. Brokered signup only creates or resolves the account.

Contract H (§15) is an **administrative provider API**, not a vendor capability surface: it registers
the RP's origin, keys, and attestation and is never placed in discovery. MCP transport, broker
federation governance, a verification SDK, and any provider resource catalog, provisioning, or
credential custody remain out of scope in v1.2.

### 2.3 Naming note

"Provision" has two distinct meanings in this system. "Auto-provision an **agent sub-identity**"
(broker-side agent registration) is **in scope**. "Provision a **compute instance**" is **vendor
scope**. See `scope-boundaries.md` §3.

---

## 3. Roles & End-to-End Flow

### 3.1 Roles

| Role | Definition |
| :--- | :--- |
| **Human owner** | Verified person who signs the Master Agent Delegation Agreement and is legally responsible for the agent. |
| **Broker / issuer** | Mints signed Agent Identity assertions and publishes JWKS. |
| **Agent** | Acting through an MCP harness; discovers the Relying Party, obtains an assertion, and completes signup. |
| **Relying Party (RP)** | Service provider that publishes `agent-signup.json` and accepts assertions. |

### 3.2 End-to-end flow

```text
Agent                         Broker                         Relying Party
  |                              |                                |
  |-- GET /.well-known/agent-signup.json ------------------------>|
  |<-- identity-only discovery document --------------------------|
  |                              |                                |
  |-- identity_prove(aud, tos_url, tos_hash, text, scopes) ------>|
  |<-- signed Ed25519 assertion ----------------------------------|
  |                              |                                |
  |-- POST endpoints.agent_signup (Bearer assertion) ------------>|
  |                              |<-- verify via issuer JWKS -----|
  |<-- credentials + optional signed signup_token + context ------|
  |                              |                                |
  |-- (after signup) GET context.url, verify context.sha256 ----->|
  |-- POST <vendor provisioning API from guide> ----------------->|
```

1. The agent fetches the discovery document **only** when the user targets that service (§8).
2. The agent verifies the document, checks `issuer_allowlist` and `required_scopes`, and computes the
   ToS hash from `terms.text` (§6).
3. The agent calls its Broker's `identity_prove` with the accepted ToS URL/hash and text to obtain a
   signed assertion scoped for `audience` (§7).
4. The agent `POST`s the assertion to `endpoints.agent_signup`.
5. The RP verifies the assertion, completes signup, and returns provider credentials plus, where
   supported, a signed `signup_token` (Contract E) and the optional `context` pointer.
6. The agent records the receipt with `identity_record_signup` (Contract F) so the broker persists the
   proof. The human sees the signup in the portal ledger.
7. After signup, the agent fetches the vendor guide from `context`, verifies its SHA-256, and treats it
   as untrusted vendor documentation (§12).

---

## 4. Discovery Endpoint & Transport

- The document **MUST** be served at `/.well-known/agent-signup.json` on the Relying Party's canonical
  HTTPS origin.
- `GET` **MUST** return `200 OK` with `Content-Type: application/json` when the service supports
  `agent-identity-v1`.
- `GET` **MUST** return `404 Not Found` (or `410 Gone`) when the service does not support the protocol.
  A `401`/`403` **MUST NOT** be used to signal non-support.
- `OPTIONS` **SHOULD** return `204` with permissive CORS headers so browser-based inspectors and agent
  tools can read the document.
- Responses **SHOULD** include `Cache-Control: public, max-age=60` (maximum recommended `300`) and
  `Access-Control-Allow-Origin: *`.
- The document **MUST** be public and **MUST NOT** contain secrets, credentials, internal hostnames, or
  personal data.
- Redirects **SHOULD NOT** be used. If used, they **MUST** remain on the same origin and are limited to
  **three hops**.
- The response body **SHOULD** be capped at **1 MiB**. Implementations **MUST** enforce a bounded read
  size when fetching.
- Agents **MUST** use HTTPS, except for explicit `http://localhost` / `http://127.0.0.1` development
  origins.
- The document is **not signed** in v1. It is a parameter document, not a trust anchor. Trust comes
  from TLS, the issuer allowlist, issuer-hosted JWKS, and the broker-signed assertion.

### 4.1 Error behavior at the discovery endpoint

- `200 OK` — the service supports `agent-identity-v1`; the body is the discovery document.
- `404 Not Found` / `410 Gone` — the service does not support `agent-identity-v1`. The agent **MUST**
  treat the service as unsupported and stop.
- `5xx` or a transport failure — treat as a transient fetch failure. The agent **SHOULD NOT** retry
  more than a small bounded number of times and **MUST NOT** proceed to signup without a valid document.
- A redirect to a different origin, a document over the size cap, a non-JSON body, or an unparseable
  body **MUST** be treated as an invalid document, not as support.

---

## 5. Discovery Document Reference (identity-only)

### 5.1 Required fields

| Field | Requirement | Semantics |
| :--- | :--- | :--- |
| `protocol` | **REQUIRED** | Literal `"agent-identity-v1"`. Any other value means the agent **MUST** treat the service as unsupported — never "best effort". |
| `audience` | **REQUIRED** | Stable, case-sensitive service identifier. The agent **MUST** place this exact value in the assertion `aud`; the RP **MUST** reject a mismatch. |
| `provider_name` | **REQUIRED** | Human-readable service name. |
| `issuer_allowlist` | **REQUIRED**, non-empty | Absolute issuer origins accepted by this RP. Trailing slashes are normalized away. The agent's broker `iss` **MUST** match one entry exactly. No wildcards. |
| `required_scopes` | **REQUIRED**, unique | Scopes the RP requires. For brokered signup this **MUST** include `tos:accept`. It **MAY** include others such as `signup:free_tier`. |
| `terms.version` | **REQUIRED** | ToS version. The RP **MUST** change it whenever the text changes materially (§6). |
| `terms.url` | **REQUIRED** | Absolute HTTPS URL (localhost development exception) where a human can read the same agreement. |
| `terms.sha256` | **REQUIRED** | `sha256:<64 lowercase hex>` over the exact UTF-8 bytes of `terms.text` (§6). |
| `terms.text` | **REQUIRED** | The **complete** ToS text the agent is accepting, not a summary. Makes the hash unambiguous. |
| `endpoints.agent_signup` | **REQUIRED** | Absolute HTTPS URL accepting `POST` with `Authorization: Bearer <assertion>`. The **only** endpoint in the protocol. |

### 5.2 Optional fields

| Field | Requirement | Semantics |
| :--- | :--- | :--- |
| `spec` | **OPTIONAL** | Absolute URL of the versioned specification. Placeholder canonical URL: `https://<canonical-spec-host>/agent-identity/v1/agent-signup` (final hosting URL undecided — §22). |
| `provider_url` | **OPTIONAL** | Canonical service URL. |
| `contact` | **OPTIONAL** | Security/abuse/legal contact (email or URL). |
| `updated_at` | **OPTIONAL** | ISO 8601 timestamp for human/debuggability. Not a freshness signal for ToS. |
| `extensions` | **OPTIONAL** | Non-capability vendor metadata keyed by reverse-DNS namespace (for example `dev.vaporcloud`). It **MUST NOT** be used to advertise provisioning, catalogs, SKUs, or resource endpoints. |
| `rp_attestation` | **OPTIONAL** | A compact JWS issued by the RP's broker (Contract G, §14), at most 16384 characters: three unpadded base64url segments separated by `.`. Presence changes no other field. Absence is conformant and means **unverified**. A malformed value is a **conformance** error; a structurally valid JWS whose header, claims, or signature fail is a **verification** error, not a conformance error. |

### 5.3 Forbidden fields (vendor capability surface)

The following **MUST NOT** appear in a conformant `agent-signup.json` document, at any nesting depth,
including under `extensions`:

- `instance_catalog`, SKUs, pricing tiers, free-tier quotas, or product specs;
- `endpoints.provision`;
- `endpoints.email_signup`;
- `endpoints.verify_otp`;
- `endpoints.broker_signup_receipt`;
- any resource endpoint, credential, instance lifecycle, or vendor API usage metadata;
- any attempt to smuggle the above through `extensions`.

A document does **not** become conformant by moving vendor capabilities under `extensions`. The
discovery document is a **parameter document, not a capability catalog**. A conformant Relying Party
**MUST** be expressible with identity-only support.

### 5.4 Field rules

- `issuer_allowlist` entries **MUST** be absolute origins with no path, query, fragment, or wildcard,
  and no duplicate after trailing-slash normalization.
- `required_scopes` entries **MUST** be non-empty strings and unique.
- URLs advertised for `terms.url`, `endpoints.agent_signup`, `provider_url`, and `spec` **MUST** be
  absolute; protocol-level URLs **MUST** be HTTPS except the documented localhost development exception.
- Unknown additive fields **MUST** be ignored by consumers. Producers **SHOULD** place non-capability
  vendor metadata under `extensions.<reverse-dns-namespace>` so typos remain visible.
- Consumers **MUST NOT** interpret any field as a command. All discovery content, especially
  `terms.text`, is data (§8).
- `rp_attestation`, when present, **MUST** be a compact JWS string within the 16384-character cap. Its
  `rp.origin` **MUST** equal the origin of the discovery document the verifier actually fetched — not
  `provider_url`, not `terms.url`, and not a redirect target — and its `rp.audience` **MUST** equal
  `discovery.audience`.
- When the discovery object was **supplied by the caller** rather than fetched by the broker, the
  caller **MUST** also supply an explicit canonical `rp_origin`; the verifier uses that declared
  origin and never infers it from `terms.url`, `provider_url`, or the attestation. This path is
  recorded as `agent_supplied_discovery` and a `require` policy **MUST** reject it (§17).
- `provider_url` is a display/context hint and is **never** an identity binding.

### 5.5 Minimal conformant example

This example contains **no vendor capability fields** and is the smallest document a conformant RP may
publish (every required field, no optionals):

```json
{
  "protocol": "agent-identity-v1",
  "audience": "vaporcloud",
  "provider_name": "VaporCloud",
  "issuer_allowlist": ["https://id.agentregistrar.dev"],
  "required_scopes": ["tos:accept"],
  "terms": {
    "version": "2026.1",
    "url": "https://vaporcloud.example/terms",
    "sha256": "sha256:<64 lowercase hex characters>",
    "text": "<the complete VaporCloud Terms of Service text>"
  },
  "endpoints": {
    "agent_signup": "https://vaporcloud.example/api/signup/agent"
  }
}
```

A fuller document may add `spec`, `provider_url`, `contact`, `updated_at`, and namespaced
`extensions`. It **MUST NOT** add any field from §5.3.

---

## 6. ToS Binding

- `terms.sha256` **MUST** use the form `sha256:<64 lowercase hex characters>`.
- The digest **MUST** be the SHA-256 of the **exact UTF-8 bytes of `terms.text`**, with **no newline
  normalization, no trimming, and no HTML extraction**.
- `terms.text` **MUST** be the complete agreement, not a summary.
- `terms.url` **MUST** be an absolute HTTPS URL (localhost development exception) where a human can
  read the same agreement.
- `terms.version` **MUST** change whenever the text changes materially.
- The agent **MUST** recompute the hash from `terms.text` and compare it with `terms.sha256` **before**
  calling the Broker, and its Broker **MUST** verify the accepted text against the hash it records. The
  agent **MUST NOT** trust the advertised `terms.sha256` alone.
- The RP **MUST** reject a signup assertion whose `delegation.accepted_rp_tos_hash` does not equal the
  currently advertised `terms.sha256` (stale-hash rejection).
- When the RP changes its terms, it **MUST** publish the new text and hash before accepting new
  signups. In-flight assertions for the previous hash **MAY** be accepted for a bounded grace period,
  but the discovery document advertises **only** the current version. The grace period **MUST** be
  documented by the RP **outside** the discovery document (for example in its `SKILL.md` or support
  page) and **SHOULD NOT** exceed 24 hours.
- The Broker records the accepted URL/hash in `tos_acceptances` and in the assertion; the RP records
  the same hash and the assertion `jti` for traceability.

### 6.1 Re-acceptance

On `tos_hash_mismatch` (or whenever `terms.sha256` differs from the previously accepted hash), the
agent **MUST** treat it as a hard stop: re-fetch discovery, re-read `terms.text`, recompute the hash,
re-consent through `identity_prove`, and obtain a **fresh** assertion. The agent **MUST NOT** replay
the old assertion or reuse the old accepted hash.

---

## 7. Assertion & `agent_signup` Handshake

The assertion format is **Contract A** and is not redefined here. This section defines how it is
presented.

### 7.1 Handshake

1. The agent obtains an assertion from its Broker for `audience`, `terms.url`, `terms.sha256`, and the
   `required_scopes`, passing the accepted ToS text so the Broker can verify the hash it records. The
   Broker **MUST** require the caller to demonstrate knowledge of `terms.text` and **MUST** reject a
   request that does not (the hash check in `identity_prove`; §10).
2. The agent sends:

   ```http
   POST <endpoints.agent_signup>
   Authorization: Bearer <agent-identity-assertion>
   Content-Type: application/json
   ```

   The request body is RP-specific and **MAY** be empty.
3. The RP verifies at minimum:
   - the JWT signature against the issuer's `GET <iss>/.well-known/jwks.json`;
   - `iss` is in `issuer_allowlist` (exact match);
   - `aud` equals `audience`;
   - `exp` has not passed (allow at most 60 seconds of clock skew);
   - `jti` is not reused in a **conflicting** context: a repeated `jti` for the **same**
     agent/account at the same service is not a replay and **MUST** resolve idempotently rather than
     fail as `assertion_replayed` (§7.2);
   - the assertion lifetime is reasonable (recommended maximum 5 minutes);
   - `delegation.scopes` includes every `required_scopes` entry;
   - `human.verified === true`;
   - `delegation.accepted_rp_tos_hash === terms.sha256`.
4. On success the RP returns `200`/`201` with an optional `resolution` field, provider credentials,
   and, where supported, a signed `signup_token` per Contract E (§11) and the optional `context`
   pointer. `resolution` is `"created"` when the account was created and `"resolved"` when an
   existing account was resolved; a `resolved` outcome **MUST NOT** be treated as a failure. When the
   outcome is `"resolved"`, the RP **MAY** omit provider credentials (show-once — §12.5) but **MUST**
   point the agent at its credential recovery path in the vendor guide.
5. On failure the RP returns a 4xx response with a stable machine-readable error code (§7.2).

### 7.2 Error envelope and stable codes

The RP **MUST** return errors as a JSON body with a stable machine-readable code:

```json
{
  "error": "tos_hash_mismatch",
  "error_description": "human-readable, non-normative detail",
  "retryable": false
}
```

- `error` **MUST** be one of the codes below.
- `error_description` **SHOULD** be present but is **not** normative and **MUST NOT** be parsed for
  control flow.
- `retryable` **SHOULD** be present; when absent, the agent infers retryability from the code.

| `error` code | HTTP status | Meaning | Agent action |
| :--- | :--- | :--- | :--- |
| `invalid_request` | `400` | Malformed request (headers/body). | Fix the request; do not retry blindly. |
| `invalid_assertion` | `401` | Assertion failed signature or shape verification. | Obtain a new assertion. |
| `issuer_not_allowed` | `403` | `iss` is not in `issuer_allowlist`. | Do not proceed; the broker is not accepted here. |
| `audience_mismatch` | `403` | `aud` does not equal `audience`. | Re-discover and mint a correct-audience assertion. |
| `assertion_expired` | `401` | `exp` has passed beyond allowed skew. | Obtain a new assertion. |
| `assertion_replayed` | `409` | `jti` was already used in a **conflicting** context (a different agent/account/service). A repeated `jti` for the same agent/account/service is not a replay and resolves idempotently (§7.2). | Obtain a new assertion; do not reuse. |
| `tos_hash_mismatch` | `409` | Accepted ToS hash is stale or wrong. | Hard stop: re-discover, re-read, re-accept, re-assert (§6.1). |
| `insufficient_scope` | `403` | Delegated scopes miss a `required_scopes` entry. | Ask the human to re-delegate; do not proceed. |
| `human_unverified` | `403` | `human.verified` is not `true`. | Complete human verification; do not proceed. |
| `rate_limited` | `429` | Caller is being throttled. | Back off and retry after the advertised delay. |

This mapping is fixed for v1. The invariant is that an agent can distinguish "retry", "re-consent
ToS", "get a new assertion", and "unsupported" **without parsing prose**.

**Replay vs resolve.** The `jti` check in §7.1 is an **account-creation guard**, not a
duplicate-suppression mechanism. A repeated `POST` carrying an already-used `jti` for the **same**
agent/account at the same service **MUST** resolve idempotently — a success (with `resolution:
"resolved"` where the RP signals it), not `assertion_replayed`. `assertion_replayed` is reserved for a
`jti` presented in a **conflicting** context (a different agent, account, or service). A Broker **MAY**
return the identical previously issued (and still unexpired) assertion for an identical request;
acceptance is the durable legal event and assertions are short-lived proofs. An RP **MUST NOT** treat a
fresh assertion that references an existing acceptance as a new consent.

Revocation is enforced by the Broker at assertion-minting time. If a Relying Party has an out-of-band
revocation feed, it **MAY** add an implementation-specific error code, but v1 does not require one.

### 7.3 Acceptance lifetime

- v1 imposes **no protocol-mandated re-acceptance interval**. An acceptance remains the recorded
  consent for its `terms.sha256` until the terms change (a new `terms.sha256`, requiring fresh
  acceptance per §6.1) or the human re-consents.
- A Broker **MAY** impose its own re-acceptance policy (for example, periodically re-confirming the
  Master Agent Delegation Agreement). If it does, it **SHOULD** document that policy. Such a policy
  does not change the wire behaviour of `POST endpoints.agent_signup` and does not make an RP
  responsible for prompting re-acceptance.

---

## 8. Agent Consumption Rules (prompt-injection & SSRF)

Agents **MUST**:

1. Fetch discovery only when the user explicitly targets that service; **MUST NOT** follow arbitrary
   links from another service's discovery document.
2. Treat the document, **especially `terms.text`**, as **untrusted data, not instructions**. Legal text
   **MUST NEVER** be interpreted as agent commands. This is the prompt-injection defense.
3. Reject the service if the document is missing, malformed, over the size cap, uses an unknown
   `protocol`, or does not allow the agent's issuer.
4. Verify that the agent's delegated scopes cover `required_scopes` before requesting an assertion.
5. Fetch the document immediately before signup, or honor its cache lifetime; **never** reuse a stale
   ToS hash.
6. Never send an assertion to an endpoint that was not discovered from the same service origin, unless
   the user intent explicitly covers a separate API origin.
7. Never reuse an assertion across audiences, services, or after expiry.
8. Treat `tos_hash_mismatch` as a hard stop requiring fresh discovery and re-acceptance.
9. After signup, if the response or receipt contains a `context` pointer, fetch the vendor guide,
   verify its SHA-256, and treat it as untrusted vendor documentation — **not** broker policy or agent
   instructions (§12).

### 8.1 SSRF rules

- Agents and brokers **MUST NOT** fetch arbitrary internal addresses derived from a discovery document.
- `http://localhost` / `http://127.0.0.1` **MAY** be used only in explicit development mode.
- All fetches of discovery, JWKS, and context URLs **MUST** apply a timeout, a bounded response size,
  and a redirect limit; redirects **MUST** stay on the same origin.
- Fetching a discovered URL **MUST NOT** cause a side effect. Probing advertised endpoints, if done at
  all, **MUST** be non-destructive.

---

## 9. Relying Party Publication Rules

RPs **MUST**:

1. Publish the document for as long as they claim `agent-identity-v1` support.
2. Keep `terms.text`, `terms.sha256`, and `terms.url` mutually consistent and atomically updated.
3. Verify assertions rather than merely parsing them; never trust an unsigned or unverified JWT.
4. Resolve issuer keys only from the issuer's canonical `/.well-known/jwks.json`; never accept a
   `jku`/`x5u` header pointing at an arbitrary host.
5. Reject unknown issuers, wrong audiences, expired assertions, replayed `jti`s, insufficient scopes,
   unverified humans, and stale ToS hashes, using the §7.2 codes.
6. Record the assertion `jti`, the accepted ToS version/hash, and attribution for audit.
7. Return stable machine-readable errors and rate-limit abusive callers.
8. Provide a contact for abuse/security reports and use Contract D.2 (§11.5) to report misbehaving
   agents to the Broker.
9. If the RP has vendor capabilities to document, publish a post-signup context guide and return its
   `url` + `sha256` on successful signup (§12). The RP **MUST NOT** advertise those capabilities in
   discovery or the receipt.

### 9.1 Account identity keys

An RP that resolves signups idempotently needs a stable account key. This section recommends one; it
does not add a protocol field.

- **Recommended keys.** Key accounts on `(iss, human.id)` for **human-scoped** accounts and on
  `(iss, agent.id)` for **agent-scoped** accounts. When a service treats one account per human
  regardless of which agent acted, `(iss, human.id)` is the right key; when each agent gets its own
  account, use `(iss, agent.id)`.
- **Emails are discouraged as keys.** Keying on `human.email` or `agent.email` is **NOT RECOMMENDED**.
  Email addresses are mutable and are not stable identifiers; use them only as a legacy fallback for
  accounts that predate attribution.
- **Namespace by issuer.** Whenever more than one issuer is trusted (`issuer_allowlist` has more than
  one entry), the key **MUST** be namespaced by the assertion's `iss` so that identities from different
  Brokers cannot collide.
- **`human.verified` is email verification, not KYC.** The flag means the human owner's email address
  was verified. It is **not** a know-your-customer or identity-document check, and an RP **MUST NOT**
  represent it as one.

---

## 10. Issuer / JWKS Assumptions

Because `issuer_allowlist` points agents and Relying Parties at issuers, the issuer-side assumptions
are part of this specification:

- Every issuer origin listed by an RP **MUST** publish `GET <issuer>/.well-known/jwks.json` as a public
  JWKS for assertion verification.
- The JWKS endpoint **MUST** use HTTPS (localhost development exception), return `application/json`,
  and be safe to cache.
- Assertions **MUST** follow **Contract A**: Ed25519 (`EdDSA`) signatures with a stable `kid`, a single
  string `aud`, and `iss`, `jti`, `iat`, `exp`, `agent`, `human`, and `delegation` claims.
- The Broker **MUST NOT** mint an assertion when the human is unverified, the Master ToS is unsigned,
  the agent is revoked, or the requested scopes exceed the delegation.
- The Broker **MUST** require the caller to demonstrate knowledge of the accepted ToS text (the hash
  check in `identity_prove`), **MUST** reject a request that does not, and **MUST** record the
  acceptance before returning an assertion. Knowledge of `terms.text` is a precondition of consent,
  not merely an integrity check on the document.

Structured issuer objects and hosted JWKS discovery beyond the fixed path are **not** part of v1
(§22). They may be added as an additive extension later if a real need appears.

---

## 11. Contract E Identity Facts & the `context` Pointer

The signed `signup_token` (Contract E) remains the proof that a signup occurred. In v1.2 it records
identity/attribution, an opaque account reference, and the RP key/attestation that signed it. It is
signed by the **RP**, not by the broker, with the key named in `service.key_id` (§11.4).

This section also defines the two non-discovery contracts that carry the same RP identity:
authenticated abuse reporting (Contract D.2, §11.5) and authenticated receipt ingestion (Contract F,
§11.6).

### 11.1 Kept claims

- `iss`, `sub`, `aud`, `jti`, `iat`
- `assertion_jti` — the `jti` of the assertion presented at signup
- `signed_up_at`
- `service.audience`, `service.origin`, `service.provider_name`, `service.provider_url`,
  `service.account_id`, `service.attestation_jti`, `service.key_id`
- `agent.id`, `agent.name`, `agent.email`
- `human.id`, `human.name`, `human.email`, `human.verified`
- `tos.version`, `tos.url`, `tos.sha256`, optional `tos.master_tos_version`
- optional deprecated `exp`

**No `exp` in v1.2.** A receipt is a historical fact, not an access token. A legacy `exp` is
**ignored** and never causes rejection (§11.4). `iss` is the RP origin; `aud` is the human broker
issuer; `sub` is the agent id; `jti` is `srp_` + a 26-character lowercase Crockford base32 ULID.
`service.origin` **MUST** equal `iss` and the attestation `rp.origin`; `service.audience` **MUST**
equal the attestation `rp.audience`; `service.attestation_jti` names the attestation that contained
`service.key_id`, and that key **MUST** be in `keys[]` with `receipt` in its purposes. The JWS header
`kid` **MUST** equal `service.key_id`.

### 11.2 Removed claims

- `service.instance_sku`
- `service.endpoint_url`

### 11.3 Added: the `context` pointer

An optional top-level `context` object:

| Field | Requirement | Semantics |
| :--- | :--- | :--- |
| `context.url` | **REQUIRED when `context` is present** | Absolute HTTPS URL (localhost development exception) to the vendor's agent-facing integration guide (such as `SKILL.md`). |
| `context.sha256` | **REQUIRED when `context.url` is present** | `sha256:<64 lowercase hex>` over the exact UTF-8 bytes of that guide. |

Because `context` is meaningless without a pointer, `context.url` is required whenever the object is
present, and `sha256` is required with it: a URL without a hash cannot be pinned, and a hash without a
URL cannot be fetched. The receipt JSON Schema enforces the same pair; an empty `context` object is not
a way to omit the pointer — omit `context` entirely instead.

**Domain constraint (normative authority).** `context.url` **MUST** stay on the vendor's registrable
domain, and the authority for that domain is the signup endpoint's origin (`endpoints.agent_signup`),
not `provider_url`. When `provider_url` is present it **MUST** share the signup endpoint's registrable
domain. The broker compares `context.url` against the signup-endpoint registrable domain and **MUST**
reject a pointer on any other domain. Because the signed receipt carries `service.provider_url` but not
the signup endpoint, a receipt that omits `service.provider_url` cannot establish the domain; the
broker **MUST** then fail closed by rejecting the pointer rather than skipping the check.

No free-form vendor `metadata` object exists in v1. Do not add resource endpoints, SKUs, pricing,
credentials, or API usage details under any other claim. The `context` pointer is deliberate: it lets
the vendor document provisioning, SKUs, API usage, and errors **without** turning the signed receipt
into a resource inventory.

### 11.4 Contract E v1.2 — signing, validity, and retention

- The receipt is a compact JWS with header `{"alg":"EdDSA","typ":"agent-signup-receipt+jwt","kid":"<rp key id>"}`.
  The typ is unchanged from v1.1; v1.2 changes the claims, not the typ.
- The signing key is the RP key named in `service.key_id`. It **MUST** appear in the attestation
  `keys[]` identified by `service.attestation_jti` with `receipt` in `purposes`. There is **no
  broker-key fallback**: a new verified proof requires a key inside a valid attestation.
- A receipt is valid when **both** `iat` and `signed_up_at` fall inside the attestation's
  `[iat, exp]` window. No ±60s extension applies to that historical comparison; clock skew applies
  only to current-time comparisons.
- The RP **MUST** retain the attestation that was current when it signed the receipt and submit that
  **exact JWS** with the receipt. It **MUST NOT** substitute its current attestation: an RP that has
  renewed has a new `jti`, and the receipt still names the old one. A broker that cannot retrieve the
  named attestation and has no stored snapshot returns `attestation_unavailable` (§11.6) instead of
  substituting the newer attestation.
- Receipts are **idempotent forever** on `jti`. The same `jti` with a different body is a conflict
  (`409 signup_conflict`); the same body returns the existing row with `deduplicated: true`.
- `service.provider_url` stays OPTIONAL and is **not** an identity binding. `context` keeps its
  v1.1 shape and domain rule (§11.3) and is not part of the attestation.

### 11.5 Contract D.2 — authenticated abuse reporting

`POST /api/broker/report-abuse` accepts:

```json
{ "rp_attestation": "<compact JWS>", "report": "<compact JWS>" }
```

The report header is `{"alg":"EdDSA","typ":"agent-rp-abuse+jwt","kid":"<rp key>"}`. Claims are
REQUIRED: `iss` (RP origin), `aud` (the human broker issuer, A), `jti` (`abr_` + 26-character
lowercase Crockford base32 ULID), `iat`, `exp` (`exp - iat` exactly 300 seconds), `agent_id`, and
`reason` (1–500 characters); `evidence` is OPTIONAL and carries OPTIONAL `assertion_jti` and
`signup_token_jti`.

Rules, in order:

1. Apply the full Contract G verification (§14) to `rp_attestation`, including the trusted
   RP-broker check **before** any JWKS fetch. For this inbound abuse path the trust list is the
   broker's configured RP-broker list only: a human's `distrust` MUST NOT suppress abuse evidence,
   and D.2 has no human session.
2. Require `typ: "agent-rp-abuse+jwt"`, `alg: "EdDSA"`, and exactly one `kid`. The `kid` MUST be in
   `attestation.keys[]` with `abuse` in `purposes`; verify the report signature with that key.
3. Require `iss` equal to the attestation `rp.origin` and `aud` equal to A's issuer.
4. Require the attestation to be current at report time with the standard 60-second skew
   (`iat - 60s <= now <= exp + 60s`). The report's own five-minute freshness bound is **never**
   extended: accept only when `now` is not before `iat - 60s` and never after `exp`.
5. Idempotent on `jti`.

A report is `verified` only when it is linked to a signup row whose `proof_source` is `rp_callback`
or `rp_signed` **and** whose attestation was verified. A link to `agent_self_reported` or
`legacy_broker_signed`, or no link at all, is `unverified`: the broker opens a case and does **not**
suspend the agent. Only a `verified`, receipt-linked report may request protective suspension pending
operator review.

Success is `202 {"case_id":"<uuid>","report_jti":"abr_...","verification_status":"verified"|"unverified","deduplicated":false}`;
a duplicate returns `200` with the same `case_id`. An unauthenticated legacy post is
`401 {"error":"unauthorized","error_description":"...","code":"unauthenticated_report"}`. Response
bodies never include key material or internal paths.

The human path — a person reporting a bad RP — is broker-local and not required for RP conformance:
`POST /api/broker/report-provider` takes the RP origin, audience, optional attestation `jti`, reason,
optional evidence, and optional `distrust_broker`/`block_provider` flags, and returns
`202 {"case_id":"...","status":"open"}`. It updates the human's trust/policy, never the protocol
attestation.

### 11.6 Contract F — authenticated receipt ingestion

`POST /api/broker/signup-receipt` accepts:

```json
{ "signup_token": "<compact Contract E receipt JWS>", "rp_attestation": "<compact JWS>" }
```

Both fields are REQUIRED. `signup_token` is the compact Contract E receipt defined in §11.1/§11.4;
the legacy field name is retained and there is no separate `receipt` field. The `rp_attestation` in
the body **MUST** be the exact attestation named by the receipt's `service.attestation_jti`, not the
RP's current discovery attestation.

Order: parse both compact JWS envelopes → resolve `assertion_jti` to the acceptance and its human (DB
lookup only; the receipt is not trusted yet) → build the effective RP-broker trust list from
`BROKER_TRUSTED_RP_BROKERS` plus that human's `trust` and minus that human's `distrust` (distrust
wins) → check `rp_attestation.iss` against it **before** any JWKS fetch → verify the attestation
signature and claims (Contract G steps 1–3 and 5–6) → parse and schema-check the receipt → check the
receipt `iat` and `signed_up_at` against the attestation `[iat, exp]` window using **the receipt's
signing time, not ingestion time** → check the receipt `typ`/`alg`/`kid`, where header `kid` MUST
equal `service.key_id` → verify the receipt signature with that key → check
`iss`/`aud`/origin/audience/`attestation_jti` → verify the receipt `sub` equals the acceptance agent id
and derive `agent`/`human` from the trusted acceptance row, never from the receipt → record an
`event_kind:"signup"` verification snapshot and `proof_source:"rp_callback"`.

The attestation is **historical** for Contract F: an attestation that has expired since the signup is
still the correct key material for a receipt whose `iat`/`signed_up_at` fall inside its `[iat, exp]`
window. Do **not** apply the current-time `exp + 60s` check to the receipt's attestation. Do apply the
current-time check to a D.2 abuse report (a current event).

There is no broker-key fallback, no legacy path for a new verified proof, and no untrusted broker. A
receipt whose `iat` or `signed_up_at` is outside the attestation window fails with
`401 code:"attestation_not_valid_at_signing"`.

The success body preserves the existing Contract F response fields (`ok`, `signup_id`,
`signup_token_jti`, `assertion_jti`, `agent_id`, `service_audience`, `provider_account_id`,
`human_email_shared`, `context_url`, `context_sha256`) and adds `proof_source:"rp_callback"` and
`deduplicated`. A duplicate returns the existing row with `deduplicated:true` and does not drop the
existing fields.

Tier 2 (`identity_record_signup`) carries the same `signup_token` and the exact `rp_attestation` as an
optional companion field; if the agent omits it, the broker fetches discovery at `service.origin` and
requires the presented attestation's `jti` to equal `service.attestation_jti`. It never substitutes a
newer attestation. Self-report is the separate `agent_self_reported` path and has no attestation.

| HTTP | `error` | `code` | Meaning |
| :--- | :--- | :--- | :--- |
| `400` | `invalid_request` | `invalid_request` | Missing/malformed body or non-JWS strings |
| `401` | `invalid_attestation` | `invalid_attestation` | Contract G parse/signature/claims/origin/audience/binding failure; **not** historical expiry |
| `401` | `invalid_attestation` | `rp_broker_untrusted` | `rp_attestation.iss` not in the effective RP-broker list (broker default + acceptance human's trust − distrust) |
| `401` | `invalid_signup_token` | `invalid_signup_token` | Receipt parse/signature/claims failure; legacy sanitized body preserved |
| `401` | `attestation_not_valid_at_signing` | `attestation_not_valid_at_signing` | Receipt `iat`/`signed_up_at` outside the attestation `[iat, exp]` window |
| `401` | `attestation_unavailable` | `attestation_unavailable` | The exact attestation is no longer retrievable and the broker has no stored snapshot |
| `409` | `signup_conflict` | `signup_conflict` | Same receipt `jti` with a different body |
| `429` | `rate_limited` | `rate_limited` | Bounded ingestion rate exceeded |
| `503` | `service_unavailable` | `rp_broker_unavailable` | Transient trusted-broker metadata/JWKS outage; retryable, no write |

The response body always uses `{error, error_description, code}` and `error_description` never
includes key material or internal paths. Broker JWKS retention matters here: the broker **MUST** keep
a retired attestation-signing public key in its JWKS for at least 30 days after the last attestation
signed with that key expires, so a receipt presented after expiry and after a broker-key rotation can
still be verified (§16). After that window, an unseen old receipt is `attestation_unavailable` rather
than silently trusted.

---

## 12. Post-Signup Context Handoff

### 12.1 Delivery

- The vendor supplies `context.url` and `context.sha256` in its `POST /api/signup/agent` response and
  in the signed `signup_token`.
- The broker persists the context and exposes it to the agent after signup (Contract F response and
  the `identity_record_signup` MCP result) and to the human (portal link).
- Discovery **does not** advertise a post-signup context field in v1. The signed receipt and signup
  response are the authoritative handoff.

### 12.2 Integrity

- The agent **MUST** fetch the guide **after** signup and recompute the SHA-256 over the exact bytes it
  received, comparing it with `context.sha256`. On mismatch, the agent **MUST NOT** act on the guide.
- The guide **SHOULD** be size-capped (1 MiB recommended) and served with a text/Markdown content type.

### 12.3 Trust

- The guide is **untrusted vendor documentation** — **not** broker policy and **not** agent
  instructions. It is subject to the same prompt-injection rule as `terms.text` (§8): content is data.
- The broker vouches for **who** signed up and **what ToS** was accepted, not for the guide's contents.
- The URL **MUST** stay on the vendor's registrable domain. The normative authority is the signup
  endpoint's origin (`endpoints.agent_signup`); `provider_url`, when present, **MUST** share that same
  registrable domain. The broker compares the pointer against the signup-endpoint registrable domain
  and **MUST** reject a pointer on any other domain. A cross-domain, missing-authority (no
  `service.provider_url` in the signed receipt), or malformed pointer **MUST** be rejected by the
  broker and **MUST NOT** be followed by the agent.
- The broker validates the pointer **statically** and **MUST NOT** fetch it during receipt ingestion,
  so the receipt path cannot become an SSRF surface.

### 12.4 Staleness

- The agent **SHOULD** re-fetch the guide when it begins a new provisioning task rather than caching it
  indefinitely, and **MUST** re-verify the hash on each fetch.
- The vendor **SHOULD** version or date the guide and update `context.sha256` whenever the served bytes
  change. A stale guide is a vendor-operations concern, not a broker failure: the portal shows
  "No post-signup guide provided" rather than inventing a fallback resource view.

### 12.5 Credential handoff & recovery

Provider credentials (for example a vendor API key) are **shown once**, at the signup handoff. The
protocol draws a firm line between the broker-mediated identity event and the vendor's own credential
lifecycle:

- **Show-once handoff.** When an RP creates an account it returns the provider credential in the
  signup response. Provider credentials are vendor scope (§2.2) and are not carried in the signed
  receipt, the discovery document, or broker storage.
- **Agents MUST persist at handoff.** An agent **MUST** persist provider credentials to a secure store
  at the moment of handoff, and **MUST NOT** rely on the conversation, context window, or transcript as
  storage.
- **The Broker MUST NOT store provider credentials.** The Broker records the identity/attribution
  proof (`signup_token`) and the optional `context` pointer; it **MUST NOT** store, proxy, or re-issue
  provider credentials.
- **Do not re-run signup for recovery.** An agent **MUST NOT** re-run signup as a credential-recovery
  mechanism. A repeated signup on an existing account **resolves** the account and does not re-issue
  credentials.
- **RPs SHOULD document recovery.** A vendor **SHOULD** document a credential recovery/rotation path
  in its guide (`context.url`), reached only after signup. Because the recovery path is vendor
  capability, it stays out of discovery and the receipt, exactly like provisioning.
- **Resolve does not mint credentials or mail.** A resolve of an existing account **MUST NOT** mint
  new provider credentials and **MUST NOT** send duplicate welcome/legal mail; a fresh `signup_token`
  **MAY** be issued so a dropped-response retry can still record the receipt.

---

## 13. Versioning, Extension & Deprecation Policy

- `protocol` carries the **major** version only: `agent-identity-v1`.
- Additive identity fields are allowed within v1. Consumers **MUST** ignore unknown fields.
- Vendor catalogs, provisioning endpoints, resource metadata, and API-usage documentation are **not**
  additive protocol fields; they belong in the post-signup guide.
- Producers **SHOULD** namespace non-capability vendor metadata under `extensions.<reverse-dns>`.
- A breaking change **MUST** use a new `protocol` value and a new discovery path (for example
  `agent-signup-v2.json`). The v1 path **MUST NOT** silently change semantics.
- The public spec URL **SHOULD** be versioned; the optional `spec` field lets agents and humans find the
  exact document.
- A future version **SHOULD** be dual-published during a migration window. The changelog (§1.2) records
  additive v1 changes.

---

## 14. Contract G — RP Attestation

The RP attestation is a public, broker-signed statement that binds a relying party's **canonical
origin**, its **audience**, its **provider identity**, and its **public signing keys**. Any agent or
broker can verify it locally against the issuing broker's JWKS. It is published as the OPTIONAL
`rp_attestation` field of the discovery document (§5.2, §5.4).

The attestation is **public and copyable**. Only the origin + audience binding prevents replay on
another service. It **MUST NOT** carry a terms hash, scopes, endpoints, `provider_url`, account ids,
prices, or private material. It is identity, not a capability catalog and not a safety rating.

Header:

```json
{ "alg": "EdDSA", "typ": "agent-rp-attestation+jwt", "kid": "<broker kid>" }
```

### 14.1 Claims

Every claim is **REQUIRED** unless the Rule says OPTIONAL.

| Claim | Rule |
| :--- | :--- |
| `iss` | Broker B issuer origin: lowercase, no path, query, or fragment. |
| `sub`, `rp.id` | Provider UUID; the two **MUST** be equal. |
| `aud` | Literal `agent-identity-v1` (protocol label, never the human broker issuer). |
| `jti` | `^rpa_[0-9a-hjkmnp-tv-z]{26}$` (lowercase Crockford base32 ULID, no `i/l/o/u`), unique per issuance. |
| `iat`, `exp` | Integer seconds; `exp - iat = 86400` exactly; the issuer **MUST NOT** widen it. |
| `rp.origin` | Canonical origin: lowercase scheme and host, default port omitted, no trailing slash, path, query, or fragment. |
| `rp.audience` | Exactly `discovery.audience`; `^\S{1,256}$`. |
| `rp.name` | 1–128 characters. |
| `rp.legal_name` | **OPTIONAL** at `domain`, **REQUIRED** at `entity`; 1–256 characters. |
| `rp.contact.email` | Single email address. |
| `rp.contact.name` | **OPTIONAL**; 1–128 characters. |
| `rp.verification.level` | `domain` or `entity` (§17.1). |
| `rp.verification.method` | `https` or `dns`: the method that verified the origin. |
| `rp.verification.origin_verified_at` | RFC 3339 UTC. |
| `rp.verification.entity_reviewed_at` | `null` at `domain`; non-null at `entity`. It records when the operator reviewed the entity and has no ordering requirement against `origin_verified_at`. |
| `keys[]` | 1–8 entries, unique `kid`. Every `active` key plus every `retiring` key whose `retire_after` is in the future. Registration rejects a ninth simultaneous key with `409 key_limit_reached`; issuance never silently omits a key. |
| `keys[].kid` | `^[A-Za-z0-9._-]{1,64}$`. |
| `keys[].kty`, `keys[].crv`, `keys[].alg` | `OKP`, `Ed25519`, `EdDSA`. |
| `keys[].x` | base64url 32-byte public key, `^[A-Za-z0-9_-]{43}$`. |
| `keys[].purposes` | Exactly `["receipt","abuse"]` in v1.2. The array is retained so a future split is additive; a v1.2 producer **MUST NOT** emit a subset. |

Cross-field rules JSON Schema cannot express are normative here and enforced by the validator:
`exp - iat = 86400`, `rp.id = sub`, unique `keys[].kid`, `legal_name` at `entity`,
`entity_reviewed_at` null at `domain`.

**Origins in a signed identity artifact are HTTPS-only.** `iss` and `rp.origin` are canonical
`https://` origins; the localhost development exception (§5.4) applies to discovery, terms, and
endpoint URLs and to origin-challenge fetches, never to a broker-signed attestation. The published
JSON Schema and the Zod schemas enforce the same rule.

### 14.2 Verification

Verification is normative and ordered:

1. Parse the compact JWS; require `typ`, `alg: EdDSA`, and exactly one `kid`.
2. `iss` **MUST** be in the verifier's trusted RP-broker list. Reject an untrusted `iss` **before**
   any JWKS or status fetch; the verifier never contacts an untrusted broker.
3. Verify against B's JWKS (`jwks_uri` from §16, else `<iss>/.well-known/jwks.json`), selecting the
   key by `kid`. Never try all keys.
4. Check `iat`/`exp` with 60 seconds of skew. `jti` **MUST** match the frozen pattern.
5. `rp.origin` **MUST** equal the origin of the discovery document actually fetched.
6. `rp.audience` **MUST** equal `discovery.audience`.
7. Record `rp.verification.level`. In v1.2 the level is informational for policy (the matrix is
   level-agnostic) and is displayed in the ledger. A future `min_level` policy is additive.
8. The status endpoint (§16) **MAY** be consulted in enforced mode; otherwise `exp` is the bound.

A verifier **MUST NOT** treat a failed step 5 or 6 as "unverified". `unverified` means **no
attestation was presented** (§18).

### 14.3 Agent-supplied discovery

The ordinary `identity_signup` path is broker-fetched: the broker fetches discovery itself and uses
that fetched origin. When a caller supplies a discovery **object** instead (an advanced
`identity_prove` path), the caller **MUST** also supply an explicit canonical `rp_origin`; the
verifier uses that declared origin and never infers it from `terms.url`, `provider_url`, or the
attestation itself. The path is recorded as `agent_supplied_discovery` and a `require` policy
**MUST** reject it with `discovery_source_not_allowed` (§17.3).

---

## 15. Contract H — Provider Registration and Status API

Contract H is the administrative API a relying party uses with its RP broker to register origins,
prove key possession, and obtain or renew an attestation. It is **never agent-facing** and never
appears in discovery. Contract H has **no public JSON Schema**: its bodies are the normative tables
below, and provider portals/CI implement them.

Routes are absolute; shorthand such as `POST /origins/:id/challenge` is relative to
`/api/provider/v1`. Provider API keys use `Authorization: Bearer <key>`. The key format is an
implementation detail, but the key is **distinct** from the RP signing key, hashed at rest, and
scoped.

| Route | Auth (scope) | Result |
| :--- | :--- | :--- |
| `GET /api/provider/v1/account` | API key (`account:read`) | `200` account, status, plan, entitlements |
| `POST /api/provider/v1/origins` | human session | `201` origin, `status:"pending"` |
| `POST /api/provider/v1/origins/:id/challenge` | human session | `201` challenge |
| `POST /api/provider/v1/origins/:id/verify` | human session | `200` origin, `status:"verified"` |
| `POST /api/provider/v1/keys/challenge` | API key (`keys:write`) | `201` PoP challenge |
| `POST /api/provider/v1/keys` | API key (`keys:write`) | `201` key, `status:"active"` |
| `POST /api/provider/v1/keys/:kid/retire` | API key (`keys:write`) | `200` key, `status:"retiring"` |
| `POST /api/provider/v1/keys/:kid/revoke` | API key (`keys:write`) | `200` key, `status:"revoked"` |
| `POST /api/provider/v1/attestation` | API key (`attestation:write`) | `201` attestation |
| `GET /api/provider/v1/attestation` | API key (`attestation:read`) | `200` attestation |
| `POST /api/provider/v1/api-keys` | human session | `201` key + show-once secret |
| `POST /api/provider/v1/api-keys/:id/revoke` | human session | `200` revoked key |

There is no key-listing endpoint in v1.2, so no `keys:read` scope is issued. Only the scopes used by
the routes above exist. If a key-listing route is added later, it adds both the route and the scope
together.

### 15.1 Origin and audience challenge

`POST /origins/:id/challenge` body `{"method":"https"|"dns"}`. The HTTPS response carries
`challenge_id`, `method`, `file_url`, `file_content`, and `expires_at`; the DNS response carries
`challenge_id`, `method`, `record_name` (`_agent-signup.<host>`), `record_type:"TXT"`, `record_value`
(`agent-signup-verify=<token>`), and `expires_at`.

- Token grammar `[A-Za-z0-9_-]{32,128}`. Tokens are single use, with a 30-minute TTL.
- The HTTPS file holds **exactly** the token plus at most one trailing LF, served as `text/plain`.
- DNS resolves one TXT value exactly equal to `agent-signup-verify=<token>`.
- Fetch rules: HTTPS only (localhost development exception), no cross-origin redirect, 5-second
  timeout, 64 KiB cap, public IP addresses only.
- Limits: 5/hour/origin and 20/day/account, answered with `429` and `Retry-After`.
- Audience is globally unique across the broker's provider origins. A revoked provider's audience is
  not reclaimed automatically. A conflict is `409 code:"audience_conflict"` and goes to operator
  review/cleanup.

### 15.2 Key proof of possession

`POST /keys/challenge` body `{"kid":"rp-key-1"}` returns
`201 {"challenge":"<opaque>","expires_at":"...","aud":"<B issuer>"}`. The challenge TTL is 10 minutes,
single use, bound to the provider and `kid`.

`POST /keys` body `{"proof":"<compact JWS>","public_jwk":{...},"purposes":["receipt","abuse"]}`. The
PoP header is `{"alg":"EdDSA","typ":"agent-rp-key-pop+jwt","kid":"rp-key-1"}` and the claims are:

```json
{ "iss": "https://vaporcloud.example", "aud": "https://agentregistrar.dev",
  "sub": "<provider UUID>", "jti": "<uuidv4>",
  "iat": 1760000000, "exp": 1760000600, "nonce": "<challenge>",
  "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "<43 chars>", "kid": "rp-key-1" },
  "purposes": ["receipt", "abuse"] }
```

`exp - iat = 600`. The `nonce` **MUST** be the unconsumed challenge; `iss` **MUST** be one of the
provider's `verified` origins; `sub` **MUST** equal the authenticated provider; `jwk` **MUST** equal
`public_jwk`; `public_jwk.kid`, the challenge `kid`, the PoP header `kid`, and `jwk.kid` **MUST** all
be the same value; the body `purposes` **MUST** equal the PoP `purposes`; and the JWS **MUST** verify
with that `jwk`. The challenge is consumed on success or on the fifth failure.

Keys are provider-scoped and appear in every attestation for every origin of that provider. Retire
sets `status:"retiring"` and `retire_after = now + 30 days`; the key stays in new attestations until
then and resolves historical receipts from the stored snapshot. Revoke is immediate.

### 15.3 Attestation issuance and renewal

`POST /api/provider/v1/attestation` body `{"origin":"https://vaporcloud.example","audience":"vaporcloud"}`
returns `201`:

```json
{ "jti": "rpa_01j...", "origin": "https://vaporcloud.example", "audience": "vaporcloud",
  "verification_level": "domain", "issued_at": "2026-10-24T00:00:00.000Z",
  "expires_at": "2026-10-25T00:00:00.000Z", "renewed_from_jti": null,
  "attestation": "<compact JWS>" }
```

Issuance requires provider `status:"active"`, `plan:"verified"` with a live subscription (the
`verified` entitlement), origin `status:"verified"`, and at least one `active` key. A `free` provider
is conformant but cannot issue an attestation; it receives `403 code:"provider_not_eligible"` with a
plan/upgrade next step. A new `jti` is minted per issuance; `exp = iat + 86400`; `renewed_from_jti` is
`null` on a first issuance and the previous `jti` on a renewal; there is no minimum renewal interval;
the limit is 24 issuances/day/origin. `GET ?origin=&audience=` returns the latest attestation by
`(issued_at desc, created_at desc, id desc)`, or `404 code:"attestation_not_found"`.

The normal renewal target is **12 hours** against a 24-hour expiry: one missed cycle is tolerable and
expiry is the primary revocation bound.

### 15.4 Account lifecycle

A new provider account is `pending`. The broker promotes it to `active` when its first origin becomes
`verified` (domain level, automated). The `free` plan may be `active` but has no attestation
entitlement. `verified` plans are set with a live subscription. An entity review is recorded before a
`level:"entity"` attestation may be issued. Suspend, revoke, and restore are operator actions.
`GET /api/provider/v1/account` remains readable in `pending`, `suspended`, and `revoked` so the
provider can see its state; `provider_suspended` and `provider_not_eligible` apply to mutations and
issuance, not that read.

### 15.5 Errors

Errors use the OAuth-style body in §18.5 with a stable `code`: `400 invalid_request` /
`invalid_jwk` / `purpose_mismatch`; `401 invalid_api_key`; `403 insufficient_provider_scope` /
`provider_suspended` / `provider_not_eligible`; `404 not_found` / `key_not_found` /
`attestation_not_found`; `409 audience_conflict` / `origin_conflict` / `key_exists` /
`key_limit_reached` / `last_active_key` / `key_not_active` / `no_active_key`; `410 challenge_expired`
/ `challenge_consumed`; `422 challenge_failed` / `pop_invalid`; `429 rate_limited`; `500
internal_error`. Key and attestation route additions are additive and keep the frozen meanings of
`provider_*`, `pop_invalid`, and `rate_limited`.

---

## 16. Contract I — Broker Metadata and Attestation Status

### 16.1 Broker metadata

`GET /.well-known/agent-broker.json`, `Content-Type: application/json`, HTTPS, no authentication. All
six fields are **REQUIRED**:

| Field | Rule |
| :--- | :--- |
| `issuer` | Origin of the document itself; **MUST** equal the `iss` of every attestation it signs. |
| `jwks_uri` | Absolute HTTPS URL, **same origin** as `issuer`. |
| `attestation_status_uri` | Absolute HTTPS URL of the status endpoint. |
| `abuse_endpoint` | Absolute HTTPS URL accepting Contract D.2 (§11.5). |
| `protocol_versions` | Non-empty, unique, **MUST** contain `agent-identity-v1`. |
| `contact` | Email address or absolute HTTPS URL. |

- `issuer` differing from the document origin **invalidates the document**. `issuer` is a canonical
  HTTPS origin, exactly like an attestation `iss` (§14.1); the localhost development exception does not
  apply to metadata.
- Clients cache metadata for at most **1 hour** and re-fetch on an unknown `kid`. This is a client
  courtesy, not a protocol claim. The attestation status cache stays at **5 minutes**.
- A client that verifies an attestation resolves JWKS as follows: it **MAY** use the metadata
  `jwks_uri` only when the fetched document's `issuer` equals the trusted attestation `iss` and
  `jwks_uri` is HTTPS and same-origin with it; otherwise it **MUST** fall back to
  `<issuer>/.well-known/jwks.json`. A cross-origin or invalid `jwks_uri` **MUST NOT** be followed.
  Without metadata, clients fall back to `<issuer>/.well-known/jwks.json` and
  `<issuer>/api/broker/report-abuse`, where `<issuer>` is the assertion `iss` (Contract A) or the
  attestation `iss` (Contract G) being verified. The fallback is a client rule, not a second format.
- Cross-origin `attestation_status_uri` and `abuse_endpoint` are allowed but **MUST** pass the same
  SSRF rules as §19.5; same-origin is RECOMMENDED.
- **Broker JWKS retention.** B **MUST** keep a retired attestation-signing public key in its JWKS for
  at least 30 days after the last attestation signed with that key expires. This lets a broker verify
  a receipt presented after the attestation expired and after a broker-key rotation. A stores the
  verified attestation/key snapshot on first ingestion; after the retention window, an unseen old
  receipt is `attestation_unavailable` rather than silently trusted. Retention is a broker rule, not a
  client cache bound.

`broker-metadata.schema.json` is the producer schema. `jwks_uri` same-origin with `issuer` is a
cross-field rule enforced by the validator.

### 16.2 Attestation status

`GET <attestation_status_uri>?jti=rpa_...` is public. The body is:

```json
{ "attestation_jti": "rpa_01j...", "status_jwt": "<compact JWS>" }
```

The plaintext field is named `attestation_jti` so it cannot be confused with the status JWS's own
`jti` (a UUID). The JWS header is
`{"alg":"EdDSA","typ":"agent-rp-attestation-status+jwt","kid":"<broker kid>"}` and the claims are:

```json
{ "iss": "https://agentregistrar.dev", "aud": "agent-identity-v1",
  "jti": "<uuidv4>", "sub": "rpa_01j...",
  "iat": 1760000000, "exp": 1760000300,
  "status": "active", "status_changed_at": "2026-10-24T00:00:00.000Z" }
```

- `sub` is the queried attestation `jti`.
- `exp - iat` is exactly **300 seconds**.
- `status` is one of `active`, `suspended`, `revoked`.
- `status_changed_at` **MUST NOT** be after `iat`.
- The plaintext `attestation_jti` is **advisory**. Clients **MUST** verify the JWS and use `sub`, and
  **MUST** check `typ`, `alg`, exactly one `kid`, the B signature, `iss` equal to the metadata
  `issuer`, `aud` equal to `agent-identity-v1`, `exp`/`iat` with 60 seconds of skew, and `sub` equal
  to the queried `attestation_jti`.
- `suspended` maps to `RpVerificationStatus.suspended`. `revoked` maps to `invalid` with
  `reason:"attestation_revoked"` because the frozen enum has no `revoked` (§18). The status response
  itself carries only `status` and `status_changed_at`.
- Producer and verifier modes apply to the status JWS too: the published JSON Schema and
  `AttestationStatusClaimsSchema` are closed producer mode, while a verifier validates every known
  claim and ignores unknown additive claims (`parseAttestationStatusVerifier`).

Caching is `Cache-Control: public, max-age=300, must-revalidate`; clients **MUST NOT** use an older
cached answer than 300 seconds except to report `unavailable`. An unknown `jti` is `404` with
`{"error":"not_found","error_description":"...","code":"attestation_not_found"}` and
`Cache-Control: no-store`. Responses **MUST NOT** include provider name, contact, or account data.

Local verification does **not** require the status endpoint: expiry is the primary bound. The status
endpoint is opt-in defense-in-depth for enforced mode and operator actions.

---

## 17. Trust Model, Assurance Levels, and Policy

The trust model rests on three separate statements:

1. **Assertion verification is permissionless.** Anyone can verify a Contract A assertion against the
   human broker's public JWKS — no account, SDK, shared secret, or broker call.
2. **Attestation verification is local.** An agent or broker verifies a Contract G attestation against
   the RP broker's cached JWKS. There is no per-signup A→B call and no broker in the RP's signup path.
3. **Trust is policy, not cryptography.** A valid attestation says "this broker vouches for this
   origin/audience at this time". Whether that broker is trusted, and what to do about an untrusted or
   missing attestation, is a local policy decision.

Trust is **not transitive**: trusting Broker B to attest an RP does not make B's other statements
authoritative, and it says nothing about the RP's safety, uptime, solvency, or conduct. "Verified" is
a **verification event**, not a badge and not a safety guarantee.

### 17.1 Assurance levels

| Level | Meaning |
| :--- | :--- |
| `domain` | Automated control of the origin via an HTTPS well-known file or a DNS TXT record (§15.1). |
| `entity` | Operator-reviewed legal-entity evidence: legal name, registry/VAT id, official contact. |

There is no `enhanced` level in v1.2. Neither level asserts safety, uptime, solvency, or conduct. Both
levels are `verified` for policy purposes; the level is recorded and displayed, and a future
`min_level` policy is additive.

### 17.2 Resolution order

Resolution is normative and follows this order:

1. Normalize the requested service to an origin.
2. Fetch and validate discovery (SSRF-safe, bounded).
3. Validate protocol, terms, scopes, and issuer allowlist.
4. Verify `rp_attestation` if present (§14.2).
5. Resolve trust: the broker default plus the human's override.
6. Resolve policy: the human's rule for `(rp_origin, rp_audience)`, else `warn`.
7. Compute `RpVerificationStatus` (§18).
8. Apply the policy matrix (§17.3).
9. Mint and record the verification event.
10. Return the prepared signup request and next steps.

### 17.3 Policy matrix

`RpPolicy` is `allow`, `warn`, `require`, or `block`. Policy is **never** a verification status.

| Policy | `verified` | `unverified` | `attested_untrusted_broker` | `invalid` / `expired` | `unavailable` | `suspended` |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| `allow` | proceed | proceed | proceed | proceed | proceed | proceed |
| `warn` | proceed | proceed + warning | proceed + warning | proceed + warning | proceed + warning | proceed + warning |
| `require` | proceed | `rp_unverified` | `rp_broker_untrusted` | `rp_attestation_invalid` / `rp_attestation_expired` | `rp_broker_unavailable` | `rp_suspended` |
| `block` | `rp_blocked` | `rp_blocked` | `rp_blocked` | `rp_blocked` | `rp_blocked` | `rp_blocked` |

- `require` is **always** broker-enforced. A `require` policy that is only advisory would be a lie.
- `allow` and `warn` **MAY** set `enforced:true`: the broker still verifies the attestation at mint
  time and records the verification row (and returns the warning), but the policy still proceeds.
  `enforced:true` is **not** a hidden hard gate for `allow`/`warn`; only `require` refuses.
- `require` **MUST** reject `source:"agent_supplied_discovery"` with `discovery_source_not_allowed`
  and accept only a broker-fetched discovery document.
- The default policy is `warn`. The trusted RP-broker list is the broker default plus the human's
  `trust` override minus the human's `distrust` override; distrust wins. There is no per-agent list
  in v1.2: agents inherit their human's policy.

This specification defines statuses, not MCP tool names or JSON-RPC envelopes.

---

## 18. Status and Error Taxonomy

### 18.1 Verification status

`RpVerificationStatus` has exactly these values, and no synonyms:

`verified`, `unverified`, `attested_untrusted_broker`, `invalid`, `expired`, `unavailable`,
`suspended`.

| Condition | Status | Validator | MCP code |
| :--- | :--- | :--- | :--- |
| Valid attestation, trusted broker, policy accepts | `verified` | `verified` | — |
| No attestation | `unverified` | `unverified` | `rp_unverified` under `require` |
| Signature/claims/origin/audience failure | `invalid` | `invalid` | `rp_attestation_invalid` |
| Past `exp` + skew | `expired` | `expired` | `rp_attestation_expired` |
| `iss` not trusted | `attested_untrusted_broker` | `attested_untrusted_broker` | `rp_broker_untrusted` |
| JWKS or status fetch failure | `unavailable` | `unavailable` | `rp_broker_unavailable` |
| Status suspended, or local provider suspended | `suspended` | n/a (no status call) | `rp_suspended` |

`unverified` means "no attestation presented". An expired, invalid, or untrusted attestation is never
downgraded to `unverified`. `blocked` is policy, not a verification result. The validator never emits
`suspended` because it never calls the status endpoint.

### 18.2 Proof sources

`proof_source` has exactly four values: `rp_callback` (Tier 1, Contract F), `rp_signed` (Tier 2 with
an RP-signed receipt), `agent_self_reported` (no RP signature), and `legacy_broker_signed` (pre-v1.2
rows, not verified). `rp_authenticated` and `agent_reported` **MUST NOT** be written for new rows.
Only `rp_callback` and `rp_signed`, with a verified attestation, can give an abuse report standing.

### 18.3 Verification source

`RpVerificationSource` has exactly two values, and no synonyms:

- `broker_fetch` — the broker fetched the discovery document itself (the ordinary `identity_signup`
  path and Contract F). The fetched document origin is authoritative.
- `agent_supplied_discovery` — the caller supplied a discovery **object** plus an explicit canonical
  `rp_origin` (the advanced `identity_prove` path; §5.4, §14.3). The declared origin is used and is
  never inferred from `terms.url`, `provider_url`, or the attestation.

`require` is broker-enforced and **MUST** reject `agent_supplied_discovery` with
`discovery_source_not_allowed`; only a broker-fetched discovery document is acceptable (§17.2,
§17.3). `allow` and `warn` may accept either source, and a same-audience `require` row on another
origin also refuses a request with no source. `RpVerificationSource` is recorded with the
`rp_verifications` event; it is not a verification status and never replaces `proof_source`.

### 18.4 Attestation status values

`AttestationStatusValue` is `active`, `suspended`, or `revoked`. It describes the attestation at a
point in time, not the RP's policy. A registry `verification.status` uses the same values and only
`active` earns a badge.

### 18.5 HTTP error body

Every new route and every path where a client branches uses the OAuth-style body:

```json
{
  "error": "invalid_request",
  "error_description": "Human-readable, non-secret explanation",
  "code": "stable_machine_code"
}
```

`code` is additive to existing OAuth bodies. Existing routes that do not yet emit `code` keep working;
new routes always emit it. `error_description` never includes key material or internal paths.

---

## 19. Verification Rules

### 19.1 Two independent results

Document **conformance** and attestation **verification** are separate outcomes:

- A malformed `rp_attestation` field (not three base64url segments, over 16384 characters, or not a
  string) is a **conformance** error with no verification verdict: the validator reports
  `attestation.status: not_checked` (reason `field_malformed`), not `invalid`, and exits `1`.
- A structurally valid JWS whose header, claims, or signature fail is a **verification** error
  (`invalid`), not a conformance error.
- An expired attestation is a verification warning; a bad signature, origin mismatch, or audience
  mismatch is a verification failure.
- A broker/JWKS fetch failure is `unavailable` — a warning by default, never a conformance failure.

**Document conformance never requires a live RP broker.** Only explicit attestation verification
(§19.2) contacts one.

### 19.2 Validator behavior

The validator ([`validator.md`](https://docs.agentregistrar.dev/agent-identity/v1/validator/)) keeps its conformance job and adds an independent
attestation result:

| Flag | Behavior |
| :--- | :--- |
| `--verify-attestation` | Parse and verify `rp_attestation` if present; opt-in issuer JWKS fetch. An absent field is `unverified` + warning, exit unchanged. |
| `--require-attestation` | Implies `--verify-attestation`; a non-`verified` result fails the run. |
| `--trusted-rp-broker <issuer>` | Repeatable. Flags override the env fallback; if any flag is present, the env list is ignored. Env fallback `AGENT_IDENTITY_TRUSTED_RP_BROKERS` (comma-separated). An empty effective list trusts nothing. |
| `--jwks <path-or-url>` | Pin exactly one issuer's JWKS for offline runs; never bypasses signature checks and never merges issuers. |
| `--at <rfc3339\|epoch>` | Freeze the clock for deterministic fixture runs; not for production use. |

Exit `0` means the document was fetched, parsed, and conformant and no required check failed. Exit `1`
means a conformance error (including a body that is not valid JSON, or a malformed `rp_attestation`
field) or an attestation failure under `--require-attestation`. Exit `2` means the run could not
obtain what it needed (document read/fetch failure, or JWKS unavailable under `--require-attestation`)
and is **never** a conformance verdict: the `conformance` verdict is not evaluated and human output
prints `DOCUMENT: NOT CHECKED`. An `unverified`, `expired`, or `invalid` attestation stays exit `0`
without `--require-attestation`. A run that explicitly requests verification warns when the optional
field is absent (exit unchanged).

The validator never emits `suspended`: that status comes from the broker or the status endpoint.

### 19.3 Trust before fetch

The trusted-issuer check happens **before** any JWKS or status fetch in Contract G, Contract F, and
Contract D.2. A verifier never contacts an untrusted broker, and Contract F/D.2 apply the full
Contract G verification, not just a signature check.

### 19.4 Caching and key handling

- JWKS are cached **per `iss`** and never merged. `jwks_uri` is same-origin with `issuer` and is used
  only when the metadata document declares the expected issuer; otherwise the fixed
  `<iss>/.well-known/jwks.json` path is used (§16.1).
- `--jwks` applies to exactly one issuer.
- Metadata is cached at most 1 hour; status responses at most 5 minutes; the attestation `exp` is the
  primary bound.
- A retired broker signing key stays in the JWKS for at least 30 days after the last attestation it
  signed expires (§16.1).

### 19.5 SSRF and bounded fetch

Every broker and origin fetch — discovery, JWKS, metadata, status, origin challenge — is HTTPS only
(localhost development exception), public IP addresses only, no cross-origin redirect, 5-second
timeout, 64 KiB cap, and never follows a URL from a response body implicitly. A fetched URL never
causes a side effect.

### 19.6 Algorithm and key confusion

Reject `alg != EdDSA`, a `typ` mismatch, a missing `kid`, and any key not selected by `kid`. Never try
all JWKS keys, never accept `none` or HMAC, and never accept a `jku`/`x5u` header pointing at an
arbitrary host.

### 19.7 Historical vs current

Contract E and Contract F compare the receipt's signing time to the attestation window; clock skew
applies only to current-time comparisons. Contract D.2 and status responses are current events with
fixed 300-second bounds. A verifier **MUST NOT** substitute a newer attestation for the one a receipt
names.

### 19.8 Fail closed

`require` and `--require-attestation` fail on `unavailable`. `warn` is advisory and never claims to be
a control. A missing origin for the attestation binding makes verification `unavailable`; it never
silently produces `verified`.

---

## 20. Security Considerations

- **Transport:** HTTPS required; localhost development exception only.
- **Issuer spoofing:** exact `iss` allowlist matching and issuer-origin JWKS only.
- **Audience confusion:** a single, exact `aud`, stable per service.
- **ToS substitution:** the agent recomputes the hash from `terms.text`; the RP rejects stale or
  mismatched hashes.
- **Replay vs resolve:** the `jti` guard is an **account-creation guard**; a repeated identical request
  for the same agent/account/service resolves idempotently, and `assertion_replayed` is reserved for
  conflicting reuse. Acceptance is the durable legal event; assertions are short-lived proofs (§7.2,
  §7.3).
- **Credential handoff & recovery:** provider credentials are **show-once**; agents persist them at the
  handoff moment, the Broker never stores them, and recovery is a separate vendor rotation path —
  never a signup re-run (§12.5).
- **Prompt injection:** discovery content, legal text, and the vendor guide are data, never
  instructions.
- **Post-signup context:** the URL stays on the vendor's registrable domain, uses HTTPS, is
  hash-pinned, and is size-capped; the broker validates it statically and never fetches it during
  receipt ingestion.
- **SSRF:** agents and brokers must not fetch arbitrary internal addresses from a discovery document;
  localhost is allowed only in explicit development mode.
- **Discovery tampering:** TLS plus the signed assertion; the discovery document itself is not a trust
  anchor.
- **DoS:** response size limits, timeouts, caching, and rate limiting.
- **Privacy:** no human PII or secrets in the public discovery document.
- **Cache poisoning:** short cache lifetimes and revalidation before signup.
- **RP attestation replay:** the attestation is public and copyable; the origin + audience binding is
  the control, never secrecy. An attestation from another origin or for another audience is
  `invalid`, not `unverified`.
- **Trust before contact:** an untrusted RP-broker `iss` is rejected before any JWKS or status fetch,
  so a malicious document cannot steer a verifier at a third-party host. JWKS are cached per `iss` and
  never merged; `--jwks` pins exactly one issuer.
- **RP key compromise and rotation:** receipts remain verifiable across rotation because the broker
  retains retired attestation-signing keys for at least 30 days after the last attestation they signed
  expires, and the verifier checks the receipt against the attestation it names, not the current one.
- **No broker in the signup path:** local attestation verification is the default. Enforced mode
  verifies at minting time; there is no per-signup A→B call and no tracking oracle.
- **Verification is identity, not safety:** neither assurance level (`domain`, `entity`) asserts
  safety, uptime, solvency, or conduct. `warn` is advisory; `require` is a hard gate; `block` never
  proceeds.
- **Fixture credentials:** the checked-in fixture signing seed is a warning-marked test vector. It is
  not a credential, is never referenced by broker configuration, and signs nothing outside
  `fixtures/`. No production or non-fixture private key is committed.

---

## 21. Conformance Checklist & Minimal Conformant Example

### 21.1 Relying Party conformance checklist

- [ ] `GET /.well-known/agent-signup.json` returns `200` with `Content-Type: application/json`.
- [ ] `GET` returns `404`/`410` (never `401`/`403`) when the service does not support the protocol.
- [ ] `protocol` is exactly `agent-identity-v1`.
- [ ] `audience`, `provider_name`, a non-empty `issuer_allowlist`, and unique `required_scopes`
      (including `tos:accept`) are present.
- [ ] `terms.text` is the complete ToS text; `terms.sha256` is `sha256:<64 lowercase hex>` over its
      exact UTF-8 bytes; `terms.url` and `terms.version` are present and consistent.
- [ ] `endpoints.agent_signup` is the **only** endpoint and is an absolute HTTPS URL.
- [ ] No §5.3 forbidden vendor field appears anywhere, including under `extensions`.
- [ ] CORS and cache headers follow §4; the body is within the 1 MiB cap.
- [ ] Assertions are verified against issuer JWKS with every check in §7.1.
- [ ] Stable §7.2 error codes are returned.
- [ ] A `context` pointer, if the vendor has capabilities to document, is on the vendor's registrable
      domain, HTTPS, and hash-pinned; it is returned in the signup response and receipt but **not** in
      discovery.
- [ ] `signup_token` (Contract E) contains identity facts, an opaque `service.account_id`, ToS facts,
      and optional `context` — and no `instance_sku` / `endpoint_url`.
- [ ] A repeated signup for an existing account, keyed as recommended in §9.1, resolves idempotently:
      it returns `resolution: "resolved"`, does not re-issue provider credentials, and sends no
      duplicate welcome/legal mail (**SHOULD**; §7.2, §12.5).
- [ ] The vendor guide documents a credential recovery/rotation path reached **after** signup, and
      that path is not advertised in discovery or the receipt (**SHOULD**; §12.5).
- [ ] `rp_attestation`, when published, is a compact JWS within the size bound; its `rp.origin` equals
      the fetched document origin and `rp.audience` equals `audience` (§5.4, §14).
- [ ] A document without `rp_attestation` is still conformant and is reported as **unverified**, never
      invalid (§1.3, §18).
- [ ] If the RP issues receipts, they are signed by a key in the attestation named by
      `service.attestation_jti` with the `receipt` purpose, and `header.kid` equals `service.key_id`
      (§11.4).
- [ ] The RP publishes `/.well-known/agent-broker.json` only if it is also an RP broker; a relying
      party needs broker metadata only when it verifies peers or accepts abuse reports (§16).
- [ ] An RP that verifies peer attestations checks the trusted-broker list **before** fetching JWKS and
      binds `rp.origin` to the fetched document origin (§14.2, §19.3).
- [ ] The attestation is renewed on the 12-hour target against the 24-hour expiry, and a receipt's
      attestation is retained rather than replaced by the current one (§15.3, §11.4).

### 21.2 Minimal conformant document

The §5.5 document is the minimal conformant example. It contains only identity fields and no vendor
capability fields. This repository is the canonical location of this specification; the `spec` field
may be omitted until a rendered copy exists.

---

## 22. Open Questions (non-blocking)

These items do **not** block v1. They are recorded so later revisions can address them deliberately.
The `v1.1.0` revision resolved the replay-versus-resolve and credential-handoff questions (see §7.2
and §12.5); canonical hosting remains open.

| Question | v1 position |
| :--- | :--- |
| Is a repeated signup with an already-used `jti` an error? | Resolved in `v1.1.0`: a repeated request for the **same** agent/account/service resolves idempotently; `assertion_replayed` is reserved for a conflicting context (§7.2). |
| How long does a ToS acceptance last? | Resolved in `v1.1.0`: no protocol-mandated re-acceptance interval; an acceptance lasts until the terms change or the human re-consents. A Broker **MAY** impose its own documented policy (§7.3). |
| How are provider credentials handed off and recovered? | Resolved in `v1.1.0`: credentials are **show-once** at signup and never stored by the Broker; recovery is a vendor rotation path documented in the guide, never a signup re-run (§12.5). |
| How does an agent verify a relying party? | Resolved in `v1.2.0`: an OPTIONAL broker-issued attestation (Contract G) is verified locally against the RP broker's JWKS (§14). |
| Where is RP verification recorded? | Resolved in `v1.2.0`: in the `rp_verifications` ledger, not as columns on immutable consent and not as a Contract A claim (§17.2, §18). |
| How does a client find the RP broker's JWKS, status, and abuse endpoints? | Resolved in `v1.2.0`: `/.well-known/agent-broker.json` (Contract I), with fixed-path fallbacks (§16.1). |
| How is revocation bounded without a per-signup call? | Resolved in `v1.2.0`: 24-hour expiry with a 12-hour renewal target and 60-second skew; the signed status endpoint is opt-in defense-in-depth with a 5-minute cache (§14.2, §16.2). |
| Does a signup receipt expire? | Resolved in `v1.2.0`: no. A receipt is a historical fact valid inside the attestation window at signing time; a legacy `exp` is ignored (§11.1, §11.4). |
| What happens to an abuse report with no linked receipt? | Resolved in `v1.2.0`: it is `unverified`, creates a case, and does not suspend the agent; only a verified, receipt-linked report can request protective suspension (§11.5). |
| How does the registry distinguish conformance from verification? | Resolved in `v1.2.0`: an OPTIONAL `verification` object in the registry with broker, level, attestation `jti`, `checked_at`, and status; only `active` earns a badge (`registry/README.md`). |
| IANA registration of the well-known URI. | Evaluate after v1 stabilizes; do **not** block v1. |
| Final canonical public hosting URL for this specification. | This repository is the canonical location of the specification until a rendered copy exists in the documentation site. The optional `spec` field points at a versioned copy and **stays OPTIONAL**; the placeholder `https://<canonical-spec-host>/agent-identity/v1/agent-signup` is illustrative only. |
| A future alias such as `agent-identity.json`. | Keep `agent-signup.json` for v1; consider an alias in v2. |
| Structured `issuers` metadata beyond `issuer_allowlist`. | Not in v1; a fixed `<issuer>/.well-known/jwks.json` path is required. Add structured metadata later only if a real need appears. |
| Signing the discovery document. | Not in v1; TLS + issuer JWKS + signed assertions are the trust path. |
| A Broker-side well-known discovery document. | Not in v1; the Broker is reached through MCP and publishes OAuth metadata + JWKS. |
| Handling multiple ToS versions during rollout. | Advertise only the current version; allow a bounded, RP-defined grace period for in-flight assertions, documented by the RP outside the discovery document (for example `SKILL.md` or a support page), **SHOULD NOT** exceed 24 hours. |

---

## 23. Relationship to Other Contracts

| Contract | Relationship |
| :--- | :--- |
| **Contract A — Agent Identity JWT assertion** | Referenced, not redefined. This spec defines discovery and presentation. |
| **Contract D.1 — Relying Party discovery** | This document **is** the normative definition; the historical reference implementation's documentation summarizes it. |
| **Contract D.2 — Abuse reporting** | Authenticated RP abuse report JWS signed by an RP key in the attestation (§11.5); unauthenticated legacy posts are rejected. |
| **Contract E — signup receipt** | RP-signed v1.2 receipt: identity facts, `service.attestation_jti`/`service.key_id`, optional `context`, no `exp` (§11.1, §11.4). |
| **Contract F — broker signup receipt callback** | Authenticated receipt ingestion profile: `{signup_token, rp_attestation}` verified in historical mode (§11.6); not agent-facing discovery. |
| **Contract G — RP attestation** | New broker-signed RP identity published as the optional `rp_attestation` field (§14). |
| **Contract H — provider registration and status API** | New administrative API: origins, keys, attestation issuance/renewal (§15); no public JSON Schema. |
| **Contract I — broker metadata and attestation status** | New public metadata document and signed status response (§16). |

The scope boundary between this identity protocol and vendor responsibilities is defined in
[`scope-boundaries.md`](https://docs.agentregistrar.dev/agent-identity/v1/scope-boundaries/). Any proposed new protocol field must pass that
document's review checklist before it is added here.

Source: https://docs.agentregistrar.dev/agent-identity/v1/specification/index.mdx
