---
title: Poppy agents
description: Direct credentials, live Auth0 proxy admission, and dedicated web agent forwarders.
sidebar:
  order: 5
---

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.

:::note[What local tests cannot show]
Locally, the API's fake issuer runs the shipped Custom Token Exchange Action in place of Auth0. CTE audiences, the
custom actor claims and CTE-to-OBO exchange still need checking against a real tenant.
:::

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](/poppy).
Independent invited agents obtain their delegated browser session through a
[separate approval and sign-in flow](/architecture/auth0/invited-agents).

## Signed-in REST

```mermaid
sequenceDiagram
  participant A as Personal agent
  participant G as Proxy Auth0
  participant T as Auth0
  participant S as Shop API
  A->>G: Account Token and API resource
  G->>T: CTE with resource, grant and key binding
  T-->>A: Auth0 token via Auth0 proxy
  A->>S: DPoP token and public-URL proof
  S->>G: Read-only session introspection
  S->>S: Check account, grant, lifecycle and permissions
  S-->>A: Operation result
```

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

```mermaid
sequenceDiagram
  participant A as Personal agent
  participant G as Proxy Auth0
  participant S as Shop API
  A->>G: Session assertion and API resource
  G-->>A: poppy-session+jwt with cnf.jkt
  A->>S: DPoP token and proof
  S->>G: Session introspection
  S-->>A: Catalog or session-cart result
```

**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

```mermaid
sequenceDiagram
  participant A as Personal agent
  participant M as MCP server
  participant G as Proxy Auth0
  participant S as Shop API
  A->>M: Canonical MCP resource token
  M->>G: Session introspection
  M->>S: OBO token or narrow MCP agent forwarder, with Poppy-Subject
  S->>G: Verify original session live
  S-->>A: Tool result via 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

```mermaid
sequenceDiagram
  participant B as Agent browser
  participant W as Webshop
  participant S as Shop API
  participant G as Proxy Auth0
  B->>W: Original browser cookie or delegated website session
  W->>S: web_agent_forwarder bearer and one original artifact
  alt Poppy browser
    S->>G: Browser-cookie introspection
  else Invited agent
    S->>S: Verify bound context and durable admission row
  end
  S-->>B: Result via webshop
```

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:

```sh
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.
