> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Widget flow model: runtime, web-call context, WebMCP guardrails, and trust policy

> How the embedded widget decides what happens next — the conversation directives its flow runtime returns, the bounded screen-pop context a browser voice call carries, the guardrails that gate browser-resident AI agents, and the trust policy that gates their actions. Read this before you wire the widget, bind a voice agent, or relax agent trust.

# Widget flow model

A tenant's embeddable widget is not one surface — it is a family of tenant-owned configurations that share one storage home (the widget configuration's feature flags) and one public session bootstrap. Four of them decide *what happens* for a visitor: the conversation flow, the AI voice button, the sign-up forms, and the trust policy that gates browser-resident AI agents. Two more support them: a bounded web-call context for browser voice, and a guardrail layer those agents must clear before they act. This page explains how the pieces fit together. The end-to-end walkthrough lives in [Web support end to end](/guides/support-widget-to-resolution); the install and voice-button setup walkthrough lives in [Embed a voice or capture widget](/guides/voice-web-widget).

## Widget objects: one storage home, one safe-read pattern

Each widget object is a self-contained JSONB blob on the widget configuration's feature flags — a settings write, not a schema change. They share one deliberate read-side convention: a safe parser returns a fully-valid default (or an empty set) for absent, drifted, or malformed stored values, so a bad row can never break the public widget surface.

| Object             | What it holds                                                                                                                 | Drift posture                                                                                                 |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Conversation flow  | Pre-chat form, after-hours behavior, FAQ deflection answers, routing rules, AI-handoff trigger, proactive engagement triggers | Bad row → treated as absent; the widget falls back to its plain configuration-bootstrapped behavior           |
| Sign-up forms      | Up to 50 published capture forms, each with a consent block, display rules, field→contact mappings, and 1–4 A/B variants      | Bad row → `[]`; individual drifted entries are dropped, they never poison siblings                            |
| Voice-agent button | Bound agent, button label, palette/position, business hours, AI-disclosure and consent copy                                   | Bad row → a fully-valid, disabled-by-default config                                                           |
| Trust policy       | Tenant trusted foreign agent directories with a grant scope (`allow` / `deny` / `rate`)                                       | Bad row → **fail closed**: an empty list, so a malformed entry can't grant trust it wasn't deliberately given |

Every object is version-stamped so future shape changes are explicit, and each names the exact tenant-owned controls below. The flow and the voice button double as consent surfaces (the pre-chat form, the AI disclosure + consent copy); the sign-up form is a consent capture surface; the trust policy is a tenant-owned security gate. Between them they define everything the widget does — and nothing here ever picks a voice or SMS provider.

## The flow runtime — deciding "what happens next"

The no-code conversation flow you publish and the embed runtime reads back is a **pure, side-effect-free evaluator**. It consumes (a) the flow object and (b) a small runtime context the caller computes — business-hours state, pre-chat completion, the visitor's latest message, the current page path — and answers one question: *what should the widget do right now?*

Its evaluation order is deterministic:

