feat(browser): auto-detect NixOS and use system Chromium (#550)
Puppeteer's bundled Chromium is a dynamically-linked FHS binary that cannot run on NixOS. On startup, resolveSystemChromium() checks for /etc/NIXOS and searches for a usable binary in order: 1. chromium on PATH 2. chromium-browser on PATH 3. ~/.nix-profile/bin/chromium 4. /run/current-system/sw/bin/chromium The resolved path is passed as executablePath to puppeteer.launch(). Result is cached per process. On non-NixOS systems the function returns undefined immediately, leaving Puppeteer's default resolution intact.
This commit is contained in:
@@ -280,6 +280,7 @@ Headless browser automation with 14 stealth scripts to evade bot detection:
|
||||
- **Selector flexibility**: CSS, `aria/`, `text/`, `xpath/`, `pierce/` query handlers for Shadow DOM piercing
|
||||
- **Reader mode**: `extract_readable` action uses Mozilla Readability for clean article extraction
|
||||
- **Headless/visible toggle**: Switch modes at runtime via `/browser` command or `browser.headless` setting
|
||||
- **NixOS support**: Automatically detects NixOS (`/etc/NIXOS`) and resolves a system Chromium (`chromium` on PATH, `~/.nix-profile/bin/chromium`, or `/run/current-system/sw/bin/chromium`) since Puppeteer's bundled binary cannot run on a non-FHS system
|
||||
|
||||
### + Cursor Provider
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { Readability } from "@mozilla/readability";
|
||||
@@ -61,6 +61,56 @@ async function loadPuppeteer(): Promise<typeof Puppeteer> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On NixOS, Puppeteer's bundled Chromium is a dynamically-linked FHS binary and
|
||||
* cannot run as-is. Detect the platform and resolve a system-installed Chromium
|
||||
* so `puppeteer.launch()` can use it instead of the bundled one.
|
||||
*
|
||||
* Detection order:
|
||||
* 1. `chromium` on PATH
|
||||
* 2. `chromium-browser` on PATH
|
||||
* 3. ~/.nix-profile/bin/chromium (user profile)
|
||||
* 4. /run/current-system/sw/bin/chromium (system profile)
|
||||
*
|
||||
* Returns `undefined` on non-NixOS systems or when no binary is found, which
|
||||
* causes Puppeteer to fall back to its default resolution.
|
||||
*/
|
||||
let _resolvedChromium: string | null | undefined; // undefined = unchecked; null = not found
|
||||
function resolveSystemChromium(): string | undefined {
|
||||
if (_resolvedChromium !== undefined) return _resolvedChromium ?? undefined;
|
||||
try {
|
||||
if (!fs.existsSync("/etc/NIXOS")) {
|
||||
_resolvedChromium = null;
|
||||
return undefined;
|
||||
}
|
||||
} catch {
|
||||
_resolvedChromium = null;
|
||||
return undefined;
|
||||
}
|
||||
const candidates = [
|
||||
Bun.which("chromium"),
|
||||
Bun.which("chromium-browser"),
|
||||
path.join(os.homedir(), ".nix-profile/bin/chromium"),
|
||||
"/run/current-system/sw/bin/chromium",
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (candidate) {
|
||||
try {
|
||||
if (fs.existsSync(candidate)) {
|
||||
_resolvedChromium = candidate;
|
||||
logger.debug("NixOS: using system Chromium", { path: candidate });
|
||||
return candidate;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
_resolvedChromium = null;
|
||||
logger.debug("NixOS detected but no Chromium binary found; Puppeteer may fail to launch");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
const MAX_TIMEOUT_SECONDS = 120;
|
||||
const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1.25 };
|
||||
const STEALTH_IGNORE_DEFAULT_ARGS = [
|
||||
"--disable-extensions",
|
||||
@@ -548,6 +598,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
||||
this.#browser = await puppeteer.launch({
|
||||
headless: this.#currentHeadless,
|
||||
defaultViewport: this.#currentHeadless ? initialViewport : null,
|
||||
executablePath: resolveSystemChromium(),
|
||||
args: launchArgs,
|
||||
ignoreDefaultArgs: [...STEALTH_IGNORE_DEFAULT_ARGS],
|
||||
});
|
||||
@@ -1382,7 +1433,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
||||
} else {
|
||||
dest = path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.png`);
|
||||
}
|
||||
await fs.mkdir(path.dirname(dest), { recursive: true });
|
||||
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
||||
// Full-res buffer when saving to a user-defined location; resized (API copy) for temp-only.
|
||||
const saveFullRes = !!(paramPath || screenshotDir);
|
||||
const savedBuffer = saveFullRes ? buffer : resized.buffer;
|
||||
|
||||
Reference in New Issue
Block a user