1The anchor is the audit
A bookmarklet's source is what you install. It can't auto-update behind your back.
When you drag a BotsKit bookmarklet onto your bar, the browser saves the JavaScript verbatim in your bookmarks. From that moment on:
- The code never changes until you re-drag.
- It contains no
<script src=…>in the host-page realm (plan §2 R2) — only DOM the bookmarklet itself injected. - Reading it top-to-bottom is feasible: ~17 LOC for the dumb streamer, ~250 LOC for the full Axis-A anchor.
Open the control room and expand "Read the trust anchor" under any axis card — the entire surface scrolls past in one panel. That is the entire audit.
2Three rings of trust
The anchor admits the collar admits the uApp. Each ring constrains the next.
Anchor (bookmarklet)
The single file you dragged. Validates origins, sources, sessions, nonces, kinds, and sizes
on every postMessage before any handler runs (plan §3).
Holds the grant table — default-deny.
Authority: total within its trust boundary; can mint or revoke caps.
Collar
Broker that lives at the HUB origin. Enforces grant chips — small toggles on the host-page
panel labelled "read page", "show overlay", "screenshot", "share URL". The chip is the consent.
A capability call (iv("rd", …)) without a flipped chip returns DENIED.
Authority: only what the chips currently permit. Updates land here without re-dragging.
uApp (chat, future widgets)
Where the actual conversation happens. Talks to the collar through the four caps it was granted. Can never reach the host page directly — only through the collar's gate.
Authority: a strict subset of what the user has chip-granted to the collar.
The four caps you'll see chips for today:
| cap | what it asks | what it returns |
|---|---|---|
rd | Read the host page's selection or visible text. | Plain text, capped at 64 KiB per call. |
ov | Draw an overlay annotation on the host page. | Acknowledgement; the overlay is host-DOM-only, no script. |
sc | Capture a screenshot of the visible host viewport. | A PNG data URL. |
lo | Share the host page URL with the chat (origin + path + hash; no query string). | An object — sanitization is in the anchor itself, not the collar. |
3Axis A vs Axis B — same rings, different shapes
One runtime question — "can our widget iframe into this page?" — forks the whole trust shape.
Axis A · bookmarklet.exit — nested-iframe control
Fires when the host's CSP frame-src permits us.
- Anchor injects an iframe at the HUB origin (the collar) into the host page.
- Collar nests another iframe (the uApp) inside itself.
- Trust flows downward: each frame can only ask its parent for what its parent's chip table says yes to.
- Host page DOM only gets touched through caps the user enabled.
- X-Frame-Options is not in play here — we never iframe the host page. We load our own origin into the host page, which the host's CSP can permit or deny.
Axis B · bookmarklet.stream — pixel quarantine
Fires when the host's CSP would block our iframe.
- Anchor opens a host_base window/tab at the HUB origin (top-level, never inside the host page).
- The real collar+uApp render there, at our origin, fully isolated.
- A small
<canvas>+postMessagetick is the host page's only footprint. PNG frames stream in; clicks/keystrokes replay back viainjector.js. - There is no DOM bridge — the streamed widget literally cannot reach into the host page. What you see is pixels.
- Same chip discipline applies inside the host_base. The collar there enforces grants identically.
4What the trust anchor enforces, line by line
Every postMessage is filtered against this quadruple before reaching any handler.
| check | against | what it stops |
|---|---|---|
| origin | allowedOrigins allowlist | Cross-origin window posts unsolicited messages into our handler. (Threat T1.) |
| source | expectedSource WindowProxy | Wrong window in our own page (sibling iframe, opener). (Threat T2.) |
| session | opaque 128-bit random | Stale messages from a prior bus. Anyone holding it is inside the bus — keep it confidential. |
| nonce | opaque 128-bit random | Same. Rotated on transport upgrade in v1. |
| kind | known protocol verbs | Unknown / app-level fields at the envelope root. |
| size | maxEnvelopeBytes (~256 KiB) | Oversized payload DoS before JSON.stringify ever runs. |
| cap + grant | local grant table, default-deny | Peer invokes a capability we never granted. (Threat T3.) |
For the full audit, see plan_caprpc.md — sections G1–G12 (grant audit) and F1–F10 (per-file findings) are the receipts.
5What this cannot defend against
A capability bus is one layer. Here's the honest line between in-scope and out-of-scope.
✓ In scope (we defend against)
- Cross-origin windows posting to our handler.
- Wrong window in our own page (sibling iframe, opener).
- Peer invoking a cap we never granted.
- We accidentally invoking a peer cap they never granted.
- Stale grants surviving policy change (
expiresAtlease). - Oversized payload DoS (envelope-size cap).
- Replay within the same tab (random 128-bit message IDs).
- OAuth confused-deputy (separate one-shot helper, mandatory
iss+state).
✗ Out of scope (we explicitly do not defend against)
- XSS in your own origin — if attacker runs JS on the host page, they own everything. No browser RPC layer changes that.
- Malicious browser extension with
<all_urls>can read allmessageevents. Don't transit secrets throughpostMessageif extensions are in scope. - Devtools / a user with the JS heap. They are inside every boundary by definition.
- Compromised peer iframe within the trust boundary. Once granted, a cap is callable as we said.
- Network-layer attacks (TLS, DNS) — browser/OS responsibility.
- Side-channel timing across origins — out of scope for an RPC layer.
The honest summary. BotsKit defends the seam between the host page and our widget,
and the seam between widget components. It does not turn an unsafe page into a safe one. CSP, COOP/COEP,
Subresource Integrity, and code review of every addEventListener("message", …) are still your job.
What we promise is that our listener is one of the audited good ones, and that the bookmarklet anchor
you dragged once is the only thing standing between us and the page.
6Want to read the source?
Three artifacts, in increasing order of size:
- bookmarklet.stream.src.js — Axis B trust anchor (~17 LOC streamer + bc.v0 control core).
- bookmarklet.exit.src.js — Axis A trust anchor (~250 LOC, full collar bootstrap).
- bookmarklet.exit_stream.src.js — AB stitched (build-time merge of both above).
And the prose receipts:
- plan_caprpc.md — full security audit (threat model, grant findings, vocabulary, plugin pattern).
- spec_unified_transport.md — the architecture (one decision, two axes, three tiers).
- plan_unified_axis.md — how the two axes are kept coherent (release shape, test discipline, build seam).