AGENTS.md@packages/browser-plugin/src/server/relay · git:20260914.69e08a2 · 2026-09-14 · sha256 5d41669713913b69
AGENTS.md@packages/browser-plugin/src/server/relay git:20260914.69e08a2A
Immutable. This exact content is served forever at /api/v1/blob/5d41669713913b69.
# DOX — packages/browser-plugin/src/server/relay
Files in this directory. One row per source file. See change: add-browser-relay (task 2.2, design D2).
| File | Purpose |
|------|---------|
| `vendor/` | Vendored playwright-core relay + shims — see `vendor/AGENTS.md`. NEVER edit anything under `vendor/playwright-core/` (hash test `../__tests__/vendor-integrity.test.ts` enforces); refresh = re-copy from upstream + regenerate the hash manifest. Adaptation (bypass `WSServer` transport, deny-list, audit hooks) happens in `relay-instance.ts` via wrapper, never by editing vendor files. |
| `deny-list.ts` | Pure policy: `ALWAYS_DENIED_METHODS` (Storage/Network cookie reads + `Browser.setDownloadBehavior`), `URL_POLICY_METHODS` (`Page.navigate`, `Target.createTarget`) fenced off `file:`/`javascript:`/`data:`/`blob:` and, when `allowedDomains` is non-empty, off host-less/outside-list URLs. `hostAllowed` = exact host, or leading-dot entry for bare host + subdomains, never a suffix lookalike. `denyError` → `{code:-32000, message:"Denied by dashboard relay policy: <m>"}`. Navigation GUARDRAIL, not a sandbox. E12–E14. |
| `extension-socket.ts` | Protocol-v2 wire adapter (`{id,method,params}` commands → promise-mapped responses; events → `onmessage`). Exists because upstream's `ExtensionConnection` is private to a hash-pinned vendor file whose only exporter also owns the HTTP listener we must not use. Registers the pending callback BEFORE sending (a synchronously-delivering transport would otherwise lose the reply). |
| `fake-relay-instance.ts` | Test-only socket-less instance for `PI_BROWSER_RELAY_FAKE=1` (design D9): one `Fake` profile tab (`tabId: 1`), a 1×1 JPEG every 100 ms to subscribers (the tick RE-ARMS itself — a one-shot timer would emit one frame then go silent), viewer input echoed into the audit ring. Denied kinds (`evaluate` etc.) audit a `denied` row, mirroring the real viewer-input allowlist. `close(reason)` clears the timer/viewers then fires `deps.onClosed` (same contract as `RelayInstance`), so the manager drops the entry and the row shows Not connected. Env-gated at activation — deliberately NOT config-gated, so a real deployment cannot enable it. Task 2.10b / E28. See change: add-browser-relay. |
| `relay-instance.ts` | One guid = one extension socket + ≤1 CDP client. Handshake gating (CDP traffic held until `extension.initialized`), 30 s no-CDP-client close (audit `detach/no-cdp-client`), 30 s `Extension not connected` for a CDP-first client, deny-list + tap + audit hooks, `Target detached: devtools` after a `canceled_by_user` detach (recorded by SESSION id — the model drops the tab session — AND by tab id in `devtoolsDetachedTabs`, because the status view needs the tab after the session is gone), `All controlled tabs detached` on last tab. `tabList()` precedence: devtools-detached → tap state → client-screencast-active → `live`; carries `reason:"devtools"` when detached. `_finalize` records the close reason BEFORE closing either socket, so the reciprocating handler cannot claim the wrong cause. Reads vendored `BrowserModel._tabSessions` through one narrow typed accessor (no public tabId→sessionId exists; vendor is uneditable) — pinned by test. Tasks 2.3, 3.2–3.4. |
| `relay-manager.ts` | Guid mint (128-bit) / claim / 60 s unclaimed expiry, `Map<guid>` + `Map<instanceId>` keyed by `profileDirectory`, connect flow (403 disabled / 503 no-desktop / 409 `not-installed` / 409 `busy` / 504 timeout), disconnect by `instanceId`, kill switch that resolves only after every instance closed, `seedFake()`. The kill switch closes the Fake too (its `onClosed` drops the entry); `setEnabled(true)` RE-SEEDS the Fake when one was seeded before (`fakeSeeded`), so a re-enable after a disable leaves the harness with a live instance again. `instanceId` is a non-secret public handle; the guid never reaches a client and there is deliberately no cdpUrl lookup endpoint. Tasks 2.4, 2.9, 2.10b / E9, E19–E21, X1, X10. See change: add-browser-relay. |
| `screencast-tap.ts` | One tap per `(instance, tabId)` with ≥1 viewer: `Page.startScreencast` on that tab's session, immediate unconditional ack (the extension throttles on acks), frames sent PER SOCKET (the only way `bufferedAmount` can skip one viewer), 2 s `no-frames`, refuse subscribe when the CDP client owns the screencast, stop on last unsubscribe/close. Framed events are filtered from the CDP-client stream by `handlesSession`. Tasks 3.2, 3.4, 3.5. |
| `viewer-input.ts` | Pure viewer→CDP allowlist: `mouse`/`key`/`scroll`/`bringToFront` → `Input.dispatchMouseEvent`/`dispatchKeyEvent`/`synthesizeScrollGesture`/`Page.bringToFront`; nothing reaches `Runtime.*`. Coordinates are normalized `[0,1]` scaled by the LAST frame's device geometry; out-of-range is REFUSED, never clamped. `key`/`bringToFront` need no geometry. E23, E24. |
Files in `__tests__/`:
| File | Purpose |
|------|---------|
| `fake-socket.ts` | In-process `RelaySocket` pair (`FakeSocket.sent` = frames it sent, `.received` = frames it received — the CDP client RECEIVES, so its assertions read `receivedJson()`) + `FakeExtension` speaking protocol v2 (`chrome.debugger.*`, `chrome.tabs.*`, `initialize()`, `detach()`, `tabRemoved()`, `emitFrame()`). Delivery is SYNCHRONOUS on purpose — that is what caught the publish-before-register bug. |
| `deny-list.test.ts` | E12 (four always-denied verbs, `-32000` + policy message, ordinary automation forwarded) + E13 (scheme fence on both verbs, fail-closed on missing/unparseable url) + E14 (exact vs leading-dot table, case-insensitivity, port/path ignored). |
| `relay-instance.test.ts` | The relay-core contract (29 tests): handshake gating + `Target.setAutoAttach`, the `tabId → pw-tab-N` mapping pin, socket exclusivity (E9/E11), deny-list on the CDP path (E12–E14), tap delivery/ack/stop/backpressure (3.2, P2), CDP-client screencast precedence (E26/E27), viewer input (E23/E24), DevTools detach (X7), and every close path (X3–X6). |
| `screencast-tap.test.ts` | X13 (viewer drops mid-stream, survivors keep streaming), X8 (no-frames → live → no-frames + the status notify), P2 (skip only the over-threshold viewer; ack always), P1 (10 fps source sustained), subscribe refusals (E27 / detached), teardown. |
| `viewer-input.test.ts` | E23 coordinate BVA (0/0.5/1 mapped, just-outside refused not clamped, no-geometry refused) + E24 kind allowlist, click as press+release, "never Runtime.*". |