codex-client-sdk 0.107.0

Rust SDK for embedding the Codex agent via CLI-over-JSONL transport
Documentation
#![allow(dead_code)]

use std::collections::HashMap;
use std::path::PathBuf;

use codex::{Codex, CodexOptions, Result};
use serde::Deserialize;
use serde_json::Value;
use tempfile::TempDir;

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

#[derive(Debug, Clone, Deserialize)]
pub struct InvocationLog {
    pub args: Vec<String>,
    pub stdin: String,
    pub env: HashMap<String, String>,
    pub call_index: usize,
    pub output_schema_path: Option<String>,
    pub output_schema_exists: bool,
    pub output_schema: Option<Value>,
}

pub struct MockCodexHarness {
    _temp_dir: TempDir,
    script_path: PathBuf,
    events_path: PathBuf,
    log_path: PathBuf,
    call_index_path: PathBuf,
}

impl MockCodexHarness {
    pub fn new(events_per_call: Vec<Vec<Value>>) -> Self {
        let temp_dir = tempfile::tempdir().expect("tempdir");
        let script_path = temp_dir.path().join("mock_codex_cli.py");
        let events_path = temp_dir.path().join("events.json");
        let log_path = temp_dir.path().join("invocations.jsonl");
        let call_index_path = temp_dir.path().join("call_index.txt");

        std::fs::copy(
            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests")
                .join("fixtures")
                .join("mock_codex_cli.py"),
            &script_path,
        )
        .expect("copy mock cli");

        #[cfg(unix)]
        {
            let mut perms = std::fs::metadata(&script_path)
                .expect("metadata")
                .permissions();
            perms.set_mode(0o755);
            std::fs::set_permissions(&script_path, perms).expect("chmod");
        }

        std::fs::write(
            &events_path,
            serde_json::to_vec(&events_per_call).expect("serialize events"),
        )
        .expect("write events");
        std::fs::write(&log_path, "").expect("initialize log");
        std::fs::write(&call_index_path, "0").expect("initialize call index");

        Self {
            _temp_dir: temp_dir,
            script_path,
            events_path,
            log_path,
            call_index_path,
        }
    }

    pub fn codex(&self, mut options: CodexOptions) -> Result<Codex> {
        options.codex_path_override = Some(self.script_path.to_string_lossy().into_owned());

        let mut env = self.base_env();
        if let Some(custom_env) = options.env.take() {
            env.extend(custom_env);
        }
        options.env = Some(env);

        Codex::new(Some(options))
    }

    pub fn logs(&self) -> Vec<InvocationLog> {
        let content = std::fs::read_to_string(&self.log_path).expect("read logs");
        content
            .lines()
            .filter(|line| !line.trim().is_empty())
            .map(|line| serde_json::from_str::<InvocationLog>(line).expect("parse log line"))
            .collect()
    }

    pub fn path_exists(&self, path: &str) -> bool {
        PathBuf::from(path).exists()
    }

    fn base_env(&self) -> HashMap<String, String> {
        let mut env = HashMap::new();
        env.insert(
            "CODEX_MOCK_EVENTS".to_string(),
            self.events_path.to_string_lossy().into_owned(),
        );
        env.insert(
            "CODEX_MOCK_LOG".to_string(),
            self.log_path.to_string_lossy().into_owned(),
        );
        env.insert(
            "CODEX_MOCK_CALL_INDEX".to_string(),
            self.call_index_path.to_string_lossy().into_owned(),
        );
        env.insert("CODEX_MOCK_ENFORCE_GIT_CHECK".to_string(), "1".to_string());
        if let Ok(path) = std::env::var("PATH") {
            env.insert("PATH".to_string(), path);
        }
        env
    }
}

pub fn expect_pair(args: &[String], pair: (&str, &str)) {
    let index = args
        .windows(2)
        .position(|window| window[0] == pair.0 && window[1] == pair.1);
    assert!(
        index.is_some(),
        "Pair {} {} not found in args: {:?}",
        pair.0,
        pair.1,
        args
    );
}

pub fn collect_config_values(args: &[String], key: &str) -> Vec<String> {
    let mut values = Vec::new();
    for window in args.windows(2) {
        if window[0] == "--config" && window[1].starts_with(&format!("{key}=")) {
            values.push(window[1].to_string());
        }
    }
    values
}