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
72 changes: 52 additions & 20 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ struct AppServerCommand {

#[derive(Debug, Parser)]
struct ExecServerCommand {
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
#[arg(long = "strict-config", default_value_t = false)]
strict_config: bool,

/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default), `stdio`, `stdio://`.
#[arg(long = "listen", value_name = "URL", conflicts_with = "remote")]
listen: Option<String>,
Expand Down Expand Up @@ -1376,11 +1380,13 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
root_remote_auth_token_env.as_deref(),
"exec-server",
)?;
let strict_config = cmd.strict_config || root_strict_config;
run_exec_server_command(
cmd,
&arg0_paths,
&root_config_overrides,
interactive.config_profile.clone(),
strict_config,
)
.await?;
}
Expand Down Expand Up @@ -1481,6 +1487,7 @@ async fn run_exec_server_command(
arg0_paths: &Arg0DispatchPaths,
root_config_overrides: &CliConfigOverrides,
config_profile: Option<String>,
strict_config: bool,
) -> anyhow::Result<()> {
let codex_self_exe = arg0_paths
.codex_self_exe
Expand All @@ -1494,12 +1501,10 @@ async fn run_exec_server_command(
let environment_id = cmd
.environment_id
.ok_or_else(|| anyhow::anyhow!("--environment-id is required when --remote is set"))?;
let auth_provider = load_exec_server_remote_auth_provider(
root_config_overrides,
config_profile,
cmd.use_agent_identity_auth,
)
.await?;
let config =
load_exec_server_config(root_config_overrides, config_profile, strict_config).await?;
let auth_provider =
load_exec_server_remote_auth_provider(&config, cmd.use_agent_identity_auth).await?;
let mut remote_config = codex_exec_server::RemoteEnvironmentConfig::new(
base_url,
environment_id,
Expand All @@ -1509,23 +1514,29 @@ async fn run_exec_server_command(
remote_config.name = name;
}
codex_exec_server::run_remote_environment(remote_config, runtime_paths).await?;
return Ok(());
Ok(())
} else {
if strict_config {
// Local exec-server startup does not consume Config, but strict
// mode should still reject unknown fields before opening a listener.
let _validated_config =
load_exec_server_config(root_config_overrides, config_profile, strict_config)
.await?;
}
let listen_url = cmd
.listen
.as_deref()
.unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL);
codex_exec_server::run_main(listen_url, runtime_paths)
.await
.map_err(anyhow::Error::from_boxed)
}
let listen_url = cmd
.listen
.as_deref()
.unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL);
codex_exec_server::run_main(listen_url, runtime_paths)
.await
.map_err(anyhow::Error::from_boxed)
}

async fn load_exec_server_remote_auth_provider(
root_config_overrides: &CliConfigOverrides,
config_profile: Option<String>,
config: &codex_core::config::Config,
use_agent_identity_auth: bool,
) -> anyhow::Result<codex_api::SharedAuthProvider> {
let config = load_exec_server_remote_config(root_config_overrides, config_profile).await?;
if use_agent_identity_auth {
let agent_identity_jwt = read_codex_access_token_from_env().ok_or_else(|| {
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
Expand All @@ -1537,7 +1548,7 @@ async fn load_exec_server_remote_auth_provider(
}

let auth = load_exec_server_remote_auth(
&config,
config,
"remote exec-server registration requires ChatGPT authentication; run `codex login` first",
)
.await?;
Expand All @@ -1551,9 +1562,10 @@ async fn load_exec_server_remote_auth_provider(
Ok(codex_model_provider::auth_provider_from_auth(&auth))
}

async fn load_exec_server_remote_config(
async fn load_exec_server_config(
root_config_overrides: &CliConfigOverrides,
config_profile: Option<String>,
strict_config: bool,
) -> anyhow::Result<codex_core::config::Config> {
let cli_kv_overrides = root_config_overrides
.parse_overrides()
Expand All @@ -1564,6 +1576,7 @@ async fn load_exec_server_remote_config(
config_profile,
..Default::default()
})
.strict_config(strict_config)
.build()
.await?)
}
Expand Down Expand Up @@ -1869,6 +1882,7 @@ fn unsupported_subcommand_name_for_strict_config(
| Some(Subcommand::Exec(_))
| Some(Subcommand::Review(_))
| Some(Subcommand::McpServer(_))
| Some(Subcommand::ExecServer(_))
| Some(Subcommand::Resume(_))
| Some(Subcommand::Fork(_))
| Some(Subcommand::Doctor(_)) => None,
Expand All @@ -1892,7 +1906,6 @@ fn unsupported_subcommand_name_for_strict_config(
Some(Subcommand::Apply(_)) => Some("apply"),
Some(Subcommand::ResponsesApiProxy(_)) => Some("responses-api-proxy"),
Some(Subcommand::StdioToUds(_)) => Some("stdio-to-uds"),
Some(Subcommand::ExecServer(_)) => Some("exec-server"),
Some(Subcommand::Features(_)) => Some("features"),
}
}
Expand Down Expand Up @@ -2897,6 +2910,25 @@ mod tests {
..
}))
);

let cli = MultitoolCli::try_parse_from(["codex", "exec-server", "--strict-config"])
.expect("parse");
assert_matches!(
cli.subcommand,
Some(Subcommand::ExecServer(ExecServerCommand {
strict_config: true,
..
}))
);
}

#[test]
fn root_strict_config_is_supported_for_exec_server() {
let cli = MultitoolCli::try_parse_from(["codex", "--strict-config", "exec-server"])
.expect("parse");

reject_root_strict_config_for_subcommand(cli.interactive.strict_config, &cli.subcommand)
.expect("exec-server should support root --strict-config");
}

#[test]
Expand Down
35 changes: 35 additions & 0 deletions codex-rs/cli/tests/exec_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::path::Path;

use anyhow::Result;
use predicates::str::contains;
use tempfile::TempDir;

fn codex_command(codex_home: &Path) -> Result<assert_cmd::Command> {
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
cmd.env("CODEX_HOME", codex_home);
Ok(cmd)
}

#[test]
fn strict_config_rejects_unknown_config_fields_for_exec_server() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
foo = "bar"
"#,
)?;

let mut cmd = codex_command(codex_home.path())?;
cmd.args([
"exec-server",
"--strict-config",
"--listen",
"http://127.0.0.1:0",
])
.assert()
.failure()
.stderr(contains("unknown configuration field"));

Ok(())
}
Loading