diff --git a/README.md b/README.md index 9bfa55d90..b9711948e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/packages/coding-agent/src/tools/browser.ts b/packages/coding-agent/src/tools/browser.ts index 7445d3271..5a103cb1b 100644 --- a/packages/coding-agent/src/tools/browser.ts +++ b/packages/coding-agent/src/tools/browser.ts @@ -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 { } } +/** + * 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