Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 43 additions & 42 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,9 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
root_remote_auth_token_env.as_deref(),
"exec",
)?;
exec_cli
.shared
.inherit_exec_root_options(&interactive.shared);
prepend_config_flags(
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
Expand Down Expand Up @@ -1242,6 +1245,7 @@ async fn run_debug_prompt_input_command(
interactive: TuiCli,
arg0_paths: Arg0DispatchPaths,
) -> anyhow::Result<()> {
let shared = interactive.shared.into_inner();
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
.map_err(anyhow::Error::msg)?;
Expand All @@ -1252,38 +1256,38 @@ async fn run_debug_prompt_input_command(
));
}

let approval_policy = if interactive.full_auto {
let approval_policy = if shared.full_auto {
Some(AskForApproval::OnRequest)
} else if interactive.dangerously_bypass_approvals_and_sandbox {
} else if shared.dangerously_bypass_approvals_and_sandbox {
Some(AskForApproval::Never)
} else {
interactive.approval_policy.map(Into::into)
};
let sandbox_mode = if interactive.full_auto {
let sandbox_mode = if shared.full_auto {
Some(codex_protocol::config_types::SandboxMode::WorkspaceWrite)
} else if interactive.dangerously_bypass_approvals_and_sandbox {
} else if shared.dangerously_bypass_approvals_and_sandbox {
Some(codex_protocol::config_types::SandboxMode::DangerFullAccess)
} else {
interactive.sandbox_mode.map(Into::into)
shared.sandbox_mode.map(Into::into)
};
let overrides = ConfigOverrides {
model: interactive.model,
config_profile: interactive.config_profile,
model: shared.model,
config_profile: shared.config_profile,
approval_policy,
sandbox_mode,
cwd: interactive.cwd,
cwd: shared.cwd,
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
show_raw_agent_reasoning: interactive.oss.then_some(true),
show_raw_agent_reasoning: shared.oss.then_some(true),
ephemeral: Some(true),
additional_writable_roots: interactive.add_dir,
additional_writable_roots: shared.add_dir,
..Default::default()
};
let config =
Config::load_with_cli_overrides_and_harness_overrides(cli_kv_overrides, overrides).await?;

let mut input = interactive
let mut input = shared
.images
.into_iter()
.chain(cmd.images)
Expand Down Expand Up @@ -1554,48 +1558,32 @@ fn finalize_fork_interactive(
/// root-level flags. Only overrides fields explicitly set on the subcommand-scoped
/// CLI. Also appends `-c key=value` overrides with highest precedence.
fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli) {
if let Some(model) = subcommand_cli.model {
interactive.model = Some(model);
}
if subcommand_cli.oss {
interactive.oss = true;
}
if let Some(profile) = subcommand_cli.config_profile {
interactive.config_profile = Some(profile);
}
if let Some(sandbox) = subcommand_cli.sandbox_mode {
interactive.sandbox_mode = Some(sandbox);
}
if let Some(approval) = subcommand_cli.approval_policy {
let TuiCli {
shared,
approval_policy,
web_search,
prompt,
config_overrides,
..
} = subcommand_cli;
interactive
.shared
.apply_subcommand_overrides(shared.into_inner());
if let Some(approval) = approval_policy {
interactive.approval_policy = Some(approval);
}
if subcommand_cli.full_auto {
interactive.full_auto = true;
}
if subcommand_cli.dangerously_bypass_approvals_and_sandbox {
interactive.dangerously_bypass_approvals_and_sandbox = true;
}
if let Some(cwd) = subcommand_cli.cwd {
interactive.cwd = Some(cwd);
}
if subcommand_cli.web_search {
if web_search {
interactive.web_search = true;
}
if !subcommand_cli.images.is_empty() {
interactive.images = subcommand_cli.images;
}
if !subcommand_cli.add_dir.is_empty() {
interactive.add_dir.extend(subcommand_cli.add_dir);
}
if let Some(prompt) = subcommand_cli.prompt {
if let Some(prompt) = prompt {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
interactive.prompt = Some(prompt.replace("\r\n", "\n").replace('\r', "\n"));
}

interactive
.config_overrides
.raw_overrides
.extend(subcommand_cli.config_overrides.raw_overrides);
.extend(config_overrides.raw_overrides);
}

fn print_completion(cmd: CompletionCommand) {
Expand Down Expand Up @@ -1715,6 +1703,19 @@ mod tests {
assert_eq!(args.prompt.as_deref(), Some("re-review"));
}

#[test]
fn dangerous_bypass_conflicts_with_approval_policy() {
let err = MultitoolCli::try_parse_from([
"codex",
"--dangerously-bypass-approvals-and-sandbox",
"--ask-for-approval",
"on-request",
])
.expect_err("conflicting permission flags should be rejected");

assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}

fn app_server_from_args(args: &[&str]) -> AppServerCommand {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let Subcommand::AppServer(app_server) = cli.subcommand.expect("app-server present") else {
Expand Down
122 changes: 68 additions & 54 deletions codex-rs/exec/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::FromArgMatches;
use clap::Parser;
use clap::ValueEnum;
use codex_utils_cli::CliConfigOverrides;
use codex_utils_cli::SharedCliOptions;
use std::path::PathBuf;

#[derive(Parser, Debug)]
Expand All @@ -15,65 +16,13 @@ pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,

/// Optional image(s) to attach to the initial prompt.
#[arg(
long = "image",
short = 'i',
value_name = "FILE",
value_delimiter = ',',
num_args = 1..
)]
pub images: Vec<PathBuf>,

/// Model the agent should use.
#[arg(long, short = 'm', global = true)]
pub model: Option<String>,

/// Use open-source provider.
#[arg(long = "oss", default_value_t = false)]
pub oss: bool,

/// Specify which local provider to use (lmstudio or ollama).
/// If not specified with --oss, will use config default or show selection.
#[arg(long = "local-provider")]
pub oss_provider: Option<String>,

/// Select the sandbox policy to use when executing model-generated shell
/// commands.
#[arg(long = "sandbox", short = 's', value_enum)]
pub sandbox_mode: Option<codex_utils_cli::SandboxModeCliArg>,

/// Configuration profile from config.toml to specify default options.
#[arg(long = "profile", short = 'p')]
pub config_profile: Option<String>,

/// Convenience alias for low-friction sandboxed automatic execution (--sandbox workspace-write).
#[arg(long = "full-auto", default_value_t = false, global = true)]
pub full_auto: bool,

/// Skip all confirmation prompts and execute commands without sandboxing.
/// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed.
#[arg(
long = "dangerously-bypass-approvals-and-sandbox",
alias = "yolo",
default_value_t = false,
global = true,
conflicts_with = "full_auto"
)]
pub dangerously_bypass_approvals_and_sandbox: bool,

/// Tell the agent to use the specified directory as its working root.
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
#[clap(flatten)]
pub shared: ExecSharedCliOptions,

/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", global = true, default_value_t = false)]
pub skip_git_repo_check: bool,

/// Additional directories that should be writable alongside the primary workspace.
#[arg(long = "add-dir", value_name = "DIR", value_hint = clap::ValueHint::DirPath)]
pub add_dir: Vec<PathBuf>,

/// Run without persisting session files to disk.
#[arg(long = "ephemeral", global = true, default_value_t = false)]
pub ephemeral: bool,
Expand Down Expand Up @@ -122,6 +71,71 @@ pub struct Cli {
pub prompt: Option<String>,
}

impl std::ops::Deref for Cli {
type Target = SharedCliOptions;

fn deref(&self) -> &Self::Target {
&self.shared.0
}
}

impl std::ops::DerefMut for Cli {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.shared.0
}
}

#[derive(Debug, Default)]
pub struct ExecSharedCliOptions(SharedCliOptions);

impl ExecSharedCliOptions {
pub fn into_inner(self) -> SharedCliOptions {
self.0
}
}

impl std::ops::Deref for ExecSharedCliOptions {
type Target = SharedCliOptions;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::ops::DerefMut for ExecSharedCliOptions {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl Args for ExecSharedCliOptions {
fn augment_args(cmd: clap::Command) -> clap::Command {
mark_exec_global_args(SharedCliOptions::augment_args(cmd))
}

fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
mark_exec_global_args(SharedCliOptions::augment_args_for_update(cmd))
}
}

impl FromArgMatches for ExecSharedCliOptions {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
SharedCliOptions::from_arg_matches(matches).map(Self)
}

fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
self.0.update_from_arg_matches(matches)
}
}

fn mark_exec_global_args(cmd: clap::Command) -> clap::Command {
cmd.mut_arg("model", |arg| arg.global(true))
.mut_arg("full_auto", |arg| arg.global(true))
.mut_arg("dangerously_bypass_approvals_and_sandbox", |arg| {
arg.global(true)
})
}

#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Resume a previous session by id or pick the most recent with --last.
Expand Down
25 changes: 15 additions & 10 deletions codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
use codex_utils_cli::SharedCliOptions;
use codex_utils_oss::ensure_oss_provider_ready;
use codex_utils_oss::get_default_model_for_oss_provider;
use event_processor_with_human_output::EventProcessorWithHumanOutput;
Expand Down Expand Up @@ -219,27 +220,31 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result

let Cli {
command,
images,
model: model_cli_arg,
oss,
oss_provider,
config_profile,
full_auto,
dangerously_bypass_approvals_and_sandbox,
cwd,
shared,
skip_git_repo_check,
add_dir,
ephemeral,
ignore_user_config,
ignore_rules,
color,
last_message_file,
json: json_mode,
sandbox_mode: sandbox_mode_cli_arg,
prompt,
output_schema: output_schema_path,
config_overrides,
} = cli;
let shared = shared.into_inner();
let SharedCliOptions {
images,
model: model_cli_arg,
oss,
oss_provider,
config_profile,
sandbox_mode: sandbox_mode_cli_arg,
full_auto,
dangerously_bypass_approvals_and_sandbox,
cwd,
add_dir,
} = shared;

let (_stdout_with_ansi, stderr_with_ansi) = match color {
cli::Color::Always => (true, true),
Expand Down
Loading
Loading