Files
codex/codex-rs/tui/src/daemon_startup.rs
T
Eric Traut bf877af418 Enable the agent command center in embedded mode (#58239)
## Why

The agent command center previously required a shared background server, preventing embedded sessions from opening it and rejecting `codex agents --no-daemon`.

## What changed

- Allow `codex agents --no-daemon` to use an embedded app server and open the command center without a shared server.
- Route `codex agents` through the normal daemon startup, eligibility, and compatibility checks, including embedded fallback where applicable.
- Return embedded sessions to the command center when canceling directory trust or archived-session confirmation.

## Testing

Add CLI coverage for `--no-daemon` before and after `agents`, plus an integration test that verifies embedded mode is used even when a daemon is running. Update archived-session tests to expect cancellation to return to the command center.

GitOrigin-RevId: 3bdf18a00456eae2e4d38103d12126e039d44119
2026-10-30 04:01:11 +00:00

249 lines
9.7 KiB
Rust

//! Local daemon launch policy. Explicit embedded launches never discover or start a daemon;
//! optional attachment may fall back to embedded mode, while automatic launches
//! require a compatible shared server and a successful connection, except when
//! the Windows launcher forbids detaching a missing server. Elevated local
//! Windows sessions use explicit embedded behavior before discovery or startup.
//! Explicit feature overrides are compared with the daemon's reported
//! configuration; removed features are inert, and unreported features are assumed disabled.
use super::*;
use codex_app_server_client::TypedRequestError;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigRequirementsReadResponse;
use codex_app_server_protocol::ExperimentalFeatureStage;
use codex_app_server_protocol::RequestId;
use std::collections::BTreeMap;
pub(super) const FAILURE_HINT: &str = "To work without the background server, rerun the same command with --no-daemon (including resume or fork and its arguments).";
pub(super) const WSL_DRVFS_EXCLUSION: &str = "a Windows-mounted WSL CODEX_HOME (DrvFS/9p)";
/// Returns whether `codex_home` is on a Windows-mounted WSL filesystem.
pub fn uses_wsl_drvfs(codex_home: &std::path::Path) -> bool {
// The managed daemon writes an executable and Unix-style state beneath CODEX_HOME.
// DrvFS does not reliably support the required permission semantics, so starting it
// there can fail before the TUI opens.
#[cfg(target_os = "linux")]
{
use std::os::fd::AsRawFd;
if !codex_utils_path::is_wsl() && std::env::var_os("WSL_INTEROP").is_none() {
return false;
}
let Some(directory) = codex_home
.ancestors()
.find_map(|path| std::fs::File::open(path).ok())
else {
return false;
};
let mut stats = std::mem::MaybeUninit::<libc::statfs>::uninit();
if unsafe { libc::fstatfs(directory.as_raw_fd(), stats.as_mut_ptr()) } != 0 {
return false;
}
unsafe { stats.assume_init() }.f_type == 0x0102_1997
}
#[cfg(not(target_os = "linux"))]
{
let _ = codex_home;
false
}
}
#[cfg(any(windows, test))]
pub(super) const ELEVATED_LAUNCH_WARNING: &str = "Running as administrator: shared background server disabled. To enable it, restart Codex in a terminal without administrator permissions.";
#[derive(Debug, thiserror::Error)]
#[error("Cannot use the shared background server: {reason}.\n{FAILURE_HINT}")]
pub(super) struct CompatibilityError {
pub reason: String,
pub restart_features: Option<BTreeMap<String, bool>>,
}
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.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.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
.iter()
.all(|(key, value)| match key.as_str() {
"suppress_unstable_features_warning" | "tui.fullscreen_transcript" => value.is_bool(),
"tui" => value.as_table().is_some_and(|tui| {
tui.len() == 1
&& tui
.get("fullscreen_transcript")
.is_some_and(toml::Value::is_bool)
}),
"features" => value.as_table().is_some_and(|features| {
!features.is_empty() && features.values().all(toml::Value::is_bool)
}),
_ => {
key.strip_prefix("features.")
.is_some_and(|name| !name.contains('.'))
&& value.is_bool()
}
})
{
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
}
}
/// Best-effort configured readback, not a guarantee about startup-captured service state.
pub(super) async fn compatibility_warning(
target: &AppServerTarget,
config: &Config,
cli_kv_overrides: &[(String, toml::Value)],
) -> Result<Option<String>, CompatibilityError> {
let AppServerTarget::LocalDaemon {
allow_embedded_fallback,
..
} = target
else {
return Ok(None);
};
// File settings and defaults belong to the daemon until it restarts.
// Only explicit invocation flags can request a shared-service restart.
let mut requested: BTreeMap<_, _> = codex_config::build_cli_overrides_layer(cli_kv_overrides)
.get("features")
.and_then(toml::Value::as_table)
.into_iter()
.flatten()
.filter_map(|(name, value)| {
let name = match codex_features::feature_for_key(name) {
Some(feature) if feature.stage() == codex_features::Stage::Removed => return None,
Some(feature) => feature.key(),
None => name,
};
Some((name.to_string(), value.as_bool()?))
})
.collect();
let mut restart_features = None;
let check = async {
// Connect even without overrides so startup can recover from an unreachable daemon.
let client = app_server_connection::connect(target)
.await
.map_err(|_| "could not connect to check daemon feature settings".to_string())?;
if requested.is_empty() {
let _ = client.shutdown().await;
return Ok(());
}
// Both legacy managed config and current requirements override CLI flags.
let layers = crate::config_update::read_effective_config_if_supported(
client.request_handle(),
config.cwd.as_path(),
)
.await
.map_err(|_| "could not read daemon configuration".to_string())?
.and_then(|config| config.layers)
.unwrap_or_default();
for layer in layers.iter().filter(|layer| {
layer["disabledReason"].is_null()
&& matches!(
layer["name"]["type"].as_str(),
Some("legacyManagedConfigTomlFromFile" | "legacyManagedConfigTomlFromMdm")
)
}) {
requested.retain(|name, _| layer["config"]["features"].get(name).is_none());
}
match client
.request_handle()
.request_typed::<ConfigRequirementsReadResponse>(
ClientRequest::ConfigRequirementsRead {
request_id: RequestId::String("tui-daemon-requirements".to_string()),
params: None,
},
)
.await
{
Ok(response) => {
if let Some(required) = response.requirements.and_then(|r| r.feature_requirements) {
requested.retain(|name, _| !required.contains_key(name));
}
}
Err(TypedRequestError::Server { source, .. })
if source.code == -32601
|| source.code == -32600
&& source.message.contains("configRequirements/read")
&& (source.message.contains("unknown variant")
|| source.message.contains("unknown method")) => {}
Err(_) => return Err("could not check daemon configuration requirements".to_string()),
}
if requested.is_empty() {
let _ = client.shutdown().await;
return Ok(());
}
let (tx, rx) = tokio::sync::oneshot::channel();
crate::experimental_features::fetch(
client.request_handle(),
/*thread_id*/ None,
"tui-daemon-features",
tx,
);
let result = rx.await;
let _ = client.shutdown().await;
let features = result.map_err(|_| "daemon feature check was interrupted".to_string())??;
requested.retain(|name, enabled| {
match features.iter().find(|feature| feature.name == *name) {
Some(feature) => {
feature.stage != ExperimentalFeatureStage::Removed
&& feature.enabled != *enabled
}
None => *enabled,
}
});
if let Some((name, enabled)) = requested.first_key_value() {
let state = if *enabled { "enabled" } else { "disabled" };
let reason = format!("This session requires {name} to be {state}");
restart_features = Some(requested);
return Err(reason);
}
Ok::<(), String>(())
}
.await;
match check {
Ok(()) => Ok(None),
Err(reason) if *allow_embedded_fallback && restart_features.is_none() => Ok(Some(format!(
"Running without the shared background server: {reason}."
))),
Err(reason) => Err(CompatibilityError {
reason,
restart_features,
}),
}
}