diff --git a/scripts/setup-linux.sh b/scripts/setup-linux.sh new file mode 100755 index 0000000..6b41a28 --- /dev/null +++ b/scripts/setup-linux.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# ZeroGravity — Linux setup +# Creates the zerogravity-ls system user for UID-scoped iptables isolation, +# installs the systemd user service, and builds the dns_redirect.so preload lib. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# ── 1. System user for UID isolation ── +echo "→ Creating zerogravity-ls system user…" +if id -u zerogravity-ls &>/dev/null; then + echo " Already exists." +else + sudo useradd --system --no-create-home --shell /usr/sbin/nologin zerogravity-ls + echo " Created." +fi + +# ── 2. Sudoers rule (run commands as zerogravity-ls without password) ── +SUDOERS="/etc/sudoers.d/zerogravity" +echo "→ Installing sudoers rule…" +if [ -f "$SUDOERS" ]; then + echo " Already exists." +else + echo "$USER ALL=(zerogravity-ls) NOPASSWD: ALL" | sudo tee "$SUDOERS" > /dev/null + sudo chmod 0440 "$SUDOERS" + echo " Installed: $SUDOERS" +fi + +# ── 3. Data directory permissions ── +echo "→ Setting up /tmp/zerogravity-standalone…" +sudo mkdir -p /tmp/zerogravity-standalone +sudo chmod 1777 /tmp/zerogravity-standalone + +# ── 4. Config directory ── +echo "→ Setting up ~/.config/zerogravity…" +mkdir -p "$HOME/.config/zerogravity" + +# ── 5. Systemd user service ── +echo "→ Installing systemd user service…" +UNIT_DIR="$HOME/.config/systemd/user" +mkdir -p "$UNIT_DIR" +cat > "$UNIT_DIR/zerogravity.service" << EOF +[Unit] +Description=ZeroGravity Proxy +After=network.target + +[Service] +Type=simple +ExecStart=$PROJECT_DIR/target/release/zerogravity +WorkingDirectory=$PROJECT_DIR +Environment=RUST_LOG=info +Restart=on-failure +RestartSec=3 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target +EOF +systemctl --user daemon-reload +echo " Installed: $UNIT_DIR/zerogravity.service" +echo " Enable with: systemctl --user enable zerogravity" + +# ── 6. Build ── +echo "→ Building release binary…" +cd "$PROJECT_DIR" +cargo build --release 2>&1 | tail -1 +echo "" +echo "✓ Setup complete. Start with: zg start" diff --git a/scripts/setup-macos.sh b/scripts/setup-macos.sh new file mode 100755 index 0000000..c6ea782 --- /dev/null +++ b/scripts/setup-macos.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# ZeroGravity — macOS setup +# Installs a launchd plist for automatic startup and sets up config directories. +# No UID isolation on macOS — runs in headless/HTTPS_PROXY mode only. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +CONFIG_DIR="$HOME/Library/Application Support/zerogravity" + +# ── 1. Config directory ── +echo "→ Setting up config directory…" +mkdir -p "$CONFIG_DIR" + +# ── 2. Data directory ── +echo "→ Setting up /tmp/zerogravity-standalone…" +mkdir -p /tmp/zerogravity-standalone + +# ── 3. Launchd plist ── +echo "→ Installing launchd plist…" +PLIST_DIR="$HOME/Library/LaunchAgents" +PLIST="$PLIST_DIR/com.zerogravity.proxy.plist" +mkdir -p "$PLIST_DIR" +cat > "$PLIST" << EOF + + + + + Label + com.zerogravity.proxy + ProgramArguments + + $PROJECT_DIR/target/release/zerogravity + + WorkingDirectory + $PROJECT_DIR + EnvironmentVariables + + RUST_LOG + info + + KeepAlive + + SuccessfulExit + + + StandardOutPath + $HOME/Library/Logs/zerogravity.log + StandardErrorPath + $HOME/Library/Logs/zerogravity.log + + +EOF +echo " Installed: $PLIST" +echo " Start with: launchctl load $PLIST" +echo " Stop with: launchctl unload $PLIST" + +# ── 4. Build ── +echo "→ Building release binary…" +cd "$PROJECT_DIR" +cargo build --release 2>&1 | tail -1 +echo "" +echo "✓ Setup complete." +echo " Start with: launchctl load $PLIST" +echo " Or manually: zg start" diff --git a/src/backend.rs b/src/backend.rs index 78bc6dc..8573caa 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -51,7 +51,7 @@ static STATIC_HEADERS: LazyLock = LazyLock::new(|| { h.insert(HeaderName::from_static("sec-ch-ua-mobile"), hv("?0")); h.insert( HeaderName::from_static("sec-ch-ua-platform"), - hv("\"Linux\""), + hv(&format!("\"{}\"", crate::platform::Platform::detect().os_name)), ); h.insert("Sec-Fetch-Dest", hv("empty")); h.insert("Sec-Fetch-Mode", hv("cors")); @@ -499,12 +499,11 @@ impl Backend { fn discover() -> Result { // Try to find the real LS binary first (when MITM wrapper is installed, - // the wrapper is a shell script named language_server_linux_x64, while - // the real binary is language_server_linux_x64.real) + // the wrapper is a shell script, while the real binary has .real suffix) let pid_output = Command::new("sh") .args([ "-c", - "pgrep -f 'language_server_linux_x64\\.real' | head -1", + "pgrep -f 'language_server.*\\.real' | head -1", ]) .output() .map_err(|e| format!("pgrep failed: {e}"))?; @@ -513,10 +512,10 @@ fn discover() -> Result { .trim() .to_string(); - // Fallback: find any language_server_linux process + // Fallback: find any language_server process if pid.is_empty() { let pid_output = Command::new("sh") - .args(["-c", "pgrep -f language_server_linux | head -1"]) + .args(["-c", "pgrep -f language_server | head -1"]) .output() .map_err(|e| format!("pgrep failed: {e}"))?; pid = String::from_utf8_lossy(&pid_output.stdout) @@ -611,8 +610,7 @@ fn discover() -> Result { .ok() .filter(|s| !s.is_empty()) .or_else(|| { - let home = std::env::var("HOME").unwrap_or_default(); - let path = format!("{home}/.config/zerogravity/token"); + let path = token_file_path(); fs::read_to_string(&path) .ok() .map(|s| s.trim().to_string()) diff --git a/src/constants.rs b/src/constants.rs index 372d05a..efba3ad 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -21,26 +21,54 @@ struct DetectedVersions { /// back to its binary, then walking up to the app root. Falls back to /// well-known install paths. fn find_install_dir() -> Option { - // 1. Try tracing the running language server → /usr/share/antigravity/resources/app/extensions/... - if let Ok(output) = Command::new("sh") - .args(["-c", "pgrep -f language_server_linux | head -1"]) - .output() + let p = crate::platform::Platform::detect(); + + // 1. Check if platform-detected app_root exists + let app_root_parent = std::path::Path::new(&p.app_root) + .parent() + .and_then(|p| p.parent()) + .map(|p| p.to_string_lossy().to_string()); + if let Some(ref dir) = app_root_parent { + if fs::metadata(format!("{dir}/resources/app/product.json")).is_ok() { + return Some(dir.clone()); + } + } + + // 2. Try tracing the running language server via /proc (Linux only) + #[cfg(target_os = "linux")] { - let pid = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if !pid.is_empty() { - if let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) { - let exe_str = exe.to_string_lossy().to_string(); - // exe is like: /usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64 - // We want: /usr/share/antigravity - if let Some(idx) = exe_str.find("/resources/") { - return Some(exe_str[..idx].to_string()); + if let Ok(output) = Command::new("sh") + .args(["-c", "pgrep -f language_server | head -1"]) + .output() + { + let pid = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !pid.is_empty() { + if let Ok(exe) = fs::read_link(format!("/proc/{pid}/exe")) { + let exe_str = exe.to_string_lossy().to_string(); + if let Some(idx) = exe_str.find("/resources/") { + return Some(exe_str[..idx].to_string()); + } } } } } - // 2. Fall back to well-known install paths - for path in &["/usr/share/antigravity", "/opt/Antigravity"] { + // 3. Fall back to well-known install paths + #[cfg(target_os = "linux")] + let candidates = ["/usr/share/antigravity", "/opt/Antigravity"]; + #[cfg(target_os = "macos")] + let candidates = [ + "/Applications/Antigravity.app/Contents", + &format!("{}/Applications/Antigravity.app/Contents", std::env::var("HOME").unwrap_or_default()), + ]; + #[cfg(target_os = "windows")] + let candidates = [ + &format!("{}\\Programs\\Antigravity", std::env::var("LOCALAPPDATA").unwrap_or_default()), + ]; + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + let candidates: [&str; 0] = []; + + for path in &candidates { if fs::metadata(format!("{path}/resources/app/product.json")).is_ok() { return Some(path.to_string()); } @@ -178,14 +206,23 @@ pub const LS_SERVICE: &str = "exa.language_server_pb.LanguageServerService"; /// Log base directory for Antigravity. pub fn log_base() -> String { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - format!("{home}/.config/Antigravity/logs") + let p = crate::platform::Platform::detect(); + // Antigravity logs live next to its state DB + let state_parent = std::path::Path::new(&p.state_db_path) + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + format!("{home}/.config/Antigravity") + }); + format!("{state_parent}/logs") } /// Token file path. pub fn token_file_path() -> String { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - format!("{home}/.config/zerogravity/token") + crate::platform::Platform::detect().token_path.to_string_lossy().to_string() } /// User-Agent string matching the Electron webview — computed once. diff --git a/src/main.rs b/src/main.rs index ab38620..8c774f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod api; mod backend; mod constants; mod mitm; +mod platform; mod proto; mod quota; mod session; @@ -123,7 +124,7 @@ async fn main() { .status(); // Also kill any leftover standalone LS processes let _ = std::process::Command::new("pkill") - .args(["-f", "language_server_linux.*antigravity-standalone"]) + .args(["-f", "language_server.*antigravity-standalone"]) .status(); // Retry once match tokio::net::TcpListener::bind(&addr).await { @@ -240,8 +241,7 @@ async fn main() { .ok() .filter(|s| !s.is_empty()) .or_else(|| { - let home = std::env::var("HOME").unwrap_or_default(); - let path = format!("{home}/.config/zerogravity/token"); + let path = crate::constants::token_file_path(); std::fs::read_to_string(&path) .ok() .map(|s| s.trim().to_string()) @@ -483,6 +483,7 @@ fn check_wrapper_installed() -> bool { } /// Find the LS binary path by reading /proc//exe for known language server processes. +#[cfg(target_os = "linux")] fn find_ls_binary_path() -> Option { // Try all running processes, look for ones that look like the LS let proc = std::path::Path::new("/proc"); @@ -505,6 +506,8 @@ fn find_ls_binary_path() -> Option { let target_clean = target_str.trim_end_matches(" (deleted)"); // Match any binary that looks like the Antigravity LS if target_clean.contains("language_server_linux") + || target_clean.contains("language_server_darwin") + || target_clean.contains("language_server_windows") || target_clean.contains("antigravity-language-server") { // Strip .real suffix — if the wrapper exec'd the backup, we want the base name @@ -517,10 +520,12 @@ fn find_ls_binary_path() -> Option { None } +#[cfg(not(target_os = "linux"))] +fn find_ls_binary_path() -> Option { + None +} + /// Get the data directory for storing MITM CA cert/key. fn dirs_data_dir() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); - std::path::PathBuf::from(home) - .join(".config") - .join("zerogravity") + crate::platform::Platform::detect().config_dir } diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..001f48c --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,316 @@ +//! Platform detection and path resolution. +//! +//! All platform-specific paths are resolved here. Every path can be overridden +//! via environment variables, falling back to OS-specific defaults. +//! +//! # Env Var Overrides +//! +//! | Variable | Description | +//! |----------|-------------| +//! | `ZEROGRAVITY_LS_PATH` | Path to the Language Server binary | +//! | `ZEROGRAVITY_APP_ROOT` | Antigravity app root directory | +//! | `ZEROGRAVITY_DATA_DIR` | Standalone LS data directory | +//! | `ZEROGRAVITY_CONFIG_DIR` | ZeroGravity config directory | +//! | `ZEROGRAVITY_LS_USER` | System user for LS isolation (Linux only) | +//! | `ZEROGRAVITY_STATE_DB` | Path to Antigravity's state.vscdb | +//! | `SSL_CERT_FILE` | System CA certificate bundle | + +use std::path::PathBuf; + +/// All platform-specific paths, resolved once at startup. +#[derive(Debug, Clone)] +pub struct Platform { + /// Path to the Language Server binary. + pub ls_binary_path: String, + /// Antigravity app root (for ANTIGRAVITY_EDITOR_APP_ROOT). + pub app_root: String, + /// Data directory for standalone LS runtime files. + pub data_dir: String, + /// Config directory (~/.config/zerogravity or platform equivalent). + pub config_dir: PathBuf, + /// System CA certificate bundle path. + pub ca_cert_path: String, + /// System user for UID-scoped isolation (Linux only). + pub ls_user: String, + /// Path to Antigravity's state.vscdb. + pub state_db_path: String, + /// Token file path. + pub token_path: PathBuf, + /// Traces directory. + pub traces_dir: PathBuf, + /// DNS redirect shared library path (Linux only). + pub dns_redirect_so_path: String, + /// OS display name for sec-ch-ua-platform header ("Linux", "macOS", "Windows"). + pub os_name: &'static str, +} + +impl Platform { + /// Detect platform and resolve all paths. + /// + /// Environment variables override platform defaults. + pub fn detect() -> Self { + let home = home_dir(); + let config_dir = env_or("ZEROGRAVITY_CONFIG_DIR", || default_config_dir(&home)); + + let ls_binary_path = env_or("ZEROGRAVITY_LS_PATH", || default_ls_binary_path()); + let app_root = env_or("ZEROGRAVITY_APP_ROOT", || default_app_root()); + let data_dir = env_or("ZEROGRAVITY_DATA_DIR", || default_data_dir()); + let ca_cert_path = env_or("SSL_CERT_FILE", || default_ca_cert_path()); + let ls_user = env_or("ZEROGRAVITY_LS_USER", || "zerogravity-ls".into()); + let state_db_path = env_or("ZEROGRAVITY_STATE_DB", || default_state_db_path(&home)); + let dns_redirect_so_path = format!("{}/dns-redirect.so", &data_dir); + + let config_dir = PathBuf::from(&config_dir); + let token_path = config_dir.join("token"); + let traces_dir = config_dir.join("traces"); + + Self { + ls_binary_path, + app_root, + data_dir, + config_dir, + ca_cert_path, + ls_user, + state_db_path, + token_path, + traces_dir, + dns_redirect_so_path, + os_name: default_os_name(), + } + } +} + +// ── Helpers ── + +fn home_dir() -> String { + std::env::var("HOME") + .or_else(|_| std::env::var("USERPROFILE")) // Windows + .unwrap_or_else(|_| "/tmp".into()) +} + +fn env_or(var: &str, default: impl FnOnce() -> String) -> String { + std::env::var(var).unwrap_or_else(|_| default()) +} + +// ── Platform defaults ── + +#[cfg(target_os = "linux")] +fn default_ls_binary_path() -> String { + "/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64" + .into() +} + +#[cfg(target_os = "macos")] +fn default_ls_binary_path() -> String { + let home = home_dir(); + // Check both /Applications and ~/Applications + for base in &[ + "/Applications/Antigravity.app", + &format!("{home}/Applications/Antigravity.app"), + ] { + let path = format!( + "{base}/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64" + ); + if std::path::Path::new(&path).exists() { + return path; + } + } + "/Applications/Antigravity.app/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64".into() +} + +#[cfg(target_os = "windows")] +fn default_ls_binary_path() -> String { + let local = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Users\\Default\\AppData\\Local".into()); + format!("{local}\\Programs\\Antigravity\\resources\\app\\extensions\\antigravity\\bin\\language_server_windows_x64.exe") +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +fn default_ls_binary_path() -> String { + "language_server".into() +} + +// ── App root ── + +#[cfg(target_os = "linux")] +fn default_app_root() -> String { + "/usr/share/antigravity/resources/app".into() +} + +#[cfg(target_os = "macos")] +fn default_app_root() -> String { + "/Applications/Antigravity.app/Contents/Resources/app".into() +} + +#[cfg(target_os = "windows")] +fn default_app_root() -> String { + let local = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\Users\\Default\\AppData\\Local".into()); + format!("{local}\\Programs\\Antigravity\\resources\\app") +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +fn default_app_root() -> String { + ".".into() +} + +// ── Data dir ── + +fn default_data_dir() -> String { + #[cfg(target_os = "windows")] + { + let temp = std::env::var("TEMP").unwrap_or_else(|_| "C:\\Temp".into()); + format!("{temp}\\zerogravity-standalone") + } + #[cfg(not(target_os = "windows"))] + { + "/tmp/zerogravity-standalone".into() + } +} + +// ── Config dir ── + +fn default_config_dir(home: &str) -> String { + #[cfg(target_os = "macos")] + { + format!("{home}/Library/Application Support/zerogravity") + } + #[cfg(target_os = "windows")] + { + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| format!("{home}\\AppData\\Roaming")); + format!("{appdata}\\zerogravity") + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + format!("{home}/.config/zerogravity") + } +} + +// ── CA certs ── + +fn default_ca_cert_path() -> String { + #[cfg(target_os = "macos")] + { + "/etc/ssl/cert.pem".into() + } + #[cfg(target_os = "windows")] + { + // Windows uses native cert store, this is a fallback + String::new() + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + // Try common Linux paths + for path in &[ + "/etc/ssl/certs/ca-certificates.crt", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem", + ] { + if std::path::Path::new(path).exists() { + return path.to_string(); + } + } + "/etc/ssl/certs/ca-certificates.crt".into() + } +} + +// ── State DB ── + +fn default_state_db_path(home: &str) -> String { + #[cfg(target_os = "macos")] + { + format!("{home}/Library/Application Support/Antigravity/User/globalStorage/state.vscdb") + } + #[cfg(target_os = "windows")] + { + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| format!("{home}\\AppData\\Roaming")); + format!("{appdata}\\Antigravity\\User\\globalStorage\\state.vscdb") + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + format!("{home}/.config/Antigravity/User/globalStorage/state.vscdb") + } +} + +// ── OS name ── + +fn default_os_name() -> &'static str { + #[cfg(target_os = "linux")] + { "Linux" } + #[cfg(target_os = "macos")] + { "macOS" } + #[cfg(target_os = "windows")] + { "Windows" } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { "Unknown" } +} + +// ── Platform queries ── + +/// Returns true if running on Linux. +pub fn is_linux() -> bool { + cfg!(target_os = "linux") +} + +/// Returns true if running on macOS. +#[allow(dead_code)] +pub fn is_macos() -> bool { + cfg!(target_os = "macos") +} + +/// Returns true if running on Windows. +#[allow(dead_code)] +pub fn is_windows() -> bool { + cfg!(target_os = "windows") +} + +/// Returns true if UID isolation (iptables + dedicated user) is available. +/// +/// Only supported on Linux with the zerogravity-ls system user. +pub fn supports_uid_isolation() -> bool { + #[cfg(target_os = "linux")] + { + std::process::Command::new("id") + .args(["-u", "zerogravity-ls"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } + #[cfg(not(target_os = "linux"))] + { + false + } +} + +/// Returns true if LD_PRELOAD DNS redirect is supported (Linux only). +pub fn supports_ld_preload() -> bool { + cfg!(target_os = "linux") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_detect_doesnt_panic() { + let p = Platform::detect(); + assert!(!p.ls_binary_path.is_empty()); + assert!(!p.app_root.is_empty()); + assert!(!p.data_dir.is_empty()); + } + + #[test] + fn test_env_override() { + std::env::set_var("ZEROGRAVITY_LS_PATH", "/custom/ls"); + let p = Platform::detect(); + assert_eq!(p.ls_binary_path, "/custom/ls"); + std::env::remove_var("ZEROGRAVITY_LS_PATH"); + } + + #[test] + fn test_config_dir_has_token_and_traces() { + let p = Platform::detect(); + assert!(p.token_path.ends_with("token")); + assert!(p.traces_dir.ends_with("traces")); + } +} diff --git a/src/standalone/discovery.rs b/src/standalone/discovery.rs index 77edbd8..0ff4fbd 100644 --- a/src/standalone/discovery.rs +++ b/src/standalone/discovery.rs @@ -1,6 +1,7 @@ //! LS process discovery — finding, inspecting, and managing LS processes. -use super::{MainLSConfig, LS_USER}; +use super::{paths, MainLSConfig}; +use crate::platform; use crate::proto::wire::extract_proto_string; use std::net::TcpListener; use std::process::{Command, Stdio}; @@ -87,6 +88,8 @@ pub(super) fn find_main_ls_pid() -> Result { let target_clean = target_str.trim_end_matches(" (deleted)"); // Must be the actual LS binary, not a bash script if target_clean.contains("language_server_linux") + || target_clean.contains("language_server_darwin") + || target_clean.contains("language_server_windows") || target_clean.contains("antigravity-language-server") { return Ok(name_str.to_string()); @@ -107,18 +110,9 @@ pub(super) fn find_free_port() -> Result { .map_err(|e| format!("Failed to get port: {e}")) } -/// Check if the dedicated LS system user exists. -/// -/// When the user exists, the proxy spawns the LS as that UID so iptables -/// can scope the :443 redirect to only the standalone LS process. +/// Check if the dedicated LS system user exists (Linux only). pub(super) fn has_ls_user() -> bool { - Command::new("id") - .args(["-u", LS_USER]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) + platform::supports_uid_isolation() } /// Find the PID of a language_server process owned by a specific user. @@ -126,7 +120,7 @@ pub(super) fn has_ls_user() -> bool { /// Used to discover the actual LS process after sudo spawns it as a different user. pub(super) fn find_ls_pid_for_user(user: &str) -> Result { let output = Command::new("pgrep") - .args(["-u", user, "-f", "language_server_linux"]) + .args(["-u", user, "-f", "language_server"]) .output() .map_err(|e| format!("pgrep failed: {e}"))?; @@ -152,9 +146,11 @@ pub(super) fn cleanup_orphaned_ls() { return; } - // Find all LS processes owned by antigravity-ls user + let ls_user = &paths().ls_user; + + // Find all LS processes owned by the LS user let output = Command::new("pgrep") - .args(["-u", LS_USER, "-f", "language_server_linux"]) + .args(["-u", ls_user.as_str(), "-f", "language_server"]) .output(); let pids: Vec = match output { @@ -180,7 +176,7 @@ pub(super) fn cleanup_orphaned_ls() { // and the sudoers rule allows ALL commands as antigravity-ls. for pid in &pids { let ok = Command::new("sudo") - .args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()]) + .args(["-n", "-u", ls_user.as_str(), "kill", "-TERM", &pid.to_string()]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() @@ -204,7 +200,7 @@ pub(super) fn cleanup_orphaned_ls() { // Force-kill any survivors let still_alive = Command::new("pgrep") - .args(["-u", LS_USER, "-f", "language_server_linux"]) + .args(["-u", ls_user.as_str(), "-f", "language_server"]) .output() .map(|o| !o.stdout.is_empty()) .unwrap_or(false); @@ -213,7 +209,7 @@ pub(super) fn cleanup_orphaned_ls() { info!("Orphaned LS still alive, force killing"); for pid in &pids { let _ = Command::new("sudo") - .args(["-n", "-u", LS_USER, "kill", "-KILL", &pid.to_string()]) + .args(["-n", "-u", ls_user.as_str(), "kill", "-KILL", &pid.to_string()]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); @@ -222,14 +218,14 @@ pub(super) fn cleanup_orphaned_ls() { // Final check let still_alive = Command::new("pgrep") - .args(["-u", LS_USER, "-f", "language_server_linux"]) + .args(["-u", ls_user.as_str(), "-f", "language_server"]) .output() .map(|o| !o.stdout.is_empty()) .unwrap_or(false); if still_alive { eprintln!("\n \x1b[1;31m⚠ Cannot kill orphaned LS process\x1b[0m"); - eprintln!(" Run: \x1b[1msudo pkill -u {LS_USER} -f language_server_linux\x1b[0m\n"); + eprintln!(" Run: \x1b[1msudo pkill -u {} -f language_server\x1b[0m\n", ls_user); } } else { info!("Orphaned LS processes cleaned up"); @@ -244,8 +240,7 @@ pub(super) fn cleanup_orphaned_ls() { pub(super) fn read_oauth_from_state_db() -> Option<(String, Vec)> { use base64::Engine; - let home = std::env::var("HOME").ok()?; - let db_path = format!("{home}/.config/Antigravity/User/globalStorage/state.vscdb"); + let db_path = paths().state_db_path; // Check the DB file exists if !std::path::Path::new(&db_path).exists() { diff --git a/src/standalone/mod.rs b/src/standalone/mod.rs index bd2a8fd..54bb426 100644 --- a/src/standalone/mod.rs +++ b/src/standalone/mod.rs @@ -9,6 +9,7 @@ mod discovery; mod spawn; mod stub; +use crate::platform::{self, Platform}; use std::process::Command; use tracing::info; use uuid::Uuid; @@ -16,25 +17,14 @@ use uuid::Uuid; // Re-export public API pub use spawn::StandaloneLS; -/// Default path to the LS binary. -const LS_BINARY_PATH: &str = - "/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64"; - -/// App root for ANTIGRAVITY_EDITOR_APP_ROOT env var. -const APP_ROOT: &str = "/usr/share/antigravity/resources/app"; - -/// Data directory for the standalone LS. -const DATA_DIR: &str = "/tmp/zerogravity-standalone"; - -/// System user for UID-scoped iptables isolation. -const LS_USER: &str = "zerogravity-ls"; - -/// Path for the compiled dns_redirect.so preload library. -const DNS_REDIRECT_SO_PATH: &str = "/tmp/zerogravity-dns-redirect.so"; - -/// Source file for the DNS redirect preload library (relative to binary). +/// Source for the DNS redirect preload library (compiled at runtime, Linux only). const DNS_REDIRECT_C_SOURCE: &str = include_str!("../mitm/dns_redirect.c"); +/// Get platform-resolved paths. Convenience accessor. +pub(crate) fn paths() -> Platform { + Platform::detect() +} + /// Config needed to bootstrap the standalone LS. /// /// In normal mode, discovered from the running main LS. @@ -76,14 +66,17 @@ pub fn discover_main_ls_config() -> Result { /// Build the dns_redirect.so preload library if it doesn't already exist. /// -/// The library hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API -/// domain lookups to 127.0.0.1. This is needed because the LS binary uses -/// CGO for DNS resolution (libc getaddrinfo) but raw syscalls for connect(), -/// so only DNS can be intercepted via LD_PRELOAD. +/// Linux only — hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API +/// domain lookups to 127.0.0.1. /// /// Returns the path to the .so on success, None on failure. fn build_dns_redirect_so() -> Option { - let so_path = DNS_REDIRECT_SO_PATH; + if !platform::supports_ld_preload() { + return None; + } + + let p = paths(); + let so_path = &p.dns_redirect_so_path; // Skip rebuild if already exists if std::path::Path::new(so_path).exists() { @@ -99,13 +92,12 @@ fn build_dns_redirect_so() -> Option { // Compile: gcc -shared -fPIC -o dns_redirect.so dns_redirect.c -ldl let output = Command::new("gcc") - .args(["-shared", "-fPIC", "-o", so_path, &c_path, "-ldl"]) + .args(["-shared", "-fPIC", "-o", so_path.as_str(), &c_path, "-ldl"]) .output(); match output { Ok(out) if out.status.success() => { info!("Built dns_redirect.so at {so_path}"); - // Clean up source let _ = std::fs::remove_file(&c_path); Some(so_path.to_string()) } diff --git a/src/standalone/spawn.rs b/src/standalone/spawn.rs index 49b9e23..8fb7286 100644 --- a/src/standalone/spawn.rs +++ b/src/standalone/spawn.rs @@ -1,8 +1,9 @@ //! StandaloneLS — process lifecycle (spawn, wait, kill). -use super::discovery::{cleanup_orphaned_ls, find_free_port, find_ls_pid_for_user, has_ls_user, read_oauth_from_state_db}; +use super::discovery::{cleanup_orphaned_ls, find_free_port, find_ls_pid_for_user, read_oauth_from_state_db}; use super::stub::stub_handle_connection; -use super::{build_dns_redirect_so, MainLSConfig, StandaloneMitmConfig, APP_ROOT, DATA_DIR, LS_BINARY_PATH, LS_USER}; +use super::{build_dns_redirect_so, paths, MainLSConfig, StandaloneMitmConfig}; +use crate::platform; use crate::constants; use crate::proto; use std::io::Write; @@ -57,13 +58,16 @@ impl StandaloneLS { 1, // DETECT_AND_USE_PROXY_ENABLED ); + let p = paths(); + let data_dir = &p.data_dir; + // Setup data dir (mode 1777 so both current user and zerogravity-ls can write) - let gemini_dir = format!("{DATA_DIR}/.gemini"); - let app_data_dir = format!("{DATA_DIR}/.gemini/zerogravity-standalone"); + let gemini_dir = format!("{data_dir}/.gemini"); + let app_data_dir = format!("{data_dir}/.gemini/zerogravity-standalone"); let annotations_dir = format!("{app_data_dir}/annotations"); let brain_dir = format!("{app_data_dir}/brain"); for dir in [ - DATA_DIR, + data_dir.as_str(), &gemini_dir, &app_data_dir, &annotations_dir, @@ -83,7 +87,7 @@ impl StandaloneLS { eprintln!( "\n ⚠ Data dir {} is not writable (owned by another user from previous sudo run)\n \ Fix with: sudo chmod -R a+rwX {}\n", - app_data_dir, DATA_DIR + app_data_dir, data_dir ); } else { let _ = std::fs::remove_file(&test_path); @@ -142,9 +146,7 @@ impl StandaloneLS { .ok() .filter(|s| !s.is_empty()) .or_else(|| { - let home = std::env::var("HOME").unwrap_or_default(); - let path = format!("{home}/.config/zerogravity/token"); - std::fs::read_to_string(&path) + std::fs::read_to_string(&p.token_path) .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -234,7 +236,7 @@ impl StandaloneLS { // Build env vars for the LS process let mut env_vars: Vec<(String, String)> = - vec![("ANTIGRAVITY_EDITOR_APP_ROOT".into(), APP_ROOT.into())]; + vec![("ANTIGRAVITY_EDITOR_APP_ROOT".into(), p.app_root.clone())]; // If MITM is enabled, add SSL + proxy env vars if let Some(mitm) = mitm_config { @@ -242,9 +244,9 @@ impl StandaloneLS { // need a combined bundle: system CAs + our MITM CA // Write to /tmp — accessible by zerogravity-ls user // (user's ~/.config/ is not traversable by other UIDs) - let combined_ca_path = "/tmp/zerogravity-mitm-ca.pem".to_string(); + let combined_ca_path = format!("{}/mitm-ca.pem", data_dir); let system_ca = - std::fs::read_to_string("/etc/ssl/certs/ca-certificates.crt").unwrap_or_default(); + std::fs::read_to_string(&p.ca_cert_path).unwrap_or_default(); let mitm_ca = std::fs::read_to_string(&mitm.ca_cert_path) .map_err(|e| format!("Failed to read MITM CA cert: {e}"))?; std::fs::write(&combined_ca_path, format!("{system_ca}\n{mitm_ca}")) @@ -271,7 +273,7 @@ impl StandaloneLS { // OR when running in headless mode (no sudo at all). // With iptables, all outbound traffic is transparently redirected at the // kernel level — setting HTTPS_PROXY on top causes double-proxying. - if headless || !has_ls_user() { + if headless || !platform::supports_uid_isolation() { // proxy_addr already includes the scheme (e.g. "http://127.0.0.1:8742") env_vars.push(("HTTPS_PROXY".into(), mitm.proxy_addr.clone())); env_vars.push(("HTTP_PROXY".into(), mitm.proxy_addr.clone())); @@ -286,7 +288,7 @@ impl StandaloneLS { env_vars.push(("LD_PRELOAD".into(), so)); env_vars.push(( "DNS_REDIRECT_LOG".into(), - "/tmp/zerogravity-dns-redirect.log".into(), + format!("{data_dir}/dns-redirect.log"), )); } } @@ -294,21 +296,23 @@ impl StandaloneLS { // In headless mode, never use sudo — run as current user // In normal mode, use sudo if 'zerogravity-ls' user exists - let use_sudo = !headless && has_ls_user(); + let use_sudo = !headless && platform::supports_uid_isolation(); + let ls_binary = &p.ls_binary_path; + let ls_user = &p.ls_user; let mut cmd = if use_sudo { - info!("Using UID isolation: spawning LS as 'zerogravity-ls' user"); + info!("Using UID isolation: spawning LS as '{}' user", ls_user); let mut c = Command::new("sudo"); - c.args(["-n", "-u", LS_USER, "--", "/usr/bin/env"]); + c.args(["-n", "-u", ls_user.as_str(), "--", "/usr/bin/env"]); for (k, v) in &env_vars { c.arg(format!("{k}={v}")); } - c.arg(LS_BINARY_PATH); + c.arg(ls_binary.as_str()); c.args(&args); c } else { debug!("Spawning LS as current user"); - let mut c = Command::new(LS_BINARY_PATH); + let mut c = Command::new(ls_binary.as_str()); c.args(&args); for (k, v) in &env_vars { c.env(k, v); @@ -317,7 +321,7 @@ impl StandaloneLS { }; // Capture stderr for debugging — logs to /tmp so we can diagnose LS failures - let stderr_file = std::fs::File::create("/tmp/zerogravity-ls-debug.log") + let stderr_file = std::fs::File::create(format!("{data_dir}/ls-debug.log")) .map_err(|e| format!("Failed to create LS debug log: {e}"))?; cmd.stdin(Stdio::piped()) .stdout(Stdio::null()) @@ -343,7 +347,7 @@ impl StandaloneLS { // Give sudo a moment to spawn the real process std::thread::sleep(std::time::Duration::from_millis(500)); // Find the LS process owned by zerogravity-ls user - find_ls_pid_for_user(LS_USER).ok() + find_ls_pid_for_user(ls_user).ok() } else { Some(child.id()) }; @@ -423,10 +427,11 @@ impl StandaloneLS { if self.use_sudo { // The child is sudo which already exited. Kill the actual LS. if let Some(pid) = self.ls_pid { - info!(pid, "Killing LS process via sudo -u {}", LS_USER); + let ls_user = &paths().ls_user; + info!(pid, "Killing LS process via sudo -u {}", ls_user); // Run kill AS the zerogravity-ls user (same UID can signal) let ok = std::process::Command::new("sudo") - .args(["-n", "-u", LS_USER, "kill", "-TERM", &pid.to_string()]) + .args(["-n", "-u", ls_user.as_str(), "kill", "-TERM", &pid.to_string()]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() @@ -437,7 +442,7 @@ impl StandaloneLS { std::thread::sleep(std::time::Duration::from_millis(500)); // Force kill if still alive let _ = std::process::Command::new("sudo") - .args(["-n", "-u", LS_USER, "kill", "-KILL", &pid.to_string()]) + .args(["-n", "-u", ls_user.as_str(), "kill", "-KILL", &pid.to_string()]) .stdout(Stdio::null()) .stderr(Stdio::null()) .status(); diff --git a/src/trace.rs b/src/trace.rs index 1da4330..431e0e0 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -20,11 +20,7 @@ pub struct TraceCollector { impl TraceCollector { pub fn new(enabled: bool) -> Self { - let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); - let traces_dir = PathBuf::from(home) - .join(".config") - .join("zerogravity") - .join("traces"); + let traces_dir = crate::platform::Platform::detect().traces_dir; Self { enabled, traces_dir,