Skip to content

fix(browser): detect Windows browsers when install roots are blank#111256

Merged
steipete merged 2 commits into
openclaw:mainfrom
LZY3538:codex/browser-blank-programfiles
Jul 22, 2026
Merged

fix(browser): detect Windows browsers when install roots are blank#111256
steipete merged 2 commits into
openclaw:mainfrom
LZY3538:codex/browser-blank-programfiles

Conversation

@LZY3538

@LZY3538 LZY3538 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where Windows browser auto-detection could miss standard Chrome, Edge, and Brave installations when LOCALAPPDATA, ProgramFiles, or ProgramFiles(x86) were present but blank.

Why This Change Was Made

Windows install-root inputs are normalized once at the browser plugin boundary before candidate paths are built. Blank values now behave like absent values, while nonblank custom roots retain their existing precedence. Registry environment expansion consumes the same resolved roots so default-browser detection and fallback scanning cannot diverge.

User Impact

Windows users can keep using browser auto-detection even when a launcher or service supplies blank install-root environment variables.

Evidence

Exact repaired head: 0c0d34244847bfc59278a6feb1ee1f26cb691a95

  • node scripts/run-vitest.mjs extensions/browser/src/browser/chrome.default-browser.test.ts — 11/11 passed.
  • git diff --check origin/main...HEAD — passed.
  • Codex autoreview — clean, no accepted/actionable findings.
  • Exact-head CI run 29957874365 — passed: https://github.com/openclaw/openclaw/actions/runs/29957874365
  • OPENCLAW_TESTBOX=1 scripts/pr prepare-run 111256 — passed in hosted_exact_or_recent_parent mode; no standalone remote lease was dispatched.
  • Tests cover blank/default roots and ordering, meaningful custom-root precedence for both discovery entry points, and blank-root registry expansion.

Fresh native Windows proof was attempted, but the broker returned HTTP 401. That boundary was not bypassed. Earlier PR history contains a real Windows run on the pre-repair head; it is not represented here as exact-head proof.

@LZY3538
LZY3538 force-pushed the codex/browser-blank-programfiles branch from d7d2b3c to 108a59a Compare July 19, 2026 06:52
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 19, 2026
@clawsweeper

clawsweeper Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 19, 2026, 3:23 AM ET / 07:23 UTC.

Summary
The PR trims blank Windows browser-install environment roots before Chrome-family executable discovery and adds a resolver regression test for blank ProgramFiles values.

PR surface: Source +2, Tests +23. Total +25 across 2 files.

Reproducibility: yes. from source: empty ProgramFiles values bypassed the existing nullish fallback and produced nonstandard candidate roots; the added win32 resolver case directly exercises that path. The reviewer did not independently execute a Windows reproduction.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • [P2] No actionable repair lane is needed because the patch has no supported defect; normal current-head CI and maintainer merge review remain.

Security
Cleared: The two-file patch only normalizes environment-string handling and adds unit coverage; it introduces no dependency, permission, secret, workflow, or execution-path change.

Review details

Best possible solution:

Merge the focused boundary normalization after the ordinary current-head CI and maintainer checks complete, keeping blank roots equivalent to absent roots and retaining nonblank custom-root precedence.

Do we have a high-confidence way to reproduce the issue?

Yes, from source: empty ProgramFiles values bypassed the existing nullish fallback and produced nonstandard candidate roots; the added win32 resolver case directly exercises that path. The reviewer did not independently execute a Windows reproduction.

Is this the best way to solve the issue?

Yes. Normalizing optional install-root inputs at the browser-plugin boundary is the narrowest fix: it restores existing defaults for blank values without changing meaningful nonblank overrides.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 7c4292ee966d.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix Windows live output for the blank-root scenario, an actual Playwright browser launch, a visible page-title round trip, and a nonblank-root preservation check; redact any private paths or endpoints if adding more artifacts.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body provides after-fix Windows live output for the blank-root scenario, an actual Playwright browser launch, a visible page-title round trip, and a nonblank-root preservation check; redact any private paths or endpoints if adding more artifacts.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.

Label justifications:

  • P2: The patch addresses a limited Windows browser auto-detection failure without evidence of broader availability, security, or data-loss impact.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body provides after-fix Windows live output for the blank-root scenario, an actual Playwright browser launch, a visible page-title round trip, and a nonblank-root preservation check; redact any private paths or endpoints if adding more artifacts.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix Windows live output for the blank-root scenario, an actual Playwright browser launch, a visible page-title round trip, and a nonblank-root preservation check; redact any private paths or endpoints if adding more artifacts.
Evidence reviewed

PR surface:

Source +2, Tests +23. Total +25 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 8 6 +2
Tests 1 24 1 +23
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 32 7 +25

