mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Gate plugin MCP servers by auth route (#27459)
## Context Some plugins expose both Apps and MCP servers. This PR moves auth-aware surface projection into `core-plugins::PluginsManager`, so callers get a consistent effective plugin view. Later PRs narrow the conflict rule and update listing/install paths. The high level goal of this PR is to set up the plumbing to conditionally filter App/MCP in the plugin manager layer. We start by removing MCP servers when using SIWC/Codex-backend auth, and removing Apps when using API-key-style auth. This PR is now stacked on #27652, which contains only the constructor plumbing for seeding `PluginsManager` with the current auth mode. ## Stack - PR1: #27652 seed plugin manager auth at construction. - PR2: #27459 route plugin surfaces by auth mode. - PR3: #27607 dedupe plugin MCP servers by App declaration name. - PR4: #27602 preserve plugin Apps in connector listings. - PR5: #27461 skip install-time plugin MCP OAuth for matching App routes. ## Summary - API-key/non-ChatGPT routes hide plugin Apps and keep plugin MCPs. - ChatGPT/SIWC with Apps enabled keeps plugin Apps and suppresses MCPs for dual-surface plugins. - MCP-only plugins stay available for ChatGPT/SIWC sessions. - Cached plugin load outcomes are re-projected when auth mode changes. ## Validation ```bash cargo test -p codex-core-plugins plugin_auth_projection cargo test -p codex-core list_tool_suggest_discoverable_plugins git diff --check ```
This commit is contained in:
committed by
GitHub
Unverified
parent
5c8136f48a
commit
5d7db08b61
@@ -56,6 +56,17 @@ impl AuthMode {
|
||||
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this mode is backed by Codex services rather than a direct model API.
|
||||
pub fn uses_codex_backend(self) -> bool {
|
||||
match self {
|
||||
Self::Chatgpt
|
||||
| Self::ChatgptAuthTokens
|
||||
| Self::AgentIdentity
|
||||
| Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! experimental_reason_expr {
|
||||
|
||||
@@ -207,9 +207,20 @@ fn featured_plugin_ids_cache_key(
|
||||
|
||||
fn project_plugin_load_outcome_for_auth(
|
||||
outcome: PluginLoadOutcome,
|
||||
_auth_mode: Option<AuthMode>,
|
||||
auth_mode: Option<AuthMode>,
|
||||
) -> PluginLoadOutcome {
|
||||
outcome
|
||||
let apps_route_available = auth_mode.is_some_and(AuthMode::uses_codex_backend);
|
||||
let mut plugins = outcome.plugins().to_vec();
|
||||
for plugin in &mut plugins {
|
||||
if apps_route_available {
|
||||
if plugin.is_active() && !plugin.apps.is_empty() {
|
||||
plugin.mcp_servers.clear();
|
||||
}
|
||||
} else {
|
||||
plugin.apps.clear();
|
||||
}
|
||||
}
|
||||
PluginLoadOutcome::from_plugins(plugins)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -70,6 +70,187 @@ fn plugins_manager_tracks_auth_mode() {
|
||||
assert_eq!(manager_with_auth.auth_mode(), Some(AuthMode::Chatgpt));
|
||||
}
|
||||
|
||||
fn write_auth_projection_plugin(codex_home: &Path, name: &str, include_app: bool) {
|
||||
let plugin_root = codex_home
|
||||
.join("plugins/cache")
|
||||
.join("test")
|
||||
.join(name)
|
||||
.join("local");
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
&format!(r#"{{"name":"{name}"}}"#),
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join(".mcp.json"),
|
||||
&format!(
|
||||
r#"{{
|
||||
"mcpServers": {{
|
||||
"{name}": {{
|
||||
"type": "stdio",
|
||||
"command": "{name}-mcp"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
),
|
||||
);
|
||||
if include_app {
|
||||
write_file(
|
||||
&plugin_root.join(".app.json"),
|
||||
&format!(r#"{{"apps":{{"{name}":{{"id":"connector_{name}"}}}}}}"#),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_projection_config(codex_home: &Path) -> PluginsConfigInput {
|
||||
let config_toml = r#"[features]
|
||||
plugins = true
|
||||
|
||||
[plugins."sample@test"]
|
||||
enabled = true
|
||||
|
||||
[plugins."docs@test"]
|
||||
enabled = true
|
||||
"#
|
||||
.to_string();
|
||||
write_file(&codex_home.join(CONFIG_TOML_FILE), &config_toml);
|
||||
load_config(codex_home, codex_home).await
|
||||
}
|
||||
|
||||
fn sorted_effective_mcp_server_names(outcome: &PluginLoadOutcome) -> Vec<String> {
|
||||
let mut names = outcome
|
||||
.effective_mcp_servers()
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_auth_projection_hides_apps_without_chatgpt_auth() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::ApiKey),
|
||||
);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
|
||||
assert!(outcome.effective_apps().is_empty());
|
||||
assert_eq!(
|
||||
sorted_effective_mcp_server_names(&outcome),
|
||||
vec!["docs".to_string(), "sample".to_string()]
|
||||
);
|
||||
let sample = outcome
|
||||
.capability_summaries()
|
||||
.iter()
|
||||
.find(|plugin| plugin.config_name == "sample@test")
|
||||
.expect("sample plugin summary should exist");
|
||||
assert_eq!(sample.mcp_server_names, vec!["sample".to_string()]);
|
||||
assert!(sample.app_connector_ids.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_auth_projection_hides_dual_surface_mcp_with_chatgpt_apps_route() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
|
||||
assert_eq!(
|
||||
outcome.effective_apps(),
|
||||
vec![AppConnectorId("connector_sample".to_string())]
|
||||
);
|
||||
assert_eq!(
|
||||
sorted_effective_mcp_server_names(&outcome),
|
||||
vec!["docs".to_string()]
|
||||
);
|
||||
let sample = outcome
|
||||
.capability_summaries()
|
||||
.iter()
|
||||
.find(|plugin| plugin.config_name == "sample@test")
|
||||
.expect("sample plugin summary should exist");
|
||||
assert!(sample.mcp_server_names.is_empty());
|
||||
assert_eq!(
|
||||
sample.app_connector_ids,
|
||||
vec![AppConnectorId("connector_sample".to_string())]
|
||||
);
|
||||
let docs = outcome
|
||||
.capability_summaries()
|
||||
.iter()
|
||||
.find(|plugin| plugin.config_name == "docs@test")
|
||||
.expect("docs plugin summary should exist");
|
||||
assert_eq!(docs.mcp_server_names, vec!["docs".to_string()]);
|
||||
assert!(docs.app_connector_ids.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_auth_projection_hides_dual_surface_mcp_with_agent_identity_apps_route() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::AgentIdentity),
|
||||
);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
|
||||
assert_eq!(
|
||||
outcome.effective_apps(),
|
||||
vec![AppConnectorId("connector_sample".to_string())]
|
||||
);
|
||||
assert_eq!(
|
||||
sorted_effective_mcp_server_names(&outcome),
|
||||
vec!["docs".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_auth_projection_reprojects_cached_outcome_when_auth_changes() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_auth_projection_plugin(codex_home.path(), "sample", /*include_app*/ true);
|
||||
write_auth_projection_plugin(codex_home.path(), "docs", /*include_app*/ false);
|
||||
let config = auth_projection_config(codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
|
||||
let chatgpt_outcome = manager.plugins_for_config(&config).await;
|
||||
assert_eq!(
|
||||
sorted_effective_mcp_server_names(&chatgpt_outcome),
|
||||
vec!["docs".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
chatgpt_outcome.effective_apps(),
|
||||
vec![AppConnectorId("connector_sample".to_string())]
|
||||
);
|
||||
|
||||
assert!(manager.set_auth_mode(Some(AuthMode::ApiKey)));
|
||||
let api_key_outcome = manager.plugins_for_config(&config).await;
|
||||
|
||||
assert_eq!(
|
||||
sorted_effective_mcp_server_names(&api_key_outcome),
|
||||
vec!["docs".to_string(), "sample".to_string()]
|
||||
);
|
||||
assert!(api_key_outcome.effective_apps().is_empty());
|
||||
}
|
||||
|
||||
fn write_plugin_with_version(
|
||||
root: &Path,
|
||||
dir_name: &str,
|
||||
@@ -145,10 +326,14 @@ fn plugin_config_toml(enabled: bool, plugins_feature_enabled: bool) -> String {
|
||||
toml::to_string(&Value::Table(root)).expect("plugin test config should serialize")
|
||||
}
|
||||
|
||||
async fn load_plugins_from_config(config_toml: &str, codex_home: &Path) -> PluginLoadOutcome {
|
||||
async fn load_plugins_from_config(
|
||||
config_toml: &str,
|
||||
codex_home: &Path,
|
||||
auth_mode: Option<AuthMode>,
|
||||
) -> PluginLoadOutcome {
|
||||
write_file(&codex_home.join(CONFIG_TOML_FILE), config_toml);
|
||||
let config = load_config(codex_home, codex_home).await;
|
||||
PluginsManager::new(codex_home.to_path_buf())
|
||||
PluginsManager::new_with_options(codex_home.to_path_buf(), Some(Product::Codex), auth_mode)
|
||||
.plugins_for_config(&config)
|
||||
.await
|
||||
}
|
||||
@@ -242,6 +427,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -285,7 +471,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
tools: HashMap::new(),
|
||||
},
|
||||
)]),
|
||||
apps: vec![AppConnectorId("connector_example".to_string())],
|
||||
apps: Vec::new(),
|
||||
hook_sources: Vec::new(),
|
||||
hook_load_warnings: Vec::new(),
|
||||
error: None,
|
||||
@@ -299,7 +485,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
description: Some("Plugin that includes the sample MCP server and Skills".to_string(),),
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["sample".to_string()],
|
||||
app_connector_ids: vec![AppConnectorId("connector_example".to_string())],
|
||||
app_connector_ids: Vec::new(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -307,10 +493,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
vec![plugin_root.join("skills").abs()]
|
||||
);
|
||||
assert_eq!(outcome.effective_mcp_servers().len(), 1);
|
||||
assert_eq!(
|
||||
outcome.effective_apps(),
|
||||
vec![AppConnectorId("connector_example".to_string())]
|
||||
);
|
||||
assert!(outcome.effective_apps().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -360,7 +543,8 @@ disabled_tools = ["delete"]
|
||||
approval_mode = "approve"
|
||||
"#;
|
||||
|
||||
let outcome = load_plugins_from_config(config_toml, codex_home.path()).await;
|
||||
let outcome =
|
||||
load_plugins_from_config(config_toml, codex_home.path(), /*auth_mode*/ None).await;
|
||||
let server = outcome.plugins()[0]
|
||||
.mcp_servers
|
||||
.get("sample")
|
||||
@@ -620,7 +804,8 @@ enabled = false
|
||||
[plugins."sample@test"]
|
||||
enabled = true
|
||||
"#;
|
||||
let outcome = load_plugins_from_config(config_toml, codex_home.path()).await;
|
||||
let outcome =
|
||||
load_plugins_from_config(config_toml, codex_home.path(), /*auth_mode*/ None).await;
|
||||
let skill_path = std::fs::canonicalize(skill_path)
|
||||
.expect("skill path should canonicalize")
|
||||
.abs();
|
||||
@@ -660,7 +845,8 @@ enabled = false
|
||||
[plugins."sample@test"]
|
||||
enabled = true
|
||||
"#;
|
||||
let outcome = load_plugins_from_config(config_toml, codex_home.path()).await;
|
||||
let outcome =
|
||||
load_plugins_from_config(config_toml, codex_home.path(), /*auth_mode*/ None).await;
|
||||
|
||||
assert!(outcome.plugins()[0].disabled_skill_paths.is_empty());
|
||||
assert!(outcome.plugins()[0].has_enabled_skills);
|
||||
@@ -745,6 +931,7 @@ async fn capability_summary_sanitizes_plugin_descriptions_to_one_line() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -784,6 +971,7 @@ async fn capability_summary_truncates_overlong_plugin_descriptions() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -868,6 +1056,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -906,10 +1095,7 @@ async fn load_plugins_uses_manifest_configured_component_paths() {
|
||||
},
|
||||
)])
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.plugins()[0].apps,
|
||||
vec![AppConnectorId("connector_custom".to_string())]
|
||||
);
|
||||
assert!(outcome.plugins()[0].apps.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -983,6 +1169,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1018,10 +1205,7 @@ async fn load_plugins_ignores_manifest_component_paths_without_dot_slash() {
|
||||
},
|
||||
)])
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.plugins()[0].apps,
|
||||
vec![AppConnectorId("connector_default".to_string())]
|
||||
);
|
||||
assert!(outcome.plugins()[0].apps.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1051,6 +1235,7 @@ async fn load_plugins_ignores_invalid_manifest_skills_shape() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1090,6 +1275,7 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions
|
||||
/*enabled*/ false, /*plugins_feature_enabled*/ true,
|
||||
),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1162,6 +1348,7 @@ async fn effective_apps_dedupes_connector_ids_across_plugins() {
|
||||
let mut root = toml::map::Map::new();
|
||||
let mut features = toml::map::Map::new();
|
||||
features.insert("plugins".to_string(), Value::Boolean(true));
|
||||
features.insert("apps".to_string(), Value::Boolean(true));
|
||||
root.insert("features".to_string(), Value::Table(features));
|
||||
|
||||
let mut plugins = toml::map::Map::new();
|
||||
@@ -1178,7 +1365,8 @@ async fn effective_apps_dedupes_connector_ids_across_plugins() {
|
||||
let config_toml =
|
||||
toml::to_string(&Value::Table(root)).expect("plugin test config should serialize");
|
||||
|
||||
let outcome = load_plugins_from_config(&config_toml, codex_home.path()).await;
|
||||
let outcome =
|
||||
load_plugins_from_config(&config_toml, codex_home.path(), Some(AuthMode::Chatgpt)).await;
|
||||
|
||||
assert_eq!(
|
||||
outcome.effective_apps(),
|
||||
@@ -1221,6 +1409,7 @@ async fn effective_apps_preserves_app_config_order() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
Some(AuthMode::Chatgpt),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1484,6 +1673,7 @@ async fn load_plugins_rejects_invalid_plugin_keys() {
|
||||
let outcome = load_plugins_from_config(
|
||||
&toml::to_string(&Value::Table(root)).expect("plugin test config should serialize"),
|
||||
codex_home.path(),
|
||||
/*auth_mode*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::RemotePluginServiceConfig;
|
||||
use codex_core_plugins::remote::fetch_and_cache_global_remote_plugin_catalog;
|
||||
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_tools::DiscoverablePluginInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
@@ -23,7 +24,11 @@ async fn list_discoverable_plugins_with_auth(
|
||||
auth: Option<&codex_login::CodexAuth>,
|
||||
loaded_plugin_app_connector_ids: &[String],
|
||||
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
||||
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||
let plugins_manager = PluginsManager::new_with_options(
|
||||
config.codex_home.to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
auth.map(codex_login::CodexAuth::api_auth_mode),
|
||||
);
|
||||
list_discoverable_plugins_with_manager_and_auth(
|
||||
config,
|
||||
&plugins_manager,
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
@@ -226,7 +227,7 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
async fn explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount_with_connector_name(&server, "Google Calendar").await?;
|
||||
@@ -252,6 +253,105 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
build_apps_enabled_plugin_test_codex(&server, codex_home, apps_server.chatgpt_base_url)
|
||||
.await?;
|
||||
let codex = Arc::clone(&test_codex.codex);
|
||||
wait_for_mcp_server(&codex, CODEX_APPS_MCP_SERVER_NAME).await?;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![codex_protocol::user_input::UserInput::Mention {
|
||||
name: "sample".into(),
|
||||
path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let request = mock.single_request();
|
||||
let developer_messages = request.message_input_texts("developer");
|
||||
assert!(
|
||||
developer_messages
|
||||
.iter()
|
||||
.any(|text| text.contains("Skills from this plugin")),
|
||||
"expected plugin skills guidance: {developer_messages:?}"
|
||||
);
|
||||
assert!(
|
||||
!developer_messages
|
||||
.iter()
|
||||
.any(|text| text.contains("MCP servers from this plugin")),
|
||||
"expected plugin MCP guidance to be suppressed for ChatGPT auth: {developer_messages:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_messages
|
||||
.iter()
|
||||
.any(|text| text.contains("Apps from this plugin")),
|
||||
"expected visible plugin app guidance: {developer_messages:?}"
|
||||
);
|
||||
let request_body = request.body_json();
|
||||
let request_tools = tool_names(&request_body);
|
||||
assert!(
|
||||
request_tools
|
||||
.iter()
|
||||
.any(|name| name == "mcp__codex_apps__google_calendar"),
|
||||
"expected plugin app tools to become visible for this turn: {request_tools:?}"
|
||||
);
|
||||
assert!(
|
||||
request.tool_by_name("mcp__sample", "echo").is_none(),
|
||||
"expected plugin MCP tool to be suppressed for ChatGPT auth"
|
||||
);
|
||||
let calendar_tool = request
|
||||
.tool_by_name("mcp__codex_apps__google_calendar", "_create_event")
|
||||
.expect("plugin app tool should be present");
|
||||
let calendar_description = calendar_tool
|
||||
.get("description")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("plugin app tool description should be present");
|
||||
assert!(
|
||||
calendar_description.contains("This tool is part of plugin `sample`."),
|
||||
"expected plugin app provenance in tool description: {calendar_description:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn explicit_plugin_mentions_use_mcp_for_api_key_dual_surface_plugins() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let codex_home = Arc::new(TempDir::new()?);
|
||||
let rmcp_test_server_bin = match stdio_server_bin() {
|
||||
Ok(bin) => bin,
|
||||
Err(err) => {
|
||||
eprintln!("test_stdio_server binary not available, skipping test: {err}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
write_plugin_skill_plugin(codex_home.as_ref());
|
||||
write_plugin_mcp_plugin(codex_home.as_ref(), &rmcp_test_server_bin);
|
||||
write_plugin_app_plugin(codex_home.as_ref());
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(codex_home)
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test_codex = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create new conversation");
|
||||
let codex = Arc::clone(&test_codex.codex);
|
||||
wait_for_mcp_server(&codex, "sample").await?;
|
||||
|
||||
codex
|
||||
@@ -283,18 +383,18 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
"expected visible plugin MCP guidance: {developer_messages:?}"
|
||||
);
|
||||
assert!(
|
||||
developer_messages
|
||||
!developer_messages
|
||||
.iter()
|
||||
.any(|text| text.contains("Apps from this plugin")),
|
||||
"expected visible plugin app guidance: {developer_messages:?}"
|
||||
"expected plugin app guidance to be suppressed for API-key auth: {developer_messages:?}"
|
||||
);
|
||||
let request_body = request.body_json();
|
||||
let request_tools = tool_names(&request_body);
|
||||
assert!(
|
||||
request_tools
|
||||
!request_tools
|
||||
.iter()
|
||||
.any(|name| name == "mcp__codex_apps__google_calendar"),
|
||||
"expected plugin app tools to become visible for this turn: {request_tools:?}"
|
||||
"expected plugin app tools to be hidden for API-key auth: {request_tools:?}"
|
||||
);
|
||||
let echo_tool = request
|
||||
.tool_by_name("mcp__sample", "echo")
|
||||
@@ -307,17 +407,6 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> {
|
||||
echo_description.contains("This tool is part of plugin `sample`."),
|
||||
"expected plugin MCP provenance in tool description: {echo_description:?}"
|
||||
);
|
||||
let calendar_tool = request
|
||||
.tool_by_name("mcp__codex_apps__google_calendar", "_create_event")
|
||||
.expect("plugin app tool should be present");
|
||||
let calendar_description = calendar_tool
|
||||
.get("description")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("plugin app tool description should be present");
|
||||
assert!(
|
||||
calendar_description.contains("This tool is part of plugin `sample`."),
|
||||
"expected plugin app provenance in tool description: {calendar_description:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -347,13 +347,7 @@ impl CodexAuth {
|
||||
}
|
||||
|
||||
pub fn uses_codex_backend(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Chatgpt(_)
|
||||
| Self::ChatgptAuthTokens(_)
|
||||
| Self::AgentIdentity(_)
|
||||
| Self::PersonalAccessToken(_)
|
||||
)
|
||||
self.api_auth_mode().uses_codex_backend()
|
||||
}
|
||||
|
||||
pub fn is_external_chatgpt_tokens(&self) -> bool {
|
||||
@@ -2079,15 +2073,8 @@ impl AuthManager {
|
||||
}
|
||||
|
||||
pub fn current_auth_uses_codex_backend(&self) -> bool {
|
||||
matches!(
|
||||
self.auth_mode(),
|
||||
Some(
|
||||
AuthMode::Chatgpt
|
||||
| AuthMode::ChatgptAuthTokens
|
||||
| AuthMode::AgentIdentity
|
||||
| AuthMode::PersonalAccessToken
|
||||
)
|
||||
)
|
||||
self.get_api_auth_mode()
|
||||
.is_some_and(AuthMode::uses_codex_backend)
|
||||
}
|
||||
|
||||
fn should_refresh_proactively(auth: &CodexAuth) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user