diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index b618a66310..acff8fd41a 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -150,6 +150,13 @@ "includeTools": { "description": "When true, include display-only public tool summaries in the returned metadata.", "type": "boolean" + }, + "threadId": { + "description": "Optional loaded thread id used to evaluate effective app configuration.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 16d5193ba8..cc54dd03a6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -7224,6 +7224,13 @@ "includeTools": { "description": "When true, include display-only public tool summaries in the returned metadata.", "type": "boolean" + }, + "threadId": { + "description": "Optional loaded thread id used to evaluate effective app configuration.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 9bfeaae430..ce794a584b 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1083,6 +1083,13 @@ "includeTools": { "description": "When true, include display-only public tool summaries in the returned metadata.", "type": "boolean" + }, + "threadId": { + "description": "Optional loaded thread id used to evaluate effective app configuration.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppsReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/AppsReadParams.json index 95450d852d..95467a745b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppsReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppsReadParams.json @@ -12,6 +12,13 @@ "includeTools": { "description": "When true, include display-only public tool summaries in the returned metadata.", "type": "boolean" + }, + "threadId": { + "description": "Optional loaded thread id used to evaluate effective app configuration.", + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index ba19ae47ce..1a280dbd2f 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 63d5c3269d..2f0968f6c7 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppsReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppsReadParams.ts index 4d58f86bcd..19a0e8f4e6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppsReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppsReadParams.ts @@ -11,6 +11,10 @@ export type AppsReadParams = { * preserving their first-request order. */ appIds: Array, +/** + * Optional loaded thread id used to evaluate effective app configuration. + */ +threadId?: string | null, /** * When true, include display-only public tool summaries in the returned metadata. */ diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7e283e3a9f..8b68b20776 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -3433,6 +3433,7 @@ mod tests { request_id: RequestId::Integer(9), params: v2::AppsReadParams { app_ids: vec!["app-a".to_string(), "app-b".to_string()], + thread_id: None, include_tools: true, }, }; @@ -3440,7 +3441,11 @@ mod tests { json!({ "method": "app/read", "id": 9, - "params": { "appIds": ["app-a", "app-b"], "includeTools": true } + "params": { + "appIds": ["app-a", "app-b"], + "threadId": null, + "includeTools": true + } }), serde_json::to_value(&request)?, ); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs index 2b36fa1746..1993c27b75 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs @@ -177,6 +177,9 @@ pub struct AppsReadParams { /// App ids to read. The server accepts at most 100 ids and deduplicates repeated ids while /// preserving their first-request order. pub app_ids: Vec, + /// Optional loaded thread id used to evaluate effective app configuration. + #[ts(optional = nullable)] + pub thread_id: Option, /// When true, include display-only public tool summaries in the returned metadata. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub include_tools: bool, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index eb44767356..8eb2a8d4d4 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -2101,6 +2101,7 @@ instead of failing the whole request. ```json { "method": "app/read", "id": 51, "params": { "appIds": ["demo-app", "missing-app"], + "threadId": "thr_123", "includeTools": true } } { "id": 51, "result": { @@ -2128,11 +2129,13 @@ instead of failing the whole request. `app/read` reads fresh metadata records from a cache partitioned by backend URL and ChatGPT account/workspace identity, then makes at most one `POST /ps/apps/batch` for missing or -expired ids. `includeTools` defaults to false and is forwarded as `include_tools`; a fresh -metadata-only cache entry is refetched when tool summaries are requested. Backend or transport -failures return an RPC error without replacing existing cache records. Its metadata shape can -include display-only public tool summaries with enabled/read-only state and intentionally excludes -runtime state, MCP tool state, full actions, and model descriptions. +expired ids. When `threadId` is provided, app feature gating, workspace policy, and plugin +attribution use that thread's effective configuration. `includeTools` defaults to false and is +forwarded as `include_tools`; a fresh metadata-only cache entry is refetched when tool summaries +are requested. Backend or transport failures return an RPC error without replacing existing cache +records. Its metadata shape can include display-only public tool summaries with enabled/read-only +state and intentionally excludes runtime state, MCP tool state, full actions, and model +descriptions. Connected apps may override the thread's approval reviewer in `config.toml`. Use `apps._default.approvals_reviewer` to set the reviewer for all apps, and a diff --git a/codex-rs/app-server/src/request_processors/apps_processor.rs b/codex-rs/app-server/src/request_processors/apps_processor.rs index 1da9db4bb4..7c0051c142 100644 --- a/codex-rs/app-server/src/request_processors/apps_processor.rs +++ b/codex-rs/app-server/src/request_processors/apps_processor.rs @@ -369,6 +369,18 @@ impl AppsRequestProcessor { .map_err(|err| internal_error(format!("failed to reload config: {err}"))) } + async fn load_apps_config(&self, thread_id: Option<&str>) -> Result { + let Some(thread_id) = thread_id else { + return self.load_latest_config(/*fallback_cwd*/ None).await; + }; + let (_, thread) = self.load_thread(thread_id).await?; + let thread_config = thread.config().await; + self.config_manager + .load_latest_config_for_thread(thread_config.as_ref()) + .await + .map_err(|err| internal_error(format!("failed to reload config: {err}"))) + } + async fn workspace_codex_plugins_enabled( &self, config: &Config, diff --git a/codex-rs/app-server/src/request_processors/apps_processor/installed.rs b/codex-rs/app-server/src/request_processors/apps_processor/installed.rs index 56b9c941c0..10ea272edf 100644 --- a/codex-rs/app-server/src/request_processors/apps_processor/installed.rs +++ b/codex-rs/app-server/src/request_processors/apps_processor/installed.rs @@ -49,7 +49,7 @@ impl AppsRequestProcessor { let mut snapshot_tool_count = 0; let result = async { let config = self - .load_apps_installed_config(params.thread_id.as_deref()) + .load_apps_config(params.thread_id.as_deref()) .await?; let auth = self.auth_manager.auth().await; let apps_enabled = config @@ -200,21 +200,6 @@ impl AppsRequestProcessor { } result } - - async fn load_apps_installed_config( - &self, - thread_id: Option<&str>, - ) -> Result { - let Some(thread_id) = thread_id else { - return self.load_latest_config(/*fallback_cwd*/ None).await; - }; - let (_, thread) = self.load_thread(thread_id).await?; - let thread_config = thread.config().await; - self.config_manager - .load_latest_config_for_thread(thread_config.as_ref()) - .await - .map_err(|err| internal_error(format!("failed to reload config: {err}"))) - } } fn connector_runtime_tool(tool: &ToolInfo) -> ConnectorRuntimeTool<'_> { diff --git a/codex-rs/app-server/src/request_processors/apps_processor/read.rs b/codex-rs/app-server/src/request_processors/apps_processor/read.rs index 0878747ba3..c9664ad9f8 100644 --- a/codex-rs/app-server/src/request_processors/apps_processor/read.rs +++ b/codex-rs/app-server/src/request_processors/apps_processor/read.rs @@ -12,6 +12,7 @@ impl AppsRequestProcessor { let started_at = Instant::now(); let AppsReadParams { app_ids, + thread_id, include_tools, } = params; if app_ids.len() > APP_READ_MAX_IDS { @@ -25,7 +26,7 @@ impl AppsRequestProcessor { .into_iter() .filter(|app_id| seen_app_ids.insert(app_id.clone())) .collect::>(); - let config = self.load_latest_config(/*fallback_cwd*/ None).await?; + let config = self.load_apps_config(thread_id.as_deref()).await?; let auth = self.auth_manager.auth().await; if !config .features diff --git a/codex-rs/app-server/tests/suite/v2/app_read.rs b/codex-rs/app-server/tests/suite/v2/app_read.rs index d8564145cd..4f147d42bd 100644 --- a/codex-rs/app-server/tests/suite/v2/app_read.rs +++ b/codex-rs/app-server/tests/suite/v2/app_read.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::sync::Mutex as StdMutex; @@ -23,6 +24,8 @@ use codex_app_server_protocol::ConnectorMetadata; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; use codex_config::types::AuthCredentialsStoreMode; use pretty_assertions::assert_eq; use serde_json::Value; @@ -286,6 +289,73 @@ async fn app_read_refetches_metadata_only_cache_entries_when_tools_are_requested Ok(()) } +#[tokio::test] +async fn app_read_thread_id_uses_effective_thread_config() -> Result<()> { + let state = BatchServerState::new( + json!({ + "apps": [app_response("alpha", "Alpha", /*icon_url*/ None)] + }), + "chatgpt-token", + "codex", + ); + let (server_url, server_handle) = start_batch_server(state.clone()).await?; + let codex_home = TempDir::new()?; + write_apps_config( + codex_home.path(), + &server_url, + /*apps_mcp_product_sku*/ None, + )?; + write_auth(codex_home.path())?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .without_managed_config() + .build_initialized_with_timeout(DEFAULT_TIMEOUT) + .await?; + + assert_eq!( + read_apps(&mut mcp, vec!["alpha"], /*include_tools*/ false).await?, + AppsReadResponse { + apps: vec![metadata_without_tools( + "alpha", "Alpha", /*icon_url*/ None + )], + missing_app_ids: Vec::new(), + } + ); + + let request_id = mcp + .send_thread_start_request_with_auto_env(ThreadStartParams { + config: Some(HashMap::from([( + "features.connectors".to_string(), + json!(false), + )])), + ..Default::default() + }) + .await?; + let ThreadStartResponse { thread, .. } = + timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; + let request_id = mcp + .send_apps_read_request(AppsReadParams { + app_ids: vec!["alpha".to_string()], + thread_id: Some(thread.id), + include_tools: false, + }) + .await?; + let response: AppsReadResponse = + timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; + assert_eq!( + response, + AppsReadResponse { + apps: Vec::new(), + missing_app_ids: vec!["alpha".to_string()], + } + ); + assert_eq!(state.requests().len(), 1); + + server_handle.abort(); + let _ = server_handle.await; + Ok(()) +} + #[tokio::test] async fn app_read_backend_failure_preserves_fresh_cached_records() -> Result<()> { let state = BatchServerState::new( @@ -322,6 +392,7 @@ async fn app_read_backend_failure_preserves_fresh_cached_records() -> Result<()> let request_id = mcp .send_apps_read_request(AppsReadParams { app_ids: vec!["cached".to_string(), "uncached".to_string()], + thread_id: None, include_tools: true, }) .await?; @@ -443,6 +514,7 @@ async fn app_read_rejects_more_than_one_hundred_input_ids() -> Result<()> { let request_id = mcp .send_apps_read_request(AppsReadParams { app_ids: (0..101).map(|index| format!("app-{index}")).collect(), + thread_id: None, include_tools: false, }) .await?; @@ -474,6 +546,7 @@ async fn read_apps_raw( let request_id = mcp .send_apps_read_request(AppsReadParams { app_ids: app_ids.into_iter().map(str::to_string).collect(), + thread_id: None, include_tools, }) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index faf88e4daa..f49bbd9f96 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -1878,6 +1878,7 @@ async fn plugin_read_stops_batching_after_app_metadata_failure() -> Result<()> { let request_id = mcp .send_apps_read_request(AppsReadParams { app_ids: vec!["app-000".to_string(), "app-100".to_string()], + thread_id: None, include_tools: false, }) .await?;