koh / sylius-poppy
Personal Agent Protocol (Poppy) for Sylius: poppy.json, a per-channel issuer, customer Direct and Device Sign-In, browser sessions, DPoP Session Tokens on the Shop API, and agent feedback.
Requires
- php: ^8.3
- doctrine/dbal: ^3.10 || ^4.0
- doctrine/migrations: ^3.9.8
- koh/poppy: ^1.1
- koh/sylius-agent-contracts: ^1.1
- nyholm/psr7: ^1.8.2
- psr/cache: ^2.0 || ^3.0
- psr/clock: ^1.0.0
- psr/http-client: ^1.0.3
- psr/http-message: ^1.1 || ^2.0
- sylius/sylius: ^2.2.9 || ^2.3.0
- symfony/clock: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/http-client: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/psr-http-message-bridge: ^7.4 || ^8.0
- symfony/rate-limiter: ^7.4 || ^8.1
- symfony/routing: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
- symfony/security-core: ^7.4 || ^8.0
- symfony/security-csrf: ^7.4 || ^8.0
- symfony/security-http: ^7.4 || ^8.0
- twig/twig: ^3.30.0
Requires (Dev)
- koh/dev-sylius: ^1.0
- phpstan/extension-installer: ^1.4.3
- phpstan/phpstan: ^2.3.4
- phpunit/phpunit: ^11.5.57
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
koh/sylius-poppy
The Personal Agent Protocol
(Poppy, draft 0.2) for a Sylius shop. A customer's own AI agent finds the shop
through /.well-known/poppy.json, starts a Session, browses and uses the Shop
API in it, and — when a task needs the customer's account — asks the customer
to sign in and approve on the shop's own page.
Built on koh/poppy, which does the protocol; this bundle provides the Sylius side: routes, the customer pages, the security integration, storage in the shop database, and an admin list of agent feedback. Part of koh/sylius-agent-ready.
What a shop gets
| Poppy 0.2 | Here |
|---|---|
| Discovery (3) | /.well-known/poppy.json, issuer metadata at /.well-known/oauth-authorization-server/poppy with poppy_domains |
| Sessions (4.2) | POST /poppy/token, signed-out Sessions for any Personal Agent |
| Direct Sign-In (4.5) | /poppy/authorize — the shop's login, then a consent page |
| Device Sign-In (4.6) | /poppy/device — the customer enters the agent's code |
| Sign-out (4.9) | POST /poppy/revoke, and Connected agents in the customer account |
| Web browsing (5) | POST /poppy/browser-session; the agent's browser is signed in and out with its Session |
| APIs (6) | DPoP Session Tokens on the Shop API (/api/v2/shop), opt-in |
| Feedback (8) | POST /poppy/feedback, listed in the admin under Agent feedback |
Mediated Sign-In (the agent sends the customer's password) and conversations with a Company Agent are not offered, and therefore not declared.
Each channel is its own Company by default: issuer
https://{channel hostname}/poppy, poppy_domains that one host. Channels
that share one customer base can be one Company — set issuer and domains.
What a Personal Agent can do, and as whom. Signed out, it is a guest: it
browses, fills a guest cart and checks out like any visitor. Signed in, it is
the customer, within what the customer approved: poppy:read to see orders,
addresses and account data, poppy:write to order and change things. The
customer sees every connected agent in their account and can disconnect it;
its access ends on the next request.
Install
composer require koh/sylius-poppy bin/console doctrine:migrations:migrate
config/bundles.php:
Koh\SyliusPoppy\KohSyliusPoppyBundle::class => ['all' => true],
config/routes/koh_agent.yaml (before api_platform.yaml):
koh_poppy: resource: '@KohSyliusPoppyBundle/config/routes.php'
Security configuration — installing is not enough
Three changes in config/packages/security.yaml. Without them the endpoints
answer, but start sessions and set cookies (the first) or do not see
Personal Agents at all (the other two).
1. The machine endpoints keep no session. Above the Sylius firewalls:
security: firewalls: koh_poppy_endpoints: pattern: '^/poppy/(token|revoke|device_authorization|feedback|browser-session)$' security: false
/.well-known/poppy.json is already covered by the agent-ready firewall
(^/(\.well-known/(?!oauth)…). The issuer metadata under
/.well-known/oauth-authorization-server/poppy is public through
access_control like the MCP OAuth metadata.
2. The agent's browser, on the shop firewall:
shop: # … the existing configuration custom_authenticators: - Koh\SyliusPoppy\Security\BrowserSessionAuthenticator
3. Session Tokens on the Shop API, if shop_api.enabled is on:
api_shop: # … the existing configuration custom_authenticators: - Koh\SyliusPoppy\Security\ShopApiAuthenticator
The JWT login keeps the Bearer scheme; this authenticator only looks at
DPoP.
Purge
Expired Sessions, tokens and one-time ids are removed by
bin/console koh:poppy:purge
Run it daily. Nothing depends on it for correctness; without it the tables only grow.
Configuration
# config/packages/koh_poppy.yaml — the defaults koh_poppy: enabled: true # serve poppy.json at all server: true # false: declare the website alone (web: {}) organization_name: ~ # default: the channel name issuer: ~ # default: https://{channel hostname}/poppy, one per channel domains: [] # with a fixed issuer: every domain that names it sign_in: direct: ['poppy:read', 'poppy:write'] # [] turns Direct Sign-In off device: ['poppy:read'] # [] turns Device Sign-In off browser_session: true feedback: true shop_api: enabled: false prefix: /api/v2/shop openapi_path: /api/v2/docs.jsonopenapi session_token_ttl: 3600 session_lifetime: 2592000 account_token_ttl: 7776000 # 0: until the customer disconnects clients: allow: [] # non-empty: only these agents (client_id URL or host) block: [] dpop_nonces: false rate_limits: # per client_id; device_page per client address token: { limit: 60, interval: '1 minute' } revoke: { limit: 30, interval: '1 minute' } device: { limit: 10, interval: '1 minute' } feedback: { limit: 20, interval: '1 hour' } device_page: { limit: 10, interval: '10 minutes' } table_prefix: koh_poppy_
Never change an issuer that agents already use: their User IDs and Account Tokens are bound to it.
Templates and translations
The pages extend @SyliusShop/shared/layout/base.html.twig through
@KohSyliusPoppy/shop/_layout.html.twig; override that one file at
templates/bundles/KohSyliusPoppyBundle/shop/_layout.html.twig to match a
theme. Translations ship in German and English under koh_poppy.*.
Security notes
- Consent pages cannot be framed and are never cached; every decision is a POST with a CSRF token. Opening a device link approves nothing.
- The agent's name and logo on the consent page are the agent's own claims; the page always shows the host its metadata is published on next to them.
- The authorization request is kept server-side while the customer signs in. The redirect URI never travels through the customer's browser.
- Agent metadata is fetched over HTTPS only, through Symfony's
NoPrivateNetworkHttpClient, without redirects and with short timeouts. - The browser cookie carries the Session id with an HMAC under the kernel
secret, is
Secure,HttpOnly,SameSite=Lax, and expires with the Session.
Licence
MIT. See LICENSE.