codex draft

This commit is contained in:
Ryan Ragona
2025-04-26 06:40:39 -07:00
parent b0ba65a936
commit 314d2216cb
8 changed files with 556 additions and 2 deletions
+128 -2
View File
@@ -578,6 +578,25 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "codex-session"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"codex-core",
"directories",
"libc",
"nix 0.27.1",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "codex-tui"
version = "0.1.0"
@@ -854,13 +873,22 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
"dirs-sys 0.5.0",
]
[[package]]
@@ -873,6 +901,18 @@ dependencies = [
"dirs-sys-next",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users 0.4.6",
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
@@ -2061,6 +2101,17 @@ dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.28.0"
@@ -2760,7 +2811,7 @@ dependencies = [
"libc",
"log",
"memchr",
"nix",
"nix 0.28.0",
"radix_trie",
"unicode-segmentation",
"unicode-width 0.1.14",
@@ -3764,6 +3815,15 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
]
[[package]]
name = "valuable"
version = "0.1.1"
@@ -4010,6 +4070,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -4028,6 +4097,21 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -4060,6 +4144,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -4072,6 +4162,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -4084,6 +4180,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -4108,6 +4210,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -4120,6 +4228,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -4132,6 +4246,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@@ -4144,6 +4264,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
+1
View File
@@ -9,5 +9,6 @@ members = [
"execpolicy",
"interactive",
"repl",
"session",
"tui",
]
+41
View File
@@ -0,0 +1,41 @@
[package]
name = "codex-session"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "codex-session"
path = "src/main.rs"
[lib]
name = "codex_session"
path = "src/lib.rs"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core" }
tokio = { version = "1", features = [
"io-std",
"macros",
"process",
"rt-multi-thread",
"signal",
] }
tracing = { version = "0.1.41", features = ["log"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
# new dependencies for session management
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
directories = "5"
# unix-only process helpers
nix = { version = "0.27", optional = true, default-features = false, features = ["process"] }
libc = { version = "0.2", optional = true }
[target.'cfg(unix)'.dependencies]
nix = { version = "0.27", default-features = false, features = ["process"] }
libc = "0.2"
+191
View File
@@ -0,0 +1,191 @@
//! Command-line interface definition and dispatch.
use crate::{spawn, store};
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
/// Top-level CLI entry (re-exported by the crate).
#[derive(Parser)]
#[command(name = "codex-session", about = "Manage detached codex-exec sessions")]
pub struct Cli {
#[command(subcommand)]
cmd: Commands,
}
impl Cli {
pub async fn dispatch(self) -> Result<()> {
match self.cmd {
Commands::Create(x) => x.run().await,
Commands::Delete(x) => x.run().await,
Commands::Logs(x) => x.run().await,
Commands::Exec(x) => x.run().await,
Commands::List(x) => x.run().await,
}
}
}
#[derive(Subcommand)]
enum Commands {
/// Spawn a new, detached agent.
Create(CreateCmd),
/// Kill a running session and delete on-disk artefacts.
Delete(DeleteCmd),
/// Show (and optionally follow) stdout / stderr logs of a session.
Logs(LogsCmd),
/// Execute a one-shot command inside an existing session.
Exec(ExecCmd),
/// List all known session IDs.
List(ListCmd),
}
// -----------------------------------------------------------------------------
// create
#[derive(Args)]
pub struct CreateCmd {
/// A custom session identifier. When omitted, a random UUIDv4 is used.
#[arg(long)]
id: Option<String>,
/// Path to the `codex-exec` binary. Defaults to relying on $PATH.
#[arg(long, default_value = "codex-exec")]
exec: String,
/// If set, terminate the agent when the CLI process exits ("attached" mode).
#[arg(long)]
kill_on_drop: bool,
}
impl CreateCmd {
pub async fn run(self) -> Result<()> {
let id = self
.id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
// Persist basic metadata & directory skeleton *before* spawning the process.
let meta = store::SessionMeta {
id: id.clone(),
created_at: chrono::Utc::now(),
};
let paths = store::paths_for(&id)?;
store::materialise(&paths, &meta)?;
// Spawn the background agent and immediately detach.
let mut child = spawn::spawn_agent(&self.exec, &id, &paths, self.kill_on_drop)?;
if self.kill_on_drop {
// Hold the handle for the lifetime of the CLI; when we drop at the end of
// `run()` the agent will be terminated by the `kill_on_drop` setting.
tokio::spawn(async move {
let _ = child.wait().await;
});
}
// When not in kill_on_drop mode we *immediately* drop the handle so the agent can
// outlive us.
println!("{id}");
Ok(())
}
}
// -----------------------------------------------------------------------------
// delete
#[derive(Args)]
pub struct DeleteCmd {
/// Session ID to terminate and remove.
id: String,
}
impl DeleteCmd {
pub async fn run(self) -> Result<()> {
store::kill_session(&self.id).await?;
store::purge(&self.id)?;
Ok(())
}
}
// -----------------------------------------------------------------------------
// logs
#[derive(Args)]
pub struct LogsCmd {
/// Session ID whose logs should be printed.
id: String,
/// Follow the file and stream appended lines (like `tail -f`).
#[arg(short, long)]
follow: bool,
/// Show stderr instead of stdout.
#[arg(long)]
stderr: bool,
}
impl LogsCmd {
pub async fn run(self) -> Result<()> {
use tokio::io::AsyncBufReadExt;
let paths = store::paths_for(&self.id)?;
let target = if self.stderr {
&paths.stderr
} else {
&paths.stdout
};
let file = tokio::fs::File::open(target).await?;
if self.follow {
let mut reader = tokio::io::BufReader::new(file);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
println!("{line}");
}
} else {
// Simply dump the file contents to stdout.
let mut stdout = tokio::io::stdout();
tokio::io::copy(&mut tokio::io::BufReader::new(file), &mut stdout).await?;
}
Ok(())
}
}
// -----------------------------------------------------------------------------
// exec (not implemented yet)
#[derive(Args)]
pub struct ExecCmd {
id: String,
/// Remaining arguments form the command to execute.
#[arg(trailing_var_arg = true)]
cmd: Vec<String>,
}
impl ExecCmd {
pub async fn run(self) -> Result<()> {
anyhow::bail!("exec inside an existing session is not yet implemented");
}
}
// -----------------------------------------------------------------------------
// list
#[derive(Args)]
pub struct ListCmd;
impl ListCmd {
pub async fn run(self) -> Result<()> {
let sessions = store::list_sessions()?;
for meta in sessions {
println!("{}\t{}", meta.id, meta.created_at);
}
Ok(())
}
}
+13
View File
@@ -0,0 +1,13 @@
//! Library entry-point re-exporting the CLI so the binary can stay tiny.
pub mod cli;
mod spawn;
mod store;
pub use cli::Cli;
/// Binary entry – the bin crate’s `main.rs` calls into this for testability.
pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
cli.dispatch().await
}
+11
View File
@@ -0,0 +1,11 @@
use clap::Parser;
use codex_session::run_main;
use codex_session::Cli;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
run_main(cli).await?;
Ok(())
}
+80
View File
@@ -0,0 +1,80 @@
//! Cross-platform helper to spawn a detached `codex-exec` agent.
use crate::store::Paths;
use anyhow::{Context, Result};
use std::fs::OpenOptions;
use tokio::process::{Child, Command};
#[cfg(unix)]
pub fn spawn_agent(exec: &str, id: &str, paths: &Paths, kill_on_drop: bool) -> Result<Child> {
use std::io;
// Prepare stdio handles first.
let stdin = OpenOptions::new().read(true).open("/dev/null")?;
let stdout = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stdout)?;
let stderr = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stderr)?;
let mut cmd = Command::new(exec);
cmd.arg("--job").arg(id)
.stdin(stdin)
.stdout(stdout)
.stderr(stderr);
if kill_on_drop {
cmd.kill_on_drop(true);
}
// Detach: make a new session and ignore SIGHUP.
unsafe {
cmd.pre_exec(|| {
unsafe {
// setsid(2)
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
libc::signal(libc::SIGHUP, libc::SIG_IGN);
}
Ok(())
});
}
let child = cmd.spawn().context("failed to spawn agent")?;
Ok(child)
}
#[cfg(windows)]
pub fn spawn_agent(exec: &str, id: &str, paths: &Paths, kill_on_drop: bool) -> Result<Child> {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
let stdout = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stdout)?;
let stderr = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stderr)?;
let mut cmd = Command::new(exec);
cmd.arg("--job").arg(id)
.stdin(std::process::Stdio::null())
.stdout(stdout)
.stderr(stderr)
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
if kill_on_drop {
cmd.kill_on_drop(true);
}
let child = cmd.spawn().context("failed to spawn agent")?;
Ok(child)
}
+91
View File
@@ -0,0 +1,91 @@
//! Session bookkeeping – on-disk layout and simple helpers.
use anyhow::{Context, Result};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct Paths {
pub dir: PathBuf,
pub stdout: PathBuf,
pub stderr: PathBuf,
pub meta: PathBuf,
}
/// Calculate canonical paths for the given session ID.
pub fn paths_for(id: &str) -> Result<Paths> {
let dir = base_dir()?.join(id);
Ok(Paths {
dir: dir.clone(),
stdout: dir.join("stdout.log"),
stderr: dir.join("stderr.log"),
meta: dir.join("meta.json"),
})
}
fn base_dir() -> Result<PathBuf> {
let dirs = ProjectDirs::from("dev", "codex", "codex-session")
.context("unable to resolve data directory")?;
Ok(dirs.data_dir().to_owned())
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SessionMeta {
pub id: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// Create the on-disk directory structure and write metadata + empty log files.
pub fn materialise(paths: &Paths, meta: &SessionMeta) -> Result<()> {
std::fs::create_dir_all(&paths.dir)?;
// Metadata (pretty-printed for manual inspection).
std::fs::write(&paths.meta, serde_json::to_vec_pretty(meta)?)?;
// Touch stdout/stderr so they exist even before the agent writes.
for p in [&paths.stdout, &paths.stderr] {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(p)?;
}
Ok(())
}
/// Enumerate all sessions by loading each `meta.json`.
pub fn list_sessions() -> Result<Vec<SessionMeta>> {
let mut res = Vec::new();
let base = base_dir()?;
if base.exists() {
for entry in std::fs::read_dir(base)? {
let entry = entry?;
let meta_path = entry.path().join("meta.json");
if let Ok(bytes) = std::fs::read(&meta_path) {
if let Ok(meta) = serde_json::from_slice::<SessionMeta>(&bytes) {
res.push(meta);
}
}
}
}
Ok(res)
}
/// Send a polite termination request to the session’s process.
///
/// NOTE: Full PID accounting is a future improvement; for now the function
/// simply returns `Ok(())` so the `delete` command doesn’t fail.
pub async fn kill_session(_id: &str) -> Result<()> {
// TODO: record PID at spawn time and terminate here.
Ok(())
}
/// Remove the session directory and all its contents.
pub fn purge(id: &str) -> Result<()> {
let paths = paths_for(id)?;
if paths.dir.exists() {
std::fs::remove_dir_all(paths.dir)?;
}
Ok(())
}