fix(browser): pin camoufox-js@0.11.0 and camoufox-browser@0.1.0 — no more @latest
Root cause: camoufox-browser@0.1.0 bundles camoufox-js@0.9.3 which downloads a BROKEN alpha binary (v150.0.2-alpha.26, missing libmozglue.dylib) on macOS. The newer camoufox-js@0.11.0 (Jun 4 2026) downloads STABLE builds. Changes: 1. PINNED VERSIONS (constants at top of file): - CAMOUFOX_JS_VERSION = "0.11.0" — for fetching browser binary - CAMOUFOX_BROWSER_VERSION = "0.1.0" — for daemon IPC protocol 2. run_fetch() — uses 'npx --yes camoufox-js@0.11.0 fetch' instead of 'camoufox-browser@latest install' (which called old camoufox-js) 3. Both packages share ~/Library/Caches/camoufox/ — fetch writes correct binary + version.json, daemon reads same cache 4. Removed Docker HTTP transport — no longer needed, daemon works on macOS 5. Removed reqwest dep from gyandoot-agent + gyandoot-browser 6. install.sh updated: uses camoufox-js@0.11.0 fetch + pinned npm package 7. install.sh verifies version.json exists, not just binary Clean build: 0 errors, 0 warnings No Docker dependency. No @latest. Pinned and stable.
This commit is contained in:
@@ -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<String> {
|
||||
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<String, String> {
|
||||
let npx = resolve_npx(node_path);
|
||||
/// Fetch the browser binary using pinned camoufox-js.
|
||||
async fn run_fetch(npx: &str, js_version: &str) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
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@<pinned> 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))?;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user