fix(coding-agent): validate Linux browser executables

This commit is contained in:
metaphorics
2026-08-05 10:50:31 +00:00
parent 06477855d1
commit ecb22957cf
2 changed files with 48 additions and 4 deletions
@@ -133,7 +133,7 @@ let chromiumExecutablePromise: Promise<string | undefined> | undefined;
export async function ensureChromiumExecutable(): Promise<string | undefined> { export async function ensureChromiumExecutable(): Promise<string | undefined> {
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH; const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
if (envPath) return envPath; if (envPath) return envPath;
const sysChrome = resolveSystemChromium(); const sysChrome = await resolveSystemChromium();
if (sysChrome) return sysChrome; if (sysChrome) return sysChrome;
if (chromiumExecutablePromise) return chromiumExecutablePromise; if (chromiumExecutablePromise) return chromiumExecutablePromise;
@@ -193,7 +193,25 @@ let resolvedChromium: string | null | undefined; // undefined = unchecked; null
function isExecutableFile(p: string): boolean { function isExecutableFile(p: string): boolean {
try { try {
const st = fs.statSync(p); const st = fs.statSync(p);
return st.isFile(); if (!st.isFile()) return false;
if (process.platform === "win32") return true;
fs.accessSync(p, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
async function isChromiumExecutable(p: string): Promise<boolean> {
if (!isExecutableFile(p)) return false;
try {
const proc = Bun.spawn([p, "--version"], {
stdout: "pipe",
stderr: "ignore",
});
const stdout = await new Response(proc.stdout).text();
await proc.exited;
return proc.exitCode === 0 && /Chrom|Edg/i.test(stdout);
} catch { } catch {
return false; return false;
} }
@@ -278,13 +296,13 @@ function systemChromiumCandidates(
return candidates; return candidates;
} }
function resolveSystemChromium(): string | undefined { async function resolveSystemChromium(): Promise<string | undefined> {
if (resolvedChromium !== undefined) return resolvedChromium ?? undefined; if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
const seen = new Set<string>(); const seen = new Set<string>();
for (const candidate of systemChromiumCandidates()) { for (const candidate of systemChromiumCandidates()) {
if (!candidate || seen.has(candidate)) continue; if (!candidate || seen.has(candidate)) continue;
seen.add(candidate); seen.add(candidate);
if (isExecutableFile(candidate)) { if (await isChromiumExecutable(candidate)) {
resolvedChromium = candidate; resolvedChromium = candidate;
logger.debug("Using system Chrome/Chromium", { path: candidate }); logger.debug("Using system Chrome/Chromium", { path: candidate });
return candidate; return candidate;
@@ -894,6 +912,10 @@ export function systemChromiumCandidatesForTest(
return systemChromiumCandidates(platform, home, which); return systemChromiumCandidates(platform, home, which);
} }
export async function chromiumExecutableProbeForTest(executablePath: string): Promise<boolean> {
return isChromiumExecutable(executablePath);
}
export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] { export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
return stealthIgnoreDefaultArgs(executablePath); return stealthIgnoreDefaultArgs(executablePath);
} }
@@ -1,7 +1,9 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import { import {
chromiumExecutableProbeForTest,
stealthIgnoreDefaultArgsForTest, stealthIgnoreDefaultArgsForTest,
systemChromiumCandidatesForTest, systemChromiumCandidatesForTest,
} from "@oh-my-pi/pi-coding-agent/tools/browser/launch"; } from "@oh-my-pi/pi-coding-agent/tools/browser/launch";
@@ -97,6 +99,26 @@ describe("system Chromium candidates", () => {
}); });
describe("browser executable selection", () => { describe("browser executable selection", () => {
it.skipIf(process.platform === "win32")(
"rejects executable wrappers that are not Chromium-family browsers",
async () => {
const tempDir = TempDir.createSync("@browser-probe-");
try {
const wrapper = path.join(tempDir.path(), "google-chrome");
const chromium = path.join(tempDir.path(), "chromium");
await Bun.write(wrapper, "#!/bin/sh\necho browser bridge\n");
await Bun.write(chromium, "#!/bin/sh\necho Chromium 123.0\n");
fs.chmodSync(wrapper, 0o755);
fs.chmodSync(chromium, 0o755);
await expect(chromiumExecutableProbeForTest(wrapper)).resolves.toBe(false);
await expect(chromiumExecutableProbeForTest(chromium)).resolves.toBe(true);
} finally {
await tempDir.remove();
}
},
);
it("honors PUPPETEER_EXECUTABLE_PATH before a detected Windows system Chrome", async () => { it("honors PUPPETEER_EXECUTABLE_PATH before a detected Windows system Chrome", async () => {
const tempDir = TempDir.createSync("@browser-executable-"); const tempDir = TempDir.createSync("@browser-executable-");
try { try {