mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user