Agora

Poppy connections (your agent at other companies)

Let your agent act on OTHER Personal Agent Protocol ("Poppy") companies for its users — agent identity, per-user connections sealed at rest, connect links that resume the conversation, company tools and agent behind your confirmation.

Experimental — tracks a draft spec

This implements the client side of the Personal Agent Protocol (Poppy) Draft 0.1. The spec is still in development; this surface will change, possibly in breaking ways and outside semver majors, as it evolves.

Poppy lets personal agents talk to your app. This is the other direction: your app's agent becomes a personal agent for its users at other Poppy companies — it discovers them, connects each user's account there, reads through their MCP tools and their own agent, and acts — always behind your app's confirmation. It is built on the framework-agnostic @adonis-agora/agent/poppy-client.

Mounting it

adonisrc.ts
providers: [
  () => import('@adonis-agora/agent/agent_provider'),
  () => import('@adonis-agora/agent/poppy_connections_provider'),
]
config/poppy_connections.ts
import env from '#start/env'
import { definePoppyConnectionsConfig } from '@adonis-agora/agent/poppy-connections'

export default definePoppyConnectionsConfig({
  baseUrl: env.get('APP_URL'),                 // HTTPS: the agent identity lives under it
  identity: {
    name: 'Acme Assistant',                    // what companies show on their consent screen
    keys: env.get('POPPY_AGENT_KEYS'),         // JSON array of private EC P-256 JWKs
  },
  // Default: NO company. The app decides where its agent may go.
  companies: {
    'shop.example': { label: 'Shop', readScopes: ['poppy:read', 'orders:read'] },
  },
})

Generate a key with generatePoppyAgentKey() (from @adonis-agora/agent/poppy-connections) and keep the JSON in an env var. Several keys rotate: the first signs, all are published.

FieldDefault
baseUrl—Your public origin. client_id is {baseUrl}/{path}/agent.json.
path'poppy-agent'Identity (agent.json, jwks.json), connect pages, oauth/callback.
identity—name, logoUri (default: a served {path}/logo.svg with the name's initial), keys.
companiesnoneA list, a map to { label, scopes, readScopes }, or a predicate.
owneractor.tenantRef ?? actor.idWhose connection a turn uses.
store'lucid''memory', redisPoppyConnectionStore(redis), or your own; connection, autoCreateTables.
secretapp keySeals connections at rest (AES-256-GCM).
insecureDevfalseDevelopment only, not spec-compliant: http companies and identity. Refused in production.
tools{ prefix: 'poppy_' }prefix, roles, ability, connect ('action' card that resumes, or 'link'), writes (false = build your own confirmed writes), register (false = you call registerPoppyConnectionTools on your own registry), twoStep (see below).
textsEnglishPages and confirmation cards.
renderbuilt-in HTML(view, texts) => string — give the connect pages your look.
api—{ actor(ctx) } mounts GET/DELETE {path}/api/connections[/:domain] for your settings UI.

Add the tools to an agent (tools: ['poppy_companies', 'poppy_connect', …]) — they are registered in the same ToolRegistry, so your RolesPolicy, personas and allow-lists apply. Personal agents never reach them (they carry no personal_agent role).

Connections

One connection per owner (person/account) and company: the company's issuer, the granted scopes, and the client state — the per-company User ID, the DPoP key, the Account Token (a refresh token) and Session Tokens — sealed with the app key. Sessions, DPoP proofs and nonces, renewals, narrowing and Account Token rotation are handled by the client; calls of one owner are serialized so a rotated token is never lost. PoppyConnections (container binding) gives your code list(owner) and disconnect(owner, domain); a disconnect revokes the Account Token at the company (Poppy §4.9), and a revocation on their side is noticed (invalid_grant) and marks the connection revoked.

poppy_connect is an action: its confirmation card carries a link to a page on your app (/{path}/connect/:id) — a link works in the web chat and in WhatsApp alike. There the person picks:

  • Sign in at the company (Direct, Poppy §4.5): PKCE, state and the RFC 9207 iss check, back to your callback;
  • a form on your page (Mediated, Poppy §4.7) when the company offers it: the fields the company declares, posted from the person's browser to your server and straight to the company — never stored, logged or shown to the model; a one-time code on the next page;
  • a code (Device, Poppy §4.6) when Direct is not offered.

Completing it decides the parked connect call (a proposal decision under actionApprovalMode: 'independent', an approval otherwise): the turn resumes with the account connected and does what was asked. The link is a 128-bit capability, keyed to that call with your secret, expiring (connectTtlSeconds, 15 min) and single-purpose. A call whose account is already connected with the scopes asked completes at once, without a card.

A loop of your own

An app that runs runAgentLoop with its own per-turn registry and its own write confirmation (no AgentService parking) sets tools: { register: false } and calls registerPoppyConnectionTools(registry, connections, { connect: 'link', writes: false }) in its registry: connect then returns the link as a read tool, and the app builds its confirmed writes on callCompanyTool(connections, owner, input, false) and talkToCompany(connections, owner, input, true).

Tools

ToolKind
poppy_companiesreadAllowed companies and the person's connection at each.
poppy_connectactionThe connect card (see above); scopes for a step-up.
poppy_toolsreadThe company's MCP tools, with readOnly.
poppy_readreadRuns a tool the company marks readOnlyHint; anything else is refused.
poppy_writeactionAny other company tool — your confirmation card shows the tool and its arguments (and the company's own preview, for a two-step tool).
poppy_askreadA question to the company's agent with a token narrowed to readScopes (default poppy:read): a question cannot make the company act.
poppy_requestactionA request for the company's agent to act — the person confirms the message.
poppy_disconnectactionRevokes the connection.

Two-step company tools. Some companies confirm writes themselves: called without confirm, the tool only answers a preview and a confirmToken. poppy_write detects a tool whose input schema has both fields: the card shows the company's preview (its summary/resumo/message), and confirming the card makes the call and commits it with the fresh token — one confirmation, by the person, in your app. tools.twoStep renames the fields ({ confirm, token, summary }) or turns it off (false).

A company answer of insufficient_scope, sign_in_required or an authorization event comes back to the model as needs_permission / not_connected with the next step — poppy_connect with the scopes — never as a dead end. Opening the company's website for the person (Poppy §5 browser sessions) is not offered: the spec requires the assertion to be posted from a browser the agent controls, not the person's.

Independent approvals and text channels

With actionApprovalMode: 'independent', completing the sign-in decides the connect proposal; once it ran and its outcome is in the thread, the provider starts the next turn on the thread in the person's voice (texts.continueAfterConnect), so the model goes on with what was asked. The turn is delivered where the conversation is: the web chat follows it, and a text channel (WhatsApp, Telegram) gets it through channels.follow.

The connect card's confirmation carries link: a text channel sends it as one message with the link in full and "Open the link to continue." (linkInstruction), no buttons nor reply commands — the link is the action. A screen can render it as the card's main button.

A proposal still waiting tells the model plainly that nothing happened yet (PENDING_ACTION_PROPOSAL_NOTE), so it says it is awaiting confirmation rather than done.

The connect card lives as long as its link (connectTtlSeconds, default 15 minutes) — not the approval policy's ttlMs (ToolSpec.approvalTtlMs), and a channel says how long the link works (linkValidity). A sign-in finished after the card expired anyway still stores the connection and continues the conversation.

On this page

Partner

Your product as the official integration for Agent, right here.

Become a partner →