unified-agent-api-codex 0.3.5

Async wrapper around the Codex CLI for programmatic prompting
Documentation
use super::*;

#[cfg(unix)]
#[tokio::test]
async fn sandbox_maps_platform_flags_and_command() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
echo "$PWD"
printf "%s\n" "$@"
"#,
    );

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();

    let request = SandboxCommandRequest::new(
        SandboxPlatform::Linux,
        [OsString::from("echo"), OsString::from("hello world")],
    )
    .full_auto(true)
    .log_denials(true)
    .config_override("foo", "bar")
    .enable_feature("alpha")
    .disable_feature("beta");

    let run = client.run_sandbox(request).await.unwrap();
    let mut lines = run.stdout.lines();
    let pwd = lines.next().unwrap();
    assert_eq!(Path::new(pwd), env::current_dir().unwrap().as_path());

    let args: Vec<_> = lines.map(str::to_string).collect();
    assert!(!args.contains(&"--log-denials".to_string()));
    assert_eq!(
        args,
        vec![
            "sandbox",
            "linux",
            "--full-auto",
            "--config",
            "foo=bar",
            "--enable",
            "alpha",
            "--disable",
            "beta",
            "--",
            "echo",
            "hello world"
        ]
    );
    assert!(run.status.success());
}

#[cfg(unix)]
#[tokio::test]
async fn sandbox_includes_log_denials_on_macos() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
printf "%s\n" "$@"
"#,
    );

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();

    let run = client
        .run_sandbox(SandboxCommandRequest::new(SandboxPlatform::Macos, ["ls"]).log_denials(true))
        .await
        .unwrap();
    let args: Vec<_> = run.stdout.lines().collect();
    assert!(args.contains(&"--log-denials"));
    assert_eq!(args[0], "sandbox");
    assert_eq!(args[1], "macos");
}

#[cfg(unix)]
#[tokio::test]
async fn sandbox_honors_working_dir_precedence() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
echo "$PWD"
"#,
    );

    let request_dir = dir.path().join("request_cwd");
    let builder_dir = dir.path().join("builder_cwd");
    std_fs::create_dir_all(&request_dir).unwrap();
    std_fs::create_dir_all(&builder_dir).unwrap();

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .working_dir(&builder_dir)
        .build();

    let run_request = client
        .run_sandbox(
            SandboxCommandRequest::new(SandboxPlatform::Windows, ["echo", "cwd"])
                .working_dir(&request_dir),
        )
        .await
        .unwrap();
    let request_pwd = run_request.stdout.lines().next().unwrap();
    let request_pwd = std_fs::canonicalize(Path::new(request_pwd)).unwrap();
    let request_dir = std_fs::canonicalize(&request_dir).unwrap();
    assert_eq!(request_pwd, request_dir);

    let run_builder = client
        .run_sandbox(SandboxCommandRequest::new(
            SandboxPlatform::Windows,
            ["echo", "builder"],
        ))
        .await
        .unwrap();
    let builder_pwd = run_builder.stdout.lines().next().unwrap();
    let builder_pwd = std_fs::canonicalize(Path::new(builder_pwd)).unwrap();
    let builder_dir = std_fs::canonicalize(&builder_dir).unwrap();
    assert_eq!(builder_pwd, builder_dir);

    let client_default = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();
    let run_default = client_default
        .run_sandbox(SandboxCommandRequest::new(
            SandboxPlatform::Windows,
            ["echo", "default"],
        ))
        .await
        .unwrap();
    let default_pwd = run_default.stdout.lines().next().unwrap();
    assert_eq!(
        Path::new(default_pwd),
        env::current_dir().unwrap().as_path()
    );
}

#[cfg(unix)]
#[tokio::test]
async fn sandbox_returns_non_zero_status_without_error() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = write_fake_codex(
        dir.path(),
        r#"#!/usr/bin/env bash
echo "failing"
exit 7
"#,
    );

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();
    let run = client
        .run_sandbox(SandboxCommandRequest::new(
            SandboxPlatform::Linux,
            ["false"],
        ))
        .await
        .unwrap();

    assert!(!run.status.success());
    assert_eq!(run.status.code(), Some(7));
    assert_eq!(run.stdout.trim(), "failing");
}

