Skip to content
Tarn & Timber Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Poppy compliance

Every Personal Agent Protocol requirement the shop meets, and the test that proves it.

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 is the normative text. Start with the Poppy overview 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; 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).

Was this page helpful?