keyclaw 0.2.1

Local MITM proxy that keeps secrets out of LLM traffic
Documentation
use std::net::SocketAddr;
use std::process::Command;
use std::time::Duration;

use crate::support::{
    can_bind, free_addr, install_fake_tool, keyclaw_command, prepend_path, wait_until,
};

#[test]
fn proxy_fails_fast_on_broken_generated_ca_pair() {
    let temp = tempfile::tempdir().expect("tempdir");
    let ca_dir = temp.path().join(".keyclaw");
    std::fs::create_dir_all(&ca_dir).expect("create keyclaw dir");
    std::fs::write(ca_dir.join("ca.crt"), "not-a-cert").expect("write malformed cert");
    std::fs::write(ca_dir.join("ca.key"), "not-a-key").expect("write malformed key");

    let bin = assert_cmd::cargo::cargo_bin!("keyclaw");
    let output = Command::new(bin)
        .arg("proxy")
        .env_clear()
        .env("HOME", temp.path())
        .env("KEYCLAW_PROXY_ADDR", "127.0.0.1:0")
        .env("KEYCLAW_PROXY_URL", "http://127.0.0.1:0")
        .output()
        .expect("run proxy");

    assert_ne!(output.status.code(), Some(0));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("keyclaw error:"), "stderr={stderr}");
    assert!(
        stderr.contains("remove the broken CA files"),
        "stderr={stderr}"
    );
}

#[cfg(unix)]
#[test]
fn proxy_detaches_by_default_and_prints_stop_instructions() {
    struct ProxyGuard(Option<i32>);

    impl Drop for ProxyGuard {
        fn drop(&mut self) {
            if let Some(pid) = self.0.take() {
                unsafe {
                    libc::kill(pid, libc::SIGKILL);
                }
            }
        }
    }

    let temp = tempfile::tempdir().expect("tempdir");
    let addr = free_addr();
    let socket_addr: SocketAddr = addr.parse().expect("parse socket addr");
    let output = keyclaw_command(temp.path())
        .arg("proxy")
        .env("KEYCLAW_PROXY_ADDR", &addr)
        .env("KEYCLAW_PROXY_URL", format!("http://{addr}"))
        .output()
        .expect("run proxy");

    assert_eq!(output.status.code(), Some(0));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("proxy running in background"),
        "stderr={stderr}"
    );
    assert!(
        stderr.contains("does not reconfigure this shell"),
        "stderr should explain that plain `keyclaw proxy` does not update the current shell env: {stderr}"
    );
    assert!(
        stderr.contains("source") && stderr.contains("env.sh"),
        "stderr should point users at sourcing env.sh for the current shell: {stderr}"
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("source") && stdout.contains("env.sh"),
        "stdout should contain eval-able source command: stdout={stdout}"
    );

    let pid_path = temp.path().join(".keyclaw").join("proxy.pid");
    let env_path = temp.path().join(".keyclaw").join("env.sh");
    wait_until(Duration::from_secs(3), || {
        pid_path.exists() && env_path.exists() && !can_bind(socket_addr)
    });

    assert!(pid_path.exists(), "proxy pid file missing");
    assert!(env_path.exists(), "proxy env.sh missing");
    assert!(!can_bind(socket_addr), "proxy listener was not started");

    let pid: i32 = std::fs::read_to_string(&pid_path)
        .expect("read proxy pid")
        .trim()
        .parse()
        .expect("parse proxy pid");
    let mut guard = ProxyGuard(Some(pid));

    unsafe {
        libc::kill(pid, libc::SIGTERM);
    }
    wait_until(Duration::from_secs(3), || can_bind(socket_addr));
    assert!(
        can_bind(socket_addr),
        "proxy listener still bound: {socket_addr}"
    );
    guard.0 = None;
}

#[cfg(target_os = "linux")]
#[test]
fn proxy_autostart_enable_writes_systemd_unit_and_invokes_systemctl() {
    let temp = tempfile::tempdir().expect("tempdir");
    let fake_bin = temp.path().join("bin");
    std::fs::create_dir_all(&fake_bin).expect("create fake bin");
    let systemctl_log = temp.path().join("systemctl.log");

    install_fake_tool(
        &fake_bin,
        "systemctl",
        r#"#!/usr/bin/env bash
printf '%s\n' "$*" >> "$KEYCLAW_TEST_SYSTEMCTL_LOG"
exit 0
"#,
    );

    let mut cmd = keyclaw_command(temp.path());
    prepend_path(&mut cmd, &fake_bin);
    let output = cmd
        .arg("proxy")
        .arg("autostart")
        .arg("enable")
        .env("KEYCLAW_TEST_SYSTEMCTL_LOG", &systemctl_log)
        .output()
        .expect("enable autostart");

    assert_eq!(output.status.code(), Some(0));
    let service_path = temp
        .path()
        .join(".config")
        .join("systemd")
        .join("user")
        .join("keyclaw-proxy.service");
    let service = std::fs::read_to_string(&service_path).expect("read service file");
    assert!(
        service.contains("ExecStart=") && service.contains("proxy --foreground"),
        "service={service}"
    );

    let calls = std::fs::read_to_string(&systemctl_log).expect("read systemctl log");
    assert!(
        calls.contains("--user daemon-reload"),
        "calls should reload user systemd units: {calls}"
    );
    assert!(
        calls.contains("--user enable --now")
            && calls.contains(&service_path.display().to_string()),
        "calls should enable and start the service by path: {calls}"
    );
}