Skip to content

fix(electron): make launch-chooser selections authoritative + harden provisioning#379

Open
danshapiro wants to merge 6 commits into
mainfrom
fix/electron-launch-chooser-flow
Open

fix(electron): make launch-chooser selections authoritative + harden provisioning#379
danshapiro wants to merge 6 commits into
mainfrom
fix/electron-launch-chooser-flow

Conversation

@danshapiro
Copy link
Copy Markdown
Owner

Why

An independent (fresh-eyes) review of PR #377 (the Electron launch-connection chooser, merged without review) surfaced several launch-flow defects. This PR fixes them, on a branch from origin/main, with Red-Green-Refactor TDD.

What

# Severity Fix
1 major alwaysAskOnLaunch no longer traps the chooser. Selections used to persist config + re-run the policy, which re-evaluated alwaysAskOnLaunch and looped back to the chooser. Selections now flow through a one-shot ForcedLaunch that runStartup executes directly.
2 major "Start local" is honored even when servers are detected. Same root cause — re-running discovery/policy overrode the pick. The forced start-local spawns a bundled server on the chosen port regardless of detected candidates or saved serverMode.
3 major "Remember this choice" is respected. When unchecked, the server selection is no longer persisted (only the standalone always-ask preference is).
4 major Saved remote URL is pre-filled in the chooser for missing/invalid-token/unreachable recovery, via a tested buildLaunchOptions().
5 major Local port is validated (integer, 1024–65535) in both the renderer and the handler before persisting/launching.
6 major Broken e2e selector fixed — the candidate button's accessible name is Connect to <label>, not Connect. The test now also exercises the always-ask path (validating #1) without the prior workaround.
7 minor Stale Windows build docs correctedprepare-bundled-node.ts uses Node http/https + the tar/extract-zip packages, not external curl/tar/unzip.
8 minor NSIS JSON-injection removed. The installer can't escape JSON, so it now writes a line-based desktop.provision file; the app converts it to a properly serialized desktop.json on first launch (all escaping in JS, fully unit-tested).

Tests

  • New unit coverage: forced-launch execution, remember gating, port validation, buildLaunchOptions, and provisioning parse/apply.
  • npm run test:vitest -- --config vitest.electron.config.ts298 passing.
  • typecheck:client (the merge gate), typecheck:server, and both electron tsc projects are clean.

Caveats

  • The NSIS installer change cannot be built/validated off native Windows (per docs/development/windows-electron-build.md); it should be confirmed by the Windows electron-build CI matrix. The change is deliberately minimal (two plain FileWrite lines) precisely because NSIS is unverifiable here.
  • One pre-existing jsx-a11y lint error in src/ is unrelated to this PR (no src/ files changed).

🤖 Generated with Claude Code

…provisioning

Independent review of PR #377 (the launch-connection chooser) surfaced
several launch-flow defects. This addresses them:

- Honor explicit chooser selections for the current launch. The handler
  used to persist config and re-run the policy, so alwaysAskOnLaunch
  looped straight back to the chooser and a "Start local" pick was
  overridden whenever any server was detected. Selections now flow
  through a one-shot ForcedLaunch that startup executes directly,
  bypassing discovery and policy.
- Respect "Remember this choice": when unchecked, the server selection
  is no longer persisted (only the standalone always-ask preference is).
- Pre-fill the saved remote URL in the chooser for saved-remote recovery
  via a tested buildLaunchOptions().
- Validate the local-server port (1024-65535, integers) in both the
  renderer and the handler before persisting or launching.
- Fix the e2e selector: the candidate button's accessible name is
  "Connect to <label>"; also exercise the always-ask path now that a
  selection works without unchecking it first.
- Correct stale Windows build prerequisites (no curl/tar/unzip needed).
- Stop hand-writing JSON in NSIS (which cannot escape values). The
  installer writes a line-based desktop.provision file; the app converts
  it to a properly serialized desktop.json on first launch.

Red-Green-Refactor TDD; new unit coverage for forced launch, remember
gating, port validation, launch options, and provisioning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

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

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: 4d105666b9

