mirror of
https://github.com/openai/codex.git
synced 2026-10-21 12:10:06 +00:00
Add --no-daemon to bypass the shared background server (#46088)
## What changed - Run with `--no-daemon` without starting or probing the shared server, even when it is already running. Preserve the flag through `resume` and `fork`, and honor it for session archive commands. - Reject combinations with `--remote`, `codex agents`, and `codex queue`, which require a server connection. Point users to `codex --no-daemon` when the agents overview cannot start its shared server. - Centralize daemon eligibility checks and exclude launches using `--profile` or `CODEX_EXEC_SERVER_URL` from implicit daemon reuse. ## Testing Add coverage for flag propagation, incompatible command combinations, and daemon eligibility. Add a PTY test verifying that `--no-daemon` starts the TUI without creating daemon state or connecting to an existing control socket. GitOrigin-RevId: acc6a7cb339df6433bb8273c796194cdceda37c1
This commit is contained in:
@@ -337,6 +337,9 @@ struct DebugTraceReduceCommand {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct AgentsCommand {
|
||||
/// The agents overview requires a shared server; this option is rejected.
|
||||
#[arg(long, hide = true)]
|
||||
no_daemon: bool,
|
||||
#[clap(flatten)]
|
||||
remote: InteractiveRemoteOptions,
|
||||
|
||||
@@ -1215,6 +1218,7 @@ async fn cli_main(
|
||||
if let Some(options) = agents_options {
|
||||
interactive.cwd = options.cwd.clone().or(interactive.cwd.take());
|
||||
interactive.no_alt_screen |= options.no_alt_screen;
|
||||
interactive.no_daemon |= options.no_daemon;
|
||||
}
|
||||
let root_strict_config = interactive.strict_config;
|
||||
interactive
|
||||
@@ -2797,6 +2801,18 @@ async fn run_interactive_tui(
|
||||
remote_auth_token_env: Option<String>,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
) -> std::io::Result<AppExitInfo> {
|
||||
if interactive.no_daemon {
|
||||
if interactive.agents_overview {
|
||||
return Ok(AppExitInfo::fatal(
|
||||
"--no-daemon cannot be used with codex agents. The agents overview requires a shared server. Use codex --no-daemon to work without it.",
|
||||
));
|
||||
}
|
||||
if remote.is_some() {
|
||||
return Ok(AppExitInfo::fatal(
|
||||
"--no-daemon cannot be used with --remote.",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(prompt) = interactive.prompt.take() {
|
||||
// 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"));
|
||||
@@ -2833,7 +2849,9 @@ async fn run_interactive_tui(
|
||||
.map_err(std::io::Error::other)?;
|
||||
codex_app_server_daemon::run(AppServerLifecycleCommand::Start)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
.map_err(|err| std::io::Error::other(format!(
|
||||
"{err:#}\nThe agents overview requires a shared server. Use codex --no-daemon to work without it."
|
||||
)))?;
|
||||
}
|
||||
|
||||
let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env.clone()) {
|
||||
@@ -3056,6 +3074,7 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli)
|
||||
approval_policy,
|
||||
web_search,
|
||||
no_alt_screen,
|
||||
no_daemon,
|
||||
prompt,
|
||||
mut config_overrides,
|
||||
..
|
||||
@@ -3076,6 +3095,7 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli)
|
||||
interactive.web_search = true;
|
||||
}
|
||||
interactive.no_alt_screen |= no_alt_screen;
|
||||
interactive.no_daemon |= no_daemon;
|
||||
if strict_config {
|
||||
interactive.strict_config = true;
|
||||
}
|
||||
@@ -4266,6 +4286,21 @@ mod tests {
|
||||
assert_eq!(interactive.resume_session_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_and_fork_preserve_no_daemon() {
|
||||
for (command, finalize) in [
|
||||
("resume", finalize_resume_from_args as fn(&[&str]) -> TuiCli),
|
||||
("fork", finalize_fork_from_args as fn(&[&str]) -> TuiCli),
|
||||
] {
|
||||
for args in [
|
||||
["codex", "--no-daemon", command, "--last"],
|
||||
["codex", command, "--last", "--no-daemon"],
|
||||
] {
|
||||
assert!(finalize(&args).no_daemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_and_fork_preserve_no_alt_screen() {
|
||||
for (command, finalize) in [
|
||||
|
||||
@@ -298,3 +298,35 @@ async fn features_list_honors_cloud_managed_feature_requirements() -> Result<()>
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_daemon_rejects_agents_and_explicit_remote_targets() -> Result<()> {
|
||||
for args in [
|
||||
"--no-daemon agents",
|
||||
"agents --no-daemon",
|
||||
"--no-daemon queue --thread example --message hello",
|
||||
"--no-daemon --remote ws://localhost:9999 agents",
|
||||
"--no-daemon --remote ws://localhost:9999",
|
||||
"--no-daemon --remote ws://localhost:9999 archive example",
|
||||
"--no-daemon --remote ws://localhost:9999 queue --thread example --message hello",
|
||||
"--remote ws://localhost:9999 resume --no-daemon --last",
|
||||
"--no-daemon fork --remote ws://localhost:9999 session-name",
|
||||
] {
|
||||
let args = args.split_whitespace().collect::<Vec<_>>();
|
||||
let home = TempDir::new()?;
|
||||
let expected = if args.contains(&"agents") {
|
||||
"--no-daemon cannot be used with codex agents."
|
||||
} else if args.contains(&"queue") && !args.contains(&"--remote") {
|
||||
"--no-daemon cannot be used with codex queue."
|
||||
} else {
|
||||
"--no-daemon cannot be used with --remote."
|
||||
};
|
||||
codex_command(home.path())?
|
||||
.args(args)
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(expected));
|
||||
assert!(!home.path().join("app-server-daemon").exists());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ pub struct Cli {
|
||||
#[arg(long = "no-alt-screen", default_value_t = false)]
|
||||
pub no_alt_screen: bool,
|
||||
|
||||
/// Run without the shared background server, even if it is already running.
|
||||
#[arg(long)]
|
||||
pub no_daemon: bool,
|
||||
|
||||
#[clap(skip)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Local daemon discovery policy. Explicit embedded launches never probe the shared server.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn exclusion(
|
||||
cli: &Cli,
|
||||
cli_kv_overrides: &[(String, toml::Value)],
|
||||
loader_overrides: &LoaderOverrides,
|
||||
workload_identity_selected: bool,
|
||||
exec_server_url: Option<&std::ffi::OsStr>,
|
||||
) -> Option<&'static str> {
|
||||
if cli.no_daemon {
|
||||
Some("--no-daemon")
|
||||
} else if cli.shared.worktree {
|
||||
Some("--worktree")
|
||||
} else if cli.oss {
|
||||
Some("--oss")
|
||||
} else if workload_identity_selected {
|
||||
Some("workload identity")
|
||||
} else if exec_server_url.is_some() {
|
||||
Some("executor selection (CODEX_EXEC_SERVER_URL)")
|
||||
} else if cli.agents_overview {
|
||||
None
|
||||
} else if cli.config_profile_v2.is_some() {
|
||||
Some("--profile")
|
||||
} else {
|
||||
config_exclusion(
|
||||
cli_kv_overrides,
|
||||
loader_overrides,
|
||||
cli.strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn config_exclusion(
|
||||
cli_kv_overrides: &[(String, toml::Value)],
|
||||
loader_overrides: &LoaderOverrides,
|
||||
strict_config: bool,
|
||||
bypass_hook_trust: bool,
|
||||
) -> Option<&'static str> {
|
||||
if !cli_kv_overrides.is_empty() {
|
||||
Some("command-line configuration overrides (-c, --enable, --disable, or --search)")
|
||||
} else if !loader_overrides_are_default(loader_overrides) {
|
||||
Some("custom configuration loader")
|
||||
} else if strict_config {
|
||||
Some("--strict-config")
|
||||
} else if bypass_hook_trust {
|
||||
Some("--dangerously-bypass-hook-trust")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -83,3 +83,82 @@ async fn daemon_startup_falls_back_only_for_implicit_endpoints() -> color_eyre::
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daemon_eligibility_preserves_launch_options_and_explains_exclusions() {
|
||||
use clap::Parser;
|
||||
for (args, expected) in [
|
||||
("--no-daemon", Some("--no-daemon")),
|
||||
("--worktree", Some("--worktree")),
|
||||
("--oss", Some("--oss")),
|
||||
("--profile test", Some("--profile")),
|
||||
("--strict-config", Some("--strict-config")),
|
||||
(
|
||||
"--dangerously-bypass-hook-trust",
|
||||
Some("--dangerously-bypass-hook-trust"),
|
||||
),
|
||||
(
|
||||
"-m test --cd /tmp -i image.png -a never -s workspace-write --add-dir /tmp --no-alt-screen hello",
|
||||
None,
|
||||
),
|
||||
] {
|
||||
let cli = Cli::parse_from(std::iter::once("codex").chain(args.split_whitespace()));
|
||||
assert_eq!(
|
||||
daemon_startup::exclusion(
|
||||
&cli,
|
||||
&[],
|
||||
&LoaderOverrides::default(),
|
||||
/*workload_identity_selected*/ false,
|
||||
/*exec_server_url*/ None
|
||||
),
|
||||
expected
|
||||
);
|
||||
}
|
||||
let mut cli = Cli::parse_from(["codex"]);
|
||||
let overrides = vec![("web_search".into(), toml::Value::String("live".into()))];
|
||||
let loader = LoaderOverrides {
|
||||
ignore_user_config: true,
|
||||
..Default::default()
|
||||
};
|
||||
for (kv, loader, workload, executor, expected) in [
|
||||
(
|
||||
&overrides[..],
|
||||
LoaderOverrides::default(),
|
||||
false,
|
||||
None,
|
||||
"command-line configuration overrides (-c, --enable, --disable, or --search)",
|
||||
),
|
||||
(&[][..], loader, false, None, "custom configuration loader"),
|
||||
(
|
||||
&[][..],
|
||||
LoaderOverrides::default(),
|
||||
true,
|
||||
None,
|
||||
"workload identity",
|
||||
),
|
||||
(
|
||||
&[][..],
|
||||
LoaderOverrides::default(),
|
||||
false,
|
||||
Some(std::ffi::OsStr::new("executor")),
|
||||
"executor selection (CODEX_EXEC_SERVER_URL)",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
daemon_startup::exclusion(&cli, kv, &loader, workload, executor),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
cli.agents_overview = true;
|
||||
cli.strict_config = true;
|
||||
assert_eq!(
|
||||
daemon_startup::exclusion(
|
||||
&cli,
|
||||
&overrides,
|
||||
&LoaderOverrides::default(),
|
||||
/*workload_identity_selected*/ false,
|
||||
/*exec_server_url*/ None
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
+2
-52
@@ -987,19 +987,6 @@ fn loader_overrides_are_default(loader_overrides: &LoaderOverrides) -> bool {
|
||||
loader_overrides_are_default
|
||||
}
|
||||
|
||||
fn can_reuse_implicit_local_daemon(
|
||||
cli_kv_overrides: &[(String, toml::Value)],
|
||||
loader_overrides: &LoaderOverrides,
|
||||
strict_config: bool,
|
||||
has_non_replayable_launch_overrides: bool,
|
||||
) -> bool {
|
||||
// A reused daemon cannot adopt this invocation's full launch config state.
|
||||
cli_kv_overrides.is_empty()
|
||||
&& loader_overrides_are_default(loader_overrides)
|
||||
&& !strict_config
|
||||
&& !has_non_replayable_launch_overrides
|
||||
}
|
||||
|
||||
/// Restore terminal modes before a fatal startup exit bypasses destructor cleanup.
|
||||
fn restore_terminal_before_fatal_exit() {
|
||||
if crossterm::terminal::is_raw_mode_enabled().unwrap_or(false) {
|
||||
@@ -2175,6 +2162,8 @@ fn should_show_bedrock_setup_wizard(
|
||||
.is_login_method_allowed(ForcedLoginMethod::Api)
|
||||
}
|
||||
|
||||
mod daemon_startup;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "daemon_startup_tests.rs"]
|
||||
mod daemon_startup_tests;
|
||||
@@ -3020,45 +3009,6 @@ requires_openai_auth = {requires_openai_auth}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_reuse_implicit_local_daemon_requires_default_launch_config() -> color_eyre::Result<()> {
|
||||
let mut loader_overrides = LoaderOverrides::default();
|
||||
let cli_kv_overrides = vec![("web_search".to_string(), toml::Value::String("live".into()))];
|
||||
|
||||
assert!(can_reuse_implicit_local_daemon(
|
||||
&[],
|
||||
&LoaderOverrides::default(),
|
||||
/*strict_config*/ false,
|
||||
/*has_non_replayable_launch_overrides*/ false,
|
||||
));
|
||||
assert!(!can_reuse_implicit_local_daemon(
|
||||
&cli_kv_overrides,
|
||||
&LoaderOverrides::default(),
|
||||
/*strict_config*/ false,
|
||||
/*has_non_replayable_launch_overrides*/ false,
|
||||
));
|
||||
loader_overrides.ignore_user_config = true;
|
||||
assert!(!can_reuse_implicit_local_daemon(
|
||||
&[],
|
||||
&loader_overrides,
|
||||
/*strict_config*/ false,
|
||||
/*has_non_replayable_launch_overrides*/ false,
|
||||
));
|
||||
assert!(!can_reuse_implicit_local_daemon(
|
||||
&[],
|
||||
&LoaderOverrides::default(),
|
||||
/*strict_config*/ true,
|
||||
/*has_non_replayable_launch_overrides*/ false,
|
||||
));
|
||||
assert!(!can_reuse_implicit_local_daemon(
|
||||
&[],
|
||||
&LoaderOverrides::default(),
|
||||
/*strict_config*/ false,
|
||||
/*has_non_replayable_launch_overrides*/ true,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_load_configured_environments_for_local_daemon() -> color_eyre::Result<()> {
|
||||
let target = AppServerTarget::LocalDaemon {
|
||||
|
||||
@@ -228,6 +228,9 @@ pub(super) async fn start_app_server_for_session_command(
|
||||
arg0_paths,
|
||||
explicit_remote_endpoint,
|
||||
} = options;
|
||||
if cli.no_daemon && explicit_remote_endpoint.is_some() {
|
||||
return Err(eyre!("--no-daemon cannot be used with --remote."));
|
||||
}
|
||||
let loader_overrides = LoaderOverrides::default();
|
||||
let strict_config = cli.strict_config;
|
||||
let raw_overrides = cli.config_overrides.raw_overrides.clone();
|
||||
@@ -245,13 +248,15 @@ pub(super) async fn start_app_server_for_session_command(
|
||||
}
|
||||
|
||||
let workload_identity_selected = codex_login::is_workload_identity_selected();
|
||||
let reuse_implicit_local_daemon = !workload_identity_selected
|
||||
&& super::can_reuse_implicit_local_daemon(
|
||||
let reuse_implicit_local_daemon = !cli.no_daemon
|
||||
&& !workload_identity_selected
|
||||
&& super::daemon_startup::config_exclusion(
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
);
|
||||
)
|
||||
.is_none();
|
||||
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
||||
super::maybe_probe_default_daemon_socket(codex_home.as_path()).await
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,11 @@ pub async fn run_session_queue_command(
|
||||
message: String,
|
||||
options: SessionArchiveCommandOptions,
|
||||
) -> Result<String> {
|
||||
if options.cli.no_daemon && options.explicit_remote_endpoint.is_none() {
|
||||
return Err(eyre!(
|
||||
"--no-daemon cannot be used with codex queue. Queuing must discover the shared server to avoid writing through a separate server."
|
||||
));
|
||||
}
|
||||
let codex_home = find_codex_home().wrap_err("failed to find Codex home")?;
|
||||
let explicit_remote = options.explicit_remote_endpoint.is_some();
|
||||
let mut app_server =
|
||||
|
||||
@@ -13,6 +13,11 @@ pub(super) async fn run_main_inner(
|
||||
loader_overrides: LoaderOverrides,
|
||||
explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
|
||||
) -> std::io::Result<AppExitInfo> {
|
||||
if cli.no_daemon && explicit_remote_endpoint.is_some() {
|
||||
return Err(std::io::Error::other(
|
||||
"--no-daemon cannot be used with --remote.",
|
||||
));
|
||||
}
|
||||
let strict_config = cli.strict_config;
|
||||
if cli.shared.worktree {
|
||||
if explicit_remote_endpoint.is_some() {
|
||||
@@ -150,16 +155,14 @@ pub(super) async fn run_main_inner(
|
||||
.await;
|
||||
}
|
||||
|
||||
let reuse_implicit_local_daemon = !cli.shared.worktree
|
||||
&& !cli.oss
|
||||
&& !workload_identity_selected
|
||||
&& (cli.agents_overview
|
||||
|| can_reuse_implicit_local_daemon(
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
strict_config,
|
||||
cli.bypass_hook_trust,
|
||||
));
|
||||
let daemon_exclusion = daemon_startup::exclusion(
|
||||
&cli,
|
||||
&cli_kv_overrides,
|
||||
&launch_loader_overrides,
|
||||
workload_identity_selected,
|
||||
std::env::var_os(codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR).as_deref(),
|
||||
);
|
||||
let reuse_implicit_local_daemon = daemon_exclusion.is_none();
|
||||
let search_only_config_override = !workload_identity_selected
|
||||
&& cli.web_search
|
||||
&& startup_preflight::has_only_search_config_override(&cli_kv_overrides)
|
||||
|
||||
@@ -14,7 +14,6 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use anyhow::ensure;
|
||||
#[cfg(target_os = "macos")]
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -396,3 +395,42 @@ pub(super) fn write_test_config(codex_home: &Path, repo_root: &Path) -> Result<(
|
||||
)
|
||||
.context("write focus-test API-key authentication")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_daemon_skips_startup_and_discovery() -> Result<()> {
|
||||
for running in [false, true] {
|
||||
let workspace = tempfile::tempdir()?;
|
||||
let home = tempfile::tempdir()?;
|
||||
write_test_config(home.path(), workspace.path())?;
|
||||
let socket_path = codex_app_server_client::app_server_control_socket_path(home.path())?;
|
||||
std::fs::create_dir_all(socket_path.as_path().parent().unwrap())?;
|
||||
let listener = if running {
|
||||
let listener = std::os::unix::net::UnixListener::bind(socket_path.as_path())?;
|
||||
listener.set_nonblocking(true)?;
|
||||
Some(listener)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut terminal = PtyCodex::start(workspace.path(), home, &["--no-daemon"])?;
|
||||
terminal.wait_for_startup()?;
|
||||
terminal.write_input(b"/status")?;
|
||||
terminal.wait_for_screen("show current session configuration")?;
|
||||
terminal.read_output(Duration::from_millis(/*millis*/ 200))?;
|
||||
terminal.write_input(b"\r")?;
|
||||
terminal.wait_for_screen("Model:")?;
|
||||
ensure!(
|
||||
!terminal
|
||||
._codex_home
|
||||
.path()
|
||||
.join("app-server-daemon")
|
||||
.exists()
|
||||
);
|
||||
if let Some(listener) = listener {
|
||||
assert_eq!(
|
||||
listener.accept().unwrap_err().kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user