ext: S1 native-messaging spike (ADR 0003) + WebSocket transport

Spike S1 — run on the target machine through real snap confinement
(apparmor snap.firefox.firefox enforced; web-ext's direct-exec of the inner
binary bypasses it, so runs were forced through `snap run firefox`):

  - manifest in ~/.mozilla/native-messaging-hosts/  -> WORKS; host launched
    unconfined with real $HOME and real $XDG_RUNTIME_DIR, bound a socket in
    the real /run/user/<uid>. Corroborated by the machine's 1Password host.
  - ~/snap/firefox/common/.mozilla/native-messaging-hosts/  -> not read
  - /usr/lib/mozilla/native-messaging-hosts/               -> not read
  - flatpak path                                           -> N/A (snap Firefox)

Decision: WebSocket stays the default; native messaging is an opportunistic
upgrade taken only when its handshake succeeds. docs/05 §4 corrected in this
commit to point the snap manifest at ~/.mozilla and mark /usr/lib as
deb/tarball-only. ADR carries a self-contained reproduction; the scratch
harness has been removed.

transport/ (build order item 1):
  - types.ts        VeloxTransport interface + error taxonomy
  - rpc.ts          JSON-RPC id correlation, per-call deadline, AbortSignal
  - backoff.ts      exponential backoff with jitter
  - discovery.ts    52000-52016 scan ordering (last-good port first)
  - websocket.ts    scan -> session.hello -> auto-pair (token in
                    storage.local) -> reconnect; -32001 fatal, refused/
                    rate-limited pairing latches needsPairing (no retry storm);
                    a mid-handshake drop aborts hello immediately
  - native.ts       connectNative(); distinguishes "not installed" (fatal,
                    lets the picker fall through) from a crash (reconnect)
  - index.ts        createTransport() runtime picker + persisted Options override

Toolchain: package.json / tsconfig (strict) / vitest; webextension-polyfill
mocked. 38 tests, incl. the WS suite against a real loopback ws server.
No manifest.json yet, so CI's extension-lint guard stays a no-op.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
This commit is contained in:
2026-09-10 01:17:43 +04:00
co-authored by Claude Sonnet 5
parent 9025085d82
commit 90e2580b05
25 changed files with 6509 additions and 3 deletions
+8 -3
View File
@@ -93,11 +93,16 @@ interface Transport {
`browser.runtime.connectNative("com.velox.host")``velox-nmhost` → Unix socket.
Manifest installed to **all** of these, because the right one depends on how Firefox was
installed:
- `~/.mozilla/native-messaging-hosts/com.velox.host.json` (deb/tarball)
- `~/snap/firefox/common/.mozilla/native-messaging-hosts/com.velox.host.json` (snap)
- `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` (system-wide)
- `~/.mozilla/native-messaging-hosts/com.velox.host.json` (deb/tarball **and snap**
spike S1 proved snap Firefox reads the real home here, not `$SNAP_USER_COMMON`; see
`docs/adr/0003-native-messaging-under-snap.md`)
- `/usr/lib/mozilla/native-messaging-hosts/com.velox.host.json` (system-wide; deb/tarball
only — snap Firefox does not read it, per S1)
- `~/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/` (flatpak)
The host binary runs **outside** the snap sandbox with the real `$HOME` and real
`$XDG_RUNTIME_DIR`, so it reaches `veloxd`'s Unix socket directly (ADR 0003 §Q2).
### B. `WebSocketTransport` (fallback, guaranteed to work under snap confinement)
`ws://127.0.0.1:<port>` where the port is discovered by trying a small fixed range and
verifying a `session.hello` handshake — the extension cannot read