Skip to content

Agent browser: --insecure flag on controller browser open (fixes #323) - #324

Merged
germanescobar merged 2 commits into
mainfrom
invalid-cert
Aug 2, 2026
Merged

Agent browser: --insecure flag on controller browser open (fixes #323)#324
germanescobar merged 2 commits into
mainfrom
invalid-cert

Conversation

@germanescobar

Copy link
Copy Markdown
Owner

Closes #323.

Summary

controller browser open <url> --insecure lets the agent reach a local dev server served over https with a self-signed certificate — something the preview pane could not do before, because Chromium blocks the navigation with NET::ERR_CERT_AUTHORITY_INVALID and the agent has no click path through the interstitial.

The bypass is scoped to loopback hosts in three independent places (CLI, server-side policy, Electron setCertificateVerifyProc) so it can't be used to talk to an arbitrary external host without cert validation.

What changed

  • cli/controller: parse --insecure on browser open; advertise it in USAGE.
  • server/lib/browser-policy.ts: validateBrowserUrl(input, root, { insecure }) accepts the option and narrows the http(s) allow-list to loopback hosts when set.
  • server/routes/browser.ts: forward params.insecure to the renderer.
  • electron/main.ts + electron/preload.ts: new controller:set-preview-cert-policy IPC handler. Installs setCertificateVerifyProc on the controller-preview session, returning 0 for loopback hosts (localhost, 127.0.0.1, [::1]) and -2 otherwise.
  • client/src/lib/usePreviewBrowserHost.ts + client/src/components/PreviewBrowserPool.tsx: thread params.insecure through openUrl; the pool calls setPreviewCertPolicy before validatePreviewUrl so the bypass is in effect before navigation starts.
  • shared/controller.ts + shared/preview-browser.ts: setPreviewCertPolicy on the bridge; document insecure in BrowserOpenParams.

Security model

The flag gives the agent exactly one new capability — reach a https://localhost:* server with a self-signed cert. It cannot be used to talk to an arbitrary external host without cert validation:

Layer Check
CLI --insecure is opt-in, default false.
Server policy validateBrowserUrl({ insecure: true }) rejects any non-loopback hostname before it reaches the renderer.
Electron setCertificateVerifyProc Returns 0 only for localhost, 127.0.0.1, [::1]; everything else falls through to Chromium's default verifier.

The bypass is intentionally per-call, not sticky: every open re-asserts the policy, so a non---insecure follow-up open restores the default verifier.

Tests

  • 4 new validateBrowserUrl tests (covers the localhost-only contract).
  • 4 new CLI parser tests for --insecure placement before/after the URL.
  • Full suite: 685 / 685 passing via node --import tsx --test ....

Verification

  • tsc -p electron/tsconfig.json and tsc -p server/tsconfig.json compile clean.
  • vite build succeeds.
  • End-to-end (open with self-signed cert → snapshot) was not exercised from this terminal — there's no Electron host available here — but every layer of the chain has the contract it needs.

Out of scope

package-lock.json carries an unrelated pre-existing diff (optional-native dev: true flags) that's left out of this commit.

Acceptance criteria

  • controller browser open https://localhost:5050 --insecure loads a self-signed localhost cert without the agent having to interact with the interstitial.
  • Without --insecure, the same URL still shows the interstitial (no behavior change for the default).
  • With --insecure, https://example.com is still rejected by cert validation (and now by the server-side policy before it reaches the renderer).
  • The bypass state is reset after the next open without the flag.
  • The docs explain the flag and the localhost scope.
  • No regression on Agent browser: gate external URLs behind approval (permission model + SSRF) #117's permission model — the URL still has to pass validatePreviewUrl.

When the agent-driven preview browser opened an `https://localhost:*` URL
served by a local dev server with a self-signed certificate, Chromium
blocked the navigation with `NET::ERR_CERT_AUTHORITY_INVALID`. The agent
had no click path to advance past the interstitial, so a whole class of
local-https dev workflows was unreachable from the agent. A regular
browser shows the bypass prompt; the preview pane did not.

`controller browser open <url> --insecure` now opts into a TLS-cert
verification bypass scoped to loopback hosts for the duration of that
single navigation. The bypass is gated in three independent places:

  1. The CLI parses `--insecure` (default false).
  2. The server-side policy in `validateBrowserUrl` rejects
     `--insecure` against any non-loopback host, so an external URL
     with the flag is refused with a clear error before it ever
     reaches the renderer.
  3. The Electron `setCertificateVerifyProc` only returns 0 (allow)
     when `request.hostname` is `localhost`, `127.0.0.1`, or `[::1]`;
     everything else still falls through to Chromium default and
     continues to be rejected on TLS errors.

This means the flag gives the agent exactly one new capability — reach
a `https://localhost:*` server with a self-signed cert — and nothing
more. The bypass is intentionally not sticky: every `open` re-asserts
the policy from the renderer-side request, so a follow-up
non-`--insecure` open restores the default verifier.

Implementation:

  - CLI (`cli/controller`): new `--insecure` parser branch; the flag
    rides through to the server as `params.insecure`.
  - Server (`server/lib/browser-policy.ts` + `routes/browser.ts`):
    `validateBrowserUrl(input, root, { insecure })` accepts the option
    and narrows the protocol allow-list to loopback hosts when set.
    Route forwards `params.insecure` to the renderer.
  - Electron (`electron/main.ts` + `preload.ts`): new
    `controller:set-preview-cert-policy` IPC handler that installs
    `setCertificateVerifyProc` on the `controller-preview` session,
    scoped to loopback hosts.
  - Renderer (`usePreviewBrowserHost.ts` + `PreviewBrowserPool.tsx`):
    the bridge handler reads `params.insecure` and threads it through
    the existing `openUrl` flow; the pool calls `setPreviewCertPolicy`
    before `validatePreviewUrl`, which guarantees the bypass is in
    effect before the navigation actually starts.
  - Shared types (`shared/controller.ts` + `shared/preview-browser.ts`):
    `setPreviewCertPolicy` on the bridge; `BrowserOpenParams` documents
    the flag.
  - Tests: 4 new policy tests (`server/lib/__tests__/browser-policy.test.ts`)
    and 4 new CLI parser tests (`cli/__tests__/controller-cli.test.mjs`).
    Full suite: 685/685 passing.
  - Docs (`docs/content/docs/browser-preview.mdx`): new "Local HTTPS
    dev servers" section with a copy-pasteable example and an explicit
    note that the bypass is loopback-scoped.
  - CHANGELOG: unreleased entry referencing #323.

Note: `package-lock.json` carries an unrelated pre-existing diff
(optional-native `dev: true` flags) that is left out of this commit.
@germanescobar

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89bfe905f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread electron/main.ts Outdated
host === "127.0.0.1" ||
host === "[::1]" ||
host === "::1";
callback(isLoopback ? 0 : -2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve default verification for non-loopback hosts

When an insecure localhost page loads any HTTPS subresource from another host—for example a CDN script, font, or external API—this callback rejects the certificate even when it is valid. Electron's Session API defines -2 as failure; -3 is the value that uses Chromium's verification result. Returning -3 for non-loopback hosts is necessary for the claimed fall-through behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — that was a real bug. Fixed in 04ae836: setPreviewCertPolicy now returns -3 (Chromium default verification) for non-loopback hosts instead of -2 ("fail"), so a legitimate HTTPS subresource loaded by an insecure-localhost page is still checked and accepted. Updated the inline comment to call out the -2 vs -3 distinction, and the CHANGELOG entry under #323 notes the fix.

Comment thread electron/main.ts
Comment on lines +462 to +464
const session = electronSession.fromPartition(PREVIEW_PARTITION);
if (!insecure) {
session.setCertificateVerifyProc(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate certificate policy per preview navigation

When two pane opens overlap, this session-wide mutation races their navigations: every pane uses the same controller-preview partition, and openUrl changes the policy before asynchronous validation and rendering, so a normal open in pane B can clear the verifier after pane A enables it but before pane A requests its self-signed certificate. The insecure open then intermittently reaches the certificate interstitial; the policy needs per-pane isolation or serialization that lasts through navigation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is a real race — the cert policy lives on a session-wide partition, so two simultaneous opens in different panes can clobber each other between the IPC flip and the actual cert handshake.

Fixing it in this PR would mean reshaping the partition per pane, which touches <webview partition> and the existing partition-level guards (attachPreviewPartitionGuards, attachPreviewWebviewGuards). That feels too big to bundle into the flag ship.

Tracking as #325: per-pane Electron session partition (derived from the existing browserKey so the grammar stays alphanumerics + _ + -). After that lands, the cert policy is per-pane by construction and any future per-pane flag — --user-agent, --cookie-store, the SSRF allowlist from #117 — doesn't have to re-derive the same race-avoidance.

Thanks for catching both.

…ecks (#324 review)

Address Codex review feedback on PR #324:

  electron/main.ts:477 — `setPreviewCertPolicy` returned `-2` ("fail")
  for non-loopback hosts, which short-circuits Chromium and rejects
  *any* TLS connection request from a non-loopback host, not just the
  ones we wanted to gate. In practice this means a legitimate HTTPS
  subresource loaded by an insecure-localhost page — a CDN script,
  font, or external API over a valid cert — would fail with
  `ERR_CERT_AUTHORITY_INVALID` even though the cert is fine and we
  have no business blocking it.

  Electron's `session.setCertificateVerifyProc` documents three
  return values: 0 = allow, -2 = fail, -3 = use Chromium's default
  verification result. Returning `-3` for non-loopback hosts preserves
  the original goal — non-loopback certs are still verified — while
  letting pages on an insecure localhost page load third-party HTTPS
  resources normally.

  The doc-comment on `setPreviewCertPolicy` already promised "fall
  through to Chromium's default verifier and continue to be rejected
  on TLS errors"; the old code only did that for the cert of the
  top-level URL, not for subresource certs. The new code matches the
  contract.

The cross-pane policy race (also raised by Codex) is deferred to
#325 — a per-pane Electron session partition is the right fix and
warrants its own PR.

  - CHANGELOG: explicit note that the `-2` → `-3` fix lands alongside
    the original #323 entry.
@germanescobar
germanescobar merged commit 899a59c into main Aug 2, 2026
@germanescobar
germanescobar deleted the invalid-cert branch August 2, 2026 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent browser: option to bypass invalid TLS certificates for local dev servers

1 participant