Files
codex/codex-rs/tui/src/session_archive_commands.rs
T
Celia Chen 265cd2e100 Initialize execution environments with the final HTTP policy (#34995)
## Why

The TUI must inspect the default execution environment before loading its final
configuration. Initializing the environment manager at that point can give
startup services the bootstrap HTTP policy instead of the effective policy after
managed requirements are applied.

## What changed

- Split environment discovery from manager construction so callers can inspect
  the default environment without starting remote connections.
- Build the environment manager after final configuration loading and pass its
  resolved `HttpClientFactory` through all construction paths.
- Add shared test support for managers that use the legacy default HTTP policy.

## Testing

- Cover connection-free environment discovery and explicit HTTP policy
  propagation.
- Verify TUI startup services use the final managed `respect_system_proxy` value.

GitOrigin-RevId: 928fa31e6b4bcfbe1a121cade2f351427fdfa0f4
2026-07-23 19:24:41 +00:00

416 lines
14 KiB
Rust

//! Shared implementation for `codex archive`, `codex delete`, and `codex unarchive`.
//!
//! The CLI commands are thin app-server clients: resolve a user-provided UUID or exact session
//! name, then call the corresponding app-server RPC.
use std::io::IsTerminal;
use std::io::Write;
use std::sync::Arc;
use crate::Cli;
use crate::app_server_session::AppServerSession;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use crate::legacy_core::config::load_config_toml_with_layer_stack;
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
use crate::legacy_core::config::resolve_bootstrap_http_client_factory;
use crate::legacy_core::config::resolve_oss_provider;
use crate::legacy_core::config::resolve_profile_v2_config_path;
use codex_app_server_protocol::Thread as AppServerThread;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey;
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadOptions;
use codex_config::LoaderOverrides;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthRouteConfig;
use codex_protocol::ThreadId;
use codex_utils_cli::CliConfigOverrides;
use codex_utils_home_dir::find_codex_home;
use codex_utils_oss::get_default_model_for_oss_provider;
use color_eyre::eyre::Result;
use color_eyre::eyre::WrapErr;
use color_eyre::eyre::eyre;
use super::RemoteAppServerEndpoint;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeleteConfirmation {
Prompt,
Skip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionArchiveAction {
Archive,
Delete(DeleteConfirmation),
Unarchive,
}
pub struct SessionArchiveCommandOptions {
pub cli: Cli,
pub arg0_paths: Arg0DispatchPaths,
pub explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
}
fn success_message(
action: SessionArchiveAction,
session_id: ThreadId,
session_name: Option<&str>,
) -> String {
let action = match action {
SessionArchiveAction::Archive => "Archived",
SessionArchiveAction::Delete(_) => "Deleted",
SessionArchiveAction::Unarchive => "Unarchived",
};
match session_name {
Some(name) => format!("{action} session {name} ({session_id})."),
None => format!("{action} session {session_id}."),
}
}
struct ResolvedSessionTarget {
session_id: ThreadId,
session_name: Option<String>,
}
pub async fn run_session_archive_command(
action: SessionArchiveAction,
target: String,
options: SessionArchiveCommandOptions,
) -> Result<String> {
let mut app_server = start_app_server_for_archive_command(options).await?;
run_session_archive_action_with_app_server(&mut app_server, action, &target).await
}
async fn run_session_archive_action_with_app_server(
app_server: &mut AppServerSession,
action: SessionArchiveAction,
target: &str,
) -> Result<String> {
let resolved = resolve_session_target(app_server, action, target).await?;
let session_name = match action {
SessionArchiveAction::Archive => {
app_server.thread_archive(resolved.session_id).await?;
resolved.session_name
}
SessionArchiveAction::Delete(confirmation) => {
if matches!(confirmation, DeleteConfirmation::Prompt)
&& !confirm_session_delete(&resolved)?
{
return Ok("Delete cancelled.".to_string());
}
app_server.thread_delete(resolved.session_id).await?;
resolved.session_name
}
SessionArchiveAction::Unarchive => {
let thread = app_server.thread_unarchive(resolved.session_id).await?;
thread.name.or(resolved.session_name)
}
};
Ok(success_message(
action,
resolved.session_id,
session_name.as_deref(),
))
}
async fn resolve_session_target(
app_server: &mut AppServerSession,
action: SessionArchiveAction,
target: &str,
) -> Result<ResolvedSessionTarget> {
if let Ok(session_id) = ThreadId::from_string(target) {
if matches!(
action,
SessionArchiveAction::Delete(DeleteConfirmation::Prompt)
) {
let thread = app_server
.thread_read(session_id, /*include_turns*/ false)
.await
.with_context(|| {
format!("No active or archived session found matching '{target}'.")
})?;
return Ok(ResolvedSessionTarget {
session_id,
session_name: thread.name,
});
}
return Ok(ResolvedSessionTarget {
session_id,
session_name: None,
});
}
let (search_scope, archived_values): (&str, &[bool]) = match action {
SessionArchiveAction::Archive => ("active", &[false]),
SessionArchiveAction::Delete(_) => ("active or archived", &[false, true]),
SessionArchiveAction::Unarchive => ("archived", &[true]),
};
for &archived in archived_values {
if let Some(thread) = lookup_session_by_exact_name(app_server, target, archived).await? {
return session_target_from_app_server_thread(thread);
}
}
Err(eyre!(
"No {search_scope} session found matching '{target}'."
))
}
async fn lookup_session_by_exact_name(
app_server: &mut AppServerSession,
name: &str,
archived: bool,
) -> Result<Option<AppServerThread>> {
// Search is the fast path, but some stores attach renamed titles after applying the filter.
for search_term in [Some(name), None] {
let mut cursor = None;
loop {
let response = app_server
.thread_list(ThreadListParams {
cursor: cursor.clone(),
limit: Some(100),
sort_key: Some(ThreadSortKey::UpdatedAt),
sort_direction: None,
model_providers: None,
source_kinds: Some(super::resume_source_kinds(
/*include_non_interactive*/ false,
)),
archived: Some(archived),
is_pinned: None,
parent_thread_id: None,
ancestor_thread_id: None,
cwd: None,
use_state_db_only: false,
search_term: search_term.map(str::to_string),
})
.await
.wrap_err("failed to list sessions while resolving session name")?;
if let Some(thread) = response
.data
.into_iter()
.find(|thread| thread.name.as_deref() == Some(name))
{
return Ok(Some(thread));
}
let Some(next_cursor) = response.next_cursor else {
break;
};
cursor = Some(next_cursor);
}
}
Ok(None)
}
fn session_target_from_app_server_thread(thread: AppServerThread) -> Result<ResolvedSessionTarget> {
let session_id = ThreadId::from_string(&thread.id)
.wrap_err_with(|| format!("app server returned invalid session id `{}`", thread.id))?;
Ok(ResolvedSessionTarget {
session_id,
session_name: thread.name,
})
}
fn confirm_session_delete(target: &ResolvedSessionTarget) -> Result<bool> {
if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) {
return Err(eyre!(
"cannot confirm session deletion without an interactive terminal; rerun with --force and a session UUID"
));
}
let mut stderr = std::io::stderr().lock();
match target.session_name.as_deref() {
Some(name) => writeln!(
stderr,
"Permanently delete session '{name}' ({})?",
target.session_id
),
None => writeln!(stderr, "Permanently delete session {}?", target.session_id),
}?;
writeln!(
stderr,
"This cannot be undone. Subagent threads will also be deleted."
)?;
write!(stderr, "Continue? [y/N]: ")?;
stderr.flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let answer = input.trim();
Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes"))
}
async fn start_app_server_for_archive_command(
options: SessionArchiveCommandOptions,
) -> Result<AppServerSession> {
let SessionArchiveCommandOptions {
cli,
arg0_paths,
explicit_remote_endpoint,
} = options;
let loader_overrides = LoaderOverrides::default();
let strict_config = cli.strict_config;
let raw_overrides = cli.config_overrides.raw_overrides.clone();
let overrides_cli = CliConfigOverrides { raw_overrides };
let cli_kv_overrides = overrides_cli
.parse_overrides()
.map_err(|err| eyre!("failed to parse -c overrides: {err}"))?;
let codex_home = find_codex_home().wrap_err("failed to find Codex home")?;
let mut launch_loader_overrides = loader_overrides.clone();
if let Some(profile_v2) = cli.config_profile_v2.as_ref() {
launch_loader_overrides.user_config_path = Some(resolve_profile_v2_config_path(
codex_home.as_path(),
profile_v2,
));
launch_loader_overrides.user_config_profile = Some(profile_v2.clone());
}
let reuse_implicit_local_daemon = super::can_reuse_implicit_local_daemon(
&cli_kv_overrides,
&launch_loader_overrides,
strict_config,
cli.bypass_hook_trust,
);
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 {
None
};
let app_server_target = super::app_server_target_for_launch(
explicit_remote_endpoint,
default_daemon,
reuse_implicit_local_daemon,
);
let remote_cwd_override = cli
.cwd
.clone()
.filter(|_| app_server_target.uses_remote_workspace());
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
arg0_paths.codex_self_exe.clone(),
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.wrap_err("failed to resolve local runtime paths")?;
let prepared_environment_manager = EnvironmentManager::prepare_from_env()
.await
.wrap_err("failed to discover execution environments")?;
let config_cwd = super::config_cwd_for_app_server_target(
cli.cwd.as_deref(),
&app_server_target,
prepared_environment_manager.default_environment_is_remote(),
)
.wrap_err("failed to resolve config cwd")?;
let mut loader_overrides = loader_overrides;
if let Some(profile_v2) = cli.config_profile_v2.as_ref() {
loader_overrides.user_config_path = Some(resolve_profile_v2_config_path(
codex_home.as_path(),
profile_v2,
));
loader_overrides.user_config_profile = Some(profile_v2.clone());
}
let bootstrap_config = load_config_toml_with_layer_stack(
codex_home.as_path(),
config_cwd.as_ref(),
cli_kv_overrides.clone(),
ConfigLoadOptions {
loader_overrides: loader_overrides.clone(),
strict_config,
cloud_config_bundle: CloudConfigBundleLoader::default(),
},
)
.await
.wrap_err("failed to load config.toml")?;
let config_toml = &bootstrap_config.config_toml;
let chatgpt_base_url = config_toml
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
let http_client_factory = resolve_bootstrap_http_client_factory(
config_toml,
bootstrap_config
.config_layer_stack
.requirements()
.feature_requirements
.as_ref(),
)?;
let environment_manager = Arc::new(
prepared_environment_manager
.build(Some(local_runtime_paths), http_client_factory.clone())
.wrap_err("failed to initialize environment manager")?,
);
let auth_route_config = AuthRouteConfig::from_http_client_factory(http_client_factory);
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
codex_home.to_path_buf(),
/*enable_codex_api_key_env*/ false,
config_toml.cli_auth_credentials_store.unwrap_or_default(),
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
chatgpt_base_url,
auth_route_config,
)
.await;
let model_provider = if cli.oss {
resolve_oss_provider(cli.oss_provider.as_deref(), config_toml)
} else {
None
};
let model = cli.model.clone().or_else(|| {
model_provider
.as_deref()
.and_then(get_default_model_for_oss_provider)
.map(ToOwned::to_owned)
});
let cwd = cli.cwd.clone();
let config = ConfigBuilder::default()
.cli_overrides(cli_kv_overrides.clone())
.harness_overrides(ConfigOverrides {
model,
cwd: if app_server_target.uses_remote_workspace() {
None
} else {
cwd
},
model_provider,
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
show_raw_agent_reasoning: cli.oss.then_some(true),
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
..Default::default()
})
.loader_overrides(loader_overrides.clone())
.strict_config(strict_config)
.cloud_config_bundle(cloud_config_bundle.clone())
.build()
.await
.wrap_err("failed to load configuration")?;
let state_db = super::init_state_db_for_app_server_target(&config, &app_server_target)
.await
.wrap_err("failed to initialize state database")?;
let app_server = super::start_app_server(
&app_server_target,
arg0_paths,
config,
cli_kv_overrides,
loader_overrides,
strict_config,
cloud_config_bundle,
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
state_db,
environment_manager,
)
.await?;
Ok(
AppServerSession::new(app_server, app_server_target.thread_params_mode())
.with_remote_cwd_override(remote_cwd_override),
)
}