unified-agent-api 0.3.5

Agent-agnostic facade and registry for wrapper backends
Documentation
use std::{
    env,
    fs::{self, OpenOptions},
    io::{self, Write},
};

use serde_json::json;

pub(crate) const ADD_DIRS_RUNTIME_REJECTION_MESSAGE: &str = "add_dirs rejected by runtime";
pub(crate) const ADD_DIR_RAW_PATH_SECRET: &str = "ADD_DIR_RAW_PATH_SECRET";
pub(crate) const ADD_DIR_STDOUT_SECRET: &str = "ADD_DIR_STDOUT_SECRET";
pub(crate) const ADD_DIR_STDERR_SECRET: &str = "ADD_DIR_STDERR_SECRET";

pub(crate) fn write_line(out: &mut (impl Write + ?Sized), line: &str) -> io::Result<()> {
    out.write_all(line.as_bytes())?;
    out.flush()?;
    Ok(())
}

pub(crate) fn write_assistant_text(out: &mut (impl Write + ?Sized), text: &str) -> io::Result<()> {
    let payload = json!({
        "type": "assistant",
        "session_id": "sess-1",
        "message": {
            "content": [{
                "type": "text",
                "text": text,
            }],
        },
    });
    write_line(out, &payload.to_string())?;
    write_line(out, "\n")?;
    Ok(())
}

pub(crate) fn write_result_error_with_message(
    out: &mut (impl Write + ?Sized),
    message: &str,
) -> io::Result<()> {
    let payload = json!({
        "type": "result",
        "subtype": "error",
        "session_id": "sess-1",
        "is_error": true,
        "message": message,
    });
    write_line(out, &payload.to_string())?;
    write_line(out, "\n")?;
    Ok(())
}

pub(crate) fn write_result_error_with_stderr_detail(
    out: &mut (impl Write + ?Sized),
    message: &str,
    stderr: &str,
) -> io::Result<()> {
    let payload = json!({
        "type": "result",
        "subtype": "error",
        "session_id": "sess-1",
        "is_error": true,
        "message": message,
        "details": {
            "stderr": stderr,
        },
    });
    write_line(out, &payload.to_string())?;
    write_line(out, "\n")?;
    Ok(())
}

pub(crate) fn write_add_dirs_runtime_rejection(out: &mut (impl Write + ?Sized)) -> io::Result<()> {
    let payload = json!({
        "type": "result",
        "subtype": "error",
        "session_id": "sess-1",
        "is_error": true,
        "message": ADD_DIRS_RUNTIME_REJECTION_MESSAGE,
        "details": {
            "raw_path": ADD_DIR_RAW_PATH_SECRET,
            "stdout": ADD_DIR_STDOUT_SECRET,
        },
    });
    write_line(out, &payload.to_string())?;
    write_line(out, "\n")?;
    Ok(())
}

pub(crate) fn exit_add_dirs_runtime_rejection(out: &mut (impl Write + ?Sized)) -> ! {
    write_add_dirs_runtime_rejection(out).expect("write add_dirs runtime rejection");
    eprintln!("{ADD_DIR_STDERR_SECRET}");
    std::process::exit(add_dirs_runtime_rejection_exit_code());
}

fn add_dirs_runtime_rejection_exit_code() -> i32 {
    match env::var("FAKE_CLAUDE_RUNTIME_REJECTION_EXIT_CODE") {
        Ok(raw) => raw
            .parse::<i32>()
            .unwrap_or_else(|err| panic!("invalid FAKE_CLAUDE_RUNTIME_REJECTION_EXIT_CODE: {err}")),
        Err(env::VarError::NotPresent) => 1,
        Err(err) => panic!("failed to read FAKE_CLAUDE_RUNTIME_REJECTION_EXIT_CODE: {err}"),
    }
}

pub(crate) fn has_flag_value(args: &[String], flag: &str, expected: &str) -> bool {
    args.iter()
        .position(|arg| arg == flag)
        .and_then(|idx| args.get(idx + 1))
        .is_some_and(|value| value == expected)
}

pub(crate) fn has_flag(args: &[String], flag: &str) -> bool {
    args.iter().any(|arg| arg == flag)
}

