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

Poppy agents

Direct credentials, live Auth0 proxy admission, and dedicated web agent forwarders.

Poppy agents call the same /v1 procedures as every other caller, with Authorization: DPoP and a proof, and discover them through GET /poppy/openapi.json, an OpenAPI 3.1.1 view of the contract filtered to the procedures Poppy callers may use.

The Auth0 proxy issues credentials and answers introspection. Direct resource traffic goes to the API or MCP host. The Auth0 proxy’s poppy.json advertises ${SHOP_API_RESOURCE}/poppy/conversations and the OpenAPI description at ${SHOP_API_RESOURCE}/poppy/openapi.json. The API runs Fern and the hub callbacks (/internal/conversations/{id}/turn), preserves cursors and checks live session and lifecycle authority every thirty seconds on SSE.

For the relationship between a Session, an account grant and its credentials, read the Poppy overview. Independent invited agents obtain their delegated browser session through a separate approval and sign-in flow.

Signed-in REST

CTE uses the shared Auth0 proxy client. The Action allows exactly the company, API and canonical MCP audiences. The API token has act.poppy_sid, act.poppy_gid and act.poppy_jkt. Its sub is the account.

Signed-out REST

Issuer containment: at+jwt verifies only against Auth0 keys; poppy-session+jwt verifies only against Auth0 proxy keys, even when their issuer strings match. Proxy Auth0 tokens must claim signed_in: false and carry no account scopes. They reach only catalog and cart operations. An Auth0 proxy key cannot mint account authority.

MCP

Signed-in subjects are independently verified for the MCP audience and must match the OBO account and Auth0 proxy actor chain. Signed-out subjects require the dedicated mcp_agent_forwarder bearer. There is no shop-domain MCP alias. The MCP transport verifies the Session before exchanging or passing on credentials.

Web agent forwarder

The webshop always uses dedicated agent-forwarder credentials for agent browser calls. The web agent forwarder never uses a customer or storefront token. Invited-agent contexts are bound to the forwarded-agent bearer, last at most 60 seconds, and retain the original immutable delegation snapshot. Every request checks the live connection, identity, admission row, lifecycle and logout cutoff. Read-only agents cannot check out or use human-only routes.

Signed-in channels share account:<sub>; signed-out channels share poppy:<sid>. Writes introspect live. Reads may use a hashed cache entry for at most five seconds. Proxy Auth0 timeouts or invalid responses fail closed with 503.

Errors are the API’s single application/problem+json body with an error member (spec 7.13), and DPoP challenges end with algs="ES256". Proofs bind the configured public API URL, never an internal service-binding URL. Nonces accept the current and previous five-minute windows. Replay protection is shared by every isolate: an accepted jti is a row in the D1 table dpop_proofs, inserted once and deleted by the hourly clean_dpop_proofs job after it expires. A Session may hold 10,000 live proofs; past that, only that Session is refused. The webshop /api/v1 proxy does not forward DPoP: direct Poppy REST clients must use the API origin.

Effective authority

A request acts with the Poppy permissions the token carries that the Session still grants. The token’s scopes are the Auth0 API scopes it was issued with, which map back to poppy:read, poppy:write and checkout; the Session’s come from Auth0 proxy introspection. A poppy:read token on a read/write Session cannot cancel an order: REST answers 403 insufficient_scope naming poppy:write, the MCP cancelOrder tool answers the same, and the Company Agent raises an authorization event. The same intersection applies to the MCP server, where the Session is read before OBO.

A token is read as it was issued. A signed-out token for a Session that has since signed in, and a signed-in token whose Session has since signed out, both act signed out: catalog and cart work on the Session cart, and an account action answers 403 sign_in_required. A Session that ended is invalid_token.

Every token failure is challenged in one scheme chosen before the token is verified: DPoP for Session Tokens, the DPoP scheme and the Poppy paths, Bearer otherwise. A missing token is invalid_token. A Bearer Session Token is not a credential outside MCP. A request over its Session or client budget gets 429 with a numeric Retry-After, on conversations too; the domain message limit adds RateLimit and RateLimit-Policy.

Conversation streams

GET /poppy/conversations/{id}/events is one URL for a JSON page or, with Accept: text/event-stream, a stream of oRPC’s native event iterator. Ownership, cursor and authority errors are thrown before the first byte, so they are ordinary problems with status codes, never inside a 200 stream. Each event is written as id: evt_... and data: {...} with the default event type; Last-Event-ID replaces cursor on reconnect, and a comment keeps the connection open every 15 seconds. The stream ends by returning (oRPC writes a data-less event: close that an SSE parser does not dispatch) when the client leaves, the conversation closes, the stream is ten minutes old or the reader lost access. A reader that lost access gets no event: error (the spec’s event set has none): its reconnect receives the normal 401 or 403 challenge. text-delta events are not sent. Request bodies on conversation writes are refused above 64 KiB.

Conversation authority

Every authenticated conversation request (start with a parent, send, JSON read, SSE connect, handoff and close) makes its token the one the Company Agent acts with, so an agent that switches to a narrower token narrows what Fern may do next. An open stream stays bound to the token that opened it. Its thirty-second liveness check introspects that token’s Session, it never changes Fern’s authority, and a newer, narrower token elsewhere does not close it.

Message retries

The first response to a message ID is stored with the ID (response_status and response_body on conversation_message_ids, written once) and a retry with the same content returns it unchanged. That holds after Fern has replied, after the conversation closed, after a restart, from another Session of the same User and client, and for a wait read, which returns the events, cursor and has_more as first sent, even when events were pruned since and the cursor would now be refused with 410. Replay records live exactly as long as their conversation. A POST read response carries at most 256 KiB of events; the rest follows with has_more: true. A retry is checked for ownership and sign-in again, so a signed-out token never gets a replay, and it does not count against the rate limits. A retry whose first attempt stored nothing builds the response, stores it, and every later attempt agrees. A handoff request answers 202 queued and adds the queued and idle state events with Fern’s explanation, since nobody from the team can join.

Local verification

The webshop suite e2e/config/poppy.ts runs the poppy-company, poppy-direct and conversations-api specs. Its launcher starts the storefront and the Shop API in one process with the API’s local issuer, then the Auth0 proxy and the MCP server, with SHOP_E2E_POPPY_DIRECT=1, one ephemeral introspection secret and the direct resources advertised. Local Worker and agent metadata URLs allow loopback HTTP explicitly; deployed resources keep their HTTPS requirements. The local issuer verifies Auth0 proxy subject tokens through the shipped Action and keeps the original personal-agent actor below the exchanging MCP and Auth0 proxy clients. The company scenario uses separate API and conversation credentials because each resource has its own audience.

Run from the repository root:

E2E_SUITE=poppy pnpm --filter @retail-demo/webshop exec e2e run
E2E_SUITE=shop pnpm --filter @retail-demo/webshop exec e2e run
pnpm --filter @retail-demo/proxy-auth0 exec e2e run

The root e2e.config.ts selects the suite while keeping paths relative to the webshop. The Poppy suite is also the last step of the webshop’s pnpm --filter @retail-demo/webshop e2e chain. With SHOP_E2E_POPPY_CONTRACT=1, poppy-direct.e2e.ts also checks that the Auth0 proxy answers 404 for the retired /api/poppy/v1/* and /mcp paths.

Was this page helpful?