[codex] Support remote exec cwd in TUI startup (#17142)

When running with remote executor the cwd is the remote path. Today we
check for existence of a local directory on startup and attempt to load
config from it.

For remote executors don't do that.
This commit is contained in:
pakrym-oai
2026-04-08 13:09:28 -07:00
committed by GitHub
Unverified
parent f383cc980d
commit e4d6702b87
11 changed files with 128 additions and 21 deletions
+1
View File
@@ -1505,6 +1505,7 @@ dependencies = [
"codex-app-server-protocol",
"codex-arg0",
"codex-core",
"codex-exec-server",
"codex-feedback",
"codex-protocol",
"futures",
+1
View File
@@ -16,6 +16,7 @@ codex-app-server = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-feedback = { workspace = true }
codex-protocol = { workspace = true }
futures = { workspace = true }
+15 -1
View File
@@ -43,6 +43,7 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
pub use codex_exec_server::EnvironmentManager;
use codex_feedback::CodexFeedback;
use codex_protocol::protocol::SessionSource;
use serde::de::DeserializeOwned;
@@ -268,6 +269,8 @@ pub struct InProcessClientStartArgs {
pub cloud_requirements: CloudRequirementsLoader,
/// Feedback sink used by app-server/core telemetry and logs.
pub feedback: CodexFeedback,
/// Environment manager used by core execution and filesystem operations.
pub environment_manager: Arc<EnvironmentManager>,
/// Startup warnings emitted after initialize succeeds.
pub config_warnings: Vec<ConfigWarningNotification>,
/// Session source recorded in app-server thread metadata.
@@ -317,6 +320,7 @@ impl InProcessClientStartArgs {
loader_overrides: self.loader_overrides,
cloud_requirements: self.cloud_requirements,
feedback: self.feedback,
environment_manager: self.environment_manager,
config_warnings: self.config_warnings,
session_source: self.session_source,
enable_codex_api_key_env: self.enable_codex_api_key_env,
@@ -893,6 +897,7 @@ mod tests {
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
config_warnings: Vec::new(),
session_source,
enable_codex_api_key_env: false,
@@ -1891,8 +1896,11 @@ mod tests {
}
#[tokio::test]
async fn runtime_start_args_leave_manager_bootstrap_to_app_server() {
async fn runtime_start_args_forward_environment_manager() {
let config = Arc::new(build_test_config().await);
let environment_manager = Arc::new(EnvironmentManager::new(Some(
"ws://127.0.0.1:8765".to_string(),
)));
let runtime_args = InProcessClientStartArgs {
arg0_paths: Arg0DispatchPaths::default(),
@@ -1901,6 +1909,7 @@ mod tests {
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
environment_manager: environment_manager.clone(),
config_warnings: Vec::new(),
session_source: SessionSource::Exec,
enable_codex_api_key_env: false,
@@ -1913,6 +1922,11 @@ mod tests {
.into_runtime_start_args();
assert_eq!(runtime_args.config, config);
assert!(Arc::ptr_eq(
&runtime_args.environment_manager,
&environment_manager
));
assert!(runtime_args.environment_manager.is_remote());
}
#[tokio::test]
+4 -1
View File
@@ -117,6 +117,8 @@ pub struct InProcessStartArgs {
pub cloud_requirements: CloudRequirementsLoader,
/// Feedback sink used by app-server/core telemetry and logs.
pub feedback: CodexFeedback,
/// Environment manager used by core execution and filesystem operations.
pub environment_manager: Arc<EnvironmentManager>,
/// Startup warnings emitted after initialize succeeds.
pub config_warnings: Vec<ConfigWarningNotification>,
/// Session source stamped into thread/session metadata.
@@ -388,7 +390,7 @@ fn start_uninitialized(args: InProcessStartArgs) -> InProcessClientHandle {
outgoing: Arc::clone(&processor_outgoing),
arg0_paths: args.arg0_paths,
config: args.config,
environment_manager: Arc::new(EnvironmentManager::from_env()),
environment_manager: args.environment_manager,
cli_overrides: args.cli_overrides,
loader_overrides: args.loader_overrides,
cloud_requirements: args.cloud_requirements,
@@ -721,6 +723,7 @@ mod tests {
loader_overrides: LoaderOverrides::default(),
cloud_requirements: CloudRequirementsLoader::default(),
feedback: CodexFeedback::new(),
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
config_warnings: Vec::new(),
session_source,
enable_codex_api_key_env: false,
+2 -2
View File
@@ -811,12 +811,12 @@ impl Config {
/// applied yet, which risks failing to enforce required constraints.
pub async fn load_config_as_toml_with_cli_overrides(
codex_home: &Path,
cwd: &AbsolutePathBuf,
cwd: Option<&AbsolutePathBuf>,
cli_overrides: Vec<(String, TomlValue)>,
) -> std::io::Result<ConfigToml> {
let config_layer_stack = load_config_layers_state(
codex_home,
Some(cwd.clone()),
cwd.cloned(),
&cli_overrides,
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
+15
View File
@@ -69,6 +69,11 @@ impl EnvironmentManager {
self.exec_server_url.as_deref()
}
/// Returns true when this manager is configured to use a remote exec server.
pub fn is_remote(&self) -> bool {
self.exec_server_url.is_some()
}
/// Returns the cached environment, creating it on first access.
pub async fn current(&self) -> Result<Option<Arc<Environment>>, ExecServerError> {
self.current_environment
@@ -227,6 +232,7 @@ mod tests {
assert!(!manager.disabled);
assert_eq!(manager.exec_server_url(), None);
assert!(!manager.is_remote());
}
#[test]
@@ -235,6 +241,15 @@ mod tests {
assert!(manager.disabled);
assert_eq!(manager.exec_server_url(), None);
assert!(!manager.is_remote());
}
#[test]
fn environment_manager_reports_remote_url() {
let manager = EnvironmentManager::new(Some("ws://127.0.0.1:8765".to_string()));
assert!(manager.is_remote());
assert_eq!(manager.exec_server_url(), Some("ws://127.0.0.1:8765"));
}
#[tokio::test]
+3 -1
View File
@@ -14,6 +14,7 @@ pub use cli::Cli;
pub use cli::Command;
pub use cli::ReviewArgs;
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
use codex_app_server_client::EnvironmentManager;
use codex_app_server_client::InProcessAppServerClient;
use codex_app_server_client::InProcessClientStartArgs;
use codex_app_server_client::InProcessServerEvent;
@@ -293,7 +294,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
&codex_home,
&config_cwd,
Some(&config_cwd),
cli_kv_overrides.clone(),
)
.await
@@ -471,6 +472,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
loader_overrides: run_loader_overrides,
cloud_requirements: run_cloud_requirements,
feedback: CodexFeedback::new(),
environment_manager: std::sync::Arc::new(EnvironmentManager::from_env()),
config_warnings,
session_source: SessionSource::Exec,
enable_codex_api_key_env: true,
+9
View File
@@ -96,6 +96,7 @@ use codex_core::config_loader::ConfigLayerStackOrdering;
use codex_core::lookup_message_history_entry;
#[cfg(target_os = "windows")]
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG;
@@ -979,6 +980,7 @@ pub(crate) struct App {
pub(crate) backtrack_render_pending: bool,
pub(crate) feedback: codex_feedback::CodexFeedback,
feedback_audience: FeedbackAudience,
environment_manager: Arc<EnvironmentManager>,
remote_app_server_url: Option<String>,
remote_app_server_auth_token: Option<String>,
/// Set when the user confirms an update; propagated on exit.
@@ -3595,6 +3597,7 @@ impl App {
should_prompt_windows_sandbox_nux_at_startup: bool,
remote_app_server_url: Option<String>,
remote_app_server_auth_token: Option<String>,
environment_manager: Arc<EnvironmentManager>,
) -> Result<AppExitInfo> {
use tokio_stream::StreamExt;
let (app_event_tx, mut app_event_rx) = unbounded_channel();
@@ -3807,6 +3810,7 @@ impl App {
backtrack_render_pending: false,
feedback: feedback.clone(),
feedback_audience,
environment_manager,
remote_app_server_url,
remote_app_server_auth_token,
pending_update_action: None,
@@ -4068,6 +4072,7 @@ impl App {
},
None => crate::AppServerTarget::Embedded,
},
self.environment_manager.clone(),
)
.await
{
@@ -9199,6 +9204,7 @@ guardian_approval = true
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
remote_app_server_url: None,
remote_app_server_auth_token: None,
pending_update_action: None,
@@ -9253,6 +9259,9 @@ guardian_approval = true
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
environment_manager: Arc::new(EnvironmentManager::new(
/*exec_server_url*/ None,
)),
remote_app_server_url: None,
remote_app_server_auth_token: None,
pending_update_action: None,
+66 -14
View File
@@ -37,6 +37,7 @@ use codex_core::format_exec_policy_error_with_source;
use codex_core::path_utils;
use codex_core::read_session_meta_line;
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
use codex_exec_server::EnvironmentManager;
use codex_login::AuthConfig;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::enforce_login_restrictions;
@@ -238,6 +239,7 @@ async fn start_embedded_app_server(
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<InProcessAppServerClient> {
start_embedded_app_server_with(
arg0_paths,
@@ -246,6 +248,7 @@ async fn start_embedded_app_server(
loader_overrides,
cloud_requirements,
feedback,
environment_manager,
InProcessAppServerClient::start,
)
.await
@@ -352,6 +355,7 @@ async fn connect_remote_app_server(
Ok(AppServerClient::Remote(app_server))
}
#[allow(clippy::too_many_arguments)]
async fn start_app_server(
target: &AppServerTarget,
arg0_paths: Arg0DispatchPaths,
@@ -360,6 +364,7 @@ async fn start_app_server(
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerClient> {
match target {
AppServerTarget::Embedded => start_embedded_app_server(
@@ -369,6 +374,7 @@ async fn start_app_server(
loader_overrides,
cloud_requirements,
feedback,
environment_manager,
)
.await
.map(AppServerClient::InProcess),
@@ -382,6 +388,7 @@ async fn start_app_server(
pub(crate) async fn start_app_server_for_picker(
config: &Config,
target: &AppServerTarget,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerSession> {
let app_server = start_app_server(
target,
@@ -391,6 +398,7 @@ pub(crate) async fn start_app_server_for_picker(
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
environment_manager,
)
.await?;
Ok(AppServerSession::new(app_server))
@@ -400,9 +408,15 @@ pub(crate) async fn start_app_server_for_picker(
pub(crate) async fn start_embedded_app_server_for_picker(
config: &Config,
) -> color_eyre::Result<AppServerSession> {
start_app_server_for_picker(config, &AppServerTarget::Embedded).await
start_app_server_for_picker(
config,
&AppServerTarget::Embedded,
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn start_embedded_app_server_with<F, Fut>(
arg0_paths: Arg0DispatchPaths,
config: Config,
@@ -410,6 +424,7 @@ async fn start_embedded_app_server_with<F, Fut>(
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
feedback: codex_feedback::CodexFeedback,
environment_manager: Arc<EnvironmentManager>,
start_client: F,
) -> color_eyre::Result<InProcessAppServerClient>
where
@@ -433,6 +448,7 @@ where
loader_overrides,
cloud_requirements,
feedback,
environment_manager,
config_warnings,
session_source: codex_protocol::protocol::SessionSource::Cli,
enable_codex_api_key_env: false,
@@ -590,15 +606,19 @@ fn latest_session_lookup_params(
fn config_cwd_for_app_server_target(
cwd: Option<&Path>,
app_server_target: &AppServerTarget,
) -> std::io::Result<AbsolutePathBuf> {
if matches!(app_server_target, AppServerTarget::Remote { .. }) {
return AbsolutePathBuf::current_dir();
environment_manager: &EnvironmentManager,
) -> std::io::Result<Option<AbsolutePathBuf>> {
if environment_manager.is_remote()
|| matches!(app_server_target, AppServerTarget::Remote { .. })
{
return Ok(None);
}
match cwd {
let cwd = match cwd {
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?),
None => AbsolutePathBuf::current_dir(),
}
}?;
Ok(Some(cwd))
}
fn latest_session_cwd_filter<'a>(
@@ -689,13 +709,15 @@ pub async fn run_main(
}
};
let environment_manager = Arc::new(EnvironmentManager::from_env());
let cwd = cli.cwd.clone();
let config_cwd = config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target)?;
let config_cwd =
config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target, &environment_manager)?;
#[allow(clippy::print_stderr)]
let config_toml = match load_config_as_toml_with_cli_overrides(
&codex_home,
&config_cwd,
config_cwd.as_ref(),
cli_kv_overrides.clone(),
)
.await
@@ -952,6 +974,7 @@ pub async fn run_main(
feedback,
remote_url,
remote_auth_token,
environment_manager,
)
.await
.map_err(|err| std::io::Error::other(err.to_string()))
@@ -971,6 +994,7 @@ async fn run_ratatui_app(
feedback: codex_feedback::CodexFeedback,
remote_url: Option<String>,
remote_auth_token: Option<String>,
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppExitInfo> {
let remote_mode = matches!(&app_server_target, AppServerTarget::Remote { .. });
color_eyre::install()?;
@@ -1026,6 +1050,7 @@ async fn run_ratatui_app(
loader_overrides.clone(),
cloud_requirements.clone(),
feedback.clone(),
environment_manager.clone(),
)
.await
{
@@ -1351,6 +1376,7 @@ async fn run_ratatui_app(
loader_overrides,
cloud_requirements.clone(),
feedback.clone(),
environment_manager.clone(),
)
.await
{
@@ -1379,6 +1405,7 @@ async fn run_ratatui_app(
should_prompt_windows_sandbox_nux_at_startup,
remote_url,
remote_auth_token,
environment_manager,
)
.await;
@@ -1697,6 +1724,7 @@ mod tests {
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnContextItem;
use pretty_assertions::assert_eq;
use serial_test::serial;
use tempfile::TempDir;
@@ -1717,6 +1745,7 @@ mod tests {
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
)
.await
}
@@ -1865,8 +1894,7 @@ mod tests {
}
#[test]
fn config_cwd_for_app_server_target_uses_current_dir_for_remote_sessions() -> std::io::Result<()>
{
fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()> {
let remote_only_cwd = if cfg!(windows) {
Path::new(r"C:\definitely\not\local\to\this\test")
} else {
@@ -1876,10 +1904,12 @@ mod tests {
websocket_url: "ws://127.0.0.1:1234/".to_string(),
auth_token: None,
};
let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None);
let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target)?;
let config_cwd =
config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?;
assert_eq!(config_cwd, AbsolutePathBuf::current_dir()?);
assert_eq!(config_cwd, None);
Ok(())
}
@@ -1887,16 +1917,37 @@ mod tests {
fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let target = AppServerTarget::Embedded;
let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None);
let config_cwd = config_cwd_for_app_server_target(Some(temp_dir.path()), &target)?;
let config_cwd =
config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?;
assert_eq!(
config_cwd,
AbsolutePathBuf::from_absolute_path(temp_dir.path().canonicalize()?)?
Some(AbsolutePathBuf::from_absolute_path(
temp_dir.path().canonicalize()?
)?)
);
Ok(())
}
#[test]
fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server() -> std::io::Result<()> {
let remote_only_cwd = if cfg!(windows) {
Path::new(r"C:\definitely\not\local\to\this\test")
} else {
Path::new("/definitely/not/local/to/this/test")
};
let target = AppServerTarget::Embedded;
let environment_manager = EnvironmentManager::new(Some("ws://127.0.0.1:8765".to_string()));
let config_cwd =
config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?;
assert_eq!(config_cwd, None);
Ok(())
}
#[tokio::test]
async fn read_session_cwd_returns_none_without_sqlite_or_rollout_path() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
@@ -2017,6 +2068,7 @@ mod tests {
LoaderOverrides::default(),
CloudRequirementsLoader::default(),
codex_feedback::CodexFeedback::new(),
Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)),
|_args| async { Err(std::io::Error::other("boom")) },
)
.await;
+3
View File
@@ -988,6 +988,9 @@ mod tests {
"https://chatgpt.com/backend-api/".to_string(),
),
feedback: codex_feedback::CodexFeedback::new(),
environment_manager: Arc::new(codex_app_server_client::EnvironmentManager::new(
/*exec_server_url*/ None,
)),
config_warnings: Vec::new(),
session_source: SessionSource::Cli,
enable_codex_api_key_env: false,
+9 -2
View File
@@ -28,6 +28,7 @@ remote_exec_server_start_timeout_seconds="${CODEX_REMOTE_EXEC_SERVER_START_TIMEO
remote_exec_server_pid=''
remote_exec_server_log_path=''
remote_exec_server_pid_path=''
remote_repo_root=''
cleanup() {
local exit_code=$?
@@ -119,6 +120,7 @@ while (( SECONDS < deadline )); do
if [[ "${listen_url}" == ws://* ]]; then
printf 'remote_exec_server_pid=%s\n' "${remote_exec_server_pid}"
printf 'remote_exec_server_log_path=%s\n' "${remote_exec_server_log_path}"
printf 'remote_repo_root=%s\n' "${remote_repo_root}"
printf 'listen_url=%s\n' "${listen_url}"
exit 0
fi
@@ -148,13 +150,16 @@ while IFS='=' read -r key value; do
remote_exec_server_log_path)
remote_exec_server_log_path="${value}"
;;
remote_repo_root)
remote_repo_root="${value}"
;;
listen_url)
listen_url="${value}"
;;
esac
done <<< "${remote_start_output}"
if [[ -z "${remote_exec_server_pid}" || -z "${listen_url}" ]]; then
if [[ -z "${remote_exec_server_pid}" || -z "${listen_url}" || -z "${remote_repo_root}" ]]; then
echo "failed to parse remote exec server startup output" >&2
exit 1
fi
@@ -169,7 +174,9 @@ echo "Remote exec server: ${listen_url}"
echo "Remote exec server log: ${remote_exec_server_log_path}"
echo "Press Ctrl-C to stop the SSH tunnel and remote exec server."
echo "Start codex via: "
echo " CODEX_EXEC_SERVER_URL=ws://127.0.0.1:${local_exec_server_port} codex -C /tmp"
printf ' CODEX_EXEC_SERVER_URL=ws://127.0.0.1:%s codex -C %q\n' \
"${local_exec_server_port}" \
"${remote_repo_root}"
ssh \
-nNT \