pub(crate) fn contains_ordered_subsequence(args: &[String], subseq: &[&str]) -> bool {
    if subseq.is_empty() {
        return true;
    }

    let mut idx = 0usize;
    for arg in args {
        if arg == subseq[idx] {
            idx += 1;
            if idx == subseq.len() {
                return true;
            }
        }
    }
    false
}

pub(crate) fn require_env_var(key: &str) -> String {
    env::var(key).unwrap_or_else(|_| panic!("missing required env var {key}"))
}

pub(crate) fn expected_add_dirs() -> Option<Vec<String>> {
    let expected_count_raw = env::var("FAKE_CLAUDE_EXPECT_ADD_DIR_COUNT").ok()?;
    let expected_count = expected_count_raw
        .parse::<usize>()
        .unwrap_or_else(|err| panic!("invalid FAKE_CLAUDE_EXPECT_ADD_DIR_COUNT: {err}"));
    Some(
        (0..expected_count)
            .map(|index| require_env_var(&format!("FAKE_CLAUDE_EXPECT_ADD_DIR_{index}")))
            .collect(),
    )
}

pub(crate) fn assert_add_dirs(args: &[String], out: &mut dyn Write) {
    if env_is_true("FAKE_CLAUDE_EXPECT_NO_ADD_DIR") {
        if has_flag(args, "--add-dir") {
            fail(out, "assertion failed: expected --add-dir to be absent");
        }
        return;
    }

    let Some(expected) = expected_add_dirs() else {
        return;
    };

    let add_dir_indices: Vec<usize> = args
        .iter()
        .enumerate()
        .filter_map(|(idx, arg)| (arg == "--add-dir").then_some(idx))
        .collect();
    if add_dir_indices.len() != 1 {
        fail(
            out,
            &format!(
                "assertion failed: expected exactly one --add-dir flag, got {}",
                add_dir_indices.len()
            ),
        );
    }

    let Some(add_dir_idx) = add_dir_indices.into_iter().next() else {
        fail(out, "assertion failed: missing --add-dir");
    };

    let actual: Vec<String> = args
        .iter()
        .skip(add_dir_idx + 1)
        .take_while(|arg| !arg.starts_with("--"))
        .cloned()
        .collect();

    if actual.len() != expected.len() {
        fail(
            out,
            &format!(
                "assertion failed: expected {} add-dir values, got {}",
                expected.len(),
                actual.len()
            ),
        );
    }

    if actual != expected {
        fail(
            out,
            &format!(
                "assertion failed: expected add-dir values {:?}, got {:?}",
                expected, actual
            ),
        );
    }
}

pub(crate) fn maybe_assert_cwd(out: &mut dyn Write) {
    let Ok(expected_cwd) = env::var("FAKE_CLAUDE_EXPECT_CWD") else {
        return;
    };
    let got = env::current_dir().expect("read current dir");
    let expected = std::fs::canonicalize(&expected_cwd)
        .unwrap_or_else(|_| std::path::PathBuf::from(&expected_cwd));
    let got_canonical = std::fs::canonicalize(&got).unwrap_or(got);
    if got_canonical != expected {
        fail(
            out,
            &format!(
                "assertion failed: expected cwd {:?}, got {:?}",
                expected, got_canonical
            ),
        );
    }
}

