Codex Handbook
app-server/src/request_processors/apps_processor.rs 418 lines
use super::*;pub(crate) struct AppsRequestProcessor {    auth_manager: Arc<AuthManager>,    thread_manager: Arc<ThreadManager>,    outgoing: Arc<OutgoingMessageSender>,    config_manager: ConfigManager,    workspace_settings_cache: Arc<workspace_settings::WorkspaceSettingsCache>,    shutdown_token: CancellationToken,    _shutdown_drop_guard: DropGuard,}impl AppsRequestProcessor {    pub(crate) fn new(        auth_manager: Arc<AuthManager>,        thread_manager: Arc<ThreadManager>,        outgoing: Arc<OutgoingMessageSender>,        config_manager: ConfigManager,        workspace_settings_cache: Arc<workspace_settings::WorkspaceSettingsCache>,        shutdown_token: CancellationToken,    ) -> Self {        let shutdown_drop_guard = shutdown_token.clone().drop_guard();        Self {            auth_manager,            thread_manager,            outgoing,            config_manager,            workspace_settings_cache,            shutdown_token,            _shutdown_drop_guard: shutdown_drop_guard,        }    }    pub(crate) async fn apps_list(        &self,        request_id: &ConnectionRequestId,        params: AppsListParams,    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {        self.apps_list_inner(request_id, params)            .await            .map(|response| response.map(Into::into))    }    async fn apps_list_inner(        &self,        request_id: &ConnectionRequestId,        params: AppsListParams,    ) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {        let thread = if let Some(thread_id) = params.thread_id.as_deref() {            let (_, loaded_thread) = self.load_thread(thread_id).await?;            Some(loaded_thread)        } else {            None        };        let fallback_cwd = match thread.as_ref() {            Some(thread) => Some(thread.config_snapshot().await.cwd().to_path_buf()),            None => None,        };        let mut config = self.load_latest_config(fallback_cwd).await?;        if let Some(thread) = thread {            let _ = config                .features                .set_enabled(Feature::Apps, thread.enabled(Feature::Apps));        }        let auth = self.auth_manager.auth().await;        if !config            .features            .apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))        {            return Ok(Some(AppsListResponse {                data: Vec::new(),                next_cursor: None,            }));        }        if !self            .workspace_codex_plugins_enabled(&config, auth.as_ref())            .await        {            return Ok(Some(AppsListResponse {                data: Vec::new(),                next_cursor: None,            }));        }        let request = request_id.clone();        let outgoing = Arc::clone(&self.outgoing);        let environment_manager = self.thread_manager.environment_manager();        let mcp_manager = self.thread_manager.mcp_manager();        let plugins_manager = self.thread_manager.plugins_manager();        let shutdown_token = self.shutdown_token.child_token();        tokio::spawn(async move {            tokio::select! {                _ = shutdown_token.cancelled() => {}                _ = Self::apps_list_task(                    outgoing,                    request,                    params,                    config,                    environment_manager,                    mcp_manager,                    plugins_manager,                ) => {}            }        });        Ok(None)    }    pub(crate) fn shutdown(&self) {        self.shutdown_token.cancel();    }    async fn apps_list_task(        outgoing: Arc<OutgoingMessageSender>,        request_id: ConnectionRequestId,        params: AppsListParams,        config: Config,        environment_manager: Arc<EnvironmentManager>,        mcp_manager: Arc<McpManager>,        plugins_manager: Arc<PluginsManager>,    ) {        let retry_params = params.clone();        let retry_config = config.clone();        let retry_environment_manager = Arc::clone(&environment_manager);        let retry_mcp_manager = Arc::clone(&mcp_manager);        let retry_plugins_manager = Arc::clone(&plugins_manager);        let result = Self::apps_list_response(            &outgoing,            params,            config,            environment_manager,            mcp_manager,            plugins_manager,        )        .await;        let should_retry = result            .as_ref()            .is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);        outgoing            .send_result(request_id, result.map(|(response, _)| response))            .await;        if should_retry && !retry_params.force_refetch {            let mut retry_params = retry_params;            retry_params.force_refetch = true;            if let Err(err) = Self::apps_list_response(                &outgoing,                retry_params,                retry_config,                retry_environment_manager,                retry_mcp_manager,                retry_plugins_manager,            )            .await            {                warn!("failed to refresh app list after codex-apps readiness retry: {err:?}");            }        }    }    async fn apps_list_response(        outgoing: &Arc<OutgoingMessageSender>,        params: AppsListParams,        config: Config,        environment_manager: Arc<EnvironmentManager>,        mcp_manager: Arc<McpManager>,        plugins_manager: Arc<PluginsManager>,    ) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {        let AppsListParams {            cursor,            limit,            thread_id: _,            force_refetch,        } = params;        let start = match cursor {            Some(cursor) => match cursor.parse::<usize>() {                Ok(idx) => idx,                Err(_) => return Err(invalid_request(format!("invalid cursor: {cursor}"))),            },            None => 0,        };        let plugin_apps = plugins_manager            .plugins_for_config(&config.plugins_config_input())            .await            .effective_apps();        let (mut accessible_connectors, mut all_connectors) = tokio::join!(            connectors::list_cached_accessible_connectors_from_mcp_tools(&config),            connectors::list_cached_all_connectors(&config, &plugin_apps)        );        let cached_all_connectors = all_connectors.clone();        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();        let accessible_config = config.clone();        let accessible_tx = tx.clone();        tokio::spawn(async move {            let result = connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(                &accessible_config,                force_refetch,                Arc::clone(&environment_manager),                mcp_manager,            )            .await            .map_err(|err| format!("failed to load accessible apps: {err}"));            let _ = accessible_tx.send(AppListLoadResult::Accessible(result));        });        let all_config = config.clone();        let all_plugin_apps = plugin_apps.clone();        tokio::spawn(async move {            let result = connectors::list_all_connectors_with_options(                &all_config,                force_refetch,                &all_plugin_apps,            )            .await            .map_err(|err| format!("failed to list apps: {err}"));            let _ = tx.send(AppListLoadResult::Directory(result));        });        let app_list_deadline = tokio::time::Instant::now() + APP_LIST_LOAD_TIMEOUT;        let mut accessible_loaded = false;        let mut all_loaded = false;        let mut codex_apps_ready = true;        let mut last_notified_apps = None;        if accessible_connectors.is_some() || all_connectors.is_some() {            let merged = connectors::with_app_enabled_state(                merge_loaded_apps(all_connectors.as_deref(), accessible_connectors.as_deref()),                &config,            );            if should_send_app_list_updated_notification(                merged.as_slice(),                accessible_loaded,                all_loaded,            ) {                send_app_list_updated_notification(outgoing, merged.clone()).await;                last_notified_apps = Some(merged);            }        }        loop {            let result = match tokio::time::timeout_at(app_list_deadline, rx.recv()).await {                Ok(Some(result)) => result,                Ok(None) => {                    return Err(internal_error("failed to load app lists"));                }                Err(_) => {                    let timeout_seconds = APP_LIST_LOAD_TIMEOUT.as_secs();                    return Err(internal_error(format!(                        "timed out waiting for app lists after {timeout_seconds} seconds"                    )));                }            };            match result {                AppListLoadResult::Accessible(Ok(status)) => {                    accessible_connectors = Some(status.connectors);                    accessible_loaded = true;                    codex_apps_ready = status.codex_apps_ready;                }                AppListLoadResult::Accessible(Err(err)) => {                    return Err(internal_error(err));                }                AppListLoadResult::Directory(Ok(connectors)) => {                    all_connectors = Some(connectors);                    all_loaded = true;                }                AppListLoadResult::Directory(Err(err)) => {                    return Err(internal_error(err));                }            }            let showing_interim_force_refetch = force_refetch && !(accessible_loaded && all_loaded);            let all_connectors_for_update =                if showing_interim_force_refetch && cached_all_connectors.is_some() {                    cached_all_connectors.as_deref()                } else {                    all_connectors.as_deref()                };            let accessible_connectors_for_update =                if showing_interim_force_refetch && !accessible_loaded {                    None                } else {                    accessible_connectors.as_deref()                };            let merged = connectors::with_app_enabled_state(                merge_loaded_apps(all_connectors_for_update, accessible_connectors_for_update),                &config,            );            if should_send_app_list_updated_notification(                merged.as_slice(),                accessible_loaded,                all_loaded,            ) && last_notified_apps.as_ref() != Some(&merged)            {                send_app_list_updated_notification(outgoing, merged.clone()).await;                last_notified_apps = Some(merged.clone());            }            if accessible_loaded && all_loaded {                let response = paginate_apps(merged.as_slice(), start, limit)?;                return Ok((response, codex_apps_ready));            }        }    }    async fn load_thread(        &self,        thread_id: &str,    ) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError> {        let thread_id = ThreadId::from_string(thread_id)            .map_err(|err| invalid_request(format!("invalid thread id: {err}")))?;        let thread = self            .thread_manager            .get_thread(thread_id)            .await            .map_err(|_| invalid_request(format!("thread not found: {thread_id}")))?;        Ok((thread_id, thread))    }    async fn load_latest_config(        &self,        fallback_cwd: Option<PathBuf>,    ) -> Result<Config, JSONRPCErrorError> {        self.config_manager            .load_latest_config(fallback_cwd)            .await            .map_err(|err| internal_error(format!("failed to reload config: {err}")))    }    async fn workspace_codex_plugins_enabled(        &self,        config: &Config,        auth: Option<&CodexAuth>,    ) -> bool {        match workspace_settings::codex_plugins_enabled_for_workspace(            config,            auth,            Some(&self.workspace_settings_cache),        )        .await        {            Ok(enabled) => enabled,            Err(err) => {                warn!(                    "failed to fetch workspace Codex plugins setting; allowing Codex plugins: {err:#}"                );                true            }        }    }}const APP_LIST_LOAD_TIMEOUT: Duration = Duration::from_secs(90);enum AppListLoadResult {    Accessible(Result<AccessibleConnectorsStatus, String>),    Directory(Result<Vec<AppInfo>, String>),}fn merge_loaded_apps(    all_connectors: Option<&[AppInfo]>,    accessible_connectors: Option<&[AppInfo]>,) -> Vec<AppInfo> {    let all_connectors_loaded = all_connectors.is_some();    let all = all_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec);    let accessible = accessible_connectors.map_or_else(Vec::new, <[AppInfo]>::to_vec);    connectors::merge_connectors_with_accessible(all, accessible, all_connectors_loaded)}fn should_send_app_list_updated_notification(    connectors: &[AppInfo],    accessible_loaded: bool,    all_loaded: bool,) -> bool {    connectors.iter().any(|connector| connector.is_accessible) || (accessible_loaded && all_loaded)}fn paginate_apps(    connectors: &[AppInfo],    start: usize,    limit: Option<u32>,) -> Result<AppsListResponse, JSONRPCErrorError> {    let total = connectors.len();    if start > total {        return Err(invalid_request(format!(            "cursor {start} exceeds total apps {total}"        )));    }    let effective_limit = limit.unwrap_or(total as u32).max(1) as usize;    let end = start.saturating_add(effective_limit).min(total);    let data = connectors[start..end].to_vec();    let next_cursor = if end < total {        Some(end.to_string())    } else {        None    };    Ok(AppsListResponse { data, next_cursor })}async fn send_app_list_updated_notification(    outgoing: &Arc<OutgoingMessageSender>,    data: Vec<AppInfo>,) {    outgoing        .send_server_notification(ServerNotification::AppListUpdated(            AppListUpdatedNotification { data },        ))        .await;}