Fix exec inheritance of root shared flags (#18630)

Addresses #18113

Problem: Shared flags provided before the exec subcommand were parsed by
the root CLI but not inherited by the exec CLI, so exec sessions could
run with stale or default sandbox and model configuration.

Solution: Move shared TUI and exec flags into a common option block and
merge root selections into exec before dispatch, while preserving exec's
global subcommand flag behavior.
This commit is contained in:
Eric Traut
2026-04-20 16:12:17 -07:00
committed by GitHub
parent 2af4f15479
commit 0f1c9b8963
7 changed files with 372 additions and 158 deletions

View File

@@ -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(),
@@ -1241,6 +1244,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)?;
@@ -1251,38 +1255,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)
@@ -1553,40 +1557,24 @@ 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"));
}
@@ -1594,7 +1582,7 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli)
interactive
.config_overrides
.raw_overrides
.extend(subcommand_cli.config_overrides.raw_overrides);
.extend(config_overrides.raw_overrides);
}
fn print_completion(cmd: CompletionCommand) {
@@ -1714,6 +1702,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 {