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:

What never leaves the browser:

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

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:

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