Skip to content

feat(native-bridge): native-messaging bridge spike (0289 Option C)#442

Merged
crs48 merged 1 commit into
mainfrom
claude/0289-native-messaging-extension-spike
Jul 9, 2026
Merged

feat(native-bridge): native-messaging bridge spike (0289 Option C)#442
crs48 merged 1 commit into
mainfrom
claude/0289-native-messaging-extension-spike

Conversation

@crs48

@crs48 crs48 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Follow-up to exploration 0289 ("Securely connecting the browser to a local model") — the one remaining unchecked item: Option C, a browser-extension + native-messaging bridge so the pure-web PWA (https://app.xnet.fyi, no bundled Electron host) can reach a local model with the strongest origin binding — no loopback HTTP port, no CORS, no DNS-rebinding surface (the 1Password pattern).

B stays the default. The shipped pairing-code bridge (Option B) remains the default trust path; this is a spike + UX comparison, proposed as an opt-in upgrade — not wired into the deployed app.

What's here — packages/native-bridge-extension/ (private, no build, no changeset)

  • MV3 extensionexternally_connectable origin allowlist + a fixed key → deterministic ID; background.js relays page ↔ native host and re-checks sender.origin.
  • Native host (xnet-bridge-host.mjs) — 4-byte-LE stdio framing → backend; drains in-flight replies before exit. Two backends: cli spawns the user's claude/codex (no port anywhere), daemon forwards to the hardened bridge over loopback carrying its pairing token (reuse MCP tools / /run / Ollama proxy).
  • Installer (install-host.mjs) pins the native-host manifest allowed_origins to the derived extension ID (crx-id.mjs) — the OS enforces that only this extension may launch the host.
  • Page connector (web/extension-connector.mjs) mirrors the ChatAgent contract so it slots into the connector ladder as an extension tier (documented drop-in; doesn't touch the churny detect.ts).

Two allowlists — the extension's externally_connectable and the native-host manifest's allowed_origins — are the entire trust story. In cli mode no loopback port faces the browser, so the DNS-rebinding/drive-by surface Option B defends with a token simply doesn't exist.

Tests — 28, registered under the vitest integration project

Framing codec (partial/multi/oversize frames, multibyte UTF-8), relay + both backends including a round-trip through the real hardened bridge daemon with its pairing token, the page protocol against a fake chrome, extension-ID derivation, and a real-process end-to-end that spawns the host and drives it with native-messaging frames. Found & fixed one real bug: the host exited on stdin-end and dropped an in-flight async reply — now drains first.

UX finding (full table in the package README)

Pairing code (B, shipped) Extension (C, this spike)
Recurring friction re-paste per launch (token rotates) none — durable, no secret
Security defends the loopback surface removes it (no port/CORS/rebinding)
Ship cost zero, already merged per-browser extension + host installer
Reach every browser Chromium + Firefox (no WebKit parity)

Recommendation: keep B as default; pursue C as an opt-in "install the xNet bridge extension" upgrade for a browser-only install with no bundled daemon.

Checks off the final item in the 0289 doc and adds a Spike Outcome section.

🤖 Generated with Claude Code

The pure-web PWA (no bundled Electron host) can reach a local model with
no loopback HTTP port, no CORS, and no DNS-rebinding surface (the
1Password pattern). Follow-up to exploration 0289; keeps the shipped
pairing-code bridge (Option B) as the default.

Private, tested POC in packages/native-bridge-extension:
- MV3 extension: externally_connectable allowlist + fixed key -> stable ID
- background.js relay: page <-> native host, sender.origin re-checked
- native host (xnet-bridge-host.mjs): 4-byte-LE stdio framing -> backend,
  drains in-flight replies before exit; cli backend spawns claude/codex
  with NO port, daemon backend forwards to the hardened bridge + token
- install-host.mjs pins native-host allowed_origins to the derived
  extension ID (crx-id.mjs); page connector mirrors the ChatAgent contract
- 28 tests (framing, relay + both backends incl. a round-trip through the
  real hardened daemon, page protocol, ext-ID derivation, real-process e2e)

Registered under the vitest integration project; checks off the last 0289
item with a UX comparison (B stays default, C is an opt-in upgrade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48 crs48 temporarily deployed to pr-442 July 9, 2026 18:00 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@crs48 crs48 added the skip-changelog Exclude this PR from the changelog label Jul 9, 2026
github-actions Bot added a commit that referenced this pull request Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #442.

@crs48 crs48 merged commit f4f62c4 into main Jul 9, 2026
14 of 15 checks passed
@crs48 crs48 deleted the claude/0289-native-messaging-extension-spike branch July 9, 2026 18:09
github-actions Bot added a commit that referenced this pull request Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude this PR from the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant