Merge 5141716dbb into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2025-04-30 10:07:13 -07:00
committed by GitHub
10 changed files with 0 additions and 582 deletions

15
codex-rs/Cargo.lock generated
View File

@@ -475,7 +475,6 @@ dependencies = [
"clap",
"codex-core",
"codex-exec",
"codex-repl",
"codex-tui",
"serde_json",
"tokio",
@@ -557,20 +556,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "codex-repl"
version = "0.0.2504292236"
dependencies = [
"anyhow",
"clap",
"codex-core",
"owo-colors 4.2.0",
"rand",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "codex-tui"
version = "0.1.0"

View File

@@ -7,7 +7,6 @@ members = [
"core",
"exec",
"execpolicy",
"repl",
"tui",
]

View File

@@ -19,5 +19,4 @@ This folder is the root of a Cargo workspace. It contains quite a bit of experim
- [`core/`](./core) contains the business logic for Codex. Ultimately, we hope this to be a library crate that is generally useful for building other Rust/native applications that use Codex.
- [`exec/`](./exec) "headless" CLI for use in automation.
- [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/).
- [`repl/`](./repl) CLI that launches a lightweight REPL similar to the Python or Node.js REPL.
- [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands.

View File

@@ -20,7 +20,6 @@ anyhow = "1"
clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core" }
codex-exec = { path = "../exec" }
codex-repl = { path = "../repl" }
codex-tui = { path = "../tui" }
serde_json = "1"
tokio = { version = "1", features = [

View File

@@ -5,7 +5,6 @@ use codex_cli::seatbelt;
use codex_cli::LandlockCommand;
use codex_cli::SeatbeltCommand;
use codex_exec::Cli as ExecCli;
use codex_repl::Cli as ReplCli;
use codex_tui::Cli as TuiCli;
use crate::proto::ProtoCli;
@@ -34,10 +33,6 @@ enum Subcommand {
#[clap(visible_alias = "e")]
Exec(ExecCli),
/// Run the REPL.
#[clap(visible_alias = "r")]
Repl(ReplCli),
/// Run the Protocol stream via stdin/stdout
#[clap(visible_alias = "p")]
Proto(ProtoCli),
@@ -75,9 +70,6 @@ async fn main() -> anyhow::Result<()> {
Some(Subcommand::Exec(exec_cli)) => {
codex_exec::run_main(exec_cli).await?;
}
Some(Subcommand::Repl(repl_cli)) => {
codex_repl::run_main(repl_cli).await?;
}
Some(Subcommand::Proto(proto_cli)) => {
proto::run_main(proto_cli).await?;
}

View File

@@ -10,10 +10,6 @@ install:
tui *args:
cargo run --bin codex -- tui {{args}}
# Run the REPL app
repl *args:
cargo run --bin codex -- repl {{args}}
# Run the Proto app
proto *args:
cargo run --bin codex -- proto {{args}}

View File

@@ -1,28 +0,0 @@
[package]
name = "codex-repl"
version = { workspace = true }
edition = "2021"
[[bin]]
name = "codex-repl"
path = "src/main.rs"
[lib]
name = "codex_repl"
path = "src/lib.rs"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
codex-core = { path = "../core", features = ["cli"] }
owo-colors = "4.2.0"
rand = "0.9"
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"] }

View File

@@ -1,65 +0,0 @@
use clap::ArgAction;
use clap::Parser;
use codex_core::ApprovalModeCliArg;
use codex_core::SandboxPermissionOption;
use std::path::PathBuf;
/// Commandline arguments.
#[derive(Debug, Parser)]
#[command(
author,
version,
about = "Interactive Codex CLI that streams all agent actions."
)]
pub struct Cli {
/// User prompt to start the session.
pub prompt: Option<String>,
/// Override the default model from ~/.codex/config.toml.
#[arg(short, long)]
pub model: Option<String>,
/// Optional images to attach to the prompt.
#[arg(long, value_name = "FILE")]
pub images: Vec<PathBuf>,
/// Increase verbosity (-v info, -vv debug, -vvv trace).
///
/// The flag may be passed up to three times. Without any -v the CLI only prints warnings and errors.
#[arg(short, long, action = ArgAction::Count)]
pub verbose: u8,
/// Don't use colored ansi output for verbose logging
#[arg(long)]
pub no_ansi: bool,
/// Configure when the model requires human approval before executing a command.
#[arg(long = "ask-for-approval", short = 'a')]
pub approval_policy: Option<ApprovalModeCliArg>,
/// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, network-disabled sandbox that can write to cwd and TMPDIR)
#[arg(long = "full-auto", default_value_t = false)]
pub full_auto: bool,
#[clap(flatten)]
pub sandbox: SandboxPermissionOption,
/// Allow running Codex outside a Git repository. By default the CLI
/// aborts early when the current working directory is **not** inside a
/// Git repo because most agents rely on `git` for interacting with the
/// codebase. Pass this flag if you really know what you are doing.
#[arg(long, action = ArgAction::SetTrue, default_value_t = false)]
pub allow_no_git_exec: bool,
/// Disable serverside response storage (sends the full conversation context with every request)
#[arg(long = "disable-response-storage", default_value_t = false)]
pub disable_response_storage: bool,
/// Record submissions into file as JSON
#[arg(short = 'S', long)]
pub record_submissions: Option<PathBuf>,
/// Record events into file as JSON
#[arg(short = 'E', long)]
pub record_events: Option<PathBuf>,
}

View File

@@ -1,448 +0,0 @@
use std::io::stdin;
use std::io::stdout;
use std::io::Write;
use std::sync::Arc;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::FileChange;
use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo;
use codex_core::util::notify_on_sigint;
use codex_core::Codex;
use owo_colors::OwoColorize;
use owo_colors::Style;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
use tokio::io::Lines;
use tokio::io::Stdin;
use tokio::sync::Notify;
use tracing::debug;
use tracing_subscriber::EnvFilter;
mod cli;
pub use cli::Cli;
/// Initialize the global logger once at startup based on the `--verbose` flag.
fn init_logger(verbose: u8, allow_ansi: bool) {
// Map -v occurrences to explicit log levels:
// 0 → warn (default)
// 1 → info
// 2 → debug
// ≥3 → trace
let default_level = match verbose {
0 => "warn",
1 => "info",
2 => "codex=debug",
_ => "codex=trace",
};
// Only initialize the logger once repeated calls are ignored. `try_init` will return an
// error if another crate (like tests) initialized it first, which we can safely ignore.
// By default `tracing_subscriber::fmt()` writes formatted logs to stderr. That is fine when
// running the CLI manually but in our smoke tests we capture **stdout** (via `assert_cmd`) and
// ignore stderr. As a result none of the `tracing::info!` banners or warnings show up in the
// recorded output making it much harder to debug live runs.
// Switch the logger's writer to stdout so both human runs and the integration tests see the
// same stream. Disable ANSI colors because the binary already prints plain text and color
// escape codes make predicate matching brittle.
let _ = tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(default_level))
.unwrap(),
)
.with_ansi(allow_ansi)
.with_writer(std::io::stdout)
.try_init();
}
pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
let ctrl_c = notify_on_sigint();
// Abort early when the user runs Codex outside a Git repository unless
// they explicitly acknowledged the risks with `--allow-no-git-exec`.
if !cli.allow_no_git_exec && !is_inside_git_repo() {
eprintln!(
"We recommend running codex inside a git repository. \
If you understand the risks, you can proceed with \
`--allow-no-git-exec`."
);
std::process::exit(1);
}
// Initialize logging before any other work so early errors are captured.
init_logger(cli.verbose, !cli.no_ansi);
let (sandbox_policy, approval_policy) = if cli.full_auto {
(
Some(SandboxPolicy::new_full_auto_policy()),
Some(AskForApproval::OnFailure),
)
} else {
let sandbox_policy = cli.sandbox.permissions.clone().map(Into::into);
(sandbox_policy, cli.approval_policy.map(Into::into))
};
// Load config file and apply CLI overrides (model & approval policy)
let overrides = ConfigOverrides {
model: cli.model.clone(),
approval_policy,
sandbox_policy,
disable_response_storage: if cli.disable_response_storage {
Some(true)
} else {
None
},
};
let config = Config::load_with_overrides(overrides)?;
codex_main(cli, config, ctrl_c).await
}
async fn codex_main(cli: Cli, cfg: Config, ctrl_c: Arc<Notify>) -> anyhow::Result<()> {
let mut builder = Codex::builder();
if let Some(path) = cli.record_submissions {
builder = builder.record_submissions(path);
}
if let Some(path) = cli.record_events {
builder = builder.record_events(path);
}
let codex = builder.spawn(Arc::clone(&ctrl_c))?;
let init_id = random_id();
let init = protocol::Submission {
id: init_id.clone(),
op: protocol::Op::ConfigureSession {
model: cfg.model,
instructions: cfg.instructions,
approval_policy: cfg.approval_policy,
sandbox_policy: cfg.sandbox_policy,
disable_response_storage: cfg.disable_response_storage,
},
};
out(
"initializing session",
MessagePriority::BackgroundEvent,
MessageActor::User,
);
codex.submit(init).await?;
// init
loop {
out(
"waiting for session initialization",
MessagePriority::BackgroundEvent,
MessageActor::User,
);
let event = codex.next_event().await?;
if event.id == init_id {
if let protocol::EventMsg::Error { message } = event.msg {
anyhow::bail!("Error during initialization: {message}");
} else {
out(
"session initialized",
MessagePriority::BackgroundEvent,
MessageActor::User,
);
break;
}
}
}
// run loop
let mut reader = InputReader::new(ctrl_c.clone());
loop {
let text = match &cli.prompt {
Some(input) => input.clone(),
None => match reader.request_input().await? {
Some(input) => input,
None => {
// ctrl + d
println!();
return Ok(());
}
},
};
if text.trim().is_empty() {
continue;
}
// Interpret certain singleword commands as immediate termination requests.
let trimmed = text.trim();
if trimmed == "q" {
// Exit gracefully.
println!("Exiting…");
return Ok(());
}
let sub = protocol::Submission {
id: random_id(),
op: protocol::Op::UserInput {
items: vec![protocol::InputItem::Text { text }],
},
};
out(
"sending request to model",
MessagePriority::TaskProgress,
MessageActor::User,
);
codex.submit(sub).await?;
// Wait for agent events **or** user interrupts (Ctrl+C).
'inner: loop {
// Listen for either the next agent event **or** a SIGINT notification. Using
// `tokio::select!` allows the user to cancel a longrunning request that would
// otherwise leave the CLI stuck waiting for a server response.
let event = {
let interrupted = ctrl_c.notified();
tokio::select! {
_ = interrupted => {
// Forward an interrupt to the agent so it can abort any inflight task.
let _ = codex
.submit(protocol::Submission {
id: random_id(),
op: protocol::Op::Interrupt,
})
.await;
// Exit the inner loop and return to the main input prompt. The agent
// will emit a `TurnInterrupted` (Error) event which is drained later.
break 'inner;
}
res = codex.next_event() => res?
}
};
debug!(?event, "Got event");
let id = event.id;
match event.msg {
protocol::EventMsg::Error { message } => {
println!("Error: {message}");
break 'inner;
}
protocol::EventMsg::TaskComplete => break 'inner,
protocol::EventMsg::AgentMessage { message } => {
out(&message, MessagePriority::UserMessage, MessageActor::Agent)
}
protocol::EventMsg::SessionConfigured { model } => {
debug!(model, "Session initialized");
}
protocol::EventMsg::ExecApprovalRequest {
command,
cwd,
reason,
} => {
let reason_str = reason
.as_deref()
.map(|r| format!(" [{r}]"))
.unwrap_or_default();
let prompt = format!(
"approve command in {} {}{} (y/N): ",
cwd.display(),
command.join(" "),
reason_str
);
let decision = request_user_approval2(prompt)?;
let sub = protocol::Submission {
id: random_id(),
op: protocol::Op::ExecApproval { id, decision },
};
out(
"submitting command approval",
MessagePriority::TaskProgress,
MessageActor::User,
);
codex.submit(sub).await?;
}
protocol::EventMsg::ApplyPatchApprovalRequest {
changes,
reason: _,
grant_root: _,
} => {
let file_list = changes
.keys()
.map(|path| path.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(", ");
let request =
format!("approve apply_patch that will touch? {file_list} (y/N): ");
let decision = request_user_approval2(request)?;
let sub = protocol::Submission {
id: random_id(),
op: protocol::Op::PatchApproval { id, decision },
};
out(
"submitting patch approval",
MessagePriority::UserMessage,
MessageActor::Agent,
);
codex.submit(sub).await?;
}
protocol::EventMsg::ExecCommandBegin {
command,
cwd,
call_id: _,
} => {
out(
&format!("running command: '{}' in '{}'", command.join(" "), cwd),
MessagePriority::BackgroundEvent,
MessageActor::Agent,
);
}
protocol::EventMsg::ExecCommandEnd {
stdout,
stderr,
exit_code,
call_id: _,
} => {
let msg = if exit_code == 0 {
"command completed (exit 0)".to_string()
} else {
// Prefer stderr but fall back to stdout if empty.
let err_snippet = if !stderr.trim().is_empty() {
stderr.trim()
} else {
stdout.trim()
};
format!("command failed (exit {exit_code}): {err_snippet}")
};
out(&msg, MessagePriority::BackgroundEvent, MessageActor::Agent);
out(
"sending results to model",
MessagePriority::TaskProgress,
MessageActor::Agent,
);
}
protocol::EventMsg::PatchApplyBegin { changes, .. } => {
// Emit PatchApplyBegin so the frontend can show progress.
let summary = changes
.iter()
.map(|(path, change)| match change {
FileChange::Add { .. } => format!("A {}", path.display()),
FileChange::Delete => format!("D {}", path.display()),
FileChange::Update { .. } => format!("M {}", path.display()),
})
.collect::<Vec<_>>()
.join(", ");
out(
&format!("applying patch: {summary}"),
MessagePriority::BackgroundEvent,
MessageActor::Agent,
);
}
protocol::EventMsg::PatchApplyEnd { success, .. } => {
let status = if success { "success" } else { "failed" };
out(
&format!("patch application {status}"),
MessagePriority::BackgroundEvent,
MessageActor::Agent,
);
out(
"sending results to model",
MessagePriority::TaskProgress,
MessageActor::Agent,
);
}
// Broad fallback; if the CLI is unaware of an event type, it will just
// print it as a generic BackgroundEvent.
e => {
out(
&format!("event: {e:?}"),
MessagePriority::BackgroundEvent,
MessageActor::Agent,
);
}
}
}
}
}
fn random_id() -> String {
let id: u64 = rand::random();
id.to_string()
}
fn request_user_approval2(request: String) -> anyhow::Result<protocol::ReviewDecision> {
println!("{}", request);
let mut line = String::new();
stdin().read_line(&mut line)?;
let answer = line.trim().to_ascii_lowercase();
let is_accepted = answer == "y" || answer == "yes";
let decision = if is_accepted {
protocol::ReviewDecision::Approved
} else {
protocol::ReviewDecision::Denied
};
Ok(decision)
}
#[derive(Debug, Clone, Copy)]
enum MessagePriority {
BackgroundEvent,
TaskProgress,
UserMessage,
}
enum MessageActor {
Agent,
User,
}
impl From<MessageActor> for String {
fn from(actor: MessageActor) -> Self {
match actor {
MessageActor::Agent => "codex".to_string(),
MessageActor::User => "user".to_string(),
}
}
}
fn out(msg: &str, priority: MessagePriority, actor: MessageActor) {
let actor: String = actor.into();
let style = match priority {
MessagePriority::BackgroundEvent => Style::new().fg_rgb::<127, 127, 127>(),
MessagePriority::TaskProgress => Style::new().fg_rgb::<200, 200, 200>(),
MessagePriority::UserMessage => Style::new().white(),
};
println!("{}> {}", actor.bold(), msg.style(style));
}
struct InputReader {
reader: Lines<BufReader<Stdin>>,
ctrl_c: Arc<Notify>,
}
impl InputReader {
pub fn new(ctrl_c: Arc<Notify>) -> Self {
Self {
reader: BufReader::new(tokio::io::stdin()).lines(),
ctrl_c,
}
}
pub async fn request_input(&mut self) -> std::io::Result<Option<String>> {
print!("user> ");
stdout().flush()?;
let interrupted = self.ctrl_c.notified();
tokio::select! {
line = self.reader.next_line() => {
match line? {
Some(input) => Ok(Some(input.trim().to_string())),
None => Ok(None),
}
}
_ = interrupted => {
println!();
Ok(Some(String::new()))
}
}
}
}

View File

@@ -1,11 +0,0 @@
use clap::Parser;
use codex_repl::run_main;
use codex_repl::Cli;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
run_main(cli).await?;
Ok(())
}