mirror of
https://github.com/openai/codex.git
synced 2026-04-30 03:12:20 +03:00
On a high-level, we try to design `config.toml` so that you don't have to "comment out a lot of stuff" when testing different options. Previously, defining a sandbox policy was somewhat at odds with this principle because you would define the policy as attributes of `[sandbox]` like so: ```toml [sandbox] mode = "workspace-write" writable_roots = [ "/tmp" ] ``` but if you wanted to temporarily change to a read-only sandbox, you might feel compelled to modify your file to be: ```toml [sandbox] mode = "read-only" # mode = "workspace-write" # writable_roots = [ "/tmp" ] ``` Technically, commenting out `writable_roots` would not be strictly necessary, as `mode = "read-only"` would ignore `writable_roots`, but it's still a reasonable thing to do to keep things tidy. Currently, the various values for `mode` do not support that many attributes, so this is not that hard to maintain, but one could imagine this becoming more complex in the future. In this PR, we change Codex CLI so that it no longer recognizes `[sandbox]`. Instead, it introduces a top-level option, `sandbox_mode`, and `[sandbox_workspace_write]` is used to further configure the sandbox when when `sandbox_mode = "workspace-write"` is used: ```toml sandbox_mode = "workspace-write" [sandbox_workspace_write] writable_roots = [ "/tmp" ] ``` This feels a bit more future-proof in that it is less tedious to configure different sandboxes: ```toml sandbox_mode = "workspace-write" [sandbox_read_only] # read-only options here... [sandbox_workspace_write] writable_roots = [ "/tmp" ] [sandbox_danger_full_access] # danger-full-access options here... ``` In this scheme, you never need to comment out the configuration for an individual sandbox type: you only need to redefine `sandbox_mode`. Relatedly, previous to this change, a user had to do `-c sandbox.mode=read-only` to change the mode on the command line. With this change, things are arguably a bit cleaner because the equivalent option is `-c sandbox_mode=read-only` (and now `-c sandbox_workspace_write=...` can be set separately). Though more importantly, we introduce the `-s/--sandbox` option to the CLI, which maps directly to `sandbox_mode` in `config.toml`, making config override behavior easier to reason about. Moreover, as you can see in the updates to the various Markdown files, it is much easier to explain how to configure sandboxing when things like `--sandbox read-only` can be used as an example. Relatedly, this cleanup also made it straightforward to add support for a `sandbox` option for Codex when used as an MCP server (see the changes to `mcp-server/src/codex_tool_config.rs`). Fixes https://github.com/openai/codex/issues/1248.
250 lines
8.7 KiB
Rust
250 lines
8.7 KiB
Rust
mod cli;
|
||
mod event_processor;
|
||
|
||
use std::io::IsTerminal;
|
||
use std::io::Read;
|
||
use std::path::Path;
|
||
use std::path::PathBuf;
|
||
use std::sync::Arc;
|
||
|
||
pub use cli::Cli;
|
||
use codex_core::codex_wrapper;
|
||
use codex_core::config::Config;
|
||
use codex_core::config::ConfigOverrides;
|
||
use codex_core::config_types::SandboxMode;
|
||
use codex_core::protocol::AskForApproval;
|
||
use codex_core::protocol::Event;
|
||
use codex_core::protocol::EventMsg;
|
||
use codex_core::protocol::InputItem;
|
||
use codex_core::protocol::Op;
|
||
use codex_core::protocol::TaskCompleteEvent;
|
||
use codex_core::util::is_inside_git_repo;
|
||
use event_processor::EventProcessor;
|
||
use tracing::debug;
|
||
use tracing::error;
|
||
use tracing::info;
|
||
use tracing_subscriber::EnvFilter;
|
||
|
||
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||
let Cli {
|
||
images,
|
||
model,
|
||
config_profile,
|
||
full_auto,
|
||
dangerously_bypass_approvals_and_sandbox,
|
||
cwd,
|
||
skip_git_repo_check,
|
||
color,
|
||
last_message_file,
|
||
sandbox_mode: sandbox_mode_cli_arg,
|
||
prompt,
|
||
config_overrides,
|
||
} = cli;
|
||
|
||
// Determine the prompt based on CLI arg and/or stdin.
|
||
let prompt = match prompt {
|
||
Some(p) if p != "-" => p,
|
||
// Either `-` was passed or no positional arg.
|
||
maybe_dash => {
|
||
// When no arg (None) **and** stdin is a TTY, bail out early – unless the
|
||
// user explicitly forced reading via `-`.
|
||
let force_stdin = matches!(maybe_dash.as_deref(), Some("-"));
|
||
|
||
if std::io::stdin().is_terminal() && !force_stdin {
|
||
eprintln!(
|
||
"No prompt provided. Either specify one as an argument or pipe the prompt into stdin."
|
||
);
|
||
std::process::exit(1);
|
||
}
|
||
|
||
// Ensure the user knows we are waiting on stdin, as they may
|
||
// have gotten into this state by mistake. If so, and they are not
|
||
// writing to stdin, Codex will hang indefinitely, so this should
|
||
// help them debug in that case.
|
||
if !force_stdin {
|
||
eprintln!("Reading prompt from stdin...");
|
||
}
|
||
let mut buffer = String::new();
|
||
if let Err(e) = std::io::stdin().read_to_string(&mut buffer) {
|
||
eprintln!("Failed to read prompt from stdin: {e}");
|
||
std::process::exit(1);
|
||
} else if buffer.trim().is_empty() {
|
||
eprintln!("No prompt provided via stdin.");
|
||
std::process::exit(1);
|
||
}
|
||
buffer
|
||
}
|
||
};
|
||
|
||
let (stdout_with_ansi, stderr_with_ansi) = match color {
|
||
cli::Color::Always => (true, true),
|
||
cli::Color::Never => (false, false),
|
||
cli::Color::Auto => (
|
||
std::io::stdout().is_terminal(),
|
||
std::io::stderr().is_terminal(),
|
||
),
|
||
};
|
||
|
||
let sandbox_mode = if full_auto {
|
||
Some(SandboxMode::WorkspaceWrite)
|
||
} else if dangerously_bypass_approvals_and_sandbox {
|
||
Some(SandboxMode::DangerFullAccess)
|
||
} else {
|
||
sandbox_mode_cli_arg.map(Into::<SandboxMode>::into)
|
||
};
|
||
|
||
// Load configuration and determine approval policy
|
||
let overrides = ConfigOverrides {
|
||
model,
|
||
config_profile,
|
||
// This CLI is intended to be headless and has no affordances for asking
|
||
// the user for approval.
|
||
approval_policy: Some(AskForApproval::Never),
|
||
sandbox_mode,
|
||
cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)),
|
||
model_provider: None,
|
||
codex_linux_sandbox_exe,
|
||
};
|
||
// Parse `-c` overrides.
|
||
let cli_kv_overrides = match config_overrides.parse_overrides() {
|
||
Ok(v) => v,
|
||
Err(e) => {
|
||
eprintln!("Error parsing -c overrides: {e}");
|
||
std::process::exit(1);
|
||
}
|
||
};
|
||
|
||
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
|
||
let mut event_processor =
|
||
EventProcessor::create_with_ansi(stdout_with_ansi, !config.hide_agent_reasoning);
|
||
// Print the effective configuration and prompt so users can see what Codex
|
||
// is using.
|
||
event_processor.print_config_summary(&config, &prompt);
|
||
|
||
if !skip_git_repo_check && !is_inside_git_repo(&config) {
|
||
eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
|
||
std::process::exit(1);
|
||
}
|
||
|
||
// TODO(mbolin): Take a more thoughtful approach to logging.
|
||
let default_level = "error";
|
||
let _ = tracing_subscriber::fmt()
|
||
// Fallback to the `default_level` log filter if the environment
|
||
// variable is not set _or_ contains an invalid value
|
||
.with_env_filter(
|
||
EnvFilter::try_from_default_env()
|
||
.or_else(|_| EnvFilter::try_new(default_level))
|
||
.unwrap_or_else(|_| EnvFilter::new(default_level)),
|
||
)
|
||
.with_ansi(stderr_with_ansi)
|
||
.with_writer(std::io::stderr)
|
||
.try_init();
|
||
|
||
let (codex_wrapper, event, ctrl_c) = codex_wrapper::init_codex(config).await?;
|
||
let codex = Arc::new(codex_wrapper);
|
||
info!("Codex initialized with event: {event:?}");
|
||
|
||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Event>();
|
||
{
|
||
let codex = codex.clone();
|
||
tokio::spawn(async move {
|
||
loop {
|
||
let interrupted = ctrl_c.notified();
|
||
tokio::select! {
|
||
_ = interrupted => {
|
||
// Forward an interrupt to the codex so it can abort any in‑flight task.
|
||
let _ = codex
|
||
.submit(
|
||
Op::Interrupt,
|
||
)
|
||
.await;
|
||
|
||
// Exit the inner loop and return to the main input prompt. The codex
|
||
// will emit a `TurnInterrupted` (Error) event which is drained later.
|
||
break;
|
||
}
|
||
res = codex.next_event() => match res {
|
||
Ok(event) => {
|
||
debug!("Received event: {event:?}");
|
||
if let Err(e) = tx.send(event) {
|
||
error!("Error sending event: {e:?}");
|
||
break;
|
||
}
|
||
},
|
||
Err(e) => {
|
||
error!("Error receiving event: {e:?}");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Send images first, if any.
|
||
if !images.is_empty() {
|
||
let items: Vec<InputItem> = images
|
||
.into_iter()
|
||
.map(|path| InputItem::LocalImage { path })
|
||
.collect();
|
||
let initial_images_event_id = codex.submit(Op::UserInput { items }).await?;
|
||
info!("Sent images with event ID: {initial_images_event_id}");
|
||
while let Ok(event) = codex.next_event().await {
|
||
if event.id == initial_images_event_id
|
||
&& matches!(
|
||
event.msg,
|
||
EventMsg::TaskComplete(TaskCompleteEvent {
|
||
last_agent_message: _,
|
||
})
|
||
)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Send the prompt.
|
||
let items: Vec<InputItem> = vec![InputItem::Text { text: prompt }];
|
||
let initial_prompt_task_id = codex.submit(Op::UserInput { items }).await?;
|
||
info!("Sent prompt with event ID: {initial_prompt_task_id}");
|
||
|
||
// Run the loop until the task is complete.
|
||
while let Some(event) = rx.recv().await {
|
||
let (is_last_event, last_assistant_message) = match &event.msg {
|
||
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
|
||
(true, last_agent_message.clone())
|
||
}
|
||
_ => (false, None),
|
||
};
|
||
event_processor.process_event(event);
|
||
if is_last_event {
|
||
handle_last_message(last_assistant_message, last_message_file.as_deref())?;
|
||
break;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn handle_last_message(
|
||
last_agent_message: Option<String>,
|
||
last_message_file: Option<&Path>,
|
||
) -> std::io::Result<()> {
|
||
match (last_agent_message, last_message_file) {
|
||
(Some(last_agent_message), Some(last_message_file)) => {
|
||
// Last message and a file to write to.
|
||
std::fs::write(last_message_file, last_agent_message)?;
|
||
}
|
||
(None, Some(last_message_file)) => {
|
||
eprintln!(
|
||
"Warning: No last message to write to file: {}",
|
||
last_message_file.to_string_lossy()
|
||
);
|
||
}
|
||
(_, None) => {
|
||
// No last message and no file to write to.
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|