* **On first open** — outside business hours → the configured offline behavior (`show_away_message`, `hide_launcher`, or `collect_email`); a mid-conversation returning visitor is *not* re-gated. Otherwise, if the pre-chat form is enabled and not yet completed → `pre_chat_form`; else it opens routing.
* **On each visitor message** — first: explicit routing rules (keyword or URL-path match, first match wins) → keyword-triggered AI handoff → FAQ deflection auto-match (highest keyword score; ties break toward the earlier-authored entry — the builder's priority signal) → `immediate` or `after_deflection_miss` AI handoff → the fallback default (`human_inbox` / `ai_agent` / `faq` / `collect_email`).

**Proactive engagement triggers** are a second, separate evaluation pass the runtime runs on scroll / timer / exit-intent ticks with freshly-measured behavioral context (page time, scroll depth, URL, visit count, cart value). Only enabled triggers that haven't exhausted their frequency cap are considered, ordered by priority with builder-order tie-break; the first trigger whose conditions all hold fires, at most one directive per tick. An unknown (future-version) condition never matches — so a drifted rule degrades to *doesn't fire*, never fires unconditionally.

A rule pointing at a missing FAQ id degrades *safely* to a human — and every directive the runtime returns is inbox / web-chat / AI-agent routing; it never returns or implies an outbound voice or SMS action. The evaluator is clock-free and network-free by design, which keeps the widget's "what happens next" logic fully testable in isolation.

## Web call context — the bounded screen-pop fields

When a visitor starts a browser call, the widget collects the page they're calling *from* (URL, title, referrer) plus an optional bag of host-supplied attributes (plan tier, cart value, account id), and posts it on session bootstrap. That context rides along as the visitor's room-participant metadata so the answering agent — or, on a warm AI→human handoff, the human's screen-pop — sees *where on the site* the call originated and *who* the visitor is, instead of a cold "anonymous web visitor."

Because the context arrives from the public, origin-gated browser surface, it's treated as untrusted at the trust boundary:

* Every field is length-bounded (URLs ≤ 2048 chars, title ≤ 300, attribute map capped at 20 entries of ≤ 64-char keys / ≤ 512-char values) and URLs are scheme-restricted to `http(s)` (`javascript:` / `data:` / `file:` are dropped — the screen-pop surfaces them as a clickable origin hint).
* The whole serialized blob is hard-capped to 6 KB before it's stamped onto a room token — staying well under the media layer's participant-metadata cap.
* The sanitizer **never throws and never blocks the call**: a malformed field is dropped, because a bad attribute must not stop the visitor from starting their call. Non-string attribute values are coerced when trivially representable (number / boolean), otherwise dropped; if the structured blob would exceed the budget, the attribute map is dropped first while the load-bearing page URL / title / referrer survive.

The visitor's session bootstrap and the agent's answering view both receive this same bounded snapshot, so the screen-pop and any audit trail see an identical, sanitized contract.

## WebMCP guardrails — gating browser-resident AI agents

A browser-resident AI agent (WebMCP / `navigator.modelContext`) can drive the widget's in-page write surface — and before it *acts*, its request passes through six tenant-owned controls, each independently observable:

1. **Action rate lane** — a sliding-window cap (5 callbacks/hour, 30 messages/hour by default) per visitor session, layered on *top of* the cluster-wide visitor rate lane, and scoping the *action* verbs specifically.
2. **Per-session caps** — a cumulative ceiling (50 actions / 100 preview-cost units per session by default) so a long-lived session can't grind under the hourly lane; a request that would breach either ceiling is rejected and charges nothing. Both defaults are an abuse brake, not a business limit — a human-driven flow never approaches them.
3. **Audit** — a structured, sanitized log line for every agent-invoked call (allowed or denied) so you can reconstruct what a browser agent did.
4. **Attestation** — the agent is verified and classified (Web Bot Auth inbound signature, and/or a scoped agent identity token on its own header, never the visitor token) so your policy can allow / deny / rate *by agent identity*. A presented-but-invalid token is a hard deny — a caller that asserts an identity it can't prove is never trusted.
5. **Reputation (KYA)** — attestation proves *who* signed; a per-tenant behavioral score (velocity vs. declared rate, burst anomalies, prior-abuse ratio) can then *tighten* — never loosen — the attestation verdict: downgrading `allow` to `rate`, or forcing a hard deny once abuse crosses a floor. This is what lets an otherwise-allowlisted agent that starts misbehaving get reined in automatically.
6. **Compliance (quiet hours)** — an agent-scheduled callback is a future outbound voice leg, so it runs through **your own** tenant-configured quiet-hours / dialing window (channel `voice`). Default-OPEN: when you haven't opted the voice channel into quiet hours the gate allows; when you have and recipient-local time is outside the window it rejects with your reason and `nextAllowedAt`. It's your window — not a platform hard gate.

A plain human visitor (no agent signature or token) pays no verification cost and their flow is byte-for-byte unchanged — these are attenuation / rejection layers only; they never originate a call.

## The transcript service — a bounded, sanitized email body

Behind the widget's "email me a copy of this chat" endpoint, a pure renderer turns a visitor's own conversation into an HTML + plain-text email body. The route owns auth, ownership, recipient resolution, and the send; keeping rendering pure and side-effect-free means the transcript content can be specified and tested without the transport.

The same trust-boundary discipline applies to the output:

* Every visitor-controlled fragment — message bodies, display names, attachment URLs — is HTML-escaped before it lands in the markup; newlines survive as `<br/>` so formatting doesn't corrupt.
* The brand accent color is validated to a strict hex value before it reaches a `style` attribute (drifted / tenant-controlled values fall back to the default); unparseable timestamps degrade to the raw string, not "Invalid Date", so the rendered output stays byte-stable.

A visitor's transcript is *their* conversation history delivered to *their* mailbox; the renderer's job is to make that self-service touch safe by construction — nothing it produces executes in anyone's browser context.

## The trust policy — how agent identities gate every widget action

The single tenant-owned gate that decides *whether* a browser agent may act at all is the trust policy object: a list of foreign agent directories (for example a partner agent platform) you choose to trust, each with a grant scope:

* `allow` — grant the surface's normal posture.
* `deny` — revoke a previously trusted directory *without deleting the row*, keeping the audit trail.
* `rate` — allow, but with the directory's `rate_limit_per_hour` instead of the surface's normal per-session cap.

Three rules hold the whole posture:

1. **Nothing is trusted by default.** An empty list (or no stored row) means every foreign-signed request still classifies invalid and is denied — exactly as before the object existed. You must deliberately opt each directory in by exact HTTPS URL.
2. **The posture matrix decides per agent.** The master writes posture is `allow_all` (default-open, still rate-capped), `deny_all`, `allowlist`, or `denylist`; explicit deny entries always win over allow, and a verified foreign signature trusted by an opted-in directory isn't blocked by an `allowlist` posture (that posture gates *unknown* callers; it doesn't gate a directory you deliberately named).
3. **The runtime can only tighten.** Reputation scoring then narrows — never widens — the verdict this policy produced, and the policy object is read back through the same fail-closed safe parser as every other widget object.

## See also

* [Voice widget install walkthrough](/guides/voice-web-widget) — install the embed, bind an AI voice agent, get the snippet
* [Web support end to end](/guides/support-widget-to-resolution) — widget → Inbox → co-browse → replay as one workflow
* [Co-browse session model](/concepts/cobrowse-session-model) — the companion privacy/consent model the same widget family carries
