---
title: Poppy compliance
description: Every Personal Agent Protocol requirement the shop meets, and the test that proves it.
sidebar:
  order: 4
---

The conformance suite runs at HTTP level against the real Worker entry (`handleRequest`). The tests are written from
the text of the protocol (draft 0.1), not from recorded output. The OpenAPI document is `/poppy/openapi.json` and the
Poppy operations are the `/v1` procedures (`POPPY_PATHS` in `apps/api/src/poppy/conformance.ts`). Passing tests show
local behaviour, not a deployed instance's.

The [Personal Agent Protocol draft 0.1](https://personalagentprotocol.org/docs/spec) is the normative text.
Start with the [Poppy overview](/poppy) for Sessions, grants and the supported sign-in channels. `MUST` rows can fail the gate; `SHOULD` and `MAY` rows record
what the shop chose. The tables below preserve the requirement row IDs alongside the conformance suites that verify them.

## Where the tests are

| Surface                       | Suite                                                                                                                  |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Sessions, tokens and APIs (A) | `apps/api/src/poppy/conformance.tokens.test.ts`, `conformance.authority.test.ts`, `conformance.document.test.ts`       |
| Conversations (C)             | `apps/api/src/poppy/conformance.conversations.test.ts`, `conformance.retries.test.ts`, `conformance.authority.test.ts` |
| MCP (M)                       | `apps/mcp/src/app.conformance.test.ts`, `infra/alchemy/composition.test.ts`, the Auth0 proxy discovery tests           |
| End to end                    | `apps/webshop/e2e/mcp-api.e2e.ts`, `conversations-api.e2e.ts`, `poppy-direct.e2e.ts`                                   |

## Sessions, tokens and APIs (spec 4 and 6)

| Row | Level    | Requirement                                                                                 | How it is met                                                                                                                                                                                              |
| --- | -------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A1  | MUST     | The `openapi` API points to an OpenAPI 3.0 or 3.1 description                               | `3.1.1` view of the contract (`/poppy/openapi.json`), `dpop` security per operation, no `QUERY` operations, every reference resolves                                                                       |
| A2  | MUST     | Every listed API accepts Session Tokens                                                     | Each operation of the document, including the checkout reads at `/v1/checkout-previews`, `/v1/me/checkout-address` and `/v1/me/checkout-methods`, succeeds with its declared status (201, 202, 204 or 200) |
| A3  | MUST     | API and conversation calls carry `Authorization: DPoP` and a proof                          | Missing proof is 401 `invalid_dpop_proof` on a REST and a conversation endpoint                                                                                                                            |
| A4  | MUST     | Signature, key, `htm`/`htu`, `iat`, `jti` not reused, `ath`                                 | One test per check; `jti` replay is refused by another handler through the D1 record                                                                                                                       |
| A5  | MAY/MUST | Nonces: `use_dpop_nonce`                                                                    | Stale nonce gets a fresh one; the retry succeeds                                                                                                                                                           |
| A6  | MUST     | Bearer Session Tokens only on MCP                                                           | A Bearer Session Token is 401 `invalid_token` with a `DPoP` challenge                                                                                                                                      |
| A7  | MUST     | Token in `Authorization`, never from a URL or cookie                                        | `?access_token=` and cookies alone are 401                                                                                                                                                                 |
| A8  | MUST     | Only tokens issued for the service                                                          | A token for the MCP audience is 401 on the API                                                                                                                                                             |
| A9  | MUST     | A narrower token bounds what it can do on a broader Session                                 | Effective scopes `token ∩ Session` on REST, MCP and the Company Agent                                                                                                                                      |
| A10 | MUST     | 429 with `Retry-After` on any endpoint                                                      | Transport limit on REST and conversations; the message limit adds `RateLimit` headers                                                                                                                      |
| A11 | MUST     | `poppy:read` and `poppy:write` are independent                                              | Each alone is refused the other's operations on REST and MCP                                                                                                                                               |
| A12 | MUST     | A signed-out token for a signed-in Session is handled signed out                            | Account action is 403 `sign_in_required`                                                                                                                                                                   |
| A13 | SHOULD   | The Session is checked on each request                                                      | After sign-out a write is 403 `sign_in_required` at once                                                                                                                                                   |
| A14 | MUST     | `invalid_token` 401, `sign_in_required` 403, `insufficient_scope` 403 naming account scopes | Missing, expired and malformed tokens are 401 in the DPoP scheme; the scope names `poppy:write`                                                                                                            |

## Conversations (spec 7)

| Row     | Level       | Requirement                                                                                           | How it is met                                                                                                                                                |
| ------- | ----------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| C1      | MUST        | Paths and methods under the endpoint                                                                  | Each answers its status; other methods are refused with a JSON error                                                                                         |
| C2      | MUST        | Other client, User or account, and unknown IDs, give the same error                                   | Byte-identical `conversation_not_found`, also for the same User signed in to another account                                                                 |
| C3, C4  | MUST/SHOULD | IDs are 1 to 256 of `[A-Za-z0-9_-]`, with a kind prefix                                               | 400 for bad message IDs; `cnv_`, `evt_`, `msg_`                                                                                                              |
| C5      | MUST        | 201 on start, 202 on later messages, with all three fields                                            | Also for a Direct Conversation                                                                                                                               |
| C6      | MUST        | A retried message returns the original response                                                       | [Message retries](/architecture/request-flows/poppy-agents#message-retries); a retry gets the stored response even after its cursor was pruned               |
| C7      | MUST        | `wait` and `cursor` on either `POST`                                                                  | A read response with the 201 or 202 status                                                                                                                   |
| C8-C11  | MUST        | Message and event fields                                                                              | 400 with `invalid_request` and `issues`; context merges; Company messages carry no context                                                                   |
| C12-C14 | MUST/MAY    | Reads carry `cursor`, `has_more`, `status`, `responder`; `invalid_cursor`, `cursor_expired`           | Also for streams; `wait` is capped at 30 seconds                                                                                                             |
| C15-C18 | MAY/MUST    | Server-sent events (oRPC's native event iterator), `Last-Event-ID`, closing a stream, no `text-delta` | A WHATWG parser sees `id` and the default event type only; a reader that loses access is closed with no `event: error`, and its reconnect gets the challenge |
| C19-C21 | MUST        | `status` and `responder`, `sender`, handoff                                                           | Handoff is `queued` then `idle`, with Fern explaining                                                                                                        |
| C22-C24 | MUST        | Direct Conversations                                                                                  | Errors, `direct_opened` and `direct_closed`, cascade on close                                                                                                |
| C25-C27 | MUST        | Sign-in during a conversation; token switches                                                         | Authorization events; every request makes its token Fern's; per-reader streams                                                                               |
| C28     | MUST        | Closing                                                                                               | 200, a `closed` state event, events stay readable                                                                                                            |
| C29     | MUST        | Non-token errors are JSON with an `error` code                                                        | The seven codes with their statuses                                                                                                                          |
| X1      | MUST NOT    | No operations unless the extension is listed                                                          | No response schema has an `operation` property                                                                                                               |

## MCP (spec 6)

| Row | Level  | Requirement                                                            | How it is met                                                                                                     |
| --- | ------ | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| M1  | MUST   | Streamable HTTP, 2025-06-18 or later                                   | `initialize` negotiates it                                                                                        |
| M2  | MUST   | Protected resource metadata names `auth.issuer`                        | `POPPY_AUTH_ISSUER`, checked against the Auth0 proxy issuer when the stack is composed, next to the tenant issuer |
| M3  | MUST   | An ordinary MCP client can sign in with MCP's own authorization flow   | Two token families: standard OAuth tokens (DCR, CIMD) and Poppy Session tokens                                    |
| M4  | MUST   | Session Tokens are accepted as Bearer tokens and their scopes enforced | Signed in and signed out                                                                                          |
| M5  | MUST   | `insufficient_scope` names the account scopes                          | For a Poppy token `scope="poppy:write"`, never `orders:write`; a standard token is told the OAuth scope           |
| M6  | MUST   | `sign_in_required` and `insufficient_scope` are Bearer 403s            | With the resource metadata URL                                                                                    |
| M7  | MUST   | Only tokens issued for the server; Bearer only                         | A token with `cnf.jkt` or `act.poppy_jkt` is refused                                                              |
| M8  | SHOULD | Don't forward a Session Token to another internal service              | Not met: `Poppy-Subject` carries it to the Shop API, which verifies it again                                      |

The canonical MCP server accepts two kinds of token:

- **Standard MCP OAuth.** Any client registered by Dynamic Client Registration or a Client ID Metadata Document gets a
  plain Auth0 token for the MCP resource, with no Poppy Session. The server treats it as a delegated caller: On-Behalf-Of
  exchange for the API, the account cart, agent scopes only. A refusal names OAuth scopes.
- **Poppy.** A Session-backed token follows the Poppy rules: effective scopes are the token's scopes intersected with
  the live Session's, the Session is introspected, and a refusal names account scopes.

The Poppy rules apply only to Poppy tokens. The protected resource metadata lists `auth.issuer` and the tenant issuer
(once, when they are the same login domain).
