Summary
The cert-verify bypass installed by PR #324 (setPreviewCertPolicy in electron/main.ts) lives on a single session-wide partition ("controller-preview"), shared by every preview pane. When two pane opens overlap, the IPC roundtrip that flips the policy can race a normal open in another pane and clobber an active insecure-mode bypass before the self-signed certificate is fetched, so the agent's --insecure navigation intermittently falls back to Chromium's default and fails with NET::ERR_CERT_AUTHORITY_INVALID.
Repro (race)
- Open two worktrees in the same window so both have an active preview pane (pane A and pane B).
- From pane A's shell, run
controller browser open --insecure https://localhost:5050 against a self-signed dev server.
- From pane B's shell, immediately (within the same IPC roundtrip window) run a plain
controller browser open https://example.com.
- Sometimes pane A loads correctly; sometimes pane A's
did-fail-load reports the cert error because pane B's setPreviewCertPolicy({ insecure: false }) IPC reached the main process between pane A's setPreviewCertPolicy({ insecure: true }) and the actual cert handshake for localhost:5050.
The window is small (single IPC roundtrip plus the URL validation) but real, and it widens if any step blocks (e.g. the server-side validatePreviewUrl doing extra work, or the renderer React reconciliation delaying the setPreviewCertPolicy IPC).
What's already there
electron/main.ts:438 — setPreviewCertPolicy(opts) calls electronSession.fromPartition(PREVIEW_PARTITION).setCertificateVerifyProc(...).
electron/main.ts:8 — PREVIEW_PARTITION = "controller-preview" is shared across every pane.
client/src/components/PreviewBrowserPool.tsx:447 — the <webview> is rendered with partition: "controller-preview" for every pane.
client/src/lib/usePreviewBrowserHost.ts — the renderer fires setPreviewCertPolicy before validatePreviewUrl in the openUrl flow, but there's no per-pane isolation underneath.
Why it matters
The race only affects --insecure flows today, but the same session-wide mutation will become a footgun as we add more per-pane state. Two natural extension points that would re-introduce the race:
Either extension lands on the same race, so fixing the partition shape now is cheaper than fixing each flag individually.
Proposed fix
Per-pane partition. Give each pane its own Electron session partition, derived from the existing browserKey (projectId:worktreeId):
- Replace
PREVIEW_PARTITION = "controller-preview" with a per-key partition string, e.g. controller-preview:${paneKey} (or controller-preview:${browserKey.replace(/[^a-zA-Z0-9_-]/g, "_")} to keep Electron's partition-name grammar happy — partitions only accept alphanumerics, _, and -).
- The
<webview partition={...}> becomes a function of the pane key (already a stable string).
setPreviewCertPolicy is called with the target partition as part of the IPC payload (or the main process looks it up from request.frame / the originating webContents — but the explicit payload is simpler).
- The existing partition-level guards (
attachPreviewPartitionGuards, attachPreviewWebviewGuards) iterate over electronSession.fromPartition per pane, or get the same treatment when a pane first appears.
After this lands, two simultaneous opens in different panes cannot race because they don't share a session. The verifier state is per-pane by construction.
Alternative (less recommended)
Per-navigation state on the shared session. Track pending-insecure requests by hostname and only flip the policy off after the cert handshake for the last insecure request completes. More invasive than the per-pane partition, and it leaves the shared-session shape in place — every future per-pane feature re-derives the same race-avoidance.
Acceptance criteria
Related
Summary
The cert-verify bypass installed by PR #324 (
setPreviewCertPolicyinelectron/main.ts) lives on a single session-wide partition ("controller-preview"), shared by every preview pane. When two pane opens overlap, the IPC roundtrip that flips the policy can race a normal open in another pane and clobber an active insecure-mode bypass before the self-signed certificate is fetched, so the agent's--insecurenavigation intermittently falls back to Chromium's default and fails withNET::ERR_CERT_AUTHORITY_INVALID.Repro (race)
controller browser open --insecure https://localhost:5050against a self-signed dev server.controller browser open https://example.com.did-fail-loadreports the cert error because pane B'ssetPreviewCertPolicy({ insecure: false })IPC reached the main process between pane A'ssetPreviewCertPolicy({ insecure: true })and the actual cert handshake forlocalhost:5050.The window is small (single IPC roundtrip plus the URL validation) but real, and it widens if any step blocks (e.g. the server-side
validatePreviewUrldoing extra work, or the renderer React reconciliation delaying thesetPreviewCertPolicyIPC).What's already there
electron/main.ts:438—setPreviewCertPolicy(opts)callselectronSession.fromPartition(PREVIEW_PARTITION).setCertificateVerifyProc(...).electron/main.ts:8—PREVIEW_PARTITION = "controller-preview"is shared across every pane.client/src/components/PreviewBrowserPool.tsx:447— the<webview>is rendered withpartition: "controller-preview"for every pane.client/src/lib/usePreviewBrowserHost.ts— the renderer firessetPreviewCertPolicybeforevalidatePreviewUrlin theopenUrlflow, but there's no per-pane isolation underneath.Why it matters
The race only affects
--insecureflows today, but the same session-wide mutation will become a footgun as we add more per-pane state. Two natural extension points that would re-introduce the race:--user-agent=...or--cookie-store=...flag (issue Agent browser: gate external URLs behind approval (permission model + SSRF) #117's approval-gated navigation work may add these).Either extension lands on the same race, so fixing the partition shape now is cheaper than fixing each flag individually.
Proposed fix
Per-pane partition. Give each pane its own Electron session partition, derived from the existing
browserKey(projectId:worktreeId):PREVIEW_PARTITION = "controller-preview"with a per-key partition string, e.g.controller-preview:${paneKey}(orcontroller-preview:${browserKey.replace(/[^a-zA-Z0-9_-]/g, "_")}to keep Electron's partition-name grammar happy — partitions only accept alphanumerics,_, and-).<webview partition={...}>becomes a function of the pane key (already a stable string).setPreviewCertPolicyis called with the target partition as part of the IPC payload (or the main process looks it up fromrequest.frame/ the originating webContents — but the explicit payload is simpler).attachPreviewPartitionGuards,attachPreviewWebviewGuards) iterate overelectronSession.fromPartitionper pane, or get the same treatment when a pane first appears.After this lands, two simultaneous opens in different panes cannot race because they don't share a session. The verifier state is per-pane by construction.
Alternative (less recommended)
Per-navigation state on the shared session. Track pending-insecure requests by hostname and only flip the policy off after the cert handshake for the last insecure request completes. More invasive than the per-pane partition, and it leaves the shared-session shape in place — every future per-pane feature re-derives the same race-avoidance.
Acceptance criteria
controller browser opencalls in different panes — one--insecure, one not — both succeed.--insecureis still scoped to loopback hosts in both the server-side policy and the Electron cert-verify proc.controller browser open --insecure https://localhost:5050against a self-signed cert) still works.<webview>webpreferences or the popup / permission guards installed inattachPreviewWebviewGuards.browserKeyto partition string.Related
controller browser open(fixes #323) #324 — the PR that introduced the shared-partition race. The-2→-3fix in that PR addresses a separate concern (non-loopback subresources from an insecure page); this issue is about the cross-pane race.