Ask Porter: a partner-scoped Vernal co-pilot on Kitchen POS
Channel partners got a portal, a storefront, and recurring pickup windows in June. The next question affiliates ask is not philosophical — it is operational: "How do I open my window?" "What is my customer link?" "Did anyone order yet?"
Those questions are not crimes. They are the sound of a program working. They are also the kind of questions that do not scale if every answer routes through Mark's phone.
Ask Porter is our answer on Kitchen POS: a Vernal co-pilot embedded in the partner portal as an orange chat bubble — scoped to one affiliate at a time, with tools that can read their menu, windows, and orders, and explicitly cannot touch operator god-mode APIs. Phase 1b is live on dev (dev.kitchen-pos.decisionsciencecorp.com) as of June 2026. Production partner Porter is not enabled until we complete a launch slice; this post describes what we built and why, not a general availability announcement.
Why partner scope is the whole design
Tasks has Ask Q — operator-facing Vernal on Sanctum Tasks admin. Kitchen POS has Rico and operator kitchen_pos__* SMCP tools for menu mutation, secret codes, and provisioning. Partners sit between those worlds: they sell Empanada Empire pickup under their slug, but they must never receive keys that can rewrite the global menu or peek at another affiliate's orders.
Porter's job rules state the boundary plainly: one partner at a time, whoever is logged in when the bubble opens. Porter gets four partner tools — me, menu, windows, orders — and is locked out of operator routes. Commit 295f359 formalized that lock after early prototypes proved how tempting it is to give a helpful bot "just one more" admin capability.
That is not stinginess. It is how you ship a co-pilot on a live ghost kitchen without turning every affiliate chat into a lateral movement exercise.
What the affiliate sees
After partner login on the Sanctum-styled portal (/partner/*), a floating Ask Porter bubble appears when channel_partner_enabled is on. The widget is not a generic ChatGPT tab — it is wired to the same session cookie as the portal, bootstrapped via partner_context on the partner-bridge API, and polled for replies the same way we hardened Ask Q for all-day use.
The affiliate types plain language. Porter answers with portal context: open window steps, customer URL hints, recent order summaries, menu items available at their prices. If they ask for operator-only work — change global inventory, provision a new partner, edit nginx — Porter says that is outside Ask Porter and names the right lane (operator or Mark).
First message from a new partner account gets a short orientation (Layer A in job rules). Subsequent messages carry page context (Layer B): which portal surface they are on, open window id if any, partner slug and display name. The model is not guessing who they are; the bridge injects identity before the turn ships to moya.
A Tuesday night at the church group
Picture an affiliate pastor who agreed to run a Saturday pickup window for Empanada Empire. Tuesday at 9:14 p.m. someone in the group chat asks whether the link is the same as last month and whether they need to open the window again. That message is not malice — it is a person trying to serve food on time.
Before Porter, that question becomes a text to Mark, a context switch, and a manual lookup in the partner admin. After Porter (on dev today), the affiliate opens the portal on their phone, taps the orange bubble, and asks in plain language. Porter answers with their slug, their next window state, and the customer URL pattern — because the bridge resolved identity from the same session cookie that gates /partner/orders.php.
We are not claiming Porter replaces relationship management. We are claiming it removes the boring lookups that erode goodwill when the human operator is asleep.
Ask Q taught us; Porter inherits the homework
Ask Q hardening was operator-facing on Tasks: poll loops, 429 backoff, session keys, Broca inbox/outbox, SQLite queue beside the app DB. Porter reuses that shape on Kitchen POS:
| Layer | Ask Q (Tasks) | Ask Porter (Kitchen POS) |
|---|---|---|
| Widget | Q bubble on admin | Porter bubble on /partner/* |
| Bridge | q-bridge PHP API |
partner-bridge PHP API |
| Broca screen | q-vernal path | broca-porter on moya |
| Letta agent | Q Vernal | Porter_Vernal |
| SMCP profile | Tasks tools | Four kitchen_pos_partner__* tools |
| Identity | Operator session | Partner session → kppb_… key |
The difference is not plumbing — it is ACL. Q may attach provisioning and IAM tools because the logged-in human is staff. Porter must never inherit that surface. Commit 295f359 locked Porter to partner tools only after we caught how easy it is to "temporarily" attach kitchen_pos__menu for demos.
Session identity end to end
Partners authenticate like any Kitchen POS affiliate user. When the bubble loads, _ask_porter.php injects JSON config: bridge base URL, partner slug as chatterUsername, page context from partner_bridge_detect_partner_page_context(), and a greeting that sets expectations ("window, menu, partner orders").
When the affiliate sends a message:
- Widget POSTs to
partner-bridge/.../messageswith session auth. - Row lands in
partner_bridge_webchat.dbinbox. - Broca on moya polls
inboxwithPARTNER_BRIDGE_POLL_API_KEY. - Broca writes
current_partner_user_id.txtand callsresolve_partner_keyso SMCP never sees raw secrets. - Porter_Vernal may call
kitchen_pos_partner__me,__menu,__windows,__orders. - Assistant text returns via
outbox→ widgetresponsespoll.
If step 4 fails — wrong user id, revoked partner, feature flag off — Porter should say so plainly. Silent failure is how affiliates decide the program is flaky.
What affiliates can ask (and what they cannot)
In scope for Phase 1b:
- "What is my customer link?" → slug-based storefront URL on
empanadaempire.us. - "When is my window?" → read windows API for their partner id.
- "Any orders yet?" → summarized list from partner orders endpoint.
- "What's on my menu?" → partner-scoped menu read.
- "How do I open a window?" → procedural guidance pointing at portal forms (read-first; mutations still via UI).
Out of scope — by design:
- Change global menu pricing for all locations.
- Provision a new partner or reset another affiliate's password.
- Operator Square sync controls, secret codes, or admin-only reports.
- Tasks, CRM, or unrelated Sanctum products.
Job rules (PORTER-VERNAL-JOB-RULES.md) encode this in Letta blocks: Layer A orientation on first turn, Layer B page context thereafter, explicit handoff language when someone asks for operator work.
Dev-only is a feature flag, not a footnote
Phase 1b runs on dev.kitchen-pos.decisionsciencecorp.com with moya broca-porter pointed at the dev bridge URL. Production partner portal does not expose Porter until we complete a launch slice: prod feature flag, rotated poll bearer, Letta tool attach verified on the prod agent id, rate limits tuned for partner poll cadence, Playwright smoke on a test affiliate.
We say this loudly because affiliates talk to each other. "I saw a chat bubble on dev" must not become "DSC said Porter is live everywhere."
Failure modes we already rehearsed
- 429 on widget poll — same lesson as Ask Q: visible retry/backoff, no infinite spinner.
- Broca screen down — inbox grows; widget shows stall; ops restarts
broca-porteron moya. - Tool calls with wrong partner scope — treated as sev-1; SMCP resolver is the control, not prompt politeness.
- Model asks for operator credentials — job rules forbid it; bridge never supplies operator keys to the partner profile.
Playwright on dev (tools/playwright_ask_porter_dev_e2e.py) checks bubble visibility, sends a probe string, asserts a non-empty bot reply. Manual smoke uses affiliate alvin per runbook when we need eyes on real copy.
How we will know it is working
Empire field metrics for Porter are operational, not vanity:
- Fewer after-hours texts to Mark about window URLs and order status.
- Time-to-first-window-open for new affiliates (self-serve vs hand-holding).
- Tool call success rate scoped to the logged-in partner slug.
- Support tickets that cite "portal confusing" vs "Porter wrong answer" — we want the second bucket tiny.
We will not publish a ROI slide. We will publish honest counts in a later chapter if the dev slice graduates to prod.
Under the hood (without duplicating the builder journal)
The pull pattern mirrors Tasks q-bridge:
- Widget POSTs user text to Kitchen POS partner-bridge (
messages). - Broca on moya (
broca-porterscreen) pollsinbox, forwards to Porter_Vernal on Letta. - Porter may call **kitchen_pos_partner_*** SMCP tools; keys resolve per partner user (`kppb…` prefix), never passed by the model.
- Replies land in
outbox; the widget pollsresponses.
SQLite for the bridge queue lives beside the kitchen DB (partner_bridge_webchat.db). Rate limits follow the same discipline we documented for Ask Q — partner session traffic is not operator traffic.
For mechanism detail — Broca plugin layout, poll bearer keys, Letta attach checklist — read the companion builder post on SanctumOS: Porter Vernal partner-bridge: Broca pull plugin and partner SMCP.
How this extends the Empire experiment
Empanada Empire is a field lab. Channel partners are a distribution hypothesis: relationship-driven pickup beside marketplace discovery. Software that makes affiliates self-sufficient is part of the hypothesis — not a nice-to-have.
Porter is the difference between "we gave you a login" and "we gave you a login that answers questions at 10 p.m. when the church group text thread is loud." We measure success in fewer interrupt texts to Mark, faster window opens, and tool calls that stay inside one partner's ACL.
What we explicitly did not ship (Phase 1b boundaries)
| Not in Phase 1b | Why |
|---|---|
| Prod partner portal Porter | Dev host + moya broca-porter target dev URL only until launch slice |
| Window open/close via Porter | Read-first on windows; affiliates still use portal forms for mutations |
| Attachments / bulk / IAM tools | Wrong layer; not partner-scoped |
| Operator menu edits | Rico / operator SMCP lane |
Smoke coverage: Playwright E2E on dev (tools/playwright_ask_porter_dev_e2e.py) — bubble visible, probe message, bot reply. Affiliate alvin used in manual smoke per runbook.
Lessons for product teams embedding Vernal
- Scope the tools before you scope the persona — four honest verbs beat twenty scary ones.
- Reuse bridge semantics — Ask Q hardening paid forward; do not invent a third poll budget story.
- Session identity is not optional — partner portal cookie → bridge → Broca
current_partner_user_id.txt→ key resolver. - Say "dev only" in public — affiliates and operators should not think Porter is prod until it is.
- Pair outcome and mechanism posts — DSC field story plus SanctumOS builder journal keeps audiences straight.
Ask Porter is the affiliate-facing voice of the same Kitchen POS stack that powers Square round-trip and the Sanctum operator shell. The empanadas are real; the partners are real; the bot is allowed to be helpful only within that truth.
Questions about Kitchen POS or partner programs: contact us.