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> {
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
if (envPath) return envPath;
const sysChrome = resolveSystemChromium();
const sysChrome = await resolveSystemChromium();
if (sysChrome) return sysChrome;
if (chromiumExecutablePromise) return chromiumExecutablePromise;
@@ -193,7 +193,25 @@ let resolvedChromium: string | null | undefined; // undefined = unchecked; null
function isExecutableFile(p: string): boolean {
try {
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 {
return false;
}
@@ -278,13 +296,13 @@ function systemChromiumCandidates(
return candidates;
}
function resolveSystemChromium(): string | undefined {
async function resolveSystemChromium(): Promise<string | undefined> {
if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
const seen = new Set<string>();
for (const candidate of systemChromiumCandidates()) {
if (!candidate || seen.has(candidate)) continue;
seen.add(candidate);
if (isExecutableFile(candidate)) {
if (await isChromiumExecutable(candidate)) {
resolvedChromium = candidate;
logger.debug("Using system Chrome/Chromium", { path: candidate });
return candidate;
@@ -894,6 +912,10 @@ export function systemChromiumCandidatesForTest(
return systemChromiumCandidates(platform, home, which);
}
export async function chromiumExecutableProbeForTest(executablePath: string): Promise<boolean> {
return isChromiumExecutable(executablePath);
}
export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
return stealthIgnoreDefaultArgs(executablePath);
}
@@ -1,7 +1,9 @@
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import {
chromiumExecutableProbeForTest,
stealthIgnoreDefaultArgsForTest,
systemChromiumCandidatesForTest,
} from "@oh-my-pi/pi-coding-agent/tools/browser/launch";
@@ -97,6 +99,26 @@ describe("system Chromium candidates", () => {
});
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 () => {
const tempDir = TempDir.createSync("@browser-executable-");
try {