Security & trust model
What the widget can and cannot do, what leaves the browser, and how to deploy it
safely. For vulnerability disclosure see /SECURITY.md.
What leaves the browser
Only a decision request goes to the edge. It contains:
- The user's goal text and any clarifications they typed.
- An indexed table of currently observed controls (
e1..eN): each row is a role, an accessible label, and coarse context โ never a selector, an XPath, or raw HTML. - A bounded slice of visible page text, redacted in the browser first (see below).
- Short history of prior steps for this goal.
What never leaves the browser:
- Password, file, and hidden inputs, and any field inside a payment form โ these are dropped from the snapshot before it is built.
- Payment-provider iframes and card-element mount points (Stripe, Braintree, Adyen, โฆ) โ treated as off-limits containers.
- Cookies,
localStorage, auth tokens, or request/response bodies โ the widget never reads them.
The model's reply is a single index into the table the browser just sent plus an operation (click/type/select/submit/navigate). The widget re-resolves that index against the live DOM and re-validates it before acting. The model never emits a selector, URL, or code, so a compromised or wrong model cannot point the widget at an arbitrary element or destination.
Redaction (always-on floor + per-site policy)
Visible text is masked in the browser before the snapshot is sent. The always-on floor cannot be disabled by any config:
| Category | Rule |
|---|---|
| Payment cards | 13โ19 digits, Luhn-valid or separator-formatted โ ****1234 |
| SSNs | 123-45-6789, spaced, or keyword-anchored 9-digit โ ***-**-**** |
| Bearer/Basic/Token headers | scheme + token โ Bearer [REDACTED] |
| API keys | Stripe/OpenAI/GitHub/Slack/AWS/Google prefixes, key=value secrets |
| JWTs & long opaque tokens | eyJโฆ triples, 48+ char mixed tokens |
A per-site redaction policy can add coverage โ email, phone, ip, and
tenant custom regexes โ but can never weaken the floor. See
USING.md.
Trust boundaries
- Runs in the user's own session. The widget can do only what the signed-in user can already do by clicking. It holds no elevated credentials.
- Same-origin scope lock. Snapshots only read the current document.
Navigation is allowed only to
http(s)origins on the site's registered allowlist; anything else is refused client-side (navigationAllowed) and the edge independently rejects a mismatchedOriginheader on/v1/siteand/decide(originAllowed). - Capability gates. Per-site gates remove whole action kinds (e.g. a read-only site disables type/select/submit) before the model ever sees them.
- Deny lists.
deny_labels/deny_pathshide specific controls and destinations from the action space. - Confirmation policy. Irreversible actions require the user to confirm, gated on the model's goal-level irreversibility judgment agreeing with the element label.
Threat model
| Threat | Mitigation |
|---|---|
| Model tricked into a dangerous action | Model returns an index, not a target; widget re-validates; gates + deny lists + confirmation bound the action space. |
| Prompt injection via page content | Page text is data, not instructions; the model only selects among observed controls, it cannot be told to emit a URL or selector. Redaction strips secrets from that text. |
| Exfiltration of secrets in page text | Sensitive fields excluded; visible card/SSN/token text redacted in-browser before send. |
| Navigation off-site / open redirect | Client + edge origin allowlist; non-registered origins refused. |
| Tampered CDN bundle | Pin SRI (below); the browser rejects a modified bundle. |
| Stolen site key | Keys are public by design (like an analytics key); they authorize decisions for registered origins only, carry no user data access, and are rotatable/revocable via the admin API. Rate limits and daily quotas cap abuse. |
| Cross-origin embedding of your key | Edge checks the Origin header against the allowlist; a key used from an unregistered origin is rejected. |
CSP integration guide
The widget uses a closed shadow root, no eval, and no inline styles
injected into the host page. A host site can run it under a strict Content
Security Policy:
Content-Security-Policy:
script-src 'self' https://cdn.example; # where nav.js is served
connect-src 'self' https://ablehand.ai; # the /decide + /v1 endpoints
style-src 'self'; # widget styles live in shadow DOM
frame-src 'none';
Notes:
- Add only the CDN origin to
script-srcand the edge origin toconnect-src. The widget makes no other network calls. - No
'unsafe-inline'or'unsafe-eval'is required for the widget. - If your CSP uses nonces, the widget's own DOM is inside a shadow root and does
not need one; only the
<script src>tag does.
Subresource Integrity (SRI)
Pin the bundle so a modified CDN copy is rejected:
pnpm --filter @nav/widget build # prints the sha384 and writes dist/nav.js.sri
<script src="https://ablehand.ai/nav.js"
integrity="sha384-โฆ"
crossorigin="anonymous"
data-site-key="pk_..."></script>
Regenerate the hash on every release โ it changes with the bundle bytes. Serve
versioned URLs (nav.<version>.js) so a rollback restores the matching hash.
Operational
- Keys server-side. The Jev and OpenAI keys live only on the edge, never in the widget or the page. The site key on the tag is public and scoped.
- Ingest scrubbing & no-log. When traces/outcomes are persisted, the user's
goal text is run through the same redaction (floor + site policy) first. A
per-site
no_logflag disables persistence entirely; usage is still metered. - Audit. Every guardrail/hint/key change is appended to
audit.jsonl. - Rate limits & quotas. Per-key token bucket + daily cap; over-limit returns
429+Retry-After. - Graceful degrade. If the decision model is unreachable the edge returns a clean degraded response and the widget backs off instead of erroring.