Skip to main content

codex_app_server/
lib.rs

1#![recursion_limit = "256"]
2#![deny(clippy::print_stdout, clippy::print_stderr)]
3
4use codex_arg0::Arg0DispatchPaths;
5use codex_config::ConfigLayerStackOrdering;
6use codex_config::LoaderOverrides;
7use codex_config::NoopThreadConfigLoader;
8use codex_config::RemoteThreadConfigLoader;
9use codex_config::ThreadConfigLoader;
10use codex_core::config::Config;
11use codex_core::resolve_installation_id;
12use codex_login::AuthManager;
13#[cfg(debug_assertions)]
14use codex_utils_absolute_path::AbsolutePathBuf;
15use codex_utils_cli::CliConfigOverrides;
16use std::collections::HashMap;
17use std::collections::HashSet;
18use std::io::ErrorKind;
19use std::io::Result as IoResult;
20use std::path::Path;
21use std::sync::Arc;
22use std::sync::RwLock;
23use std::sync::atomic::AtomicBool;
24
25use crate::analytics_utils::analytics_events_client_from_config;
26use crate::config_manager::ConfigManager;
27use crate::connection_cleanup::ConnectionCleanupTasks;
28use crate::message_processor::MessageProcessor;
29use crate::message_processor::MessageProcessorArgs;
30use crate::outgoing_message::ConnectionId;
31use crate::outgoing_message::OutgoingEnvelope;
32use crate::outgoing_message::OutgoingMessageSender;
33use crate::outgoing_message::QueuedOutgoingMessage;
34use crate::transport::CHANNEL_CAPACITY;
35use crate::transport::ConnectionState;
36use crate::transport::OutboundConnectionState;
37use crate::transport::RemoteControlPolicy;
38use crate::transport::RemoteControlStartConfig;
39use crate::transport::TransportEvent;
40use crate::transport::acquire_app_server_startup_lock;
41use crate::transport::app_server_startup_lock_path;
42use crate::transport::auth::policy_from_settings;
43use crate::transport::prepare_control_socket_path;
44use crate::transport::route_outgoing_envelope;
45use crate::transport::start_control_socket_acceptor;
46use crate::transport::start_remote_control;
47use crate::transport::start_stdio_connection;
48use crate::transport::start_websocket_acceptor;
49use codex_analytics::AppServerRpcTransport;
50use codex_app_server_protocol::ConfigWarningNotification;
51use codex_app_server_protocol::JSONRPCMessage;
52use codex_app_server_protocol::ServerNotification;
53use codex_app_server_protocol::TextPosition as AppTextPosition;
54use codex_app_server_protocol::TextRange as AppTextRange;
55use codex_config::ConfigLayerSource;
56use codex_config::ConfigLoadError;
57use codex_config::TextRange as CoreTextRange;
58use codex_core::ExecPolicyError;
59use codex_core::check_execpolicy_for_warnings;
60use codex_core::config::find_codex_home;
61use codex_exec_server::EnvironmentManager;
62use codex_exec_server::ExecServerRuntimePaths;
63use codex_feedback::CodexFeedback;
64use codex_protocol::protocol::SessionSource;
65use codex_rollout::state_db as rollout_state_db;
66use codex_state::log_db;
67use tokio::sync::mpsc;
68use tokio::sync::oneshot;
69use tokio::task::JoinHandle;
70use tokio_util::sync::CancellationToken;
71use tracing::error;
72use tracing::info;
73use tracing::warn;
74use tracing_subscriber::EnvFilter;
75use tracing_subscriber::Layer;
76use tracing_subscriber::layer::SubscriberExt;
77use tracing_subscriber::registry::Registry;
78use tracing_subscriber::util::SubscriberInitExt;
79
80const SQLITE_RECOVERY_CONFIG_WARNING_SUMMARY: &str = "Codex rebuilt its local database.";
81
82mod analytics_utils;
83mod app_info;
84mod app_server_tracing;
85mod attestation;
86mod auth_mode;
87mod bespoke_event_handling;
88mod command_exec;
89mod config_layer;
90mod config_manager;
91mod config_manager_service;
92mod connection_cleanup;
93mod connection_rpc_gate;
94mod current_time;
95mod dynamic_tools;
96mod effective_plugin_change;
97mod error_code;
98mod extensions;
99mod external_agent_migration;
100mod external_auth;
101mod filters;
102mod fs_watch;
103mod fuzzy_file_search;
104mod image_url;
105pub mod in_process;
106mod mcp_refresh;
107mod message_processor;
108mod models;
109mod models_refresh_worker;
110mod outgoing_message;
111mod request_processors;
112mod request_serialization;
113mod server_request_error;
114mod skills_watcher;
115mod thread_state;
116mod thread_status;
117mod transport;
118
119pub use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE;
120pub use crate::error_code::INVALID_PARAMS_ERROR_CODE;
121pub use crate::transport::AppServerTransport;
122pub use crate::transport::RemoteControlStartupMode;
123pub use crate::transport::app_server_control_socket_path;
124pub use crate::transport::auth::AppServerWebsocketAuthArgs;
125pub use crate::transport::auth::AppServerWebsocketAuthSettings;
126pub use crate::transport::auth::WebsocketAuthCliMode;
127pub use crate::transport::take_remote_control_disabled_env;
128
129const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT";
130const OTEL_SERVICE_NAME: &str = "codex-app-server";
131#[cfg(debug_assertions)]
132const TEST_USER_CONFIG_FILE_ENV_VAR: &str = "CODEX_APP_SERVER_TEST_USER_CONFIG_FILE";
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135enum LogFormat {
136    Default,
137    Json,
138}
139
140type StderrLogLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
141
142fn configured_thread_config_loader(config: &Config) -> Arc<dyn ThreadConfigLoader> {
143    match config.experimental_thread_config_endpoint.as_deref() {
144        Some(endpoint) => Arc::new(RemoteThreadConfigLoader::new(endpoint)),
145        None => Arc::new(NoopThreadConfigLoader),
146    }
147}
148
149/// Control-plane messages from the processor/transport side to the outbound router task.
150///
151/// `run_main_with_transport_options` uses two loops/tasks:
152/// - processor loop: handles incoming JSON-RPC and request dispatch
153/// - outbound loop: performs potentially slow writes to per-connection writers
154///
155/// `OutboundControlEvent` keeps those loops coordinated without sharing mutable
156/// connection state directly. In particular, the outbound loop needs to know
157/// when a connection opens/closes so it can route messages correctly.
158enum OutboundControlEvent {
159    /// Register a new writer for an opened connection.
160    Opened {
161        connection_id: ConnectionId,
162        writer: mpsc::Sender<QueuedOutgoingMessage>,
163        disconnect_sender: Option<CancellationToken>,
164        initialized: Arc<AtomicBool>,
165        experimental_api_enabled: Arc<AtomicBool>,
166        opted_out_notification_methods: Arc<RwLock<HashSet<String>>>,
167    },
168    /// Remove state for a closed/disconnected connection.
169    Closed { connection_id: ConnectionId },
170    /// Disconnect all connection-oriented clients during graceful restart.
171    DisconnectAll,
172}
173
174#[derive(Default)]
175struct ShutdownState {
176    requested: bool,
177    forced: bool,
178    last_logged_running_turn_count: Option<usize>,
179}
180
181enum ShutdownAction {
182    Noop,
183    Finish,
184}
185
186#[derive(Clone, Copy)]
187enum ShutdownSignal {
188    Forceable,
189    #[cfg(unix)]
190    GracefulOnly,
191}
192
193async fn shutdown_signal() -> IoResult<ShutdownSignal> {
194    #[cfg(unix)]
195    {
196        use tokio::signal::unix::SignalKind;
197        use tokio::signal::unix::signal;
198
199        let mut term = signal(SignalKind::terminate())?;
200        let mut hangup = signal(SignalKind::hangup())?;
201        tokio::select! {
202            ctrl_c_result = tokio::signal::ctrl_c() => ctrl_c_result.map(|_| ShutdownSignal::Forceable),
203            _ = term.recv() => Ok(ShutdownSignal::Forceable),
204            _ = hangup.recv() => Ok(ShutdownSignal::GracefulOnly),
205        }
206    }
207
208    #[cfg(not(unix))]
209    {
210        tokio::signal::ctrl_c()
211            .await
212            .map(|_| ShutdownSignal::Forceable)
213    }
214}
215
216impl ShutdownState {
217    fn requested(&self) -> bool {
218        self.requested
219    }
220
221    fn forced(&self) -> bool {
222        self.forced
223    }
224
225    fn on_signal(
226        &mut self,
227        signal: ShutdownSignal,
228        connection_count: usize,
229        running_turn_count: usize,
230    ) {
231        if self.requested {
232            if matches!(signal, ShutdownSignal::Forceable) {
233                self.forced = true;
234            }
235            return;
236        }
237
238        self.requested = true;
239        self.last_logged_running_turn_count = None;
240        info!(
241            "received shutdown signal; entering graceful restart drain (connections={}, runningAssistantTurns={}, requests still accepted until no assistant turns are running)",
242            connection_count, running_turn_count,
243        );
244    }
245
246    fn update(&mut self, running_turn_count: usize, connection_count: usize) -> ShutdownAction {
247        if !self.requested {
248            return ShutdownAction::Noop;
249        }
250
251        if self.forced || running_turn_count == 0 {
252            if self.forced {
253                info!(
254                    "received second shutdown signal; forcing restart with {running_turn_count} running assistant turn(s) and {connection_count} connection(s)"
255                );
256            } else {
257                info!(
258                    "shutdown signal restart: no assistant turns running; stopping acceptor and disconnecting {connection_count} connection(s)"
259                );
260            }
261            return ShutdownAction::Finish;
262        }
263
264        if self.last_logged_running_turn_count != Some(running_turn_count) {
265            info!(
266                "shutdown signal restart: waiting for {running_turn_count} running assistant turn(s) to finish"
267            );
268            self.last_logged_running_turn_count = Some(running_turn_count);
269        }
270
271        ShutdownAction::Noop
272    }
273}
274
275fn config_warning_from_error(
276    summary: impl Into<String>,
277    err: &std::io::Error,
278) -> ConfigWarningNotification {
279    let (path, range) = match config_error_location(err) {
280        Some((path, range)) => (Some(path), Some(range)),
281        None => (None, None),
282    };
283    ConfigWarningNotification {
284        summary: summary.into(),
285        details: Some(err.to_string()),
286        path,
287        range,
288    }
289}
290
291fn config_error_location(err: &std::io::Error) -> Option<(String, AppTextRange)> {
292    err.get_ref()
293        .and_then(|err| err.downcast_ref::<ConfigLoadError>())
294        .map(|err| {
295            let config_error = err.config_error();
296            (
297                config_error.path.to_string_lossy().to_string(),
298                app_text_range(&config_error.range),
299            )
300        })
301}
302
303fn exec_policy_warning_location(err: &ExecPolicyError) -> (Option<String>, Option<AppTextRange>) {
304    match err {
305        ExecPolicyError::ParsePolicy { path, source } => {
306            if let Some(location) = source.location() {
307                let range = AppTextRange {
308                    start: AppTextPosition {
309                        line: location.range.start.line,
310                        column: location.range.start.column,
311                    },
312                    end: AppTextPosition {
313                        line: location.range.end.line,
314                        column: location.range.end.column,
315                    },
316                };
317                return (Some(location.path), Some(range));
318            }
319            (Some(path.clone()), None)
320        }
321        _ => (None, None),
322    }
323}
324
325fn exec_policy_config_warning(err: &ExecPolicyError) -> ConfigWarningNotification {
326    let (path, range) = exec_policy_warning_location(err);
327    ConfigWarningNotification {
328        summary: "Error parsing rules; custom rules not applied.".to_string(),
329        details: Some(err.to_string()),
330        path,
331        range,
332    }
333}
334
335fn app_text_range(range: &CoreTextRange) -> AppTextRange {
336    AppTextRange {
337        start: AppTextPosition {
338            line: range.start.line,
339            column: range.start.column,
340        },
341        end: AppTextPosition {
342            line: range.end.line,
343            column: range.end.column,
344        },
345    }
346}
347
348fn project_config_warning(config: &Config) -> Option<ConfigWarningNotification> {
349    let mut disabled_folders = Vec::new();
350
351    for layer in config.config_layer_stack.get_layers(
352        ConfigLayerStackOrdering::LowestPrecedenceFirst,
353        /*include_disabled*/ true,
354    ) {
355        let ConfigLayerSource::Project { dot_codex_folder } = &layer.name else {
356            continue;
357        };
358        let Some(disabled_reason) = &layer.disabled_reason else {
359            continue;
360        };
361        disabled_folders.push((
362            dot_codex_folder.as_path().display().to_string(),
363            disabled_reason.clone(),
364        ));
365    }
366
367    if disabled_folders.is_empty() {
368        return None;
369    }
370
371    let mut message = concat!(
372        "Project-local config, hooks, and exec policies are disabled in the following folders ",
373        "until the project is trusted, but skills still load.\n",
374    )
375    .to_string();
376    for (index, (folder, reason)) in disabled_folders.iter().enumerate() {
377        let display_index = index + 1;
378        message.push_str(&format!("    {display_index}. {folder}\n"));
379        message.push_str(&format!("       {reason}\n"));
380    }
381
382    Some(ConfigWarningNotification {
383        summary: message,
384        details: None,
385        path: None,
386        range: None,
387    })
388}
389
390impl LogFormat {
391    fn from_env_value(value: Option<&str>) -> Self {
392        match value.map(str::trim).map(str::to_ascii_lowercase) {
393            Some(value) if value == "json" => Self::Json,
394            _ => Self::Default,
395        }
396    }
397}
398
399fn log_format_from_env() -> LogFormat {
400    let value = std::env::var(LOG_FORMAT_ENV_VAR).ok();
401    LogFormat::from_env_value(value.as_deref())
402}
403
404pub async fn run_main(
405    arg0_paths: Arg0DispatchPaths,
406    cli_config_overrides: CliConfigOverrides,
407    loader_overrides: LoaderOverrides,
408    strict_config: bool,
409    default_analytics_enabled: bool,
410) -> IoResult<()> {
411    run_main_with_transport_options(
412        arg0_paths,
413        cli_config_overrides,
414        loader_overrides,
415        strict_config,
416        default_analytics_enabled,
417        AppServerTransport::Stdio,
418        SessionSource::VSCode,
419        AppServerWebsocketAuthSettings::default(),
420        AppServerRuntimeOptions::default(),
421    )
422    .await
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426pub enum PluginStartupTasks {
427    Start,
428    Skip,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub struct AppServerRuntimeOptions {
433    pub plugin_startup_tasks: PluginStartupTasks,
434    pub remote_control_startup_mode: RemoteControlStartupMode,
435    pub install_shutdown_signal_handler: bool,
436}
437
438impl Default for AppServerRuntimeOptions {
439    fn default() -> Self {
440        Self {
441            plugin_startup_tasks: PluginStartupTasks::Start,
442            remote_control_startup_mode: RemoteControlStartupMode::ResolvePersisted,
443            install_shutdown_signal_handler: true,
444        }
445    }
446}
447
448#[allow(clippy::too_many_arguments)]
449pub async fn run_main_with_transport_options(
450    arg0_paths: Arg0DispatchPaths,
451    cli_config_overrides: CliConfigOverrides,
452    loader_overrides: LoaderOverrides,
453    strict_config: bool,
454    default_analytics_enabled: bool,
455    transport: AppServerTransport,
456    session_source: SessionSource,
457    auth: AppServerWebsocketAuthSettings,
458    runtime_options: AppServerRuntimeOptions,
459) -> IoResult<()> {
460    let loader_overrides = loader_overrides_with_test_user_config_file(
461        loader_overrides,
462        test_user_config_file_from_env(),
463    )?;
464    let (transport_event_tx, mut transport_event_rx) =
465        mpsc::channel::<TransportEvent>(CHANNEL_CAPACITY);
466    let (outgoing_tx, mut outgoing_rx) = mpsc::channel::<OutgoingEnvelope>(CHANNEL_CAPACITY);
467    let (outbound_control_tx, mut outbound_control_rx) =
468        mpsc::channel::<OutboundControlEvent>(CHANNEL_CAPACITY);
469
470    // Parse CLI overrides once and derive the base Config eagerly so later
471    // components do not need to work with raw TOML values.
472    let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
473        std::io::Error::new(
474            ErrorKind::InvalidInput,
475            format!("error parsing -c overrides: {e}"),
476        )
477    })?;
478    let codex_home = find_codex_home()?;
479    let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
480        arg0_paths.codex_self_exe.clone(),
481        arg0_paths.codex_linux_sandbox_exe.clone(),
482    )?;
483    let environment_manager = if loader_overrides.ignore_user_config {
484        EnvironmentManager::from_env(Some(local_runtime_paths)).await
485    } else {
486        EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await
487    }
488    .map(Arc::new)
489    .map_err(std::io::Error::other)?;
490    let config_manager = ConfigManager::new(
491        codex_home.to_path_buf(),
492        cli_kv_overrides.clone(),
493        loader_overrides,
494        strict_config,
495        Default::default(),
496        arg0_paths.clone(),
497        Arc::new(NoopThreadConfigLoader),
498    );
499    match config_manager
500        .load_latest_config(/*fallback_cwd*/ None)
501        .await
502    {
503        Ok(config) => {
504            let discovered_thread_config_loader = configured_thread_config_loader(&config);
505            config_manager
506                .replace_thread_config_loader(Arc::clone(&discovered_thread_config_loader));
507            let auth_manager =
508                AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
509            config_manager.replace_cloud_config_bundle_loader(
510                auth_manager,
511                config.chatgpt_base_url.clone(),
512                config.http_client_factory(),
513            );
514        }
515        Err(err) => {
516            warn!(error = %err, "Failed to preload config for cloud config bundle");
517            // TODO: Decide whether bootstrap config preload failures should block startup.
518            // If this fails, we cannot install cloud/thread config loaders, so non-strict
519            // startup may continue without managed cloud config.
520        }
521    };
522    let mut config_warnings = Vec::new();
523    let config = match config_manager
524        .load_latest_config(/*fallback_cwd*/ None)
525        .await
526    {
527        Ok(config) => config,
528        Err(err) => {
529            if strict_config {
530                return Err(err);
531            }
532
533            let message = config_warning_from_error("Invalid configuration; using defaults.", &err);
534            config_warnings.push(message);
535            config_manager.load_default_config().await.map_err(|e| {
536                std::io::Error::new(
537                    ErrorKind::InvalidData,
538                    format!("error loading default config after config error: {e}"),
539                )
540            })?
541        }
542    };
543
544    let otel = codex_core::otel_init::build_provider(
545        &config,
546        env!("CARGO_PKG_VERSION"),
547        Some(OTEL_SERVICE_NAME),
548        default_analytics_enabled,
549    )
550    .map_err(|e| {
551        std::io::Error::new(
552            ErrorKind::InvalidData,
553            format!("error loading otel config: {e}"),
554        )
555    })?;
556    codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME);
557    codex_core::otel_init::install_sqlite_telemetry(otel.as_ref(), OTEL_SERVICE_NAME);
558    let unix_socket_startup_lock = match &transport {
559        AppServerTransport::UnixSocket { socket_path } => {
560            let startup_lock_path = app_server_startup_lock_path(&codex_home)?;
561            let startup_lock = acquire_app_server_startup_lock(startup_lock_path).await?;
562            prepare_control_socket_path(socket_path.as_path()).await?;
563            Some(startup_lock)
564        }
565        _ => None,
566    };
567    let state_db_init = match init_sqlite_state_db_with_fresh_start_on_corruption(&config).await {
568        Ok(state_db_init) => state_db_init,
569        Err(err) => {
570            return Err(std::io::Error::other(format!(
571                "failed to initialize sqlite state runtime under {}: {err}",
572                config.sqlite_home.display()
573            )));
574        }
575    };
576    let state_db = state_db_init.state_db;
577    if let Some(recovery_notice) = state_db_init.recovery_notice {
578        config_warnings.push(ConfigWarningNotification {
579            summary: SQLITE_RECOVERY_CONFIG_WARNING_SUMMARY.to_string(),
580            details: Some(recovery_notice.details),
581            path: None,
582            range: None,
583        });
584    }
585
586    if let Ok(Some(err)) = check_execpolicy_for_warnings(&config.config_layer_stack).await {
587        config_warnings.push(exec_policy_config_warning(&err));
588    }
589
590    if let Some(warning) = project_config_warning(&config) {
591        config_warnings.push(warning);
592    }
593    for warning in &config.startup_warnings {
594        config_warnings.push(ConfigWarningNotification {
595            summary: warning.clone(),
596            details: None,
597            path: None,
598            range: None,
599        });
600    }
601    if let Some(warning) =
602        codex_core::config::system_bwrap_warning(config.permissions.permission_profile())
603    {
604        config_warnings.push(ConfigWarningNotification {
605            summary: warning,
606            details: None,
607            path: None,
608            range: None,
609        });
610    }
611
612    let feedback = CodexFeedback::new();
613
614    // Install a simple subscriber so `tracing` output is visible. Users can
615    // control the log level with `RUST_LOG` and switch to JSON logs with
616    // `LOG_FORMAT=json`.
617    let stderr_fmt: StderrLogLayer = match log_format_from_env() {
618        LogFormat::Json => tracing_subscriber::fmt::layer()
619            .json()
620            .with_writer(std::io::stderr)
621            .with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
622            .with_filter(EnvFilter::from_default_env())
623            .boxed(),
624        LogFormat::Default => tracing_subscriber::fmt::layer()
625            .with_writer(std::io::stderr)
626            .with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
627            .with_filter(EnvFilter::from_default_env())
628            .boxed(),
629    };
630
631    let feedback_layer = feedback.logger_layer();
632    let feedback_metadata_layer = feedback.metadata_layer();
633    let log_db = state_db.clone().map(log_db::start);
634    let log_db_layer = log_db
635        .clone()
636        .map(|layer| layer.with_filter(log_db::default_filter()));
637    let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());
638    let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
639    let _ = tracing_subscriber::registry()
640        .with(stderr_fmt)
641        .with(feedback_layer)
642        .with(feedback_metadata_layer)
643        .with(log_db_layer)
644        .with(otel_logger_layer)
645        .with(otel_tracing_layer)
646        .try_init();
647    for warning in &config_warnings {
648        match &warning.details {
649            Some(details) => error!("{} {}", warning.summary, details),
650            None => error!("{}", warning.summary),
651        }
652    }
653    let remote_control_policy = if config
654        .config_layer_stack
655        .requirements()
656        .allow_remote_control
657        .as_ref()
658        .is_some_and(|requirement| !requirement.value)
659    {
660        RemoteControlPolicy::DisabledByRequirements
661    } else {
662        RemoteControlPolicy::Allowed
663    };
664    let remote_control_startup_mode = runtime_options.remote_control_startup_mode;
665    let remote_control_explicitly_requested =
666        remote_control_startup_mode == RemoteControlStartupMode::EnabledEphemeral;
667    if remote_control_explicitly_requested
668        && remote_control_policy == RemoteControlPolicy::DisabledByRequirements
669    {
670        return Err(std::io::Error::new(
671            ErrorKind::InvalidInput,
672            "remote control is disabled by managed requirements",
673        ));
674    }
675    let installation_id = resolve_installation_id(&config.codex_home).await?;
676    let transport_shutdown_token = CancellationToken::new();
677    let mut transport_accept_handles = Vec::<JoinHandle<()>>::new();
678
679    let single_client_mode = matches!(&transport, AppServerTransport::Stdio);
680    let shutdown_when_no_connections = single_client_mode;
681    let graceful_signal_restart_enabled =
682        runtime_options.install_shutdown_signal_handler && !single_client_mode;
683    let mut app_server_client_name_rx = None;
684
685    match &transport {
686        AppServerTransport::Stdio => {
687            let (stdio_client_name_tx, stdio_client_name_rx) = oneshot::channel::<String>();
688            app_server_client_name_rx = Some(stdio_client_name_rx);
689            start_stdio_connection(
690                transport_event_tx.clone(),
691                &mut transport_accept_handles,
692                stdio_client_name_tx,
693            )
694            .await?;
695        }
696        AppServerTransport::UnixSocket { socket_path } => {
697            let accept_handle = start_control_socket_acceptor(
698                socket_path.clone(),
699                transport_event_tx.clone(),
700                transport_shutdown_token.clone(),
701            )
702            .await?;
703            transport_accept_handles.push(accept_handle);
704        }
705        AppServerTransport::WebSocket { bind_address } => {
706            let accept_handle = start_websocket_acceptor(
707                *bind_address,
708                transport_event_tx.clone(),
709                transport_shutdown_token.clone(),
710                policy_from_settings(&auth)?,
711            )
712            .await?;
713            transport_accept_handles.push(accept_handle);
714        }
715        AppServerTransport::Off => {}
716    }
717    drop(unix_socket_startup_lock);
718
719    let auth_manager =
720        AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
721
722    let remote_control_enabled = remote_control_policy == RemoteControlPolicy::Allowed
723        && remote_control_explicitly_requested
724        && state_db.is_some();
725    if remote_control_explicitly_requested && state_db.is_none() {
726        error!("remote control disabled because sqlite state db is unavailable");
727    }
728    let no_local_transport = transport_accept_handles.is_empty();
729    if no_local_transport
730        && remote_control_startup_mode != RemoteControlStartupMode::ResolvePersisted
731        && !remote_control_enabled
732    {
733        return Err(std::io::Error::new(
734            ErrorKind::InvalidInput,
735            if remote_control_policy == RemoteControlPolicy::DisabledByRequirements {
736                "no transport configured; remote control disabled by managed requirements"
737            } else if remote_control_explicitly_requested && state_db.is_none() {
738                "no transport configured; remote control disabled because sqlite state db is unavailable"
739            } else {
740                "no transport configured; use --listen or enable remote control"
741            },
742        ));
743    }
744
745    let (remote_control_accept_handle, remote_control_handle) = start_remote_control(
746        RemoteControlStartConfig {
747            remote_control_url: config.chatgpt_base_url.clone(),
748            installation_id: installation_id.clone(),
749            policy: remote_control_policy,
750        },
751        state_db.clone(),
752        auth_manager.clone(),
753        transport_event_tx.clone(),
754        transport_shutdown_token.clone(),
755        app_server_client_name_rx,
756        remote_control_startup_mode,
757    )
758    .await?;
759    if no_local_transport
760        && remote_control_startup_mode == RemoteControlStartupMode::ResolvePersisted
761    {
762        let persisted_enabled = match remote_control_handle
763            .resolve_persisted_preference(/*app_server_client_name*/ None)
764            .await
765        {
766            Ok(persisted_enabled) => persisted_enabled,
767            Err(err) => {
768                warn!("failed to resolve persisted remote control preference: {err}");
769                false
770            }
771        };
772        if !persisted_enabled {
773            transport_shutdown_token.cancel();
774            let _ = remote_control_accept_handle.await;
775            return Err(std::io::Error::new(
776                ErrorKind::InvalidInput,
777                if remote_control_policy == RemoteControlPolicy::DisabledByRequirements {
778                    "no transport configured; remote control disabled by managed requirements"
779                } else {
780                    "no transport configured; use --listen or enable remote control"
781                },
782            ));
783        }
784    }
785    transport_accept_handles.push(remote_control_accept_handle);
786
787    let outbound_handle = tokio::spawn(async move {
788        let mut outbound_connections = HashMap::<ConnectionId, OutboundConnectionState>::new();
789        loop {
790            tokio::select! {
791                    biased;
792                    event = outbound_control_rx.recv() => {
793                        let Some(event) = event else {
794                            break;
795                        };
796                        match event {
797                            OutboundControlEvent::Opened {
798                                connection_id,
799                                writer,
800                                disconnect_sender,
801                                initialized,
802                                experimental_api_enabled,
803                                opted_out_notification_methods,
804                            } => {
805                                outbound_connections.insert(
806                                    connection_id,
807                                    OutboundConnectionState::new(
808                                        writer,
809                                        initialized,
810                                        experimental_api_enabled,
811                                        opted_out_notification_methods,
812                                        disconnect_sender,
813                                    ),
814                                );
815                            }
816                            OutboundControlEvent::Closed { connection_id } => {
817                                outbound_connections.remove(&connection_id);
818                            }
819                            OutboundControlEvent::DisconnectAll => {
820                                info!(
821                                    "disconnecting {} outbound websocket connection(s) for graceful restart",
822                                    outbound_connections.len()
823                                );
824                                for connection_state in outbound_connections.values() {
825                                    connection_state.request_disconnect();
826                                }
827                                outbound_connections.clear();
828                            }
829                        }
830                    }
831                    envelope = outgoing_rx.recv() => {
832                    let Some(envelope) = envelope else {
833                        break;
834                    };
835                    route_outgoing_envelope(&mut outbound_connections, envelope).await;
836                }
837            }
838        }
839        info!("outbound router task exited (channel closed)");
840    });
841
842    let processor_handle = tokio::spawn({
843        let auth_manager = Arc::clone(&auth_manager);
844        let analytics_events_client =
845            analytics_events_client_from_config(Arc::clone(&auth_manager), &config);
846        let outgoing_message_sender = Arc::new(OutgoingMessageSender::new(
847            outgoing_tx,
848            analytics_events_client.clone(),
849        ));
850        let initialize_notification_sender = outgoing_message_sender.clone();
851        let outbound_control_tx = outbound_control_tx;
852        let processor = Arc::new(MessageProcessor::new(MessageProcessorArgs {
853            outgoing: outgoing_message_sender,
854            analytics_events_client,
855            arg0_paths,
856            config: Arc::new(config),
857            config_manager,
858            environment_manager,
859            feedback: feedback.clone(),
860            log_db,
861            state_db: state_db.clone(),
862            config_warnings,
863            session_source,
864            auth_manager,
865            installation_id,
866            rpc_transport: analytics_rpc_transport(&transport),
867            remote_control_handle: Some(remote_control_handle.clone()),
868            plugin_startup_tasks: runtime_options.plugin_startup_tasks,
869        }));
870        let mut thread_created_rx = processor.thread_created_receiver();
871        let mut running_turn_count_rx = processor.subscribe_running_assistant_turn_count();
872        let mut connections = HashMap::<ConnectionId, ConnectionState>::new();
873        let mut connection_cleanup_tasks = ConnectionCleanupTasks::new();
874        let mut remote_control_status_rx = remote_control_handle.status_receiver();
875        let mut remote_control_status = remote_control_status_rx.borrow().clone();
876        let transport_shutdown_token = transport_shutdown_token.clone();
877        async move {
878            let mut listen_for_threads = true;
879            let mut shutdown_state = ShutdownState::default();
880            let exit_reason = loop {
881                let running_turn_count = {
882                    let running_turn_count = running_turn_count_rx.borrow();
883                    *running_turn_count
884                };
885                if matches!(
886                    shutdown_state.update(running_turn_count, connections.len()),
887                    ShutdownAction::Finish
888                ) {
889                    transport_shutdown_token.cancel();
890                    let _ = outbound_control_tx
891                        .send(OutboundControlEvent::DisconnectAll)
892                        .await;
893                    break "shutdown_requested";
894                }
895
896                tokio::select! {
897                    shutdown_signal_result = shutdown_signal(), if graceful_signal_restart_enabled && !shutdown_state.forced() => {
898                        let signal = match shutdown_signal_result {
899                            Ok(signal) => signal,
900                            Err(err) => {
901                                warn!("failed to listen for shutdown signal during graceful restart drain: {err}");
902                                continue;
903                            }
904                        };
905                        let running_turn_count = *running_turn_count_rx.borrow();
906                        shutdown_state.on_signal(signal, connections.len(), running_turn_count);
907                    }
908                    changed = running_turn_count_rx.changed(), if graceful_signal_restart_enabled && shutdown_state.requested() => {
909                        if changed.is_err() {
910                            warn!("running-turn watcher closed during graceful restart drain");
911                        }
912                    }
913                    event = transport_event_rx.recv() => {
914                        let Some(event) = event else {
915                            break "transport_channel_closed";
916                        };
917                        match event {
918                            TransportEvent::ConnectionOpened {
919                                connection_id,
920                                origin,
921                                writer,
922                                disconnect_sender,
923                            } => {
924                                let outbound_initialized = Arc::new(AtomicBool::new(false));
925                                let outbound_experimental_api_enabled =
926                                    Arc::new(AtomicBool::new(false));
927                                let outbound_opted_out_notification_methods =
928                                    Arc::new(RwLock::new(HashSet::new()));
929                                if outbound_control_tx
930                                    .send(OutboundControlEvent::Opened {
931                                        connection_id,
932                                        writer,
933                                        disconnect_sender,
934                                        initialized: Arc::clone(&outbound_initialized),
935                                        experimental_api_enabled: Arc::clone(
936                                            &outbound_experimental_api_enabled,
937                                        ),
938                                        opted_out_notification_methods: Arc::clone(
939                                            &outbound_opted_out_notification_methods,
940                                        ),
941                                    })
942                                    .await
943                                    .is_err()
944                                {
945                                    break "outbound_router_closed";
946                                }
947                                connections.insert(
948                                    connection_id,
949                                    ConnectionState::new(
950                                        origin,
951                                        outbound_initialized,
952                                        outbound_experimental_api_enabled,
953                                        outbound_opted_out_notification_methods,
954                                    ),
955                                );
956                            }
957                            TransportEvent::ConnectionClosed { connection_id } => {
958                                let Some(connection_state) = connections.remove(&connection_id) else {
959                                    continue;
960                                };
961                                connection_state.session.rpc_gate.close().await;
962                                let outbound_closed = outbound_control_tx
963                                    .send(OutboundControlEvent::Closed { connection_id })
964                                    .await
965                                    .is_ok();
966                                let processor = Arc::clone(&processor);
967                                connection_cleanup_tasks.spawn(async move {
968                                    processor
969                                        .connection_closed(connection_id, &connection_state.session)
970                                        .await;
971                                });
972                                if !outbound_closed {
973                                    break "outbound_router_closed";
974                                }
975                                if shutdown_when_no_connections && connections.is_empty() {
976                                    break "last_connection_closed";
977                                }
978                            }
979                            TransportEvent::IncomingMessage { connection_id, message } => {
980                                match message {
981                                    JSONRPCMessage::Request(request) => {
982                                        let Some(connection_state) = connections.get_mut(&connection_id) else {
983                                            warn!("dropping request from unknown connection: {connection_id:?}");
984                                            continue;
985                                        };
986                                        let was_initialized =
987                                            connection_state.session.initialized();
988                                        processor
989                                            .process_request(
990                                                connection_id,
991                                                request,
992                                                &transport,
993                                                Arc::clone(&connection_state.session),
994                                            )
995                                            .await;
996                                        let opted_out_notification_methods_snapshot = connection_state
997                                            .session
998                                            .opted_out_notification_methods();
999                                        let experimental_api_enabled =
1000                                            connection_state.session.experimental_api_enabled();
1001                                        let is_initialized = connection_state.session.initialized();
1002                                        if let Ok(mut opted_out_notification_methods) = connection_state
1003                                            .outbound_opted_out_notification_methods
1004                                            .write()
1005                                        {
1006                                            *opted_out_notification_methods =
1007                                                opted_out_notification_methods_snapshot;
1008                                        } else {
1009                                            warn!(
1010                                                "failed to update outbound opted-out notifications"
1011                                            );
1012                                        }
1013                                        connection_state
1014                                            .outbound_experimental_api_enabled
1015                                            .store(
1016                                                experimental_api_enabled,
1017                                                std::sync::atomic::Ordering::Release,
1018                                            );
1019                                        if !was_initialized && is_initialized {
1020                                            processor
1021                                                .send_initialize_notifications_to_connection(
1022                                                    connection_id,
1023                                                )
1024                                                .await;
1025                                            initialize_notification_sender
1026                                                .send_server_notification_to_connections(
1027                                                    &[connection_id],
1028                                                    ServerNotification::RemoteControlStatusChanged(
1029                                                        remote_control_status.clone(),
1030                                                    ),
1031                                                )
1032                                                .await;
1033                                            processor
1034                                                .connection_initialized(
1035                                                    connection_id,
1036                                                    connection_state
1037                                                        .session
1038                                                        .request_attestation(),
1039                                                )
1040                                                .await;
1041                                            connection_state
1042                                                .outbound_initialized
1043                                                .store(true, std::sync::atomic::Ordering::Release);
1044                                        }
1045                                    }
1046                                    JSONRPCMessage::Response(response) => {
1047                                        if !connections.contains_key(&connection_id) {
1048                                            warn!("dropping response from unknown connection: {connection_id:?}");
1049                                            continue;
1050                                        }
1051                                        processor.process_response(response).await;
1052                                    }
1053                                    JSONRPCMessage::Notification(notification) => {
1054                                        if !connections.contains_key(&connection_id) {
1055                                            warn!("dropping notification from unknown connection: {connection_id:?}");
1056                                            continue;
1057                                        }
1058                                        processor.process_notification(notification).await;
1059                                    }
1060                                    JSONRPCMessage::Error(err) => {
1061                                        if !connections.contains_key(&connection_id) {
1062                                            warn!("dropping error from unknown connection: {connection_id:?}");
1063                                            continue;
1064                                        }
1065                                        processor.process_error(err).await;
1066                                    }
1067                                }
1068                            }
1069                        }
1070                    }
1071                    _ = connection_cleanup_tasks.reap_next() => {}
1072                    changed = remote_control_status_rx.changed() => {
1073                        if changed.is_err() {
1074                            continue;
1075                        }
1076                        let status = remote_control_status_rx.borrow().clone();
1077                        if remote_control_status == status {
1078                            continue;
1079                        }
1080                        remote_control_status = status.clone();
1081                        let notification = ServerNotification::RemoteControlStatusChanged(status);
1082                        initialize_notification_sender
1083                            .send_server_notification(notification)
1084                            .await;
1085                    }
1086                    created = thread_created_rx.recv(), if listen_for_threads => {
1087                        match created {
1088                            Ok(thread_id) => {
1089                                let mut initialized_connection_ids = Vec::new();
1090                                for (connection_id, connection_state) in &connections {
1091                                    if connection_state.session.initialized() {
1092                                        initialized_connection_ids.push(*connection_id);
1093                                    }
1094                                }
1095                                processor
1096                                    .try_attach_thread_listener(
1097                                        thread_id,
1098                                        initialized_connection_ids,
1099                                    )
1100                                    .await;
1101                            }
1102                            Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
1103                                // TODO(jif) handle lag.
1104                                // Assumes thread creation volume is low enough that lag never happens.
1105                                // If it does, we log and continue without resyncing to avoid attaching
1106                                // listeners for threads that should remain unsubscribed.
1107                                warn!("thread_created receiver lagged; skipping resync");
1108                            }
1109                            Err(tokio::sync::broadcast::error::RecvError::Closed) => {
1110                                listen_for_threads = false;
1111                            }
1112                        }
1113                    }
1114                }
1115            };
1116
1117            if !shutdown_state.forced() {
1118                futures::future::join_all(
1119                    connections
1120                        .values()
1121                        .map(|connection_state| connection_state.session.rpc_gate.shutdown()),
1122                )
1123                .await;
1124                connection_cleanup_tasks.drain().await;
1125                processor.drain_background_tasks().await;
1126                processor.shutdown_threads().await;
1127            } else {
1128                connection_cleanup_tasks.abort();
1129            }
1130            info!(
1131                exit_reason,
1132                remaining_connection_count = connections.len(),
1133                shutdown_forced = shutdown_state.forced(),
1134                "processor task exited"
1135            );
1136        }
1137    });
1138
1139    drop(transport_event_tx);
1140
1141    let _ = processor_handle.await;
1142    let _ = outbound_handle.await;
1143
1144    transport_shutdown_token.cancel();
1145    for handle in transport_accept_handles {
1146        let _ = handle.await;
1147    }
1148
1149    if let Some(otel) = otel {
1150        otel.shutdown();
1151    }
1152
1153    Ok(())
1154}
1155
1156struct SqliteRecoveryNotice {
1157    details: String,
1158}
1159
1160struct RecoveredSqliteDatabase {
1161    database_path: String,
1162    backup_folder: String,
1163}
1164
1165struct StateDbInitResult {
1166    state_db: Option<rollout_state_db::StateDbHandle>,
1167    recovery_notice: Option<SqliteRecoveryNotice>,
1168}
1169
1170async fn init_sqlite_state_db_with_fresh_start_on_corruption(
1171    config: &Config,
1172) -> anyhow::Result<StateDbInitResult> {
1173    let mut attempted_backups = HashSet::new();
1174    let mut recovered_databases = Vec::new();
1175    loop {
1176        let err = match rollout_state_db::try_init(config).await {
1177            Ok(state_db) => {
1178                let recovery_notice = sqlite_recovery_notice(&recovered_databases);
1179                if recovery_notice.is_some() {
1180                    emit_state_db_backup_warning(SQLITE_RECOVERY_CONFIG_WARNING_SUMMARY);
1181                    for recovered_database in &recovered_databases {
1182                        emit_state_db_backup_warning(&format!(
1183                            "Database path: {}",
1184                            recovered_database.database_path
1185                        ));
1186                        emit_state_db_backup_warning(&format!(
1187                            "Backup folder: {}",
1188                            recovered_database.backup_folder
1189                        ));
1190                    }
1191                }
1192                return Ok(StateDbInitResult {
1193                    state_db: Some(state_db),
1194                    recovery_notice,
1195                });
1196            }
1197            Err(err) => err,
1198        };
1199        let database_path = codex_state::runtime_db_path_for_corruption_error(&err)
1200            .unwrap_or_else(|| codex_state::state_db_path(config.sqlite_home.as_path()));
1201        if !codex_state::is_sqlite_corruption_error(&err)
1202            && !sqlite_home_is_blocking_file(database_path.as_path())
1203        {
1204            return Err(err);
1205        }
1206
1207        if !attempted_backups.insert(database_path.clone()) {
1208            return Err(anyhow::anyhow!(
1209                "failed to initialize sqlite state runtime after moving damaged database file into a backup folder: {err}"
1210            ));
1211        }
1212
1213        let original_error = err.to_string();
1214        emit_state_db_backup_warning(&format!(
1215            "Codex local database at {} appears damaged. Moving it into a backup folder so the app server can rebuild it from saved data.",
1216            database_path.display()
1217        ));
1218        let backups = codex_state::backup_runtime_db_for_fresh_start(database_path.as_path())
1219            .await
1220            .map_err(|backup_err| {
1221                anyhow::anyhow!(
1222                    "failed to move damaged sqlite state database files into a backup folder: {backup_err}; original error: {original_error}"
1223                )
1224            })?;
1225        for backup in &backups {
1226            emit_state_db_backup_warning(&format!(
1227                "Moved damaged Codex local database file {} to {}",
1228                backup.original_path.display(),
1229                backup.backup_path.display()
1230            ));
1231        }
1232        if let Some(first_backup) = backups.first()
1233            && let Some(backup_folder) = first_backup.backup_path.parent()
1234        {
1235            recovered_databases.push(RecoveredSqliteDatabase {
1236                database_path: first_backup.original_path.display().to_string(),
1237                backup_folder: backup_folder.display().to_string(),
1238            });
1239        }
1240    }
1241}
1242
1243fn sqlite_home_is_blocking_file(database_path: &Path) -> bool {
1244    database_path
1245        .parent()
1246        .and_then(|path| std::fs::metadata(path).ok())
1247        .is_some_and(|metadata| metadata.is_file())
1248}
1249
1250fn sqlite_recovery_notice(
1251    recovered_databases: &[RecoveredSqliteDatabase],
1252) -> Option<SqliteRecoveryNotice> {
1253    if recovered_databases.is_empty() {
1254        return None;
1255    }
1256
1257    let details = recovered_databases
1258        .iter()
1259        .map(|recovered_database| {
1260            format!(
1261                "Database path: {}\nBackup folder: {}",
1262                recovered_database.database_path, recovered_database.backup_folder
1263            )
1264        })
1265        .collect::<Vec<_>>()
1266        .join("\n\n");
1267    Some(SqliteRecoveryNotice { details })
1268}
1269
1270fn emit_state_db_backup_warning(message: &str) {
1271    warn!("{message}");
1272    if !tracing::dispatcher::has_been_set() {
1273        #[allow(clippy::print_stderr)]
1274        {
1275            eprintln!("{message}");
1276        }
1277    }
1278}
1279
1280fn test_user_config_file_from_env() -> Option<std::path::PathBuf> {
1281    #[cfg(debug_assertions)]
1282    {
1283        std::env::var_os(TEST_USER_CONFIG_FILE_ENV_VAR)
1284            .filter(|value| !value.is_empty())
1285            .map(std::path::PathBuf::from)
1286    }
1287
1288    #[cfg(not(debug_assertions))]
1289    None
1290}
1291
1292fn loader_overrides_with_test_user_config_file(
1293    mut loader_overrides: LoaderOverrides,
1294    test_user_config_file: Option<std::path::PathBuf>,
1295) -> IoResult<LoaderOverrides> {
1296    #[cfg(debug_assertions)]
1297    if let Some(path) = test_user_config_file {
1298        let path = AbsolutePathBuf::from_absolute_path(path).map_err(|err| {
1299            std::io::Error::new(
1300                ErrorKind::InvalidInput,
1301                format!("invalid test user config path: {err}"),
1302            )
1303        })?;
1304        warn!(
1305            path = %path.as_path().display(),
1306            "using debug-only app-server test user config file"
1307        );
1308        loader_overrides.user_config_path = Some(path);
1309    }
1310
1311    #[cfg(not(debug_assertions))]
1312    let _ = test_user_config_file;
1313
1314    Ok(loader_overrides)
1315}
1316
1317fn analytics_rpc_transport(transport: &AppServerTransport) -> AppServerRpcTransport {
1318    match transport {
1319        AppServerTransport::Stdio => AppServerRpcTransport::Stdio,
1320        AppServerTransport::UnixSocket { .. }
1321        | AppServerTransport::WebSocket { .. }
1322        | AppServerTransport::Off => AppServerRpcTransport::Websocket,
1323    }
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328    use super::LogFormat;
1329    #[cfg(debug_assertions)]
1330    use super::loader_overrides_with_test_user_config_file;
1331    #[cfg(debug_assertions)]
1332    use codex_config::LoaderOverrides;
1333    #[cfg(debug_assertions)]
1334    use codex_utils_absolute_path::AbsolutePathBuf;
1335    use pretty_assertions::assert_eq;
1336
1337    #[test]
1338    fn log_format_from_env_value_matches_json_values_case_insensitively() {
1339        assert_eq!(LogFormat::from_env_value(Some("json")), LogFormat::Json);
1340        assert_eq!(LogFormat::from_env_value(Some("JSON")), LogFormat::Json);
1341        assert_eq!(LogFormat::from_env_value(Some("  Json  ")), LogFormat::Json);
1342    }
1343
1344    #[test]
1345    fn log_format_from_env_value_defaults_for_non_json_values() {
1346        assert_eq!(
1347            LogFormat::from_env_value(/*value*/ None),
1348            LogFormat::Default
1349        );
1350        assert_eq!(LogFormat::from_env_value(Some("")), LogFormat::Default);
1351        assert_eq!(LogFormat::from_env_value(Some("text")), LogFormat::Default);
1352        assert_eq!(LogFormat::from_env_value(Some("jsonl")), LogFormat::Default);
1353    }
1354
1355    #[cfg(debug_assertions)]
1356    #[test]
1357    fn debug_test_user_config_file_overrides_loader_path() {
1358        let path = std::env::temp_dir().join("codex-app-server-test-config.toml");
1359        let loader_overrides = loader_overrides_with_test_user_config_file(
1360            LoaderOverrides::default(),
1361            Some(path.clone()),
1362        )
1363        .expect("test config path should be valid");
1364
1365        assert_eq!(
1366            loader_overrides.user_config_path,
1367            Some(AbsolutePathBuf::from_absolute_path(path).expect("absolute test path"))
1368        );
1369    }
1370}