pub(crate) fn maybe_assert_model_mapping(args: &[String], out: &mut dyn Write) {
    let expect_model = env::var("FAKE_CLAUDE_EXPECT_MODEL").ok();
    let expect_no_model = env_is_true("FAKE_CLAUDE_EXPECT_NO_MODEL");
    let expect_no_fallback_model = env_is_true("FAKE_CLAUDE_EXPECT_NO_FALLBACK_MODEL");

    if expect_model.is_some() && expect_no_model {
        panic!("FAKE_CLAUDE_EXPECT_MODEL and FAKE_CLAUDE_EXPECT_NO_MODEL are mutually exclusive");
    }

    if expect_no_fallback_model && has_flag(args, "--fallback-model") {
        fail(
            out,
            "assertion failed: expected --fallback-model to be absent",
        );
    }

    if expect_no_model {
        if has_flag(args, "--model") {
            fail(out, "assertion failed: expected --model to be absent");
        }
        return;
    }

    let Some(expected_model) = expect_model else {
        return;
    };

    let model_indices: Vec<usize> = args
        .iter()
        .enumerate()
        .filter_map(|(idx, arg)| (arg == "--model").then_some(idx))
        .collect();
    if model_indices.len() != 1 {
        fail(
            out,
            &format!(
                "assertion failed: expected exactly one --model flag, got {}",
                model_indices.len()
            ),
        );
    }
    let model_idx = model_indices[0];
    let actual = args.get(model_idx + 1).cloned().unwrap_or_default();
    if actual != expected_model {
        fail(
            out,
            &format!(
                "assertion failed: expected --model value {:?}, got {:?}",
                expected_model, actual
            ),
        );
    }

    if let Some(permission_idx) = args.iter().position(|arg| arg == "--permission-mode") {
        if model_idx > permission_idx {
            fail(
                out,
                "assertion failed: expected --model to precede --permission-mode",
            );
        }
    }

    for flag in [
        "--add-dir",
        "--continue",
        "--fork-session",
        "--resume",
        "--fallback-model",
        "--verbose",
    ] {
        if let Some(idx) = args.iter().position(|arg| arg == flag) {
            if model_idx > idx {
                fail(
                    out,
                    &format!("assertion failed: expected --model to precede {flag}"),
                );
            }
        }
    }
}

pub(crate) fn selector_assertion_subsequence(tail: &[&str]) -> Vec<String> {
    let mut subseq = vec![
        "--print".to_string(),
        "--output-format".to_string(),
        "stream-json".to_string(),
        "--permission-mode".to_string(),
        "bypassPermissions".to_string(),
    ];
    if let Some(add_dirs) = expected_add_dirs() {
        subseq.push("--add-dir".to_string());
        subseq.extend(add_dirs);
    }
    subseq.extend(tail.iter().map(|item| (*item).to_string()));
    subseq
}

pub(crate) fn require(env_key: &str) -> String {
    env::var(env_key).unwrap_or_else(|_| panic!("missing required env var {env_key}"))
}

pub(crate) fn fail(mut out: impl Write, message: &str) -> ! {
    let _ = write_assistant_text(&mut out, message);
    std::process::exit(2);
}

pub(crate) fn env_is_true(key: &str) -> bool {
    matches!(env::var(key).as_deref(), Ok("1") | Ok("true") | Ok("TRUE"))
}

pub(crate) fn maybe_log_invocation(kind: &str) {
    let Ok(path) = env::var("FAKE_CLAUDE_INVOCATION_LOG") else {
        return;
    };

    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)
        .expect("open FAKE_CLAUDE_INVOCATION_LOG");
    writeln!(file, "{kind}").expect("append invocation log");
}

pub(crate) fn maybe_assert_flag_presence(
    args: &[String],
    env_key: &str,
    flag: &str,
    out: &mut dyn Write,
) {
    let Ok(raw) = env::var(env_key) else {
        return;
    };
    let expect_present = match raw.as_str() {
        "1" => true,
        "0" => false,
        other => panic!("{env_key} must be \"1\" or \"0\" (got {other:?})"),
    };

    let present = has_flag(args, flag);
    if expect_present != present {
        let expectation = if expect_present { "present" } else { "absent" };
        fail(
            out,
            &format!("assertion failed: expected {flag} to be {expectation}"),
        );
    }
}

pub(crate) fn maybe_write_env_snapshot() {
    let Ok(path) = env::var("FAKE_CLAUDE_ENV_SNAPSHOT_PATH") else {
        return;
    };

    let mut snapshot = String::new();
    for key in [
        "CLAUDE_HOME",
        "HOME",
        "XDG_CONFIG_HOME",
        "XDG_DATA_HOME",
        "XDG_CACHE_HOME",
    ] {
        let value = env::var(key).unwrap_or_default();
        snapshot.push_str(key);
        snapshot.push('=');
        snapshot.push_str(&value);
        snapshot.push('\n');
    }

    fs::write(path, snapshot).expect("write FAKE_CLAUDE_ENV_SNAPSHOT_PATH");
}