ℹ️ 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 on lines +57 to +61
await deps.patchDesktopConfig({
serverMode: 'remote',
remoteUrl,
remoteToken,
setupCompleted: true,
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 Clear always-ask when applying silent provisioning

When a silent installer provisions an already-configured desktop that previously enabled alwaysAskOnLaunch, this patch merges only the remote fields into the existing config, so the preserved alwaysAskOnLaunch: true makes chooseLaunchAction() show the chooser instead of connecting to the newly provisioned remote server on first launch. The old installer overwrote desktop.json without this field, causing the schema default (false) to apply, so this is a regression for managed reinstall/update flows; include alwaysAskOnLaunch: false (or otherwise force the provisioned launch) with the provisioning patch.

Useful? React with 👍 / 👎.

codex and others added 5 commits May 30, 2026 19:26
…oning reads

Addresses an independent review of the prior commit:

- Scope choose-launch-option to the chooser window (isAllowedSender) and
  validate the chosen URL scheme (http/https) in the main process, so a
  crafted IPC choice can no longer force the app to load an arbitrary
  URL or file:// path. (critical)
- Refuse "Start local" when the chosen port is already served by a
  detected local Freshell, before closing the chooser, avoiding a doomed
  spawn onto an occupied port. (major)
- Catch read failures in applyProvisioningFile (locked file, a directory,
  bad permissions) so an unreadable desktop.provision can never abort
  startup; clear it best-effort instead. (major)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The port-collision guard was renderer-only and therefore advisory. Add an
authoritative check in the choose-launch-option handler (injected
isPortAvailable, wired to is-port-reachable on localhost in entry.ts).
Reject before closing the chooser when the chosen port is occupied or when
availability cannot be determined, so a stale candidate list, a click
before getLaunchOptions resolves, an occupied non-Freshell port, or a
crafted request can no longer trigger a doomed spawn onto a live port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… provisioned values

Addresses a further review pass:

- Runtime-validate the choose-launch-option payload with a Zod schema
  (LaunchChoiceSchema) at the IPC boundary, and reject an unknown kind via
  an explicit default instead of letting it fall through to start-local. A
  non-object payload now returns a controlled error rather than throwing.
- Replace the localhost reachability probe with a real net.Server bind
  attempt (createPortAvailabilityCheck), so a port occupied on another local
  interface (e.g. a 0.0.0.0 bind under WSL) is detected before spawning.
- Preserve provisioned values verbatim (no trim), matching the documented
  raw-preservation contract so tokens with meaningful whitespace aren't
  corrupted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ative check

The renderer's "Start local" occupancy check used the initial candidate
snapshot, so if a detected server exited while the chooser stayed open it
could false-reject starting on the now-free port. Remove that stale-snapshot
guard (and the now-unused localCandidatePort helper); the renderer keeps only
port-range validation and submits the choice, letting the main process's fresh
bind test (isPortAvailable) decide occupancy. The chooser stays open and shows
the handler's error if the port is genuinely taken.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The renderer reads the token back via URLSearchParams.get, so a token
containing +, &, #, or whitespace was corrupted by raw string concatenation
in loadMainWindow -- a server that passed the new main-process token checks
could then load unauthenticated. Percent-encode the token so it round-trips
through URLSearchParams. Existing alphanumeric/hyphen tokens are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danshapiro
Copy link
Copy Markdown
Owner Author

Independent (fresh-eyes / GPT) review log

Ran 5 rounds of independent review, fixing every finding TDD-style between rounds. Findings narrowed each round (the original PR #377 issues were cleared in round 1; later rounds surfaced progressively deeper hardening gaps in the new launch path):

Round Commit reviewed Findings Resolution
1 4d10566 IPC could force-load arbitrary URL/file: (critical); start-local port collision; provisioning readFile outside try/finally dc35143
2 dc35143 start-local collision guard was renderer-only f33c67e
3 f33c67e port check not runtime-validated; localhost-only probe misses 0.0.0.0 binds; provisioning trim vs. "raw" comment 328348f
4 328348f renderer collision guard used a stale candidate snapshot (false-rejects a freed port) 57b3049
5 57b3049 loadMainWindow concatenated the auth token raw → tokens with + & #/space corrupted by the renderer's URLSearchParams decode (could load unauthenticated) 2cbd540

Stopped at the 5-round cap. The final fix (2cbd540, token URL-encoding) is locally verified (unit test + typecheck) but was not itself re-reviewed by fresheyes, since the cap was reached.

Local verification (final commit): electron unit suite 310 passing; typecheck:client (merge gate), typecheck:server, and both electron tsc projects clean.

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.

2 participants