#[cfg(unix)]
#[tokio::test]
async fn execpolicy_maps_policies_and_overrides() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = dir.path().join("codex-execpolicy");
    std_fs::write(
        &script_path,
        r#"#!/usr/bin/env bash
printf "%s\n" "$PWD" "$@" 1>&2
cat <<'JSON'
{"match":{"decision":"prompt","rules":[{"name":"rule1","decision":"forbidden"}]}}
JSON
"#,
    )
    .unwrap();
    let mut perms = std_fs::metadata(&script_path).unwrap().permissions();
    perms.set_mode(0o755);
    std_fs::set_permissions(&script_path, perms).unwrap();

    let workdir = dir.path().join("workdir");
    std_fs::create_dir_all(&workdir).unwrap();
    let policy_one = dir.path().join("policy_a.codexpolicy");
    let policy_two = dir.path().join("policy_b.codexpolicy");
    std_fs::write(&policy_one, "").unwrap();
    std_fs::write(&policy_two, "").unwrap();

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .working_dir(&workdir)
        .approval_policy(ApprovalPolicy::OnRequest)
        .build();

    let result = client
        .check_execpolicy(
            ExecPolicyCheckRequest::new([
                OsString::from("bash"),
                OsString::from("-lc"),
                OsString::from("echo ok"),
            ])
            .policies([&policy_one, &policy_two])
            .pretty(true)
            .profile("dev")
            .config_override("features.execpolicy", "true"),
        )
        .await
        .unwrap();

    assert_eq!(result.decision(), Some(ExecPolicyDecision::Prompt));
    let match_result = result.evaluation.match_result.unwrap();
    assert_eq!(match_result.rules.len(), 1);
    assert_eq!(match_result.rules[0].name.as_deref(), Some("rule1"));
    assert_eq!(
        match_result.rules[0].decision,
        Some(ExecPolicyDecision::Forbidden)
    );

    let mut lines = result.stderr.lines();
    let pwd = lines.next().unwrap();
    let pwd = std_fs::canonicalize(Path::new(pwd)).unwrap();
    let workdir = std_fs::canonicalize(&workdir).unwrap();
    assert_eq!(pwd, workdir);

    let args: Vec<_> = lines.map(str::to_string).collect();
    assert_eq!(
        args,
        vec![
            "--config",
            "features.execpolicy=true",
            "--profile",
            "dev",
            "--ask-for-approval",
            "on-request",
            "execpolicy",
            "check",
            "--policy",
            policy_one.to_string_lossy().as_ref(),
            "--policy",
            policy_two.to_string_lossy().as_ref(),
            "--pretty",
            "--",
            "bash",
            "-lc",
            "echo ok"
        ]
    );
}

#[tokio::test]
async fn execpolicy_rejects_empty_command() {
    let _guard = env_guard_async().await;
    let client = CodexClient::builder().build();
    let request = ExecPolicyCheckRequest::new(Vec::<OsString>::new());
    let err = client.check_execpolicy(request).await.unwrap_err();
    assert!(matches!(err, CodexError::EmptyExecPolicyCommand));
}

#[cfg(unix)]
#[tokio::test]
async fn execpolicy_surfaces_parse_errors() {
    let _guard = env_guard_async().await;
    let dir = tempfile::tempdir().unwrap();
    let script_path = dir.path().join("codex-execpolicy-bad");
    std_fs::write(
        &script_path,
        r#"#!/usr/bin/env bash
echo "not-json"
"#,
    )
    .unwrap();
    let mut perms = std_fs::metadata(&script_path).unwrap().permissions();
    perms.set_mode(0o755);
    std_fs::set_permissions(&script_path, perms).unwrap();

    let client = CodexClient::builder()
        .binary(&script_path)
        .mirror_stdout(false)
        .quiet(true)
        .build();

    let err = client
        .check_execpolicy(
            ExecPolicyCheckRequest::new([OsString::from("echo"), OsString::from("noop")])
                .policy(dir.path().join("policy.codexpolicy")),
        )
        .await
        .unwrap_err();

    match err {
        CodexError::ExecPolicyParse { stdout, .. } => assert!(stdout.contains("not-json")),
        other => panic!("expected ExecPolicyParse, got {other:?}"),
    }
}

#[tokio::test]
async fn sandbox_rejects_empty_command() {
    let _guard = env_guard_async().await;
    let client = CodexClient::builder().build();
    let request = SandboxCommandRequest::new(SandboxPlatform::Linux, Vec::<OsString>::new());
    let err = client.run_sandbox(request).await.unwrap_err();
    assert!(matches!(err, CodexError::EmptySandboxCommand));
}