diff --git a/crates/gyandoot-agent/src/browser_manager.rs b/crates/gyandoot-agent/src/browser_manager.rs index b5707fc..b5a511b 100644 --- a/crates/gyandoot-agent/src/browser_manager.rs +++ b/crates/gyandoot-agent/src/browser_manager.rs @@ -1,10 +1,14 @@ //! BrowserManager — Start, connect, and manage lifecycle of the camoufox-browser daemon. //! -//! Two transport backends: -//! 1. **HTTP** (Docker container on port 9377) — the old REST API, stable -//! 2. **Unix socket** (camoufox-browser@0.1.0 daemon) — new JSON-line IPC +//! Uses `camoufox-js@0.11.0` (pinned, not @latest) for fetching the browser binary, +//! and `camoufox-browser@0.1.0` (pinned) for the daemon Unix socket protocol. //! -//! The BrowserManager tries HTTP first (Docker), then falls back to npx daemon. +//! Pinning rationale: +//! - `camoufox-browser@0.1.0` bundles `camoufox-js@0.9.3` which downloads BROKEN +//! alpha macOS builds (v150.0.2-alpha.26, missing libmozglue.dylib) +//! - `camoufox-js@0.11.0` (latest, Jun 4 2026) downloads STABLE browser binaries +//! - Both packages share the same cache directory (~/Library/Caches/camoufox/). +//! We use the pinned `camoufox-js` for fetch, `camoufox-browser` for daemon. use std::time::Duration; @@ -13,6 +17,18 @@ use tracing::{info, warn}; use gyandoot_browser::{check_daemon_socket, default_socket_path, BrowserClient}; +// ─── Pinned Versions ───────────────────────────────────────────────────── +// These are pinned to specific versions to avoid upstream breakage. +// Update ONLY after testing the new version on both macOS and Linux. + +/// Version of camoufox-js for downloading the browser binary. +/// v0.11.0 (Jun 4 2026) — latest stable, downloads correct macOS arm64/x64 builds. +const CAMOUFOX_JS_VERSION: &str = "0.11.0"; + +/// Version of camoufox-browser for daemon IPC protocol. +/// v0.1.0 (Mar 15 2026) — the only published version, daemon protocol is stable. +const CAMOUFOX_BROWSER_VERSION: &str = "0.1.0"; + /// Default session name. const DEFAULT_SESSION: &str = "default"; @@ -29,35 +45,6 @@ pub enum BrowserInitResult { Unavailable(String), } -/// Platform category for platform-specific error messages. -#[derive(Debug, PartialEq)] -enum OsKind { - Mac, - Linux, - Windows, - Wsl, -} - -fn detect_os() -> OsKind { - if cfg!(target_os = "windows") { - return OsKind::Windows; - } - if cfg!(target_os = "macos") { - return OsKind::Mac; - } - if std::fs::read_to_string("/proc/version") - .map(|c| { - let l = c.to_lowercase(); - l.contains("microsoft") || l.contains("wsl") - }) - .unwrap_or(false) - { - OsKind::Wsl - } else { - OsKind::Linux - } -} - /// Resolve the npx executable name, respecting config and platform. fn resolve_npx(node_path: Option<&str>) -> &str { if let Some(p) = node_path { @@ -72,201 +59,217 @@ fn resolve_npx(node_path: Option<&str>) -> &str { } } -/// Check if the Camoufox Docker container is running on port 9377. -/// Uses a quick HTTP health check. -async fn check_docker_http_api() -> bool { - // Build a reqwest client with a short timeout - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(2)) - .build() - .ok(); - - match client { - Some(c) => match c.get("http://localhost:9377/health").send().await { - Ok(resp) => resp.status().is_success(), - Err(_) => false, - }, - None => false, - } -} - -/// Check if the Camoufox browser binary is installed and uncorrupted. -fn is_browser_installed() -> bool { +/// Cache directory for the Camoufox browser binary. +fn browser_cache_dir() -> std::path::PathBuf { let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/tmp")); - let cache_dir = if cfg!(target_os = "macos") { + if cfg!(target_os = "macos") { home.join("Library").join("Caches").join("camoufox") } else { home.join(".cache").join("camoufox") - }; + } +} - if !cache_dir.exists() { +/// Check if the Camoufox browser binary is installed and appears intact. +fn is_browser_installed() -> bool { + let cache = browser_cache_dir(); + if !cache.exists() { + return false; + } + // Check for the version marker file (written by camoufox-js fetch) + if !cache.join("version.json").exists() { return false; } if cfg!(target_os = "macos") { - let binary = cache_dir.join("Camoufox.app").join("Contents").join("MacOS").join("camoufox"); - binary.exists() + cache.join("Camoufox.app").join("Contents").join("MacOS").join("camoufox").exists() } else { - cache_dir.join("camoufox").exists() + cache.join("camoufox").exists() } } /// Check for known error patterns in daemon stderr. -fn check_known_errors(log: &str) -> Option { +fn check_known_errors(log: &str) -> Option<&'static str> { let lower = log.to_lowercase(); - if lower.contains("libmozglue.dylib") { - return Some(String::from( - "camoufox-browser download corrupted (missing libmozglue.dylib).", - )); + return Some("Browser binary corrupted (missing libmozglue.dylib). Re-run 'gyandoot run' to re-download with the pinned version."); } - if lower.contains("error while loading shared libraries") { - return Some(String::from("Missing Linux system libraries.")); + return Some("Missing Linux system libraries."); } - - if lower.contains("killed") && (lower.contains("sigkill") || lower.contains("exitcode=null")) { - return Some(String::from( - "Browser process killed by OS — corrupted or incompatible binary.", - )); - } - None } -/// Initialize the camoufox-browser daemon and return a ready client. +/// Initialize the browser daemon and return a ready client. /// -/// Detection order: -/// 1. Docker HTTP API on port 9377 (most reliable on macOS) -/// 2. Unix socket daemon via npx (native install) +/// Steps: +/// 1. Ensure browser binary is downloaded using pinned `camoufox-js@0.11.0` +/// 2. Ensure npm package is available for daemon +/// 3. Start daemon via pinned `camoufox-browser@0.1.0` +/// 4. Connect to Unix socket pub async fn init_browser(node_path: Option<&str>) -> BrowserInitResult { let session = DEFAULT_SESSION; let socket_path = default_socket_path(session); + let npx = resolve_npx(node_path); - // ── Try Docker HTTP API first ──────────────────────────────────── - if check_docker_http_api().await { - info!("Camoufox Docker container detected on port 9377 (HTTP API)"); - let client = BrowserClient::new_http("http://localhost:9377"); - return BrowserInitResult::Ready(client); - } - - // ── Try Unix socket daemon ─────────────────────────────────────── + // 1. Already running? if check_daemon_socket(session).await { info!("Camoufox daemon already running on socket: {}", socket_path.display()); - let client = BrowserClient::new().with_session(session); - return BrowserInitResult::Ready(client); + return BrowserInitResult::Ready(BrowserClient::new().with_session(session)); } - info!("Camoufox daemon not running, triggering startup..."); + info!("Camoufox daemon not running, starting up..."); - // Check if browser binary is cached + // 2. Download browser binary if missing (using PINNED camoufox-js) if !is_browser_installed() { - info!("Camoufox browser not cached. Running install..."); - match run_install(node_path).await { - Ok(msg) => info!("Camoufox browser install: {}", msg), + info!("Downloading Camoufox browser using camoufox-js@{}...", CAMOUFOX_JS_VERSION); + match run_fetch(npx, CAMOUFOX_JS_VERSION).await { + Ok(msg) => info!("Camoufox browser fetch: {}", msg), Err(e) => { - let npx_name = resolve_npx(node_path); return BrowserInitResult::Unavailable(format!( "Failed to download Camoufox browser: {}\n\n\ - Run: {} camoufox-browser@latest install\n\n\ - Or use Docker: docker run -d --name camoufox -p 9377:9377 \\\n \ - ghcr.io/nousresearch/camoufox-browser:latest", - e, npx_name, + Try manually: {} camoufox-js@{} fetch", + e, npx, CAMOUFOX_JS_VERSION )); } } } - // Trigger daemon start - match trigger_daemon_start(session, node_path).await { + // 3. Ensure the `camoufox-browser` npm package is globally installed for daemon + if !is_npm_package_installed(npx, "camoufox-browser", CAMOUFOX_BROWSER_VERSION).await { + info!("Installing camoufox-browser@{} (daemon)...", CAMOUFOX_BROWSER_VERSION); + match install_npm_package(npx, "camoufox-browser", CAMOUFOX_BROWSER_VERSION).await { + Ok(_) => info!("camoufox-browser@{} installed", CAMOUFOX_BROWSER_VERSION), + Err(e) => { + return BrowserInitResult::Unavailable(format!( + "Failed to install camoufox-browser: {}\n\n\ + Try manually: npm install -g camoufox-browser@{}", + e, CAMOUFOX_BROWSER_VERSION + )); + } + } + } + + // 4. Trigger daemon start + match trigger_daemon_start(npx, session).await { Ok(()) => { - info!("Waiting for daemon socket at {}...", socket_path.display()); + info!("Waiting for daemon socket..."); match wait_for_socket(session).await { Ok(true) => { - info!("Camoufox daemon started successfully"); - let client = BrowserClient::new().with_session(session); + info!("Camoufox daemon ready"); tokio::time::sleep(Duration::from_millis(1000)).await; - BrowserInitResult::Ready(client) + BrowserInitResult::Ready(BrowserClient::new().with_session(session)) } Ok(false) => { - BrowserInitResult::Unavailable(format!( + let msg = format!( "Daemon did not start within {}s.\n\n\ - The camoufox-browser npm package has a corrupted binary on macOS.\n\ - Use Docker instead:\n \ - docker run -d --name camoufox -p 9377:9377 \\\n \ - ghcr.io/nousresearch/camoufox-browser:latest\n\n\ - Then run 'gyandoot run' again.", + Possible causes:\n \ + 1. Missing Linux system libraries (WSL/Ubuntu):\n \ + sudo apt-get update && sudo apt-get install -y \\\n \ + libgtk-3-0 libnss3 libnspr4 libatk1.0-0t64 \\\n \ + libatk-bridge2.0-0t64 libcups2t64 libdrm2 \\\n \ + libxkbcommon0 libxcomposite1 libxdamage1 \\\n \ + libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \\\n \ + libcairo2 libasound2t64 libxcb-shm0\n \ + 2. Try debugging: {} camoufox-browser@{} tab_list --session default\n \ + Log: {}", DAEMON_STARTUP_TIMEOUT_SECS, - )) + npx, CAMOUFOX_BROWSER_VERSION, socket_path.display() + ); + warn!("{}", msg); + BrowserInitResult::Unavailable(msg) } Err(e) => BrowserInitResult::Unavailable(format!("Daemon error: {}", e)), } } Err(e) => { - let error_detail = e.to_string(); - let known_error = check_known_errors(&error_detail); - let mut msg = format!( - "Camoufox daemon failed to start.\n\nError:\n {}\n\n", - error_detail + "Camoufox daemon failed to start.\n\n{}\n\n", + e ); - - if known_error.is_some() { - msg.push_str( - "The camoufox-browser npm package has a corrupted browser binary on macOS.\n\ - Use Docker instead:\n \ - docker run -d --name camoufox -p 9377:9377 \\\n \ - ghcr.io/nousresearch/camoufox-browser:latest\n\n\ - Then run 'gyandoot run' again.", - ); + if let Some(hint) = check_known_errors(&msg) { + msg.push_str(hint); + msg.push_str(&format!( + "\n\nTry: rm -rf ~/Library/Caches/camoufox && {} gyandoot run", + npx + )); } else { msg.push_str(&format!( - "Try running: {} camoufox-browser@latest tab_list --session default\n\n\ - Or use Docker: docker run -d --name camoufox -p 9377:9377 \\\n \ - ghcr.io/nousresearch/camoufox-browser:latest", - resolve_npx(node_path), + "Debug: {} camoufox-browser@{} tab_list --session default", + npx, CAMOUFOX_BROWSER_VERSION )); } - warn!("{}", msg); BrowserInitResult::Unavailable(msg) } } } -/// Run `npx camoufox-browser@latest install`. -async fn run_install(node_path: Option<&str>) -> Result { - let npx = resolve_npx(node_path); +/// Fetch the browser binary using pinned camoufox-js. +async fn run_fetch(npx: &str, js_version: &str) -> Result { let output = Command::new(npx) - .args(&["camoufox-browser@latest", "install"]) + .args(&["--yes", &format!("camoufox-js@{}", js_version), "fetch"]) .output() .await - .map_err(|e| format!("Cannot run '{} camoufox-browser install': {}", npx, e))?; + .map_err(|e| format!("'{} camoufox-js' not found: {}", npx, e))?; let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); if !output.status.success() { - return Err(format!( - "Install failed (exit: {}): stderr: {}", - output.status.code().unwrap_or(-1), - stderr.lines().last().unwrap_or("(unknown)") - )); + let detail = if !stderr.is_empty() { + stderr.lines().filter(|l| l.contains("Error") || l.contains("error")).last().unwrap_or("(check output)") + } else { + stdout.lines().last().unwrap_or("(unknown)") + }; + return Err(format!("exit {}: {}", output.status.code().unwrap_or(-1), detail)); } - Ok(stdout.lines().last().unwrap_or("Install completed").to_string()) + Ok(stdout.lines().filter(|l| l.contains("complete") || l.contains("Installed") || l.contains("Downloaded")) + .last() + .unwrap_or("Fetch completed") + .to_string()) } -/// Trigger daemon startup via `npx camoufox-browser@latest tab_list`. -async fn trigger_daemon_start(session: &str, node_path: Option<&str>) -> Result<(), String> { +/// Check if an npm package is already installed globally. +async fn is_npm_package_installed(npx: &str, package: &str, version: &str) -> bool { + Command::new(npx) + .args(&["--yes", &format!("{package}@{version}"), "--version"]) + .output() + .await + .map(|o| o.status.success()) + .unwrap_or(false) +} + +/// Install an npm package globally. +async fn install_npm_package(_npx: &str, package: &str, version: &str) -> Result { + let output = Command::new("npm") + .args(&["install", "-g", &format!("{package}@{version}")]) + .output() + .await + .map_err(|e| format!("npm not found: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(stderr.lines().last().unwrap_or("(unknown)").to_string()); + } + + Ok(format!("{package}@{version} installed")) +} + +/// Trigger daemon startup via `npx camoufox-browser@ tab_list`. +async fn trigger_daemon_start(npx: &str, session: &str) -> Result<(), String> { let home = dirs::home_dir().ok_or("Cannot determine home directory")?; let log_dir = home.join(".gyandoot"); std::fs::create_dir_all(&log_dir) .map_err(|e| format!("Cannot create {}: {}", log_dir.display(), e))?; let log_file = log_dir.join("camoufox-daemon-spawn.log"); - let npx = resolve_npx(node_path); let output = Command::new(npx) - .args(&["camoufox-browser@latest", "tab_list", "--session", session]) + .args(&[ + "--yes", + &format!("camoufox-browser@{}", CAMOUFOX_BROWSER_VERSION), + "tab_list", + "--session", session, + ]) .output() .await .map_err(|e| format!("{} not found: {}", npx, e))?; diff --git a/install.sh b/install.sh index db6742e..721f672 100755 --- a/install.sh +++ b/install.sh @@ -192,16 +192,16 @@ install_camofox_browser() { # Step 2: Install the npm package if command -v npx &>/dev/null || command -v npm &>/dev/null; then info "Installing npm package..." - npm install -g camoufox-browser 2>&1 | tail -3 + npm install -g camoufox-browser@0.1.0 2>&1 | tail -3 info "Downloading Camoufox browser binary (this may take a while, ~662MB)..." - npx camoufox-browser@latest install 2>&1 | tail -5 + npx camoufox-js@0.11.0 fetch 2>&1 | tail -5 # Verify the browser binary was actually downloaded if ls ~/.cache/camoufox/camoufox 2>/dev/null; then ok "Camoufox browser binary downloaded (~/.cache/camoufox/)" else - warn "Browser binary not found at ~/.cache/camoufox/. May need manual 'npx camoufox-browser@latest install'" + warn "Browser binary not found at ~/.cache/camoufox/. May need manual 'npx camoufox-js@0.11.0 fetch'" fi ok "Camoufox browser npm package installed" @@ -213,7 +213,7 @@ install_camofox_browser() { ghcr.io/nousresearch/camoufox-browser:latest 2>&1 | tail -3 ok "Camoufox browser installed (Docker)" else - warn "Neither npm nor docker found. Install manually: npm install -g camoufox-browser" + warn "Neither npm nor docker found. Install manually: npm install -g camoufox-browser@0.1.0" fi }