What I checked:

  • Production boundary change: The PR normalizes LOCALAPPDATA, ProgramFiles, and ProgramFiles(x86) before applying the existing standard Windows-root defaults in both Chromium-family and Google-Chrome-specific discovery paths. (extensions/browser/src/browser/chrome.executables.ts:669, a64c13a42c41)
  • Regression coverage: The added test stubs blank ProgramFiles roots and exposes only the standard Chrome path, then verifies the platform resolver returns that absolute executable path. (extensions/browser/src/browser/chrome.default-browser.test.ts:218, a64c13a42c41)
  • After-fix behavior proof: The PR body reports a Windows current-head run with PATH and all three install roots blank, successful Chrome detection, a real Playwright launch, and a page-title round trip; it also reports the nonblank ProgramFiles case continuing to resolve the same browser. (a64c13a42c41)
  • Prior review continuity: The prior review's only blocker was contributor-supplied real Windows behavior proof; the updated PR body supplies a direct after-fix Windows scenario rather than only the mocked resolver test. (a64c13a42c41)

Likely related people:

  • unidentified: The affected browser-discovery ownership trail could not be established from the available read-only review evidence; route through the browser extension area rather than attributing ownership to the PR author alone. (role: current-main history owner; confidence: low; files: extensions/browser/src/browser/chrome.executables.ts, extensions/browser/src/browser/chrome.default-browser.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (2 earlier review cycles)
  • reviewed 2026-07-19T07:03:37.801Z sha 108a59a :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-19T07:08:34.780Z sha 108a59a :: needs real behavior proof before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jul 19, 2026
@LZY3538
LZY3538 force-pushed the codex/browser-blank-programfiles branch from 108a59a to a64c13a Compare July 19, 2026 07:19
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 19, 2026

@Yigtwxx Yigtwxx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The set-but-blank distinction this is built on is the right one — ?? only catches undefined, and a blank ProgramFiles sails through it into a path join, which is precisely the failure mode that's annoying to diagnose from a bug report.

Two things I checked that hold up: localAppData is guarded by if (localAppData) at :677 and :747 rather than being joined blank, so the ?? "" there is deliberate and safe; and the new test is load-bearing — it stubs " ", which does not trigger ?? pre-fix, so the test genuinely fails without the change rather than passing either way.

One more site in the same file has the identical hole, inline.

function findChromeExecutableWindows(): BrowserExecutable | null {
const localAppData = process.env.LOCALAPPDATA ?? "";
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
const localAppData = normalizeOptionalString(process.env.LOCALAPPDATA) ?? "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a third instance of this exact flaw in the same file, ~190 lines above — and it already imports the helper you're adopting here:

// chrome.executables.ts:474-478 (not in this diff)
function expandWindowsEnvVars(value: string): string {
  return value.replace(/%([^%]+)%/g, (_match, name) => {
    const key = normalizeOptionalString(name) ?? "";
    return key ? (process.env[key] ?? `%${key}%`) : _match;
  });
}

Line 476 calls normalizeOptionalString on the variable name; line 477 then reads process.env[key] with a bare ??, which is the same set-but-blank hole this PR closes at :669 and :740.

That matters under this PR's own scenario, because it's a different code path to the same goal. expandWindowsEnvVars feeds detectDefaultChromiumExecutableWindows (:316), which expands the registry shell\open\command value. With ProgramFiles blank, a registry value of

%ProgramFiles%\Google\Chrome\Application\chrome.exe

expands to \Google\Chrome\Application\chrome.exe — an absolute path on the current drive that won't exist. So after this PR the fallback scan path recovers on a blank-env machine while default-browser detection still fails, which is an awkward split to debug later.

The fix at :477 is one line, in the same shape as what you did here:

return key ? (normalizeOptionalString(process.env[key]) ?? `%${key}%`) : _match;

(Deliberately not a suggestion block — the target line is outside this diff, so it would apply to the wrong place.)

@steipete
steipete force-pushed the codex/browser-blank-programfiles branch from a64c13a to 0c0d342 Compare July 22, 2026 21:06
return {
localAppData:
normalizeOptionalString(process.env.LOCALAPPDATA) ??
path.win32.join(os.homedir(), "AppData", "Local"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

resolveWindowsBrowserInstallRoots() adds an unguarded os.homedir() to the Windows detection path. On Windows os.homedir() is itself derived from USERPROFILE, and it does not treat a blank value the way this PR treats blank install roots — it throws.

Isolating USERPROFILE only (real Windows, Node v22.20.0, child process per row):

USERPROFILE os.homedir()
normal (control) C:\Users\Asus
absent (deleted) C:\Users\Asus — libuv falls back to the profile API
blank "" throws ERR_SYSTEM_ERROR: uv_os_homedir returned ENOENT
whitespace " " returns " " → root becomes \AppData\Local, not absolute

For this one input, blank is not like absent — the inverse of the invariant this helper establishes two lines above it.

End-to-end through the real resolver (resolveBrowserExecutableForPlatform({} , "win32"), no mocks, same machine, same command, only the tree differs). Head 0c0d3424484, base ab2ad22533b (this PR's merge-base):

env base PR head
normal env (CONTROL) {"kind":"chromium","path":"…\Opera GX\opera.exe"} same
LOCALAPPDATA + ProgramFiles + ProgramFiles(x86) + USERPROFILE all blank {"kind":"chromium","path":"…\Opera GX\opera.exe"} ERR_SYSTEM_ERROR: A system error occurred: uv_os_homedir returned ENOENT

The control row is only there to show the harness resolves on both trees; just the blank-env row diverges.

It fires early and unconditionally: expandWindowsEnvVars calls the helper before it inspects the string, so the registry-derived default-browser step throws before a %VAR% even has to be present.

Neither caller catches it:

  • chrome.ts:1025 expects null and converts it into No supported browser found (…). That becomes an uncaught ERR_SYSTEM_ERROR instead.
  • chrome.ts:707 calls the resolver inside a catch (err) block, so the new throw replaces err and masks the original port-in-use failure it was recovering from.

The added tests can't see this: they vi.mock("node:os") and stub os.homedir, so the real uv_os_homedir path is never exercised.

The repo already has a house rule for this exact input — packages/terminal-core/src/display-string.ts:13:

/** Run a home resolver defensively because some runtimes throw for missing passwd data. */
function normalizeSafe(fn: () => string | undefined): string | undefined {

used as normalize(env.USERPROFILE) ?? … ?? normalizeSafe(homedir), and its test stubs USERPROFILE to "" — blank USERPROFILE is an already-exercised case in this codebase. src/daemon/schtasks.ts:77 has the same shape: env.USERPROFILE?.trim(), then a domain error, never a system error.

A shape that keeps this PR's thesis and the pre-PR fail-soft behaviour (both call sites still guard with if (localAppData), which is exactly what empty LOCALAPPDATA used to hit):

function resolveWindowsUserAppDataRoot(): string | null {
  const configured = normalizeOptionalString(process.env.LOCALAPPDATA);
  if (configured) {
    return configured;
  }
  let home: string | null = null;
  try {
    home = normalizeOptionalString(os.homedir()) ?? null;
  } catch {
    home = null;
  }
  return home ? path.win32.join(home, "AppData", "Local") : null;
}

localAppData then widens to string | null and installRootByEnvName to Record<string, string | null>; the existing ?? \%${key}%`` fallback already covers a null entry.

The whitespace row above is the same hole in its quieter form: os.homedir() is the only root joined without normalizeOptionalString, so " " yields a drive-relative candidate root instead of falling back to a usable default.

@steipete

steipete commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Maintainer repair is ready on exact head 0c0d34244847bfc59278a6feb1ee1f26cb691a95.

What changed:

  • rebased the accepted repair onto current main without broadening scope;
  • kept the Windows install-root policy inside the browser plugin;
  • centralized blank/default/custom LOCALAPPDATA, ProgramFiles, and ProgramFiles(x86) handling so fallback scanning and registry expansion use the same roots;
  • preserved LZY3538's authored commit and Co-authored-by credit.

Proof on this head:

  • node scripts/run-vitest.mjs extensions/browser/src/browser/chrome.default-browser.test.ts — 11/11 passed;
  • git diff --check origin/main...HEAD — passed;
  • Codex autoreview against origin/main — clean, no accepted/actionable findings;
  • exact-head CI run 29957874365 — passed: https://github.com/openclaw/openclaw/actions/runs/29957874365;
  • review artifacts validate as READY FOR /prepare-pr with zero findings;
  • OPENCLAW_TESTBOX=1 scripts/pr prepare-run 111256 — passed at 2026-07-22T21:37:11Z in hosted_exact_or_recent_parent mode. The prepare gate reused eligible hosted evidence and did not dispatch a standalone remote lease, so there is no separate Testbox lease ID or run URL.

Known proof gap: a fresh native Windows run was attempted, but the broker returned HTTP 401. I did not bypass that boundary or represent the older-head Windows run as exact-head proof. The focused resolver tests cover blank defaults, explicit custom-root precedence, Chrome-only discovery, and registry expansion.

ClawSweeper: the latest completed review has no rank-up moves. The fresh exact-head worker left only a started placeholder and its lease expired without a verdict; there are therefore no new moves to apply. I checked that state before merge rather than treating the placeholder as proof.

@steipete
steipete merged commit 954a508 into openclaw:main Jul 22, 2026
130 of 133 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 23, 2026
…penclaw#111256)

* fix(browser): ignore blank Windows install roots

* fix(browser): complete Windows install root fallbacks

Co-authored-by: LZY3538 <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants