link-assistant-router 1.15.2

Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs
Documentation
//! Shared, pre-compilation preparation for real vendor verification.

use serde_json::{Value, json};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;

/// Native macOS HOME overrides do not isolate the user's Keychain daemon.
///
/// No environment switch can opt out of this boundary. Use a disposable Linux
/// container or runner; a disposable macOS account/VM needs its own verifier.
pub fn safety() -> Result<(), &'static str> {
    safety_for(std::env::consts::OS)
}

fn safety_for(os: &str) -> Result<(), &'static str> {
    if os == "macos" {
        Err(
            "safe OS credential-store boundary unavailable: native macOS vendor probes are refused before version, doctor or TUI; use the disposable Linux verification environment",
        )
    } else {
        Ok(())
    }
}

/// Strip inherited tokens, profile paths, IPC addresses and proxy credentials.
pub fn environment(command: &mut Command, home: &Path) {
    let path = std::env::var_os("PATH").unwrap_or_default();
    command
        .env_clear()
        .env("PATH", path)
        .env("HOME", home)
        .env("XDG_CONFIG_HOME", home.join(".config"))
        .env("XDG_DATA_HOME", home.join(".local/share"))
        .env("XDG_CACHE_HOME", home.join(".cache"))
        .env("CODEX_HOME", home.join(".codex"))
        .env("CLAUDE_CONFIG_DIR", home.join(".claude"))
        .env("CI", "1")
        .env("NO_COLOR", "1")
        .env("NO_PROXY", "127.0.0.1,localhost")
        .env("no_proxy", "127.0.0.1,localhost")
        .env("HTTP_PROXY", "http://127.0.0.1:9")
        .env("HTTPS_PROXY", "http://127.0.0.1:9")
        .env("ALL_PROXY", "http://127.0.0.1:9")
        .stdin(Stdio::null());
}

/// Extract one semantic version; extra banners may vary by vendor.
pub fn parse_version(text: &str) -> Option<String> {
    text.split(|ch: char| !ch.is_ascii_digit() && ch != '.')
        .find(|word| {
            let parts: Vec<_> = word.split('.').collect();
            parts.len() == 3 && parts.iter().all(|part| !part.is_empty())
        })
        .map(str::to_string)
}

pub const CLIENTS: &[(&str, &str)] = &[
    ("claude", "ROUTER_REAL_CLIENT_CLAUDE_VERSION"),
    ("codex", "ROUTER_REAL_CLIENT_CODEX_VERSION"),
    ("opencode", "ROUTER_REAL_CLIENT_OPENCODE_VERSION"),
];

/// Discover before Cargo builds `option_env!` expectations. Caller/CI pins win,
/// but a pin mismatch is a preparation failure, never a compatibility result.
#[must_use]
pub fn prepare(clients: &[&str]) -> (Vec<Value>, Vec<(String, String)>) {
    prepare_using(
        clients,
        |variable| std::env::var(variable).ok(),
        |client| {
            if let Err(reason) = safety() {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::PermissionDenied,
                    reason,
                ));
            }
            tempfile::tempdir().and_then(|home| {
                let mut command = Command::new(client);
                environment(&mut command, home.path());
                command.arg("--version");
                crate::bounded_process::output(&mut command, Duration::from_secs(15))
            })
        },
    )
}

