keyclaw 0.2.1

Local MITM proxy that keeps secrets out of LLM traffic
Documentation
use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;

use crate::support::{
    TEST_SECRET_CODEX, free_addr, rewrite_json_command, run_mitm, run_mitm_with_log_level,
    start_upstream,
};

#[test]
fn logs_contain_no_raw_secrets() {
    let (upstream_url, rx, _guard) = start_upstream();

    let (stderr, exit_code) = run_mitm_with_log_level(
        "codex",
        free_addr(),
        &upstream_url,
        &format!(r#"{{"prompt":"api_key = {}"}}"#, TEST_SECRET_CODEX),
        Some("debug"),
    );

    assert_eq!(exit_code, 0, "stderr={stderr}");
    let _ = rx
        .recv_timeout(Duration::from_secs(2))
        .expect("upstream body");
    assert!(!stderr.contains(TEST_SECRET_CODEX));
    assert!(
        stderr.contains("request rewritten for host"),
        "stderr={stderr}"
    );
}

#[test]
fn mitm_info_log_level_hides_per_request_proxy_activity() {
    let (upstream_url, rx, _guard) = start_upstream();

    let (stderr, exit_code) = run_mitm_with_log_level(
        "codex",
        free_addr(),
        &upstream_url,
        &format!(r#"{{"prompt":"api_key = {}"}}"#, TEST_SECRET_CODEX),
        Some("info"),
    );

    assert_eq!(exit_code, 0, "stderr={stderr}");
    let _ = rx
        .recv_timeout(Duration::from_secs(2))
        .expect("upstream body");
    assert!(stderr.contains("keyclaw info:"), "stderr={stderr}");
    assert!(!stderr.contains("intercept POST /"), "stderr={stderr}");
    assert!(
        !stderr.contains("request rewritten for host"),
        "stderr={stderr}"
    );
    assert!(
        !stderr.contains("response: resolved placeholders back to secrets"),
        "stderr={stderr}"
    );
}

#[test]
fn mitm_debug_log_level_preserves_per_request_proxy_activity() {
    let (upstream_url, rx, _guard) = start_upstream();

    let (stderr, exit_code) = run_mitm_with_log_level(
        "codex",
        free_addr(),
        &upstream_url,
        &format!(r#"{{"prompt":"api_key = {}"}}"#, TEST_SECRET_CODEX),
        Some("debug"),
    );

    assert_eq!(exit_code, 0, "stderr={stderr}");
    let _ = rx
        .recv_timeout(Duration::from_secs(2))
        .expect("upstream body");
    assert!(
        stderr.contains("keyclaw debug: intercept POST /"),
        "stderr={stderr}"
    );
    assert!(
        stderr.contains("keyclaw debug: request rewritten for host 127.0.0.1: replaced 1 secrets"),
        "stderr={stderr}"
    );
}

#[test]
fn coded_errors_emit_a_single_code_prefix() {
    let bin = assert_cmd::cargo::cargo_bin!("keyclaw");
    let temp = tempfile::tempdir().expect("tempdir");
    let vault_path = temp.path().join("vault.enc");

    let output = Command::new(bin)
        .arg("mitm")
        .arg("codex")
        .env_clear()
        .env("HOME", temp.path())
        .env("NO_PROXY", "*")
        .env("KEYCLAW_PROXY_ADDR", "127.0.0.1:0")
        .env("KEYCLAW_PROXY_URL", "http://127.0.0.1:0")
        .env("KEYCLAW_REQUIRE_MITM_EFFECTIVE", "true")
        .env("KEYCLAW_VAULT_PATH", &vault_path)
        .env("KEYCLAW_VAULT_PASSPHRASE", "test-passphrase")
        .output()
        .expect("run mitm");

    assert_eq!(output.status.code(), Some(1));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("keyclaw error: mitm_not_effective: NO_PROXY=*"),
        "stderr={stderr}"
    );
    assert!(
        !stderr.contains("mitm_not_effective: mitm_not_effective:"),
        "stderr={stderr}"
    );
}

#[test]
fn mitm_runtime_logs_use_leveled_prefixes() {
    let (upstream_url, rx, _guard) = start_upstream();

    let (stderr, exit_code) = run_mitm(
        "codex",
        free_addr(),
        &upstream_url,
        &format!(r#"{{"prompt":"api_key = {}"}}"#, TEST_SECRET_CODEX),
    );

    assert_eq!(exit_code, 0, "stderr={stderr}");
    let _ = rx
        .recv_timeout(Duration::from_secs(2))
        .expect("upstream body");
    let lines = stderr
        .lines()
        .filter(|line| !line.trim().is_empty())
        .collect::<Vec<_>>();
    assert!(!lines.is_empty(), "stderr={stderr}");
    assert!(
        lines.iter().all(|line| line.starts_with("keyclaw info: ")),
        "stderr={stderr}"
    );
}

#[test]
fn rewrite_json_unsafe_logging_warning_uses_warn_prefix() {
    let temp = tempfile::tempdir().expect("tempdir");
    let payload = r#"{"prompt":"hello"}"#;

    let mut child = rewrite_json_command(temp.path())
        .env("KEYCLAW_UNSAFE_LOG", "true")
        .env("KEYCLAW_LOG_LEVEL", "warn")
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn rewrite-json");

    child
        .stdin
        .as_mut()
        .expect("stdin")
        .write_all(payload.as_bytes())
        .expect("write payload");
    let output = child.wait_with_output().expect("wait rewrite-json");

    assert_eq!(output.status.code(), Some(0));
    let stderr = String::from_utf8_lossy(&output.stderr);
    let lines = stderr
        .lines()
        .filter(|line| !line.trim().is_empty())
        .collect::<Vec<_>>();
    assert_eq!(
        lines,
        vec!["keyclaw warn: unsafe logging enabled; secrets may appear in logs"],
        "stderr={stderr}"
    );
}