fix: forward originator to Codex Apps MCP (#31481)

## Summary
- Forward Codexs canonical `originator` header on ChatGPT-hosted Apps
and plugin-runtime MCP requests.
- Preserve the configured `X-OpenAI-Product-Sku` header.
- Cover originator-only and originator-plus-SKU configurations.

## Why
Sites project creation is logged downstream of Apps MCP. Production
validation found `CODEX_UNKNOWN_DEFAULT` project-created threads that
matched `codex_surface=desktop_app` and `originator=Codex Desktop` in
`fact_codex_cli`
([query](https://kepler.gateway.data-1.internal.api.openai.org/permalink/H_mVoVPqLQ0)).

The hosted Apps MCP configuration forwarded the product SKU but not
Codexs canonical originator, so codex-backend could not derive
`CODEX_DESKTOP_APP` for those tool calls.

## Validation
- `just fmt`
- `CARGO_HOME=/private/tmp/codex-cargo-home
CARGO_TARGET_DIR=/private/tmp/codex-target just test -p codex-mcp` (106
passed)
- `CARGO_HOME=/private/tmp/codex-cargo-home
CARGO_TARGET_DIR=/private/tmp/codex-target just fix -p codex-mcp`
This commit is contained in:
raquel-openai authored and GitHub committed 2026-07-09 16:51:44 +00:00
1 parent cf6d8ad921
commit b58952b0fa
13 files changed
+208 -20

No files matched your search

@@ -1418,6 +1418,7 @@ fn server_metadata_preserves_tool_approval_policy() {
let mut config = crate::codex_apps_mcp_server_config( let mut config = crate::codex_apps_mcp_server_config(
"https://docs.example", "https://docs.example",
/*apps_mcp_product_sku*/ None, /*apps_mcp_product_sku*/ None,
/*originator*/ None,
); );
config.environment_id = "remote".to_string(); config.environment_id = "remote".to_string();
config.default_tools_approval_mode = Some(AppToolApproval::Prompt); config.default_tools_approval_mode = Some(AppToolApproval::Prompt);
@@ -1462,6 +1463,7 @@ fn host_owned_codex_apps_matches_reserved_name_with_server_metadata() {
let server = EffectiveMcpServer::configured(crate::codex_apps_mcp_server_config( let server = EffectiveMcpServer::configured(crate::codex_apps_mcp_server_config(
"https://chatgpt.com", "https://chatgpt.com",
/*apps_mcp_product_sku*/ None, /*apps_mcp_product_sku*/ None,
/*originator*/ None,
)); ));
manager.server_metadata.insert( manager.server_metadata.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(), CODEX_APPS_MCP_SERVER_NAME.to_string(),
+11 -5
View File
@@ -493,10 +493,12 @@ fn codex_apps_mcp_url_for_base_url(base_url: &str) -> String {
pub fn codex_apps_mcp_server_config( pub fn codex_apps_mcp_server_config(
chatgpt_base_url: &str, chatgpt_base_url: &str,
apps_mcp_product_sku: Option<&str>, apps_mcp_product_sku: Option<&str>,
originator: Option<&str>,
) -> McpServerConfig { ) -> McpServerConfig {
mcp_server_config_for_url( mcp_server_config_for_url(
codex_apps_mcp_url_for_base_url(chatgpt_base_url), codex_apps_mcp_url_for_base_url(chatgpt_base_url),
apps_mcp_product_sku, apps_mcp_product_sku,
originator,
McpServerAuth::ChatGpt, McpServerAuth::ChatGpt,
) )
} }
@@ -505,6 +507,7 @@ pub fn codex_apps_mcp_server_config(
pub fn hosted_plugin_runtime_mcp_server_config( pub fn hosted_plugin_runtime_mcp_server_config(
chatgpt_base_url: &str, chatgpt_base_url: &str,
apps_mcp_product_sku: Option<&str>, apps_mcp_product_sku: Option<&str>,
originator: Option<&str>,
) -> McpServerConfig { ) -> McpServerConfig {
let base_url = normalize_codex_apps_base_url(chatgpt_base_url); let base_url = normalize_codex_apps_base_url(chatgpt_base_url);
let base_url = if base_url.contains("/backend-api") || base_url.contains("/api/codex") { let base_url = if base_url.contains("/backend-api") || base_url.contains("/api/codex") {
@@ -515,6 +518,7 @@ pub fn hosted_plugin_runtime_mcp_server_config(
mcp_server_config_for_url( mcp_server_config_for_url(
format!("{base_url}/ps/mcp"), format!("{base_url}/ps/mcp"),
apps_mcp_product_sku, apps_mcp_product_sku,
originator,
McpServerAuth::ChatGpt, McpServerAuth::ChatGpt,
) )
} }
@@ -522,19 +526,21 @@ pub fn hosted_plugin_runtime_mcp_server_config(
fn mcp_server_config_for_url( fn mcp_server_config_for_url(
url: String, url: String,
apps_mcp_product_sku: Option<&str>, apps_mcp_product_sku: Option<&str>,
originator: Option<&str>,
auth_mode: McpServerAuth, auth_mode: McpServerAuth,
) -> McpServerConfig { ) -> McpServerConfig {
let product_sku = apps_mcp_product_sku.unwrap_or(DEFAULT_CODEX_APPS_MCP_PRODUCT_SKU); let product_sku = apps_mcp_product_sku.unwrap_or(DEFAULT_CODEX_APPS_MCP_PRODUCT_SKU);
let http_headers = Some(HashMap::from([( let mut http_headers =
"X-OpenAI-Product-Sku".to_string(), HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())]);
product_sku.to_string(), if let Some(originator) = originator {
)])); http_headers.insert("originator".to_string(), originator.to_string());
}
McpServerConfig { McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp { transport: McpServerTransportConfig::StreamableHttp {
url, url,
bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(), bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(),
http_headers, http_headers: Some(http_headers),
env_http_headers: None, env_http_headers: None,
}, },
auth: auth_mode, auth: auth_mode,
+75 -5
View File
@@ -130,7 +130,11 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
"alpha".to_string(), "alpha".to_string(),
McpPluginAttribution::new("alpha@test".to_string(), "alpha-plugin".to_string()), McpPluginAttribution::new("alpha@test".to_string(), "alpha-plugin".to_string()),
/*plugin_order*/ 0, /*plugin_order*/ 0,
codex_apps_mcp_server_config("https://alpha.example", /*apps_mcp_product_sku*/ None), codex_apps_mcp_server_config(
"https://alpha.example",
/*apps_mcp_product_sku*/ None,
/*originator*/ None,
),
)); ));
config.mcp_server_catalog = catalog.build(); config.mcp_server_catalog = catalog.build();
config.connector_snapshot = config.connector_snapshot =
@@ -197,7 +201,11 @@ fn selected_mcp_attribution_does_not_join_an_unrelated_local_summary() {
"Executor GitHub".to_string(), "Executor GitHub".to_string(),
), ),
/*selection_order*/ 0, /*selection_order*/ 0,
codex_apps_mcp_server_config("https://github.example", /*apps_mcp_product_sku*/ None), codex_apps_mcp_server_config(
"https://github.example",
/*apps_mcp_product_sku*/ None,
/*originator*/ None,
),
)); ));
config.mcp_server_catalog = catalog.build(); config.mcp_server_catalog = catalog.build();
config.connector_snapshot = config.connector_snapshot =
@@ -252,8 +260,11 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
#[test] #[test]
fn codex_apps_server_config_uses_legacy_codex_apps_path() { fn codex_apps_server_config_uses_legacy_codex_apps_path() {
let config = let config = codex_apps_mcp_server_config(
codex_apps_mcp_server_config("https://chatgpt.com", /*apps_mcp_product_sku*/ None); "https://chatgpt.com",
/*apps_mcp_product_sku*/ None,
/*originator*/ None,
);
let url = match &config.transport { let url = match &config.transport {
McpServerTransportConfig::StreamableHttp { url, .. } => url, McpServerTransportConfig::StreamableHttp { url, .. } => url,
_ => panic!("expected streamable http transport for codex apps"), _ => panic!("expected streamable http transport for codex apps"),
@@ -262,10 +273,41 @@ fn codex_apps_server_config_uses_legacy_codex_apps_path() {
assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps"); assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps");
} }
#[test]
fn codex_apps_server_config_forwards_thread_originator_header() {
let config = codex_apps_mcp_server_config(
"https://chatgpt.com",
/*apps_mcp_product_sku*/ None,
Some("thread_originator"),
);
match &config.transport {
McpServerTransportConfig::StreamableHttp {
http_headers,
env_http_headers,
..
} => {
assert_eq!(
http_headers,
&Some(HashMap::from([
("originator".to_string(), "thread_originator".to_string()),
("X-OpenAI-Product-Sku".to_string(), "codex".to_string()),
]))
);
assert!(env_http_headers.is_none());
}
other => panic!("expected streamable http transport, got {other:?}"),
}
}
#[test] #[test]
fn codex_apps_server_config_sets_product_sku_header() { fn codex_apps_server_config_sets_product_sku_header() {
for (configured_product_sku, expected_product_sku) in [(None, "codex"), (Some("tpp"), "tpp")] { for (configured_product_sku, expected_product_sku) in [(None, "codex"), (Some("tpp"), "tpp")] {
let config = codex_apps_mcp_server_config("https://chatgpt.com", configured_product_sku); let config = codex_apps_mcp_server_config(
"https://chatgpt.com",
configured_product_sku,
/*originator*/ None,
);
match &config.transport { match &config.transport {
McpServerTransportConfig::StreamableHttp { McpServerTransportConfig::StreamableHttp {
@@ -287,6 +329,33 @@ fn codex_apps_server_config_sets_product_sku_header() {
} }
} }
#[test]
fn codex_apps_server_config_forwards_originator_and_configured_product_sku_headers() {
let config = codex_apps_mcp_server_config(
"https://chatgpt.com",
Some("tpp"),
Some("thread_originator"),
);
match &config.transport {
McpServerTransportConfig::StreamableHttp {
http_headers,
env_http_headers,
..
} => {
assert_eq!(
http_headers,
&Some(HashMap::from([
("originator".to_string(), "thread_originator".to_string()),
("X-OpenAI-Product-Sku".to_string(), "tpp".to_string()),
]))
);
assert!(env_http_headers.is_none());
}
other => panic!("expected streamable http transport, got {other:?}"),
}
}
#[tokio::test] #[tokio::test]
async fn effective_mcp_servers_preserve_runtime_servers() { async fn effective_mcp_servers_preserve_runtime_servers() {
let codex_home = tempfile::tempdir().expect("tempdir"); let codex_home = tempfile::tempdir().expect("tempdir");
@@ -352,6 +421,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
codex_apps_mcp_server_config( codex_apps_mcp_server_config(
&config.chatgpt_base_url, &config.chatgpt_base_url,
config.apps_mcp_product_sku.as_deref(), config.apps_mcp_product_sku.as_deref(),
/*originator*/ None,
), ),
)); ));
config.mcp_server_catalog = catalog.build(); config.mcp_server_catalog = catalog.build();
+22 -9
View File
@@ -79,9 +79,15 @@ impl McpManager {
/// Returns the MCP config after applying compatibility built-ins and /// Returns the MCP config after applying compatibility built-ins and
/// runtime-only extension overlays. /// runtime-only extension overlays.
pub async fn runtime_config(&self, config: &Config) -> McpConfig { pub async fn runtime_config(&self, config: &Config) -> McpConfig {
self.runtime_config_with_context(McpServerContributionContext::global(config)) self.runtime_config_with_context(
.await McpServerContributionContext::global(config),
.config // Threadless discovery and control-plane paths have no effective thread
// originator; active-thread tool calls use runtime_config_for_step below.
/*originator*/
None,
)
.await
.config
} }
pub(crate) async fn runtime_config_for_step( pub(crate) async fn runtime_config_for_step(
@@ -89,20 +95,26 @@ impl McpManager {
config: &Config, config: &Config,
thread_init: &ExtensionDataInit, thread_init: &ExtensionDataInit,
thread_store: &ExtensionData, thread_store: &ExtensionData,
originator: &str,
available_environment_ids: &[String], available_environment_ids: &[String],
) -> McpRuntimeProjection { ) -> McpRuntimeProjection {
self.runtime_config_with_context(McpServerContributionContext::for_step( self.runtime_config_with_context(
config, McpServerContributionContext::for_step(
thread_init, config,
thread_store, thread_init,
available_environment_ids, thread_store,
)) originator,
available_environment_ids,
),
Some(originator),
)
.await .await
} }
async fn runtime_config_with_context( async fn runtime_config_with_context(
&self, &self,
context: McpServerContributionContext<'_, Config>, context: McpServerContributionContext<'_, Config>,
originator: Option<&str>,
) -> McpRuntimeProjection { ) -> McpRuntimeProjection {
let config = context.config(); let config = context.config();
let mut selected_plugin_available = false; let mut selected_plugin_available = false;
@@ -181,6 +193,7 @@ impl McpManager {
codex_apps_mcp_server_config( codex_apps_mcp_server_config(
&mcp_config.chatgpt_base_url, &mcp_config.chatgpt_base_url,
mcp_config.apps_mcp_product_sku.as_deref(), mcp_config.apps_mcp_product_sku.as_deref(),
originator,
), ),
)); ));
} else { } else {
+1
View File
@@ -1435,6 +1435,7 @@ async fn host_owned_codex_apps_manager(
codex_mcp::EffectiveMcpServer::configured(codex_mcp::codex_apps_mcp_server_config( codex_mcp::EffectiveMcpServer::configured(codex_mcp::codex_apps_mcp_server_config(
"https://chatgpt.com", "https://chatgpt.com",
/*apps_mcp_product_sku*/ None, /*apps_mcp_product_sku*/ None,
Some(&turn_context.originator),
)), )),
)]); )]);
let manager = codex_mcp::McpConnectionManager::new( let manager = codex_mcp::McpConnectionManager::new(
+5
View File
@@ -78,6 +78,7 @@ impl ElicitationReviewer for GuardianMcpElicitationReviewer {
impl Session { impl Session {
pub(crate) async fn runtime_mcp_config(&self, config: &Config) -> McpConfig { pub(crate) async fn runtime_mcp_config(&self, config: &Config) -> McpConfig {
let originator = self.originator().await;
let environments = self.services.turn_environments.snapshot().await; let environments = self.services.turn_environments.snapshot().await;
let selected_capability_roots = self let selected_capability_roots = self
.resolve_selected_capability_roots_for_step(&environments) .resolve_selected_capability_roots_for_step(&environments)
@@ -90,6 +91,7 @@ impl Session {
config, config,
&self.services.mcp_thread_init, &self.services.mcp_thread_init,
&self.services.thread_extension_data, &self.services.thread_extension_data,
&originator,
&available_environment_ids, &available_environment_ids,
) )
.await .await
@@ -132,6 +134,7 @@ impl Session {
&turn_context.config, &turn_context.config,
&self.services.mcp_thread_init, &self.services.mcp_thread_init,
&self.services.thread_extension_data, &self.services.thread_extension_data,
&turn_context.originator,
&available_environment_ids, &available_environment_ids,
) )
.await; .await;
@@ -475,6 +478,7 @@ impl Session {
&refresh_config, &refresh_config,
&self.services.mcp_thread_init, &self.services.mcp_thread_init,
&self.services.thread_extension_data, &self.services.thread_extension_data,
&turn_context.originator,
&available_environment_ids, &available_environment_ids,
) )
.await; .await;
@@ -543,6 +547,7 @@ impl Session {
refresh_config, refresh_config,
&self.services.mcp_thread_init, &self.services.mcp_thread_init,
&self.services.thread_extension_data, &self.services.thread_extension_data,
&turn_context.originator,
&available_environment_ids, &available_environment_ids,
) )
.await; .await;
+2
View File
@@ -672,6 +672,7 @@ impl Session {
let mcp_manager_for_mcp = Arc::clone(&mcp_manager); let mcp_manager_for_mcp = Arc::clone(&mcp_manager);
let mcp_thread_init_for_startup = &mcp_thread_init; let mcp_thread_init_for_startup = &mcp_thread_init;
let thread_extension_data_for_mcp = &thread_extension_data; let thread_extension_data_for_mcp = &thread_extension_data;
let mcp_originator = session_configuration.originator.clone();
let mcp_runtime_cwd = session_configuration let mcp_runtime_cwd = session_configuration
.environment_selections() .environment_selections()
.first() .first()
@@ -688,6 +689,7 @@ impl Session {
&config_for_mcp, &config_for_mcp,
mcp_thread_init_for_startup, mcp_thread_init_for_startup,
thread_extension_data_for_mcp, thread_extension_data_for_mcp,
&mcp_originator,
/*available_environment_ids*/ &[], /*available_environment_ids*/ &[],
) )
.await; .await;
+25 -1
View File
@@ -553,6 +553,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
let mut server = codex_mcp::codex_apps_mcp_server_config( let mut server = codex_mcp::codex_apps_mcp_server_config(
"https://selected.invalid", "https://selected.invalid",
/*apps_mcp_product_sku*/ None, /*apps_mcp_product_sku*/ None,
/*originator*/ None,
); );
let CapabilityRootLocation::Environment { environment_id, .. } = let CapabilityRootLocation::Environment { environment_id, .. } =
&selected_root.location; &selected_root.location;
@@ -574,6 +575,10 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
let mut config = test_config().await; let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs(); config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs(); config.cwd = config.codex_home.abs();
config
.features
.enable(Feature::Apps)
.expect("test config should allow apps");
std::fs::create_dir_all(&config.codex_home).expect("create codex home"); std::fs::create_dir_all(&config.codex_home).expect("create codex home");
let lifecycle_observed = Arc::new(std::sync::Mutex::new(Vec::new())); let lifecycle_observed = Arc::new(std::sync::Mutex::new(Vec::new()));
@@ -620,7 +625,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
session_source: None, session_source: None,
thread_source: None, thread_source: None,
dynamic_tools: Vec::new(), dynamic_tools: Vec::new(),
metrics_service_name: None, metrics_service_name: Some("codex_work_desktop".to_string()),
parent_trace: None, parent_trace: None,
environments: Vec::new(), environments: Vec::new(),
thread_extension_init: selected_root_init("selected-a", "env-a"), thread_extension_init: selected_root_init("selected-a", "env-a"),
@@ -646,6 +651,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
.await .await
.expect("start second thread"); .expect("start second thread");
let first_session = &first_thread.thread.codex.session; let first_session = &first_thread.thread.codex.session;
let first_originator = first_session.originator().await;
let first_resolved = first_session let first_resolved = first_session
.services .services
.mcp_manager .mcp_manager
@@ -653,10 +659,12 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
&config, &config,
&first_session.services.mcp_thread_init, &first_session.services.mcp_thread_init,
&first_session.services.thread_extension_data, &first_session.services.thread_extension_data,
&first_originator,
/*available_environment_ids*/ &[], /*available_environment_ids*/ &[],
) )
.await; .await;
let second_session = &second_thread.thread.codex.session; let second_session = &second_thread.thread.codex.session;
let second_originator = second_session.originator().await;
let second_resolved = second_session let second_resolved = second_session
.services .services
.mcp_manager .mcp_manager
@@ -664,6 +672,7 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
&config, &config,
&second_session.services.mcp_thread_init, &second_session.services.mcp_thread_init,
&second_session.services.thread_extension_data, &second_session.services.thread_extension_data,
&second_originator,
/*available_environment_ids*/ &[], /*available_environment_ids*/ &[],
) )
.await; .await;
@@ -706,6 +715,21 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
selected_servers(&second_resolved.config), selected_servers(&second_resolved.config),
std::collections::BTreeMap::from([("selected-b".to_string(), "env-b".to_string())]) std::collections::BTreeMap::from([("selected-b".to_string(), "env-b".to_string())])
); );
let codex_apps_server = codex_mcp::configured_mcp_servers(&first_resolved.config)
.remove(codex_mcp::CODEX_APPS_MCP_SERVER_NAME)
.expect("Codex Apps server should be configured");
let codex_apps_headers = match codex_apps_server.transport {
codex_config::McpServerTransportConfig::StreamableHttp { http_headers, .. } => http_headers,
codex_config::McpServerTransportConfig::Stdio { .. } => {
panic!("Codex Apps server should use streamable HTTP")
}
};
assert_eq!(
codex_apps_headers
.expect("Codex Apps headers should be configured")
.get("originator"),
Some(&"codex_work_desktop".to_string())
);
} }
#[tokio::test] #[tokio::test]
@@ -66,6 +66,7 @@ async fn hosted_plugin_runtime_ps_mcp_tool_calls_use_current_auth_manager_token(
let mut hosted_plugin_runtime_config = codex_mcp::hosted_plugin_runtime_mcp_server_config( let mut hosted_plugin_runtime_config = codex_mcp::hosted_plugin_runtime_mcp_server_config(
&apps_server.chatgpt_base_url, &apps_server.chatgpt_base_url,
/*apps_mcp_product_sku*/ None, /*apps_mcp_product_sku*/ None,
/*originator*/ None,
); );
let McpServerTransportConfig::StreamableHttp { let McpServerTransportConfig::StreamableHttp {
bearer_token_env_var, bearer_token_env_var,
@@ -15,6 +15,8 @@ pub struct McpServerContributionContext<'a, C> {
thread_store: Option<&'a ExtensionData>, thread_store: Option<&'a ExtensionData>,
/// Stable host inputs for the active thread, when resolution is thread-scoped. /// Stable host inputs for the active thread, when resolution is thread-scoped.
thread_init: Option<&'a ExtensionDataInit>, thread_init: Option<&'a ExtensionDataInit>,
/// Effective request originator for the active thread, when resolution is thread-scoped.
originator: Option<&'a str>,
/// Environment IDs whose selected roots may contribute to this exact step. /// Environment IDs whose selected roots may contribute to this exact step.
available_environment_ids: Option<&'a [String]>, available_environment_ids: Option<&'a [String]>,
} }
@@ -34,6 +36,7 @@ impl<'a, C> McpServerContributionContext<'a, C> {
config, config,
thread_store: None, thread_store: None,
thread_init: None, thread_init: None,
originator: None,
available_environment_ids: None, available_environment_ids: None,
} }
} }
@@ -43,12 +46,14 @@ impl<'a, C> McpServerContributionContext<'a, C> {
config: &'a C, config: &'a C,
thread_init: &'a ExtensionDataInit, thread_init: &'a ExtensionDataInit,
thread_store: &'a ExtensionData, thread_store: &'a ExtensionData,
originator: &'a str,
available_environment_ids: &'a [String], available_environment_ids: &'a [String],
) -> Self { ) -> Self {
Self { Self {
config, config,
thread_store: Some(thread_store), thread_store: Some(thread_store),
thread_init: Some(thread_init), thread_init: Some(thread_init),
originator: Some(originator),
available_environment_ids: Some(available_environment_ids), available_environment_ids: Some(available_environment_ids),
} }
} }
@@ -68,6 +73,11 @@ impl<'a, C> McpServerContributionContext<'a, C> {
self.thread_init self.thread_init
} }
/// Returns the effective request originator when resolving for a running thread.
pub fn originator(&self) -> Option<&'a str> {
self.originator
}
/// Returns the exact environment availability projection for a model step. /// Returns the exact environment availability projection for a model step.
/// ///
/// `Some` means contributors must omit selected roots whose environment ID is absent from the /// `Some` means contributors must omit selected roots whose environment ID is absent from the
+5
View File
@@ -32,6 +32,7 @@ impl McpServerContributor<Config> for HostedPluginRuntimeExtension {
config: Box::new(hosted_plugin_runtime_mcp_server_config( config: Box::new(hosted_plugin_runtime_mcp_server_config(
&config.chatgpt_base_url, &config.chatgpt_base_url,
config.apps_mcp_product_sku.as_deref(), config.apps_mcp_product_sku.as_deref(),
context.originator(),
)), )),
}] }]
}) })
@@ -51,3 +52,7 @@ pub fn install_executor_plugins(
executor_plugin::SelectedExecutorPluginMcpContributor::new(environment_manager), executor_plugin::SelectedExecutorPluginMcpContributor::new(environment_manager),
)); ));
} }
#[cfg(test)]
#[path = "lib_tests.rs"]
mod tests;
+48
View File
@@ -0,0 +1,48 @@
use super::*;
use codex_config::McpServerTransportConfig;
use codex_core::config::ConfigBuilder;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionDataInit;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn hosted_plugin_runtime_forwards_thread_originator() -> Result<(), Box<dyn std::error::Error>>
{
let codex_home = tempfile::tempdir()?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cli_overrides(vec![
("features.apps".to_string(), true.into()),
("chatgpt_base_url".to_string(), "https://chatgpt.com".into()),
])
.build()
.await?;
let thread_init = ExtensionDataInit::new();
let thread_store = ExtensionData::new("thread");
let contributions = HostedPluginRuntimeExtension
.contribute(McpServerContributionContext::for_step(
&config,
&thread_init,
&thread_store,
"codex_work_desktop",
/*available_environment_ids*/ &[],
))
.await;
let [McpServerContribution::Set { config: server, .. }] = contributions.as_slice() else {
panic!("hosted plugin runtime should contribute one server");
};
let McpServerTransportConfig::StreamableHttp { http_headers, .. } = &server.transport else {
panic!("hosted plugin runtime should use streamable HTTP");
};
assert_eq!(
http_headers
.as_ref()
.and_then(|headers| headers.get("originator")),
Some(&"codex_work_desktop".to_string())
);
Ok(())
}
@@ -202,6 +202,7 @@ async fn raw_selected_plugin_contributions(
config, config,
&thread_init, &thread_init,
&thread_store, &thread_store,
"test_originator",
&available_environment_ids, &available_environment_ids,
)) ))
.await) .await)