diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8e2c07b03..e43ea1f0f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -64,6 +64,9 @@ - Fixed install.sh falsely reporting success on musl-based systems (such as Alpine Linux) when the binary fails to start; the installer now smoke-tests the binary, exits non-zero on failure, and provides remediation steps. - Fixed Codex config.toml discovery incorrectly importing MCP servers that are configured with enabled = false. - Fixed bash.patterns allow rules rejecting valid commands when quoted arguments contained shell metacharacters (such as Cargo benchmark regex filters). +### Added + +- Added automatic detection of common Ungoogled Chromium Linux installations for the browser tool. ## [17.2.6] - 2026-08-03 diff --git a/packages/coding-agent/src/tools/browser/launch.ts b/packages/coding-agent/src/tools/browser/launch.ts index a3e7c1f5b..76cccf66b 100644 --- a/packages/coding-agent/src/tools/browser/launch.ts +++ b/packages/coding-agent/src/tools/browser/launch.ts @@ -202,10 +202,13 @@ function isExecutableFile(p: string): boolean { /** Flatpak application id published by the Ungoogled Chromium project. */ const UNGOOGLED_CHROMIUM_FLATPAK_ID = "io.github.ungoogled_software.ungoogled_chromium"; -function systemChromiumCandidates(): string[] { - const home = os.homedir(); +function systemChromiumCandidates( + platform: NodeJS.Platform = process.platform, + home = os.homedir(), + which: (name: string) => string | undefined = $which, +): string[] { const candidates: string[] = []; - switch (process.platform) { + switch (platform) { case "darwin": { for (const root of ["/Applications", path.join(home, "Applications")]) { candidates.push( @@ -220,17 +223,9 @@ function systemChromiumCandidates(): string[] { break; } case "linux": { - const names = [ - "google-chrome-stable", - "google-chrome", - "chromium", - "chromium-browser", - "chrome", - "ungoogled-chromium", - "ungoogled-chromium-browser", - ]; + const names = ["google-chrome-stable", "google-chrome", "chromium", "chromium-browser", "chrome"]; for (const name of names) { - const found = $which(name); + const found = which(name); if (found) candidates.push(found); } candidates.push( @@ -241,13 +236,6 @@ function systemChromiumCandidates(): string[] { "/snap/bin/chromium", "/var/lib/flatpak/exports/bin/com.google.Chrome", "/var/lib/flatpak/exports/bin/org.chromium.Chromium", - // Ungoogled Chromium. Distro and AUR packages that keep the plain - // `chromium` name are already covered above; these are the paths - // unique to it, including the system and per-user Flatpak shims. - "/usr/bin/ungoogled-chromium", - "/usr/bin/ungoogled-chromium-browser", - `/var/lib/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`, - path.join(home, ".local/share/flatpak/exports/bin", UNGOOGLED_CHROMIUM_FLATPAK_ID), ); let onNixos = false; try { @@ -256,6 +244,19 @@ function systemChromiumCandidates(): string[] { if (onNixos) { candidates.push(path.join(home, ".nix-profile/bin/chromium"), "/run/current-system/sw/bin/chromium"); } + for (const name of ["ungoogled-chromium", "ungoogled-chromium-browser"]) { + const found = which(name); + if (found) candidates.push(found); + } + candidates.push( + // Ungoogled Chromium. Distro and AUR packages that keep the plain + // `chromium` name are already covered above; these are the paths + // unique to it, including the system and per-user Flatpak shims. + "/usr/bin/ungoogled-chromium", + "/usr/bin/ungoogled-chromium-browser", + `/var/lib/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`, + path.join(home, ".local/share/flatpak/exports/bin", UNGOOGLED_CHROMIUM_FLATPAK_ID), + ); break; } case "win32": { @@ -884,9 +885,13 @@ export async function applyStealthPatches( await injectStealthScripts(page); } -/** Exposes the resolved executable candidate list for detection tests. */ -export function systemChromiumCandidatesForTest(): string[] { - return systemChromiumCandidates(); +/** Exposes executable candidates for detection tests. */ +export function systemChromiumCandidatesForTest( + platform: NodeJS.Platform = process.platform, + home?: string, + which?: (name: string) => string | undefined, +): string[] { + return systemChromiumCandidates(platform, home, which); } export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] { diff --git a/packages/coding-agent/test/tools/browser-launch.test.ts b/packages/coding-agent/test/tools/browser-launch.test.ts index f7e0948e4..c545fed31 100644 --- a/packages/coding-agent/test/tools/browser-launch.test.ts +++ b/packages/coding-agent/test/tools/browser-launch.test.ts @@ -45,24 +45,23 @@ describe("browser launch stealth defaults", () => { const UNGOOGLED_CHROMIUM_FLATPAK_ID = "io.github.ungoogled_software.ungoogled_chromium"; -describe("system Chromium candidates on Linux", () => { - it("offers Ungoogled Chromium executables", () => { - if (process.platform !== "linux") return; - const candidates = systemChromiumCandidatesForTest(); +describe("system Chromium candidates", () => { + const linuxCandidates = (which: (name: string) => string | undefined = () => undefined) => + systemChromiumCandidatesForTest("linux", "/home/test", which); + + it("offers Ungoogled Chromium executables on Linux", () => { + const candidates = linuxCandidates(); expect(candidates).toContain("/usr/bin/ungoogled-chromium"); expect(candidates).toContain("/usr/bin/ungoogled-chromium-browser"); expect(candidates).toContain(`/var/lib/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`); - - const perUserFlatpak = candidates.find(candidate => - candidate.endsWith(`/.local/share/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`), + expect(candidates).toContain( + `/home/test/.local/share/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`, ); - expect(perUserFlatpak).toBeDefined(); }); - it("keeps the previously supported executables", () => { - if (process.platform !== "linux") return; - const candidates = systemChromiumCandidatesForTest(); + it("keeps the previously supported Linux executables", () => { + const candidates = linuxCandidates(); for (const executablePath of [ "/usr/bin/google-chrome-stable", @@ -77,10 +76,10 @@ describe("system Chromium candidates on Linux", () => { } }); - it("ranks Ungoogled Chromium below the stock builds", () => { - if (process.platform !== "linux") return; - const candidates = systemChromiumCandidatesForTest(); - const ungoogled = candidates.indexOf("/usr/bin/ungoogled-chromium"); + it("ranks PATH-resolved Ungoogled Chromium below stock builds", () => { + const ungoogledPath = "/custom/bin/ungoogled-chromium"; + const candidates = linuxCandidates(name => (name === "ungoogled-chromium" ? ungoogledPath : undefined)); + const ungoogled = candidates.indexOf(ungoogledPath); for (const executablePath of [ "/usr/bin/google-chrome-stable", @@ -91,6 +90,13 @@ describe("system Chromium candidates on Linux", () => { expect(ungoogled).toBeGreaterThan(candidates.indexOf(executablePath)); } }); + + it("does not add Ungoogled Chromium candidates on macOS or Windows", () => { + for (const platform of ["darwin", "win32"] as const) { + const candidates = systemChromiumCandidatesForTest(platform, "/home/test", () => "/custom/ungoogled"); + expect(candidates.some(candidate => candidate.toLowerCase().includes("ungoogled"))).toBeFalse(); + } + }); }); describe("browser executable selection", () => {