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
149enum OutboundControlEvent {
159 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 Closed { connection_id: ConnectionId },
170 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 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 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(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, 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 }
521 };
522 let mut config_warnings = Vec::new();
523 let config = match config_manager
524 .load_latest_config(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 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, 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(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 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(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}