From 44e95c857f37f81a5731eab72c32a3d334d0e2c4 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 21 Aug 2026 06:02:19 +0000 Subject: [PATCH] Allow session configuration with `codex agents` (#39870) ## Why `codex agents` rejected invocation-specific configuration, preventing the dashboard from applying settings when starting a shared thread. ## What changed - Accept interactive options such as model, approval, sandbox, search, working directory, and configuration overrides when opening the agents dashboard. - Forward supported session-flag configuration into threads started through an embedded or remote app server, while excluding unrelated values. - Continue to reject initial prompts and images, along with local provider and additional-directory settings that cannot be applied to a remote server. ## Testing - Cover accepted dashboard options and rejected incompatible inputs. - Verify that explicit feature and sandbox overrides reach shared threads in both embedded and remote modes. GitOrigin-RevId: f10aa1e16ff62b49d55679e987d9e458438ba3f8 --- codex-rs/cli/src/main.rs | 42 ++++---- codex-rs/cli/tests/app_server.rs | 57 ++++++++++- codex-rs/tui/src/app_server_session.rs | 112 +++++++++++++++++++++- codex-rs/tui/src/startup_orchestration.rs | 13 +-- 4 files changed, 195 insertions(+), 29 deletions(-) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 45218a6d39..bb56637103 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1092,25 +1092,31 @@ async fn cli_main( let open_agents_overview = matches!(&subcommand, Some(Subcommand::Agents(_))); match subcommand { None | Some(Subcommand::Agents(_)) => { + prepend_config_flags( + &mut interactive.config_overrides, + root_config_overrides.clone(), + ); if open_agents_overview { - if !root_config_overrides.raw_overrides.is_empty() - || root_strict_config - || interactive.prompt.is_some() - || !interactive.images.is_empty() - || interactive.model.is_some() - || interactive.oss - || interactive.oss_provider.is_some() - || interactive.config_profile_v2.is_some() - || interactive.sandbox_mode.is_some() - || interactive.dangerously_bypass_approvals_and_sandbox - || interactive.bypass_hook_trust - || interactive.cwd.is_some() && root_remote.is_none() - || !interactive.add_dir.is_empty() - || interactive.approval_policy.is_some() - || interactive.web_search + if interactive.prompt.is_some() || !interactive.images.is_empty() { + anyhow::bail!("`codex agents` does not accept an initial prompt or images"); + } + if root_remote.is_some() + && (interactive.oss + || interactive.oss_provider.is_some() + || !interactive.add_dir.is_empty() + || interactive + .config_overrides + .parse_overrides() + .map_err(anyhow::Error::msg)? + .iter() + .any(|(key, value)| { + key == "sandbox_workspace_write.writable_roots" + || (key == "sandbox_workspace_write" + && value.get("writable_roots").is_some()) + })) { anyhow::bail!( - "`codex agents` cannot attach to shared sessions with invocation-specific configuration overrides" + "`codex agents` cannot apply local provider or additional-directory overrides to a remote server" ); } if is_workload_identity_selected() { @@ -1128,10 +1134,6 @@ async fn cli_main( } interactive.agents_overview = true; } - prepend_config_flags( - &mut interactive.config_overrides, - root_config_overrides.clone(), - ); let exit_info = run_interactive_tui( interactive, root_remote.clone(), diff --git a/codex-rs/cli/tests/app_server.rs b/codex-rs/cli/tests/app_server.rs index 6a90759b9f..4d3c0d445d 100644 --- a/codex-rs/cli/tests/app_server.rs +++ b/codex-rs/cli/tests/app_server.rs @@ -33,19 +33,72 @@ foo = "bar" } #[test] -fn agents_reject_session_overrides_before_starting_the_daemon() -> Result<()> { +fn agents_accept_interactive_configuration_overrides() -> Result<()> { let codex_home = TempDir::new()?; for args in [ + ["-c", "features.multi_agent_mode=true", "agents"].as_slice(), + ["--enable", "multi_agent_mode", "agents"].as_slice(), ["--yolo", "agents"].as_slice(), ["--search", "agents"].as_slice(), ["--model", "gpt-5", "agents"].as_slice(), + ["--approve-for-me", "agents"].as_slice(), + ["--cd", ".", "agents"].as_slice(), + ] { + let mut cmd = codex_command(codex_home.path())?; + cmd.env("TERM", "xterm-256color").args(args); + #[cfg(not(unix))] + cmd.args(["--remote", "ws://127.0.0.1:4512"]); + + cmd.assert() + .failure() + .stderr(contains("stdin is not a terminal")); + } + + Ok(()) +} + +#[test] +fn agents_reject_inputs_that_cannot_be_applied() -> Result<()> { + let codex_home = TempDir::new()?; + + for (args, expected_error) in [ + ( + ["--image=image.png", "agents"].as_slice(), + "does not accept an initial prompt or images", + ), + ( + ["--oss", "agents", "--remote", "ws://127.0.0.1:4512"].as_slice(), + "cannot apply local provider or additional-directory overrides", + ), + ( + [ + "--add-dir", + ".", + "agents", + "--remote", + "ws://127.0.0.1:4512", + ] + .as_slice(), + "cannot apply local provider or additional-directory overrides", + ), + ( + [ + "-c", + "sandbox_workspace_write.writable_roots=[\"../shared\"]", + "agents", + "--remote", + "ws://127.0.0.1:4512", + ] + .as_slice(), + "cannot apply local provider or additional-directory overrides", + ), ] { let mut cmd = codex_command(codex_home.path())?; cmd.args(args) .assert() .failure() - .stderr(contains("invocation-specific configuration overrides")); + .stderr(contains(expected_error)); } Ok(()) diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 09d6f798a0..0116846832 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -115,6 +115,7 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerResponse; use codex_app_server_protocol::UserInput; +use codex_config::ConfigLayerSource; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; use codex_protocol::approvals::GuardianAssessmentEvent; @@ -1535,7 +1536,34 @@ fn approvals_reviewer_override_from_config( fn config_request_overrides_from_config( config: &Config, ) -> Option> { - let mut overrides = HashMap::new(); + let mut session_config = toml::Value::Table(toml::Table::new()); + for layer in config.config_layer_stack.layers_low_to_high() { + if matches!(&layer.name, ConfigLayerSource::SessionFlags) { + codex_config::merge_toml_values(&mut session_config, &layer.config); + } + } + let mut overrides: HashMap<_, _> = session_config + .as_table() + .into_iter() + .flatten() + .filter(|(key, _)| { + matches!( + key.as_str(), + "allow_login_shell" + | "default_permissions" + | "features" + | "network" + | "permissions" + | "sandbox_workspace_write" + | "shell_environment_policy" + ) + }) + .filter_map(|(key, value)| { + serde_json::to_value(value) + .ok() + .map(|value| (key.clone(), value)) + }) + .collect(); let mut insert = |key: &str, value: Option| { if let Some(value) = value { overrides.insert(key.to_string(), serde_json::Value::String(value)); @@ -2371,6 +2399,88 @@ mod tests { assert_eq!(params.history_mode, None); } + #[tokio::test] + async fn shared_thread_start_preserves_explicit_session_overrides() -> Result<()> { + let codex_home = tempfile::tempdir()?; + let workspace = codex_home.path().join("workspace"); + std::fs::create_dir(&workspace)?; + std::fs::write( + codex_home.path().join("config.toml"), + "sandbox_mode = \"workspace-write\"\n[sandbox_workspace_write]\nnetwork_access = true\n", + )?; + let server_config = build_config(&codex_home).await; + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .harness_overrides(ConfigOverrides { + cwd: Some(workspace.clone()), + ..ConfigOverrides::default() + }) + .cli_overrides(vec![ + ( + "features.multi_agent_mode".to_string(), + toml::Value::Boolean(true), + ), + ( + "sandbox_workspace_write.network_access".to_string(), + toml::Value::Boolean(false), + ), + ( + "instructions".to_string(), + toml::Value::String("unsafe ".repeat(10_000)), + ), + ("model".to_string(), "gpt-5".into()), + ("approval_policy".to_string(), "never".into()), + ]) + .build() + .await?; + + let params = thread_start_params_from_config( + &config, + ThreadParamsMode::Remote, + /*remote_cwd_override*/ None, + /*session_start_source*/ None, + ); + + let overrides = params.config.expect("config overrides"); + assert_eq!( + ( + overrides.get("features").cloned(), + overrides.get("sandbox_workspace_write").cloned(), + overrides.get("instructions").cloned(), + ), + ( + Some(serde_json::json!({ "multi_agent_mode": true })), + Some(serde_json::json!({ "network_access": false })), + None, + ) + ); + for mode in [ThreadParamsMode::Embedded, ThreadParamsMode::Remote] { + let mut app_server = + crate::start_embedded_app_server_for_picker(&server_config).await?; + app_server.thread_params_mode = mode; + app_server.remote_cwd_override = Some(workspace.clone()); + + let started = app_server.start_thread(&config).await?; + + assert_eq!( + ( + started.session.permission_profile.network_sandbox_policy(), + started.session.model.as_str(), + started.session.approval_policy, + started.session.cwd.as_path(), + ), + ( + NetworkSandboxPolicy::Restricted, + "gpt-5", + AskForApproval::Never, + workspace.as_path(), + ) + ); + app_server.shutdown().await?; + } + Ok(()) + } + #[tokio::test] async fn thread_start_params_include_cwd_for_embedded_sessions() { let temp_dir = tempfile::tempdir().expect("tempdir"); diff --git a/codex-rs/tui/src/startup_orchestration.rs b/codex-rs/tui/src/startup_orchestration.rs index 8df90803eb..3bcb8fea87 100644 --- a/codex-rs/tui/src/startup_orchestration.rs +++ b/codex-rs/tui/src/startup_orchestration.rs @@ -136,12 +136,13 @@ pub(super) async fn run_main_inner( } let reuse_implicit_local_daemon = !workload_identity_selected - && can_reuse_implicit_local_daemon( - &cli_kv_overrides, - &launch_loader_overrides, - strict_config, - cli.bypass_hook_trust, - ); + && (cli.agents_overview + || can_reuse_implicit_local_daemon( + &cli_kv_overrides, + &launch_loader_overrides, + strict_config, + cli.bypass_hook_trust, + )); let search_only_config_override = !workload_identity_selected && cli.web_search && startup_preflight::has_only_search_config_override(&cli_kv_overrides)