use nix::libc;
use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet, Signal, sigaction};
use serde_json::json;
use std::env;
use std::fs;
use std::io::{self, Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::process::CommandExt;
use std::process::{self, Command};
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Duration;
static CHILD: AtomicI32 = AtomicI32::new(0);
extern "C" fn cancelled(signal: i32) {
unsafe {
libc::write(libc::STDERR_FILENO, b"cancelled\n".as_ptr().cast(), 10);
let child = CHILD.swap(0, Ordering::SeqCst);
if child > 0 {
libc::kill(child, signal);
while libc::waitpid(child, std::ptr::null_mut(), 0) < 0 {
if io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
break;
}
}
}
libc::_exit(128 + signal);
}
}
fn json_mode() -> bool {
env::args_os().any(|arg| arg == "--json")
}
fn message(text: &str) {
if json_mode() {
println!(
"{}",
json!({"type":"item.completed","item":{"type":"agent_message","text":text}})
);
} else {
println!("{text}");
}
}
fn checked_git(args: &[&str]) {
if json_mode() {
let output = Command::new("git")
.args(args)
.output()
.expect("fixture Git");
println!(
"{}",
json!({"type":"item.completed","item":{"type":"command_execution","command":format!("git {}", args.join(" ")),"status":if output.status.success() {"completed"} else {"failed"},"exit_code":output.status.code(),"aggregated_output":format!("{}{}",String::from_utf8_lossy(&output.stdout),String::from_utf8_lossy(&output.stderr))}})
);
if !output.status.success() {
process::exit(output.status.code().unwrap_or(1));
}
return;
}
let status = Command::new("git")
.args(args)
.status()
.expect("run fixture Git");
if !status.success() {
process::exit(status.code().unwrap_or(1));
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let action = SigAction::new(
SigHandler::Handler(cancelled),
SaFlags::empty(),
SigSet::empty(),
);
unsafe {
sigaction(Signal::SIGINT, &action)?;
sigaction(Signal::SIGTERM, &action)?;
}
let args: Vec<_> = env::args_os().skip(1).collect();
let argument_bytes: Vec<_> = args.iter().map(|arg| arg.as_bytes()).collect();
fs::write(
env::var_os("FAKE_CODEX_LOG").expect("fixture log path"),
serde_json::to_vec(&json!({
"argv": argument_bytes,
"cwd": env::current_dir()?.as_os_str().as_bytes(),
"pid": process::id(),
}))?,
)?;
if json_mode() {
println!(
"{}",
json!({"type":"thread.started","thread_id":"mock-session"})
);
println!("{}", json!({"type":"turn.started"}));
}
let mode = env::var("FAKE_CODEX_MODE").unwrap_or_default();
match mode.as_str() {
"stream" | "wait" | "wait-child" => {
if mode == "wait-child" {
let child = Command::new("/bin/sleep").arg("60").spawn()?;
let pid = i32::try_from(child.id())?;
CHILD.store(pid, Ordering::SeqCst);
drop(child);
let pgid = nix::unistd::getpgid(Some(nix::unistd::Pid::from_raw(pid)))?;
fs::write(
env::var_os("FAKE_CODEX_CHILD_LOG").expect("fixture child log path"),
serde_json::to_vec(&json!({"pid": pid, "pgid": pgid.as_raw()}))?,
)?;
message("child ready");
}
eprintln!("live progress");
message("live output");
if env::var_os("FAKE_CODEX_LARGE_PROGRESS").is_some() {
message(&format!("BLOCKED_PROGRESS{}", "x".repeat(200000)));
}
if json_mode() {
println!(
"{}",
json!({"type":"item.started","item":{"type":"command_execution","command":"wait","status":"in_progress"}})
);
}
io::stdout().flush()?;
if mode == "stream" {
std::thread::sleep(Duration::from_millis(800));
} else {
loop {
std::thread::sleep(Duration::from_millis(100));
}
}
}
"git" => {
checked_git(&["add", "--all"]);
match Command::new("git")
.args(["diff", "--cached", "--quiet"])
.status()?
.code()
{
Some(0) => {}
Some(1) => checked_git(&["commit", "-m", "Commit fixture change"]),
Some(code) => process::exit(code),
None => process::exit(1),
}
let prompt = args.last().and_then(|arg| arg.to_str()).unwrap_or("");
let policy = prompt.rsplit("Push policy: ").next().unwrap_or("");
match policy.lines().next() {
Some("enabled.") => checked_git(&["push", "--set-upstream", "origin", "HEAD"]),
Some("disabled.") => {}
_ => return Err("missing final push policy".into()),
}
}
"large-final" => {
message(&"final output".repeat(20000));
println!("{}", json!({"type":"turn.completed"}));
io::stdout().flush()?;
return Ok(());
}
"partial-wait" => {
print!("{{\"type\":\"item.completed\",\"item\":");
io::stdout().flush()?;
eprintln!("partial ready");
loop {
std::thread::sleep(Duration::from_millis(10));
}
}
"escaped" => {
let mut command = Command::new("/bin/sleep");
command.arg("60");
unsafe {
command.pre_exec(|| {
if libc::setsid() < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
let child = command.spawn()?;
fs::write(
env::var_os("FAKE_CODEX_CHILD_LOG").expect("child log"),
child.id().to_string(),
)?;
drop(child);
}
"stdin" => {
let mut text = String::new();
io::stdin().read_to_string(&mut text)?;
message(&text);
}
"large" => {
eprintln!("{}", "x".repeat(131072));
println!(
"{}",
json!({"type":"item.completed","item":{"type":"command_execution","command":"large diff","exit_code":0,"status":"completed","aggregated_output":"large successful diff".repeat(131072)}})
);
}
"malformed" => {
println!("not-json");
return Ok(());
}
"truncated" => {
return Ok(());
}
"diagnostic" => {
eprintln!("hook: PreToolUse");
eprintln!("hook: PostToolUse Completed");
eprintln!("warning: keep this warning");
println!(
"{}",
json!({"type":"item.completed","item":{"type":"command_execution","command":"git push","exit_code":128,"status":"failed","aggregated_output":"fatal: exact push error"}})
);
}
_ => {}
}
message("finished");
if json_mode() {
println!("{}", json!({"type":"turn.completed"}));
}
io::stdout().flush()?;
process::exit(
env::var("FAKE_CODEX_EXIT")
.unwrap_or_default()
.parse()
.unwrap_or(0),
);
}