mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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
This commit is contained in:
1 parent
9ab176f488
commit
44e95c857f
4 files changed
+195
-29
No files matched your search
+22
-20
@@ -1092,25 +1092,31 @@ async fn cli_main(
|
|||||||
let open_agents_overview = matches!(&subcommand, Some(Subcommand::Agents(_)));
|
let open_agents_overview = matches!(&subcommand, Some(Subcommand::Agents(_)));
|
||||||
match subcommand {
|
match subcommand {
|
||||||
None | Some(Subcommand::Agents(_)) => {
|
None | Some(Subcommand::Agents(_)) => {
|
||||||
|
prepend_config_flags(
|
||||||
|
&mut interactive.config_overrides,
|
||||||
|
root_config_overrides.clone(),
|
||||||
|
);
|
||||||
if open_agents_overview {
|
if open_agents_overview {
|
||||||
if !root_config_overrides.raw_overrides.is_empty()
|
if interactive.prompt.is_some() || !interactive.images.is_empty() {
|
||||||
|| root_strict_config
|
anyhow::bail!("`codex agents` does not accept an initial prompt or images");
|
||||||
|| interactive.prompt.is_some()
|
}
|
||||||
|| !interactive.images.is_empty()
|
if root_remote.is_some()
|
||||||
|| interactive.model.is_some()
|
&& (interactive.oss
|
||||||
|| interactive.oss
|
|| interactive.oss_provider.is_some()
|
||||||
|| interactive.oss_provider.is_some()
|
|| !interactive.add_dir.is_empty()
|
||||||
|| interactive.config_profile_v2.is_some()
|
|| interactive
|
||||||
|| interactive.sandbox_mode.is_some()
|
.config_overrides
|
||||||
|| interactive.dangerously_bypass_approvals_and_sandbox
|
.parse_overrides()
|
||||||
|| interactive.bypass_hook_trust
|
.map_err(anyhow::Error::msg)?
|
||||||
|| interactive.cwd.is_some() && root_remote.is_none()
|
.iter()
|
||||||
|| !interactive.add_dir.is_empty()
|
.any(|(key, value)| {
|
||||||
|| interactive.approval_policy.is_some()
|
key == "sandbox_workspace_write.writable_roots"
|
||||||
|| interactive.web_search
|
|| (key == "sandbox_workspace_write"
|
||||||
|
&& value.get("writable_roots").is_some())
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
anyhow::bail!(
|
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() {
|
if is_workload_identity_selected() {
|
||||||
@@ -1128,10 +1134,6 @@ async fn cli_main(
|
|||||||
}
|
}
|
||||||
interactive.agents_overview = true;
|
interactive.agents_overview = true;
|
||||||
}
|
}
|
||||||
prepend_config_flags(
|
|
||||||
&mut interactive.config_overrides,
|
|
||||||
root_config_overrides.clone(),
|
|
||||||
);
|
|
||||||
let exit_info = run_interactive_tui(
|
let exit_info = run_interactive_tui(
|
||||||
interactive,
|
interactive,
|
||||||
root_remote.clone(),
|
root_remote.clone(),
|
||||||
|
|||||||
@@ -33,19 +33,72 @@ foo = "bar"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agents_reject_session_overrides_before_starting_the_daemon() -> Result<()> {
|
fn agents_accept_interactive_configuration_overrides() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
|
|
||||||
for args in [
|
for args in [
|
||||||
|
["-c", "features.multi_agent_mode=true", "agents"].as_slice(),
|
||||||
|
["--enable", "multi_agent_mode", "agents"].as_slice(),
|
||||||
["--yolo", "agents"].as_slice(),
|
["--yolo", "agents"].as_slice(),
|
||||||
["--search", "agents"].as_slice(),
|
["--search", "agents"].as_slice(),
|
||||||
["--model", "gpt-5", "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())?;
|
let mut cmd = codex_command(codex_home.path())?;
|
||||||
cmd.args(args)
|
cmd.args(args)
|
||||||
.assert()
|
.assert()
|
||||||
.failure()
|
.failure()
|
||||||
.stderr(contains("invocation-specific configuration overrides"));
|
.stderr(contains(expected_error));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ use codex_app_server_protocol::TurnStartResponse;
|
|||||||
use codex_app_server_protocol::TurnSteerParams;
|
use codex_app_server_protocol::TurnSteerParams;
|
||||||
use codex_app_server_protocol::TurnSteerResponse;
|
use codex_app_server_protocol::TurnSteerResponse;
|
||||||
use codex_app_server_protocol::UserInput;
|
use codex_app_server_protocol::UserInput;
|
||||||
|
use codex_config::ConfigLayerSource;
|
||||||
use codex_otel::TelemetryAuthMode;
|
use codex_otel::TelemetryAuthMode;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::approvals::GuardianAssessmentEvent;
|
use codex_protocol::approvals::GuardianAssessmentEvent;
|
||||||
@@ -1535,7 +1536,34 @@ fn approvals_reviewer_override_from_config(
|
|||||||
fn config_request_overrides_from_config(
|
fn config_request_overrides_from_config(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
) -> Option<HashMap<String, serde_json::Value>> {
|
) -> Option<HashMap<String, serde_json::Value>> {
|
||||||
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<String>| {
|
let mut insert = |key: &str, value: Option<String>| {
|
||||||
if let Some(value) = value {
|
if let Some(value) = value {
|
||||||
overrides.insert(key.to_string(), serde_json::Value::String(value));
|
overrides.insert(key.to_string(), serde_json::Value::String(value));
|
||||||
@@ -2371,6 +2399,88 @@ mod tests {
|
|||||||
assert_eq!(params.history_mode, None);
|
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]
|
#[tokio::test]
|
||||||
async fn thread_start_params_include_cwd_for_embedded_sessions() {
|
async fn thread_start_params_include_cwd_for_embedded_sessions() {
|
||||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
|||||||
@@ -136,12 +136,13 @@ pub(super) async fn run_main_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reuse_implicit_local_daemon = !workload_identity_selected
|
let reuse_implicit_local_daemon = !workload_identity_selected
|
||||||
&& can_reuse_implicit_local_daemon(
|
&& (cli.agents_overview
|
||||||
&cli_kv_overrides,
|
|| can_reuse_implicit_local_daemon(
|
||||||
&launch_loader_overrides,
|
&cli_kv_overrides,
|
||||||
strict_config,
|
&launch_loader_overrides,
|
||||||
cli.bypass_hook_trust,
|
strict_config,
|
||||||
);
|
cli.bypass_hook_trust,
|
||||||
|
));
|
||||||
let search_only_config_override = !workload_identity_selected
|
let search_only_config_override = !workload_identity_selected
|
||||||
&& cli.web_search
|
&& cli.web_search
|
||||||
&& startup_preflight::has_only_search_config_override(&cli_kv_overrides)
|
&& startup_preflight::has_only_search_config_override(&cli_kv_overrides)
|
||||||
|
|||||||
Reference in new issue
Block a user