Add an --approve-for-me CLI flag (#36373)

## What changed

- Add `--approve-for-me` to interactive and exec commands to route approval requests through automatic review.
- Configure the mode with `approval_policy="on-request"` and the `workspace-write` sandbox.
- Propagate the option across root, `exec`, `resume`, and `fork` argument handling while preserving later subcommand permission overrides.

## Testing

- Cover parsing, permission conflicts, root/subcommand precedence, resume handling, and the effective exec approval and sandbox modes.

GitOrigin-RevId: ae969e8c18f925f943049fefff56255f10b25659
This commit is contained in:
Dylan Hurd
2026-07-31 18:20:10 +00:00
committed by copyberry
parent 845497f483
commit b7a6106608
7 changed files with 283 additions and 21 deletions

View File

@@ -990,6 +990,9 @@ async fn cli_main(
let root_remote = remote.remote;
let root_remote_auth_token_env = remote.remote_auth_token_env;
let root_strict_config = interactive.strict_config;
interactive
.shared
.take_auto_review_config_overrides(&mut root_config_overrides);
reject_root_strict_config_for_subcommand(root_strict_config, &subcommand)?;
if let Some(subcommand) = subcommand.as_ref() {
profile_v2_for_subcommand(&interactive, subcommand)?;
@@ -2570,13 +2573,19 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli)
approval_policy,
web_search,
prompt,
config_overrides,
mut config_overrides,
..
} = subcommand_cli;
let subcommand_auto_review = shared.auto_review;
interactive
.shared
.apply_subcommand_overrides(shared.into_inner());
if let Some(approval) = approval_policy {
interactive
.shared
.take_auto_review_config_overrides(&mut config_overrides);
if subcommand_auto_review {
interactive.approval_policy = None;
} else if let Some(approval) = approval_policy {
interactive.approval_policy = Some(approval);
}
if web_search {
@@ -2698,12 +2707,15 @@ mod tests {
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
interactive,
config_overrides: root_overrides,
mut interactive,
config_overrides: mut root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
interactive
.shared
.take_auto_review_config_overrides(&mut root_overrides);
let Subcommand::Resume(ResumeCommand {
session_id,
@@ -2732,12 +2744,15 @@ mod tests {
fn finalize_fork_from_args(args: &[&str]) -> TuiCli {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
interactive,
config_overrides: root_overrides,
mut interactive,
config_overrides: mut root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
interactive
.shared
.take_auto_review_config_overrides(&mut root_overrides);
let Subcommand::Fork(ForkCommand {
session_id,
@@ -2754,6 +2769,22 @@ mod tests {
finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli)
}
fn finalize_exec_from_args(args: &[&str]) -> ExecCli {
let mut cli = MultitoolCli::try_parse_from(args).expect("parse");
cli.interactive
.shared
.take_auto_review_config_overrides(&mut cli.config_overrides);
let Some(Subcommand::Exec(mut exec)) = cli.subcommand else {
panic!("expected exec subcommand");
};
exec.shared
.inherit_exec_root_options(&cli.interactive.shared);
prepend_config_flags(&mut exec.config_overrides, cli.config_overrides);
exec.shared
.take_auto_review_config_overrides(&mut exec.config_overrides);
exec
}
fn finalize_archive_from_args(args: &[&str]) -> (String, TuiCli, InteractiveRemoteOptions) {
let cli = MultitoolCli::try_parse_from(args).expect("parse");
let MultitoolCli {
@@ -2921,6 +2952,157 @@ mod tests {
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn approve_for_me_configures_interactive_mode() {
for flag in ["--approve-for-me", "--not-so-yolo"] {
let mut cli = MultitoolCli::try_parse_from(["codex", flag]).expect("parse flag");
assert!(cli.interactive.auto_review);
cli.interactive
.shared
.take_auto_review_config_overrides(&mut cli.interactive.config_overrides);
assert_eq!(
cli.interactive.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
assert!(!cli.interactive.auto_review);
}
}
#[test]
fn not_so_yolo_alias_is_hidden_from_help() {
for args in [&["codex", "--help"][..], &["codex", "exec", "--help"][..]] {
let help = help_from_args(args);
assert!(!help.contains("--not-so-yolo"), "{help}");
}
}
#[test]
fn approve_for_me_defaults_propagate_from_root_to_exec() {
let exec = finalize_exec_from_args(&["codex", "--approve-for-me", "exec", "summarize"]);
assert_eq!(
exec.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
assert!(exec.sandbox_mode.is_none());
}
#[test]
fn later_exec_sandbox_partially_overrides_approve_for_me() {
let exec = finalize_exec_from_args(&[
"codex",
"--approve-for-me",
"exec",
"--sandbox",
"read-only",
]);
assert_matches!(
exec.sandbox_mode,
Some(codex_utils_cli::SandboxModeCliArg::ReadOnly)
);
assert_eq!(
exec.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn later_approve_for_me_overrides_root_exec_sandbox() {
let exec = finalize_exec_from_args(&[
"codex",
"--sandbox",
"read-only",
"exec",
"--approve-for-me",
]);
assert!(exec.sandbox_mode.is_none());
assert_eq!(
exec.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn later_resume_approval_policy_partially_overrides_approve_for_me() {
let interactive = finalize_resume_from_args(&[
"codex",
"--approve-for-me",
"resume",
"--ask-for-approval",
"never",
]);
assert_matches!(
interactive.approval_policy,
Some(codex_utils_cli::ApprovalModeCliArg::Never)
);
assert_eq!(
interactive.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn later_approve_for_me_overrides_root_tui_approval_policy() {
let interactive = finalize_resume_from_args(&[
"codex",
"--ask-for-approval",
"never",
"resume",
"--approve-for-me",
]);
assert!(interactive.approval_policy.is_none());
assert_eq!(
interactive.config_overrides.raw_overrides,
vec![
r#"approvals_reviewer="auto_review""#.to_string(),
r#"approval_policy="on-request""#.to_string(),
r#"sandbox_mode="workspace-write""#.to_string(),
]
);
}
#[test]
fn approve_for_me_conflicts_with_explicit_interactive_permissions() {
for conflicting_args in [
vec!["--sandbox", "read-only"],
vec!["--ask-for-approval", "on-request"],
vec!["--dangerously-bypass-approvals-and-sandbox"],
] {
let mut args = vec!["codex", "--approve-for-me"];
args.extend(conflicting_args);
let error =
MultitoolCli::try_parse_from(args).expect_err("permission flags should conflict");
assert_eq!(error.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 {