fn prepare_using(
    clients: &[&str],
    expected_version: impl Fn(&str) -> Option<String>,
    discover: impl Fn(&str) -> std::io::Result<std::process::Output>,
) -> (Vec<Value>, Vec<(String, String)>) {
    let mut report = Vec::new();
    let mut variables = Vec::new();
    for &(client, variable) in CLIENTS {
        if !clients.is_empty() && !clients.contains(&client) {
            continue;
        }
        let expected = expected_version(variable);
        let result = discover(client);
        let observed = result
            .as_ref()
            .ok()
            .filter(|output| output.status.success())
            .and_then(|output| {
                parse_version(&format!(
                    "{}{}",
                    String::from_utf8_lossy(&output.stdout),
                    String::from_utf8_lossy(&output.stderr)
                ))
            });
        let (status, reason) = match (&result, &observed, &expected) {
            (Err(error), _, _) if error.kind() == std::io::ErrorKind::PermissionDenied => (
                "not-proven",
                "safe OS credential-store boundary unavailable; vendor launch refused",
            ),
            (Err(error), _, _) if error.kind() == std::io::ErrorKind::NotFound => {
                ("not-proven", "client is not installed")
            }
            (Err(_), _, _) => (
                "failed",
                "version discovery failed or exceeded its deadline",
            ),
            (_, None, _) => (
                "failed",
                "version discovery returned an invalid version or exit status",
            ),
            (_, Some(actual), Some(wanted)) if actual != wanted => {
                ("failed", "expected and installed client versions differ")
            }
            _ => (
                "prepared",
                "installed version discovered before compilation",
            ),
        };
        if status == "prepared" {
            variables.push((
                variable.to_string(),
                observed.clone().expect("prepared version"),
            ));
        }
        report.push(json!({"client":client,"expected":expected,"observed":observed,"status":status,"reason":reason}));
    }
    (report, variables)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn macos_refuses_even_a_version_probe_without_an_os_boundary() {
        assert!(safety_for("macos").is_err());
        assert!(safety_for("linux").is_ok());
    }

    #[test]
    fn versions_are_detected_without_pinning_a_workstation() {
        assert_eq!(parse_version("codex-cli 0.158.0\n"), Some("0.158.0".into()));
        assert_eq!(
            parse_version("2.1.284 (Claude Code)"),
            Some("2.1.284".into())
        );
        assert_eq!(parse_version("1.19.1\n"), Some("1.19.1".into()));
        assert_eq!(parse_version("unknown 1.2"), None);
    }

    #[cfg(unix)]
    #[test]
    fn preparation_reports_missing_invalid_mismatched_and_refused_clients() {
        use std::os::unix::process::ExitStatusExt as _;
        let output = |version: &str, status| std::process::Output {
            status: std::process::ExitStatus::from_raw(status),
            stdout: version.as_bytes().to_vec(),
            stderr: Vec::new(),
        };
        let (report, variables) =
            prepare_using(&["codex"], |_| None, |_| Ok(output("codex-cli 0.158.0", 0)));
        assert_eq!(report.len(), 1);
        assert_eq!(report[0]["status"], "prepared");
        assert_eq!(
            variables,
            [("ROUTER_REAL_CLIENT_CODEX_VERSION".into(), "0.158.0".into())]
        );
        let (report, variables) = prepare_using(
            &["codex"],
            |_| Some("0.154.0".into()),
            |_| Ok(output("0.158.0", 0)),
        );
        assert_eq!(report[0]["status"], "failed");
        assert!(variables.is_empty());
        for (version, status) in [("", 0), ("unknown", 0), ("0.158.0", 256)] {
            let (report, variables) =
                prepare_using(&["claude"], |_| None, |_| Ok(output(version, status)));
            assert_eq!(report[0]["status"], "failed");
            assert!(variables.is_empty());
        }
        for (kind, status) in [
            (std::io::ErrorKind::NotFound, "not-proven"),
            (std::io::ErrorKind::PermissionDenied, "not-proven"),
            (std::io::ErrorKind::TimedOut, "failed"),
        ] {
            let (report, variables) =
                prepare_using(&[], |_| None, |_| Err(std::io::Error::from(kind)));
            assert_eq!(report.len(), 3);
            assert!(report.iter().all(|row| row["status"] == status));
            assert!(variables.is_empty());
        }
    }

    #[test]
    fn private_environment_clears_inherited_profile_and_credential_variables() {
        let mut command = Command::new("fixture");
        command
            .env("ANTHROPIC_API_KEY", "inherited-secret")
            .env("CODEX_HOME", "/original");
        environment(&mut command, Path::new("/private/fixture"));
        let values: std::collections::BTreeMap<_, _> = command.get_envs().collect();
        assert!(!values.contains_key(std::ffi::OsStr::new("ANTHROPIC_API_KEY")));
        assert_eq!(
            values[std::ffi::OsStr::new("CODEX_HOME")].unwrap(),
            Path::new("/private/fixture/.codex")
        );
        assert_eq!(
            values[std::ffi::OsStr::new("HTTPS_PROXY")].unwrap(),
            "http://127.0.0.1:9"
        );
    }
}