falsegreen 0.1.19

FalseGreen client — independent verification for coding agents
//! Device activation: generate a stable device ID and activate/deactivate
//! with the Polar API via the FalseGreen service.

use std::process::Command;

/// Generate a stable device label from machine properties.
///
/// This produces a consistent identifier so re-login on the same machine
/// doesn't consume a new activation slot.
pub fn device_label() -> String {
    let hostname = std::env::var("HOSTNAME")
        .or_else(|_| {
            // Fall back to reading /etc/hostname.
            std::fs::read_to_string("/etc/hostname")
                .map(|s| s.trim().to_string())
                .or_else(|_| {
                    Command::new("hostname")
                        .output()
                        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
                })
        })
        .unwrap_or_else(|_| "unknown".to_string());

    let user = std::env::var("USER")
        .or_else(|_| std::env::var("USERNAME"))
        .unwrap_or_else(|_| "unknown".to_string());

    let os = std::env::consts::OS;
    let arch = std::env::consts::ARCH;

    // Hash to keep it short and stable.
    let raw = format!("{}|{}|{}|{}", hostname, user, os, arch);
    let hash = hash_string(&raw);
    format!("{}-{}", &hash[..8], hostname)
}

fn hash_string(s: &str) -> String {
    // Simple FNV-1a hash (no external crypto dependency needed for a label).
    let mut hash: u64 = 0xcbf29ce484222325;
    for byte in s.as_bytes() {
        hash ^= *byte as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }
    format!("{:016x}", hash)
}

/// Activate a device via the FalseGreen service.
///
/// The service proxies to Polar's activation API and returns the activation_id.
pub fn activate(key: &str, label: &str) -> Result<String, String> {
    let api_url = crate::shim::config::api_url();
    // The activation endpoint is separate from the MCP endpoint.
    let activate_url = api_url.replace("/v1/mcp", "/v1/activate");

    let body = format!(
        r#"{{"key":"{}","label":"{}"}}"#,
        key.replace('"', "\\\""),
        label.replace('"', "\\\"")
    );

    let result = Command::new("curl")
        .arg("-s")
        .arg("-X")
        .arg("POST")
        .arg(&activate_url)
        .arg("-H")
        .arg("Content-Type: application/json")
        .arg("-H")
        .arg(format!("X-API-KEY: {}", key))
        .arg("-d")
        .arg(&body)
        .arg("--max-time")
        .arg("30")
        .output();

    match result {
        Ok(output) if output.status.success() => {
            let body = String::from_utf8_lossy(&output.stdout);
            // Parse the activation_id from the JSON response.
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(&body) {
                if let Some(id) = v.get("activation_id").and_then(|i| i.as_str()) {
                    return Ok(id.to_string());
                }
                if let Some(id) = v.get("id").and_then(|i| i.as_str()) {
                    return Ok(id.to_string());
                }
                if let Some(err) = v.get("detail").and_then(|d| d.as_str()) {
                    return Err(err.to_string());
                }
            }
            Err(format!("invalid response: {}", body))
        }
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            Err(format!("request failed: {}", stderr))
        }
        Err(e) => Err(format!("failed to send request: {}", e)),
    }
}

/// Deactivate a device via the FalseGreen service.
pub fn deactivate(key: &str, activation_id: &str) -> Result<(), String> {
    let api_url = crate::shim::config::api_url();
    let deactivate_url = api_url.replace("/v1/mcp", "/v1/deactivate");

    let body = format!(
        r#"{{"key":"{}","activation_id":"{}"}}"#,
        key.replace('"', "\\\""),
        activation_id.replace('"', "\\\"")
    );

    let result = Command::new("curl")
        .arg("-s")
        .arg("-X")
        .arg("POST")
        .arg(&deactivate_url)
        .arg("-H")
        .arg("Content-Type: application/json")
        .arg("-H")
        .arg(format!("X-API-KEY: {}", key))
        .arg("-d")
        .arg(&body)
        .arg("--max-time")
        .arg("30")
        .output();

    match result {
        Ok(output) if output.status.success() => Ok(()),
        Ok(output) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let stdout = String::from_utf8_lossy(&output.stdout);
            Err(format!("request failed: {} {}", stderr, stdout))
        }
        Err(e) => Err(format!("failed to send request: {}", e)),
    }
}