Kitchen POS Ask Porter is a partner-scoped Vernal webchat on the affiliate portal. This post is the builder journal: how we wired partner-bridge on the PHP host to Broca on moya to Porter_Vernal on Letta, with a four-tool SMCP profile that cannot see operator APIs.
The outcome story β why affiliates need a co-pilot, what Phase 1b includes, prod boundaries β lives on the DSC blog: Ask Porter: a partner-scoped Vernal co-pilot on Kitchen POS. Read that for field context; read this for transport and tool governance.
Design constraint: copy Ask Q, not invent Broca semantics
Tasks q-bridge already solved poll-based webchat against a PHP app with rate limits, session keys, and Broca inbox/outbox. Porter's bridge (public/partner-bridge/api/v1/index.php) deliberately mirrors those actions:
| Action | Role |
|---|---|
partner_context / partner_session |
Widget bootstrap + canonical session id |
messages |
User β Porter inbox |
inbox / outbox |
Broca poll dequeue / enqueue |
responses |
Widget poll for assistant text |
resolve_partner_key |
Broca fetches kppb_β¦ key for current partner user |
history |
Recent turns for UI |
config |
Health metadata for ops |
Poll routes authenticate with PARTNER_BRIDGE_POLL_API_KEY (env or partner_bridge_poll_api_key config). Widget routes authenticate with the partner portal session β same cookie as /partner/.
Queue storage: {DB_PARENT}/partner_bridge_webchat.db (SQLite beside kitchen DB). Idempotency and processed flags follow the same mental model as q-bridge: Broca owns sequencing; PHP owns persistence.
Phase 1b ops: broca-porter on moya polls dev Kitchen POS only. Do not point prod bridge URLs at moya until launch slice is approved.
Broca plugin: porter_vernal_webchat
Repo: broca/plugins/porter_vernal_webchat/ (synced to moya via kitchen-pos/tools/moya_setup_porter_broca.sh).
The plugin is a thin poll loop:
PorterWebChatAPIClienthits partner-bridgeinboxwith bearer token.- New rows become Letta messages for Porter_Vernal (
agent-b871ebbf-6185-4899-8881-8efadc9224eaon moya). - Assistant output posts to
outbox; widget clients pick it up viaresponses.
Settings load from env (PorterWebChatSettings.from_env()): base URL, poll interval, platform name. Screen name on moya: broca-porter.
This is the same architectural slot as other webchat plugins β platform adapter in Broca, intelligence in Letta, contract in PHP. The novelty is partner ACL, not the loop shape.
Partner SMCP: four tools, server-side key resolution
Plugin tree: kitchen-pos/smcp_plugin/kitchen_pos_partner/ + porter_vernal_partner/resolve_key.py.
| Tool | API |
|---|---|
kitchen_pos_partner__me |
GET /api/partner/me.php |
kitchen_pos_partner__menu |
GET /api/partner/menu.php |
kitchen_pos_partner__windows |
GET/POST /api/partner/windows.php (Phase 1b read-first emphasis) |
kitchen_pos_partner__orders |
GET /api/partner/orders.php |
Explicit exclusions: operator kitchen_pos__, Tasks q_vernal_tasks__*, provision routes, attachments, bulk, other partners' slugs.
Key resolution path:
- Broca writes
current_partner_user_id.txtunder/opt/broca-porter/run/when dequeuing a message. resolve_key.phpmaps user id βkppb_β¦partner bridge key.- SMCP stdio server (
run-smcp-stdio-for-letta.shon moya) never exposes the key to the model β tools call Kitchen POS with server-injected credentials.
Letta attach checklist (docs/PORTER-VERNAL-TOOL-PROFILE.md):
- MCP server:
kitchen-pos-partner-smcp - Attach only the four tools above
- Verify with
GET /v1/agents/{porter_id}/tools - Smoke: dev partner sends Ask Porter message β tool call succeeds for their slug only
Persona + job_rules blocks follow the Q pattern (PORTER-VERNAL-JOB-RULES.md, moya_setup_porter_agent_profile.py). Porter introduces himself as Ask Porter in the UI; Letta name remains Porter_Vernal.
Widget front-end
public/partner-bridge/widget/ β porter-chat-boot.js, porter-chat-widget.js, included from public/partner/includes/_ask_porter.php on logged-in partner pages.
Bootstrap flow:
- Load config JSON from PHP (bridge base, feature flags).
partner_contextβ session + display metadata.- Poll
responseswith backoff on 429 (same lesson as Ask Q hardening β visible retry, no silent stall).
CSP requires external boot script (porter-chat-boot.js) so script-src 'self' stays satisfied.
E2E: tools/playwright_ask_porter_dev_e2e.py on dev.kitchen-pos.decisionsciencecorp.com β bubble visible, probe string, non-empty bot reply.
Commits and repos (June 2026)
| Area | Ref |
|---|---|
| Partner bridge foundation | 06ee055, f35bda3 |
| Ask Porter widget + SMCP wrapper | 9252f53 |
| Porter tool lock (no operator tools) | 295f359 |
| Letta profile alignment | 70c8571, 0335934 |
| Dev chat E2E | 0335934 |
| Broca plugin | broca repo 8d6b524 (poll plugin) |
| SMCP partner profile | smcp 6ad1f6c, 25632da |
Cross-link DSC channel partners chapter β Porter without partners is a demo; partners without self-service answers is a support ticket factory.
Launch checklist (when Mark clears prod)
channel_partner_enabled+ Porter feature flag on prod host (explicit slice β not cron accident).- moya
broca-porterenv β prod bridge URL + rotated poll bearer. - Letta tool attach verified on Porter_Vernal prod agent id.
- Rate limits tuned for partner session poll cadence (reuse q-bridge runbook math).
- Playwright smoke against prod partner test account β read-only unless Mark authorizes mutations.
Until then: dev only, documented honestly in both blogs.
Why this belongs on SanctumOS
SanctumOS is where we publish how Sanctum building blocks compose β Broca, Letta, SMCP, PHP bridge apps on multihost. Porter is a reference implementation of narrow tool profiles (see also SMCP tool governance): expose a wide API on the server, attach a thin slice to the agent, resolve credentials out of band.
If you are wiring your own product portal to Vernal, steal the pattern before you steal the code: bridge queue in the app, Broca poll plugin, SMCP profile per persona, no operator tools on affiliate agents.
Questions or PRs: SanctumOS contributing Β· Kitchen POS docs/partner-bridge.md.