diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 89558ac889..5206ea6368 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1,3 +1,12 @@ +# Model catalog provider requirements + +`model/list` and periodic model catalog refreshes check the startup provider against +current managed provider requirements before using the catalog. If that provider no longer +complies, `model/list` returns JSON-RPC error `-32600` asking the client to restart Codex, +and background refreshes skip the old endpoint. Requirement load failures also block these +operations. Checks apply even when the catalog is cached. Existing startup provider selection +and caching behavior remain in effect while the provider satisfies current requirements. + # MCP App UI `mcpToolCall.mcpAppUi` records the invoked descriptor's `resourceUri` diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index e0149fa3ce..fc11b838a0 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -120,6 +120,7 @@ mod image_url; pub mod in_process; mod mcp_refresh; mod message_processor; +mod model_catalog; mod models; mod models_refresh_worker; mod notification_media; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 7bc5c527de..118106d710 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -370,9 +370,12 @@ impl MessageProcessor { None => manager, } }); - let models_manager = thread_manager.get_models_manager(); - let models_refresh_worker = - crate::models_refresh_worker::spawn(&models_manager, config.http_client_factory()); + let model_catalog = Arc::new(crate::model_catalog::ModelCatalog::new( + config_manager.clone(), + Arc::clone(&config), + thread_manager.get_models_manager(), + )); + let models_refresh_worker = crate::models_refresh_worker::spawn(&model_catalog); let turn_cost_worker = TurnCostWorker::spawn(Arc::clone(&config), Arc::clone(&auth_manager)); thread_manager @@ -424,6 +427,7 @@ impl MessageProcessor { Arc::clone(&thread_manager), Arc::clone(&config), config_manager.clone(), + model_catalog, ); let command_exec_processor = CommandExecRequestProcessor::new( arg0_paths.clone(), diff --git a/codex-rs/app-server/src/model_catalog.rs b/codex-rs/app-server/src/model_catalog.rs new file mode 100644 index 0000000000..54d7ff8501 --- /dev/null +++ b/codex-rs/app-server/src/model_catalog.rs @@ -0,0 +1,46 @@ +//! Gates access to the retained startup model catalog on current managed provider requirements. + +use std::sync::Arc; + +use codex_core::config::Config; +use codex_models_manager::manager::RefreshStrategy; +use codex_models_manager::manager::SharedModelsManager; +use codex_protocol::openai_models::ModelPreset; + +use crate::config_manager::ConfigManager; + +/// Checks the retained catalog route before model/list and background refreshes. +pub(crate) struct ModelCatalog { + config_manager: ConfigManager, + config: Arc, + models_manager: SharedModelsManager, +} + +impl ModelCatalog { + pub(crate) fn new( + config_manager: ConfigManager, + config: Arc, + models_manager: SharedModelsManager, + ) -> Self { + Self { + config_manager, + config, + models_manager, + } + } + + pub(crate) async fn list_models( + &self, + refresh_strategy: RefreshStrategy, + ) -> std::io::Result> { + // Check before consulting even a warm cache, just as turn admission checks + // the retained session route before using it. + self.config_manager + .check_thread_model_provider(&self.config) + .await?; + Ok(self + .models_manager + .list_models(refresh_strategy, self.config.http_client_factory()) + .await) + } +} diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index b42c8bccaf..b9f2720589 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -1,23 +1,12 @@ -use std::sync::Arc; - use codex_app_server_protocol::Model; use codex_app_server_protocol::ModelServiceTier; use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; -use codex_core::ThreadManager; -use codex_http_client::HttpClientFactory; -use codex_models_manager::manager::RefreshStrategy; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; -pub async fn supported_models( - thread_manager: Arc, - include_hidden: bool, - http_client_factory: HttpClientFactory, -) -> Vec { - thread_manager - .list_models(RefreshStrategy::OnlineIfUncached, http_client_factory) - .await +pub fn supported_models(models: Vec, include_hidden: bool) -> Vec { + models .into_iter() .filter(|preset| include_hidden || preset.show_in_picker) .map(model_from_preset) diff --git a/codex-rs/app-server/src/models_refresh_worker.rs b/codex-rs/app-server/src/models_refresh_worker.rs index e57abcc938..794aa7f4d1 100644 --- a/codex-rs/app-server/src/models_refresh_worker.rs +++ b/codex-rs/app-server/src/models_refresh_worker.rs @@ -1,12 +1,12 @@ use std::sync::Arc; use std::time::Duration; -use codex_http_client::HttpClientFactory; use codex_models_manager::manager::RefreshStrategy; -use codex_models_manager::manager::SharedModelsManager; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; +use crate::model_catalog::ModelCatalog; + const MODELS_REFRESH_INTERVAL: Duration = Duration::from_secs(4 * 60 + 30); #[derive(Debug)] @@ -27,19 +27,15 @@ impl Drop for ModelsRefreshWorker { } } -pub(crate) fn spawn( - models_manager: &SharedModelsManager, - http_client_factory: HttpClientFactory, -) -> ModelsRefreshWorker { - spawn_with_interval(models_manager, http_client_factory, MODELS_REFRESH_INTERVAL) +pub(crate) fn spawn(model_catalog: &Arc) -> ModelsRefreshWorker { + spawn_with_interval(model_catalog, MODELS_REFRESH_INTERVAL) } fn spawn_with_interval( - models_manager: &SharedModelsManager, - http_client_factory: HttpClientFactory, + model_catalog: &Arc, refresh_interval: Duration, ) -> ModelsRefreshWorker { - let models_manager = Arc::downgrade(models_manager); + let model_catalog = Arc::downgrade(model_catalog); let shutdown = CancellationToken::new(); let worker_shutdown = shutdown.clone(); let task = tokio::spawn(async move { @@ -47,13 +43,14 @@ fn spawn_with_interval( if worker_shutdown.is_cancelled() { break; } - let Some(models_manager) = models_manager.upgrade() else { + let Some(model_catalog) = model_catalog.upgrade() else { break; }; - models_manager - .list_models(RefreshStrategy::Online, http_client_factory.clone()) - .await; - drop(models_manager); + if let Err(err) = model_catalog.list_models(RefreshStrategy::Online).await { + // Parser diagnostics can include provider credentials from the source TOML. + tracing::warn!(error_kind = ?err.kind(), "model catalog refresh blocked by provider requirements"); + } + drop(model_catalog); tokio::select! { _ = worker_shutdown.cancelled() => break, diff --git a/codex-rs/app-server/src/models_refresh_worker_tests.rs b/codex-rs/app-server/src/models_refresh_worker_tests.rs index 0a7e132407..5356eb2af3 100644 --- a/codex-rs/app-server/src/models_refresh_worker_tests.rs +++ b/codex-rs/app-server/src/models_refresh_worker_tests.rs @@ -3,8 +3,14 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::time::Duration; +use codex_config::CloudConfigBundle; +use codex_config::CloudConfigBundleLoader; +use codex_config::LoaderOverrides; +use codex_config::test_support::CloudConfigBundleFixture; +use codex_core::config::ConfigBuilder; +use codex_core::config::ConfigOverrides; +use codex_feedback::CodexFeedback; use codex_http_client::HttpClientFactory; -use codex_http_client::OutboundProxyPolicy; use codex_models_manager::manager::ModelsEndpointClient; use codex_models_manager::manager::ModelsEndpointFuture; use codex_models_manager::manager::ModelsEndpointResponse; @@ -15,6 +21,9 @@ use codex_protocol::error::Result as CoreResult; use pretty_assertions::assert_eq; use tempfile::tempdir; use tokio::sync::Notify; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use tracing_subscriber::layer::SubscriberExt; use super::*; @@ -85,16 +94,36 @@ impl ModelsEndpointClient for TestModelsEndpoint { async fn refreshes_immediately_periodically_and_stops_when_dropped() { let codex_home = tempdir().expect("temp dir"); let endpoint = TestModelsEndpoint::new(); + let requirements_path = codex_home.path().join("requirements.toml"); + std::fs::write(&requirements_path, "model_provider = 'ollama'").unwrap(); + let config_manager = crate::config_manager::ConfigManager::new_for_tests( + codex_home.path().to_path_buf(), + Vec::new(), + LoaderOverrides::with_managed_config_path_for_tests( + codex_home.path().join("managed_config.toml"), + ), + CloudConfigBundleLoader::default(), + ); + // --oss selects the provider through typed overrides, outside ConfigManager's raw -c flags. + let config = Arc::new( + ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .harness_overrides(ConfigOverrides { + model_provider: Some("ollama".to_string()), + ..Default::default() + }) + .build() + .await + .unwrap(), + ); let models_manager: SharedModelsManager = Arc::new(OpenAiModelsManager::new( codex_home.path().to_path_buf(), endpoint.clone(), /*auth_manager*/ None, )); - let worker = spawn_with_interval( - &models_manager, - HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), - Duration::from_millis(10), - ); + let catalog = Arc::new(ModelCatalog::new(config_manager, config, models_manager)); + let worker = spawn_with_interval(&catalog, Duration::from_millis(/*millis*/ 10)); endpoint.wait_for_fetch_count(/*expected*/ 2).await; drop(worker); @@ -102,4 +131,101 @@ async fn refreshes_immediately_periodically_and_stops_when_dropped() { tokio::time::sleep(Duration::from_millis(30)).await; assert_eq!(endpoint.fetch_count.load(Ordering::SeqCst), 2); + + // Reloading config would select the newly required provider. The catalog must + // keep validating the typed startup selection, even when only reading its cache. + std::fs::write(&requirements_path, "model_provider = 'openai'").unwrap(); + let error = catalog + .list_models(RefreshStrategy::Offline) + .await + .unwrap_err(); + assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied); + assert!( + error + .get_ref() + .unwrap() + .is::() + ); + assert_eq!(endpoint.fetch_count.load(Ordering::SeqCst), 2); +} + +#[tokio::test] +async fn background_refresh_blocks_changed_and_invalid_requirements_then_recovers() +-> anyhow::Result<()> { + let home = tempdir()?; + let config = Arc::new( + crate::config_manager::ConfigManager::without_managed_config_for_tests( + home.path().to_path_buf(), + ) + .load_non_project_config() + .await?, + ); + let (checks_tx, mut checks_rx) = mpsc::unbounded_channel(); + let loader = CloudConfigBundleLoader::from_getter(move || { + let (reply_tx, reply_rx) = oneshot::channel::>(); + checks_tx + .send(reply_tx) + .expect("requirements check receiver"); + async move { Ok(reply_rx.await.expect("requirements check response")) } + }); + let config_manager = crate::config_manager::ConfigManager::new_for_tests( + home.path().to_path_buf(), + Vec::new(), + LoaderOverrides::without_managed_config_for_tests(), + loader, + ); + let endpoint = TestModelsEndpoint::new(); + let models_manager: SharedModelsManager = Arc::new(OpenAiModelsManager::new( + home.path().to_path_buf(), + endpoint.clone(), + /*auth_manager*/ None, + )); + let catalog = Arc::new(ModelCatalog::new(config_manager, config, models_manager)); + let feedback = CodexFeedback::new(); + // This test uses Tokio's current-thread runtime, so spawned refreshes share the subscriber. + let _subscriber_guard = tracing::subscriber::set_default( + tracing_subscriber::registry().with(feedback.logger_layer()), + ); + let worker = spawn_with_interval(&catalog, Duration::from_millis(/*millis*/ 10)); + tokio::time::timeout(Duration::from_secs(/*secs*/ 10), async { + for expected in 1..=2 { + checks_rx.recv().await.unwrap().send(/*t*/ None).unwrap(); + endpoint.wait_for_fetch_count(expected).await; + } + + // Let the in-flight request finish, then control each refresh's requirements. + endpoint.release_second_fetch.notify_one(); + let mut next_check = checks_rx.recv().await.unwrap(); + for requirements in [ + "model_provider = 'other'", + "model_provider = []", + "[model_providers.gateway]\nexperimental_bearer_token = 'private-provider-token-marker' trailing", + ] { + next_check + .send(Some( + CloudConfigBundleFixture::enterprise_requirement(requirements).into_bundle(), + )) + .unwrap(); + // Reaching the next check proves the previous refresh completed. Keep + // this check blocked while asserting that the previous one did not fetch. + next_check = checks_rx.recv().await.unwrap(); + assert_eq!(endpoint.fetch_count.load(Ordering::SeqCst), 2); + } + + next_check.send(/*t*/ None).unwrap(); + endpoint.wait_for_fetch_count(/*expected*/ 3).await; + }) + .await?; + drop(worker); + let logs = String::from_utf8( + feedback + .snapshot(/*session_id*/ None) + .log_attachment(/*logs_override*/ None) + .buffer, + )?; + assert!(logs.contains("model catalog refresh blocked by provider requirements")); + assert!(logs.contains("error_kind=PermissionDenied")); + assert!(logs.contains("error_kind=InvalidData")); + assert!(!logs.contains("private-provider-token-marker")); + Ok(()) } diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 45b8f132ff..cc11ef8737 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -1,4 +1,5 @@ use super::*; +use crate::model_catalog::ModelCatalog; use codex_config::ConfigPathContext; use codex_core::config::permission_profile_catalog; use codex_hooks::HookListEntryHandler; @@ -14,6 +15,7 @@ pub(crate) struct CatalogRequestProcessor { pub(super) thread_manager: Arc, pub(super) config: Arc, pub(super) config_manager: ConfigManager, + model_catalog: Arc, } const SKILLS_LIST_CWD_CONCURRENCY: usize = 5; @@ -124,6 +126,7 @@ impl CatalogRequestProcessor { thread_manager: Arc, config: Arc, config_manager: ConfigManager, + model_catalog: Arc, ) -> Self { Self { outgoing, @@ -131,6 +134,7 @@ impl CatalogRequestProcessor { thread_manager, config, config_manager, + model_catalog, } } @@ -174,13 +178,9 @@ impl CatalogRequestProcessor { &self, params: ModelListParams, ) -> Result, JSONRPCErrorError> { - Self::list_models( - self.thread_manager.clone(), - self.config.http_client_factory(), - params, - ) - .await - .map(|response| Some(response.into())) + self.list_models(params) + .await + .map(|response| Some(response.into())) } pub(crate) async fn experimental_feature_list( @@ -245,8 +245,7 @@ impl CatalogRequestProcessor { } async fn list_models( - thread_manager: Arc, - http_client_factory: codex_http_client::HttpClientFactory, + &self, params: ModelListParams, ) -> Result { let ModelListParams { @@ -254,12 +253,12 @@ impl CatalogRequestProcessor { cursor, include_hidden, } = params; - let models = supported_models( - thread_manager, - include_hidden.unwrap_or(false), - http_client_factory, - ) - .await; + let presets = self + .model_catalog + .list_models(codex_models_manager::manager::RefreshStrategy::OnlineIfUncached) + .await + .map_err(|err| config_load_error(&err))?; + let models = supported_models(presets, include_hidden.unwrap_or(false)); let total = models.len(); if total == 0 { diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 91c6ddab00..0ea66030c1 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -74,6 +74,7 @@ mod memory_reset; mod misalignment_policy; mod model_auto_review; mod model_list; +mod model_list_requirements_tests; mod model_provider_capabilities_read; #[path = "model_provider_enforcement_tests.rs"] mod model_provider_enforcement; diff --git a/codex-rs/app-server/tests/suite/v2/model_list_requirements_tests.rs b/codex-rs/app-server/tests/suite/v2/model_list_requirements_tests.rs new file mode 100644 index 0000000000..77964124dd --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/model_list_requirements_tests.rs @@ -0,0 +1,205 @@ +//! Exercises live catalog routing through the public model/list RPC. + +use anyhow::Result; +use app_test_support::ChatGptAuthFixture; +use app_test_support::TestAppServer; +use app_test_support::write_chatgpt_auth; +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::ModelListParams; +use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::RequestId; +use codex_config::types::AuthCredentialsStoreMode; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelsResponse; +use core_test_support::responses::mount_models_once; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::time::Duration; +use tempfile::TempDir; +use test_case::test_case; +use wiremock::MockServer; + +fn catalog(slug: &str) -> Result { + Ok(ModelsResponse { + models: vec![serde_json::from_value::(json!({ + "slug": slug, + "display_name": slug, + "supported_reasoning_levels": [], + "shell_type": "shell_command", + "visibility": "list", + "supported_in_api": true, + "priority": 0, + "support_verbosity": false, + "truncation_policy": {"mode": "bytes", "limit": 10000}, + "experimental_supported_tools": [] + }))?], + }) +} + +async fn list_models(server: &mut TestAppServer) -> Result { + server + .request(|request_id| ClientRequest::ModelList { + request_id, + params: ModelListParams::default(), + }) + .await +} + +#[test_case("other"; "selection_changes")] +#[test_case("gateway"; "definition_changes")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn model_list_blocks_noncompliant_cached_provider_until_requirements_allow_it( + required_selection: &str, +) -> Result<()> { + let gateway = MockServer::start().await; + let other = MockServer::start().await; + mount_models_once(&gateway, catalog("gateway-model")?).await; + mount_models_once(&other, catalog("other-model")?).await; + let home = TempDir::new()?; + let providers = format!( + r#" +[model_providers.gateway] +name = "Gateway" +base_url = "{}/v1" +requires_openai_auth = true +[model_providers.other] +name = "Other" +base_url = "{}/v1" +requires_openai_auth = true +"#, + gateway.uri(), + other.uri(), + ); + std::fs::write( + home.path().join("config.toml"), + format!("model_provider = 'gateway'\n{providers}"), + )?; + write_chatgpt_auth( + home.path(), + ChatGptAuthFixture::new("chatgpt-access-token").plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; + let requirements_path = home.path().join("requirements.toml"); + std::fs::write( + &requirements_path, + format!("model_provider = 'gateway'\n{providers}"), + )?; + let mut server = TestAppServer::builder() + .with_codex_home(home.path()) + .with_env_overrides(&[("OPENAI_API_KEY", None)]) + .build_initialized() + .await?; + + // Let the startup refresh populate the cache before exercising cached RPCs. + tokio::time::timeout(Duration::from_secs(/*secs*/ 10), async { + loop { + if let Ok(bytes) = tokio::fs::read(home.path().join("models_cache.json")).await + && let Ok(cache) = serde_json::from_slice::(&bytes) + && cache["models"][0]["slug"] == "gateway-model" + { + break; + } + tokio::time::sleep(Duration::from_millis(/*millis*/ 10)).await; + } + }) + .await?; + + let initial = list_models(&mut server).await?; + assert_eq!( + initial + .data + .iter() + .map(|model| model.id.as_str()) + .collect::>(), + vec!["gateway-model"] + ); + assert_eq!(list_models(&mut server).await?, initial); + assert_eq!( + gateway + .received_requests() + .await + .expect("request recording enabled") + .len(), + 1 + ); + assert!( + other + .received_requests() + .await + .expect("request recording enabled") + .is_empty() + ); + + let updated_providers = if required_selection == "gateway" { + providers.replace(&gateway.uri(), &other.uri()) + } else { + providers.clone() + }; + let updated = format!("model_provider = '{required_selection}'\n{updated_providers}"); + std::fs::write(&requirements_path, &updated)?; + let id = server + .send_raw_request("model/list", Some(json!({}))) + .await?; + let error = server + .read_stream_until_error_message(RequestId::Integer(id)) + .await?; + assert_eq!(error.error, codex_app_server_protocol::JSONRPCErrorError { + code: -32600, + message: "failed to load configuration: Your organization's required model provider settings changed. Restart Codex to apply them; this request was not sent".to_string(), + data: None, + }); + assert_eq!( + gateway + .received_requests() + .await + .expect("request recording enabled") + .len(), + 1 + ); + assert!( + other + .received_requests() + .await + .expect("request recording enabled") + .is_empty() + ); + + // A warm catalog must not hide a policy load failure or make any stale request. + std::fs::write(&requirements_path, "model_provider = []")?; + let id = server + .send_raw_request("model/list", Some(json!({}))) + .await?; + let error = server + .read_stream_until_error_message(RequestId::Integer(id)) + .await?; + assert_eq!(error.error.code, -32600); + assert!(error.error.message.contains("failed to load configuration")); + assert_eq!( + gateway + .received_requests() + .await + .expect("request recording enabled") + .len(), + 1 + ); + assert!( + other + .received_requests() + .await + .expect("request recording enabled") + .is_empty() + ); + + // Removing the requirement makes the retained provider permissible again. + std::fs::remove_file(requirements_path)?; + assert_eq!(list_models(&mut server).await?, initial); + assert_eq!( + gateway + .received_requests() + .await + .expect("request recording enabled") + .len(), + 1 + ); + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 1a971b0e6c..00f25e78d2 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -52,6 +52,7 @@ use serde_json::json; use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; +use test_case::test_case; use tokio::net::TcpListener; use tokio::sync::oneshot; use tokio::time::timeout; @@ -1416,8 +1417,12 @@ async fn thread_start_does_not_wait_for_optional_http_mcp_auth_discovery() -> Re Ok(()) } +#[test_case("thread/start"; "thread_start")] +#[test_case("model/list"; "model_list")] #[tokio::test] -async fn thread_start_surfaces_cloud_config_bundle_load_errors() -> Result<()> { +async fn config_requests_surface_cloud_config_bundle_load_errors( + request_method: &str, +) -> Result<()> { let server = MockServer::start().await; Mock::given(method("GET")) .and(path("/backend-api/wham/config/bundle")) @@ -1468,9 +1473,13 @@ async fn thread_start_surfaces_cloud_config_bundle_load_errors() -> Result<()> { .build_initialized() .await?; - let req_id = mcp - .send_thread_start_request_with_auto_env(ThreadStartParams::default()) - .await?; + let req_id = if request_method == "thread/start" { + mcp.send_thread_start_request_with_auto_env(ThreadStartParams::default()) + .await? + } else { + mcp.send_raw_request(request_method, Some(json!({}))) + .await? + }; let err: JSONRPCError = timeout( DEFAULT_READ_TIMEOUT,