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

+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(
chatgpt_base_url: &str,
apps_mcp_product_sku: Option<&str>,
originator: Option<&str>,
) -> McpServerConfig {
mcp_server_config_for_url(
codex_apps_mcp_url_for_base_url(chatgpt_base_url),
apps_mcp_product_sku,
originator,
McpServerAuth::ChatGpt,
)
}
@@ -505,6 +507,7 @@ pub fn codex_apps_mcp_server_config(
pub fn hosted_plugin_runtime_mcp_server_config(
chatgpt_base_url: &str,
apps_mcp_product_sku: Option<&str>,
originator: Option<&str>,
) -> McpServerConfig {
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") {
@@ -515,6 +518,7 @@ pub fn hosted_plugin_runtime_mcp_server_config(
mcp_server_config_for_url(
format!("{base_url}/ps/mcp"),
apps_mcp_product_sku,
originator,
McpServerAuth::ChatGpt,
)
}
@@ -522,19 +526,21 @@ pub fn hosted_plugin_runtime_mcp_server_config(
fn mcp_server_config_for_url(
url: String,
apps_mcp_product_sku: Option<&str>,
originator: Option<&str>,
auth_mode: McpServerAuth,
) -> McpServerConfig {
let product_sku = apps_mcp_product_sku.unwrap_or(DEFAULT_CODEX_APPS_MCP_PRODUCT_SKU);
let http_headers = Some(HashMap::from([(
"X-OpenAI-Product-Sku".to_string(),
product_sku.to_string(),
)]));
let mut http_headers =
HashMap::from([("X-OpenAI-Product-Sku".to_string(), product_sku.to_string())]);
if let Some(originator) = originator {
http_headers.insert("originator".to_string(), originator.to_string());
}
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var: codex_apps_mcp_bearer_token_env_var(),
http_headers,
http_headers: Some(http_headers),
env_http_headers: None,
},
auth: auth_mode,
+75 -5
View File
@@ -130,7 +130,11 @@ fn tool_plugin_provenance_collects_app_and_mcp_sources() {
"alpha".to_string(),
McpPluginAttribution::new("alpha@test".to_string(), "alpha-plugin".to_string()),
/*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.connector_snapshot =
@@ -197,7 +201,11 @@ fn selected_mcp_attribution_does_not_join_an_unrelated_local_summary() {
"Executor GitHub".to_string(),
),
/*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.connector_snapshot =
@@ -252,8 +260,11 @@ fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() {
#[test]
fn codex_apps_server_config_uses_legacy_codex_apps_path() {
let config =
codex_apps_mcp_server_config("https://chatgpt.com", /*apps_mcp_product_sku*/ None);
let config = codex_apps_mcp_server_config(
"https://chatgpt.com",
/*apps_mcp_product_sku*/ None,
/*originator*/ None,
);
let url = match &config.transport {
McpServerTransportConfig::StreamableHttp { url, .. } => url,
_ => 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");
}
#[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]
fn codex_apps_server_config_sets_product_sku_header() {
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 {
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]
async fn effective_mcp_servers_preserve_runtime_servers() {
let codex_home = tempfile::tempdir().expect("tempdir");
@@ -352,6 +421,7 @@ async fn effective_mcp_servers_preserve_runtime_servers() {
codex_apps_mcp_server_config(
&config.chatgpt_base_url,
config.apps_mcp_product_sku.as_deref(),
/*originator*/ None,
),
));
config.mcp_server_catalog = catalog.build();