mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Thread first-party plugin execution context
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use crate::app_mcp_routing::apply_app_mcp_routing_policy;
|
||||
use crate::app_mcp_routing::apps_route_available;
|
||||
use crate::is_openai_curated_marketplace_name;
|
||||
use crate::manifest::PluginManifest;
|
||||
use crate::manifest::PluginManifestHooks;
|
||||
use crate::manifest::PluginManifestMcpServers;
|
||||
use crate::manifest::PluginManifestPaths;
|
||||
@@ -10,6 +11,8 @@ use crate::marketplace::list_marketplaces;
|
||||
use crate::marketplace::load_marketplace;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::startup_sync::curated_plugins_repo_path;
|
||||
use crate::startup_sync::read_curated_plugins_sha;
|
||||
use crate::store::PluginStore;
|
||||
use crate::store::plugin_version_for_source;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
@@ -654,7 +657,7 @@ async fn load_plugin(
|
||||
let active_plugin_root = plugin_id
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|plugin_id| store.active_plugin_root(plugin_id));
|
||||
.and_then(|plugin_id| active_plugin_root(plugin_id, store));
|
||||
let root = active_plugin_root
|
||||
.clone()
|
||||
.unwrap_or_else(|| match &plugin_id {
|
||||
@@ -665,6 +668,7 @@ async fn load_plugin(
|
||||
config_name,
|
||||
manifest_name: None,
|
||||
manifest_description: None,
|
||||
is_first_party: false,
|
||||
root,
|
||||
enabled: plugin.enabled,
|
||||
skill_roots: Vec::new(),
|
||||
@@ -705,6 +709,7 @@ async fn load_plugin(
|
||||
return loaded_plugin;
|
||||
};
|
||||
|
||||
loaded_plugin.is_first_party = is_openai_curated_plugin(&loaded_plugin_id, &manifest, store);
|
||||
let manifest_paths = &manifest.paths;
|
||||
match scope {
|
||||
PluginLoadScope::AllCapabilities {
|
||||
@@ -746,6 +751,59 @@ async fn load_plugin(
|
||||
loaded_plugin
|
||||
}
|
||||
|
||||
fn active_plugin_root(plugin_id: &PluginId, store: &PluginStore) -> Option<AbsolutePathBuf> {
|
||||
if is_openai_curated_marketplace_name(&plugin_id.marketplace_name)
|
||||
&& let Some(curated_sha) = read_curated_plugins_sha(store.codex_home())
|
||||
{
|
||||
let expected_version = curated_plugin_cache_version(&curated_sha);
|
||||
let expected_root = store.plugin_root(plugin_id, &expected_version);
|
||||
if expected_root.as_path().is_dir() {
|
||||
return Some(expected_root);
|
||||
}
|
||||
}
|
||||
store.active_plugin_root(plugin_id)
|
||||
}
|
||||
|
||||
fn is_openai_curated_plugin(
|
||||
plugin_id: &PluginId,
|
||||
manifest: &PluginManifest,
|
||||
store: &PluginStore,
|
||||
) -> bool {
|
||||
if !is_openai_curated_marketplace_name(&plugin_id.marketplace_name)
|
||||
|| manifest
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.developer_name.as_deref())
|
||||
!= Some("OpenAI")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(curated_sha) = read_curated_plugins_sha(store.codex_home()) else {
|
||||
return false;
|
||||
};
|
||||
let expected_version = curated_plugin_cache_version(&curated_sha);
|
||||
if !store
|
||||
.plugin_root(plugin_id, &expected_version)
|
||||
.as_path()
|
||||
.is_dir()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let marketplace_path =
|
||||
curated_plugins_repo_path(store.codex_home()).join(".agents/plugins/marketplace.json");
|
||||
let Ok(marketplace_path) = AbsolutePathBuf::try_from(marketplace_path) else {
|
||||
return false;
|
||||
};
|
||||
load_marketplace(&marketplace_path).is_ok_and(|marketplace| {
|
||||
marketplace
|
||||
.plugins
|
||||
.iter()
|
||||
.any(|plugin| plugin.name == plugin_id.plugin_name)
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_plugin_mcp_server_policy(config: &mut McpServerConfig, policy: &PluginMcpServerConfig) {
|
||||
config.enabled = policy.enabled;
|
||||
if let Some(approval_mode) = policy.default_tools_approval_mode {
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::LoadedPlugin;
|
||||
use super::PluginLoadOutcome;
|
||||
use crate::app_mcp_routing::apply_app_mcp_routing_policy;
|
||||
use crate::installed_marketplaces::installed_marketplace_roots_from_layer_stack;
|
||||
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use crate::is_openai_curated_marketplace_name;
|
||||
use crate::loader::PluginHookLoadOutcome;
|
||||
use crate::loader::configured_curated_plugin_ids_from_codex_home;
|
||||
@@ -1091,6 +1092,7 @@ impl PluginsManager {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
self.validate_curated_marketplace_install_source(&request.marketplace_path, &resolved)?;
|
||||
let plugin_id = resolved.plugin_id.clone();
|
||||
match self.install_resolved_plugin(resolved).await {
|
||||
Ok(outcome) => Ok(outcome),
|
||||
@@ -1122,6 +1124,7 @@ impl PluginsManager {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
self.validate_curated_marketplace_install_source(&request.marketplace_path, &resolved)?;
|
||||
let plugin_id = resolved.plugin_id.as_key();
|
||||
// This only forwards the backend mutation before the local install flow.
|
||||
if let Err(err) = crate::remote_legacy::enable_remote_plugin(
|
||||
@@ -1208,6 +1211,33 @@ impl PluginsManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_curated_marketplace_install_source(
|
||||
&self,
|
||||
marketplace_path: &AbsolutePathBuf,
|
||||
resolved: &ResolvedMarketplacePlugin,
|
||||
) -> Result<(), PluginInstallError> {
|
||||
if resolved.plugin_id.marketplace_name != OPENAI_CURATED_MARKETPLACE_NAME {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let expected_path = curated_plugins_repo_path(self.codex_home.as_path())
|
||||
.join(".agents/plugins/marketplace.json");
|
||||
let expected_path = AbsolutePathBuf::try_from(expected_path)
|
||||
.ok()
|
||||
.and_then(|path| path.canonicalize().ok());
|
||||
let requested_path = marketplace_path.canonicalize().ok();
|
||||
if requested_path
|
||||
.zip(expected_path)
|
||||
.is_none_or(|(requested, expected)| requested != expected)
|
||||
{
|
||||
return Err(PluginStoreError::Invalid(format!(
|
||||
"marketplace '{OPENAI_CURATED_MARKETPLACE_NAME}' can only be installed from the synced curated marketplace"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_resolved_plugin(
|
||||
&self,
|
||||
resolved: ResolvedMarketplacePlugin,
|
||||
|
||||
@@ -574,7 +574,10 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "sample",
|
||||
"description": "Plugin that includes the sample MCP server and Skills"
|
||||
"description": "Plugin that includes the sample MCP server and Skills",
|
||||
"interface": {
|
||||
"developerName": "OpenAI"
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
write_file(
|
||||
@@ -622,6 +625,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
manifest_description: Some(
|
||||
"Plugin that includes the sample MCP server and Skills".to_string(),
|
||||
),
|
||||
is_first_party: false,
|
||||
root: AbsolutePathBuf::try_from(plugin_root.clone()).unwrap(),
|
||||
enabled: true,
|
||||
skill_roots: vec![plugin_root.join("skills").abs()],
|
||||
@@ -1586,6 +1590,7 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions
|
||||
config_name: "sample@test".to_string(),
|
||||
manifest_name: None,
|
||||
manifest_description: None,
|
||||
is_first_party: false,
|
||||
root: AbsolutePathBuf::try_from(plugin_root).unwrap(),
|
||||
enabled: false,
|
||||
skill_roots: Vec::new(),
|
||||
@@ -1754,6 +1759,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
config_name: config_name.to_string(),
|
||||
manifest_name: Some(manifest_name.to_string()),
|
||||
manifest_description: None,
|
||||
is_first_party: false,
|
||||
root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(),
|
||||
enabled: true,
|
||||
skill_roots: Vec::new(),
|
||||
@@ -2077,6 +2083,38 @@ async fn install_openai_curated_plugin_uses_short_sha_cache_version() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn install_plugin_rejects_spoofed_openai_curated_marketplace() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let curated_root = curated_plugins_repo_path(tmp.path());
|
||||
write_openai_curated_marketplace(&curated_root, &["slack"]);
|
||||
write_curated_plugin_sha(tmp.path(), TEST_CURATED_PLUGIN_SHA);
|
||||
let spoofed_root = tmp.path().join("spoofed-marketplace");
|
||||
write_openai_curated_marketplace(&spoofed_root, &["slack"]);
|
||||
|
||||
let err = PluginsManager::new(tmp.path().to_path_buf())
|
||||
.install_plugin(PluginInstallRequest {
|
||||
plugin_name: "slack".to_string(),
|
||||
marketplace_path: AbsolutePathBuf::try_from(
|
||||
spoofed_root.join(".agents/plugins/marketplace.json"),
|
||||
)
|
||||
.unwrap(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.is_invalid_request());
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("can only be installed from the synced curated marketplace")
|
||||
);
|
||||
assert!(
|
||||
!tmp.path()
|
||||
.join("plugins/cache/openai-curated/slack")
|
||||
.exists()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn install_plugin_uses_manifest_version_for_non_curated_plugins() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct PluginInstallResult {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginStore {
|
||||
codex_home: AbsolutePathBuf,
|
||||
root: AbsolutePathBuf,
|
||||
data_root: AbsolutePathBuf,
|
||||
}
|
||||
@@ -42,8 +43,18 @@ impl PluginStore {
|
||||
let data_root =
|
||||
AbsolutePathBuf::from_absolute_path_checked(codex_home.join(PLUGINS_DATA_DIR))
|
||||
.map_err(|err| PluginStoreError::io("failed to resolve plugin data root", err))?;
|
||||
let codex_home = AbsolutePathBuf::from_absolute_path_checked(codex_home)
|
||||
.map_err(|err| PluginStoreError::io("failed to resolve Codex home", err))?;
|
||||
|
||||
Ok(Self { root, data_root })
|
||||
Ok(Self {
|
||||
codex_home,
|
||||
root,
|
||||
data_root,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn codex_home(&self) -> &AbsolutePathBuf {
|
||||
&self.codex_home
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &AbsolutePathBuf {
|
||||
|
||||
@@ -136,6 +136,7 @@ pub(super) async fn spawn_review_thread(
|
||||
turn_metadata_state,
|
||||
extension_data,
|
||||
turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.snapshot.clone()),
|
||||
first_party_plugin_roots: Arc::clone(&parent_turn_context.first_party_plugin_roots),
|
||||
turn_timing_state: Arc::new(TurnTimingState::default()),
|
||||
terminal_error: Arc::new(Mutex::new(None)),
|
||||
server_model_warning_emitted: AtomicBool::new(false),
|
||||
|
||||
@@ -5048,6 +5048,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
.plugins_manager
|
||||
.plugins_for_config(&per_turn_config.plugins_config_input())
|
||||
.await;
|
||||
let first_party_plugin_roots = plugin_outcome.effective_first_party_plugin_roots();
|
||||
let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots();
|
||||
let skills_input =
|
||||
crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots);
|
||||
@@ -5075,6 +5076,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
session_configuration.cwd().clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_snapshot,
|
||||
first_party_plugin_roots,
|
||||
);
|
||||
|
||||
let session = Session {
|
||||
@@ -7091,6 +7093,7 @@ where
|
||||
.plugins_manager
|
||||
.plugins_for_config(&per_turn_config.plugins_config_input())
|
||||
.await;
|
||||
let first_party_plugin_roots = plugin_outcome.effective_first_party_plugin_roots();
|
||||
let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots();
|
||||
let skills_input =
|
||||
crate::skills_load_input_from_config(&per_turn_config, effective_skill_roots);
|
||||
@@ -7118,6 +7121,7 @@ where
|
||||
session_configuration.cwd().clone(),
|
||||
"turn_id".to_string(),
|
||||
skills_snapshot,
|
||||
first_party_plugin_roots,
|
||||
));
|
||||
|
||||
let session = Arc::new(Session {
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_core_skills::HostSkillsSnapshot;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_model_provider::SharedModelProvider;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_plugin::FirstPartyPluginRoot;
|
||||
use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -138,6 +139,7 @@ pub struct TurnContext {
|
||||
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
|
||||
pub(crate) extension_data: Arc<codex_extension_api::ExtensionData>,
|
||||
pub(crate) turn_skills: TurnSkillsContext,
|
||||
pub(crate) first_party_plugin_roots: Arc<Vec<FirstPartyPluginRoot>>,
|
||||
pub(crate) turn_timing_state: Arc<TurnTimingState>,
|
||||
pub(crate) terminal_error: Arc<Mutex<Option<String>>>,
|
||||
pub(crate) server_model_warning_emitted: AtomicBool,
|
||||
@@ -289,6 +291,7 @@ impl TurnContext {
|
||||
turn_metadata_state: self.turn_metadata_state.clone(),
|
||||
extension_data: Arc::clone(&self.extension_data),
|
||||
turn_skills: self.turn_skills.clone(),
|
||||
first_party_plugin_roots: Arc::clone(&self.first_party_plugin_roots),
|
||||
turn_timing_state: Arc::clone(&self.turn_timing_state),
|
||||
terminal_error: Arc::clone(&self.terminal_error),
|
||||
server_model_warning_emitted: AtomicBool::new(
|
||||
@@ -494,6 +497,7 @@ impl Session {
|
||||
cwd: AbsolutePathBuf,
|
||||
sub_id: String,
|
||||
skills_snapshot: HostSkillsSnapshot,
|
||||
first_party_plugin_roots: Vec<FirstPartyPluginRoot>,
|
||||
) -> TurnContext {
|
||||
let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort();
|
||||
let reasoning_summary = session_configuration
|
||||
@@ -575,6 +579,7 @@ impl Session {
|
||||
turn_metadata_state,
|
||||
extension_data,
|
||||
turn_skills: TurnSkillsContext::new(skills_snapshot),
|
||||
first_party_plugin_roots: Arc::new(first_party_plugin_roots),
|
||||
turn_timing_state: Arc::new(TurnTimingState::default()),
|
||||
terminal_error: Arc::new(Mutex::new(None)),
|
||||
server_model_warning_emitted: AtomicBool::new(false),
|
||||
@@ -727,6 +732,7 @@ impl Session {
|
||||
.plugins_manager
|
||||
.plugins_for_config(&per_turn_config.plugins_config_input())
|
||||
.await;
|
||||
let first_party_plugin_roots = plugin_outcome.effective_first_party_plugin_roots();
|
||||
let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots();
|
||||
let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots);
|
||||
let fs = primary_turn_environment
|
||||
@@ -764,6 +770,7 @@ impl Session {
|
||||
cwd,
|
||||
sub_id,
|
||||
skills_snapshot,
|
||||
first_party_plugin_roots,
|
||||
);
|
||||
turn_context.realtime_active = self.conversation.running_state().await.is_some();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ mod provider;
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
pub use load_outcome::EffectiveSkillRoots;
|
||||
pub use load_outcome::FirstPartyPluginRoot;
|
||||
pub use load_outcome::LoadedPlugin;
|
||||
pub use load_outcome::PluginLoadOutcome;
|
||||
pub use load_outcome::prompt_safe_plugin_description;
|
||||
|
||||
@@ -11,6 +11,11 @@ use crate::PluginHookSource;
|
||||
use crate::app_connector_ids_from_declarations;
|
||||
|
||||
const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FirstPartyPluginRoot {
|
||||
pub plugin_id: String,
|
||||
pub plugin_root: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
/// A plugin that was loaded from disk, including merged MCP server definitions.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -18,6 +23,7 @@ pub struct LoadedPlugin<M> {
|
||||
pub config_name: String,
|
||||
pub manifest_name: Option<String>,
|
||||
pub manifest_description: Option<String>,
|
||||
pub is_first_party: bool,
|
||||
pub root: AbsolutePathBuf,
|
||||
pub enabled: bool,
|
||||
pub skill_roots: Vec<AbsolutePathBuf>,
|
||||
@@ -141,6 +147,26 @@ impl<M: Clone> PluginLoadOutcome<M> {
|
||||
skill_roots
|
||||
}
|
||||
|
||||
pub fn effective_first_party_plugin_roots(&self) -> Vec<FirstPartyPluginRoot> {
|
||||
let mut plugin_roots = Vec::new();
|
||||
let mut seen_paths = HashSet::new();
|
||||
for plugin in self
|
||||
.plugins
|
||||
.iter()
|
||||
.filter(|plugin| plugin.is_active() && plugin.is_first_party)
|
||||
{
|
||||
if seen_paths.insert(plugin.root.clone()) {
|
||||
plugin_roots.push(FirstPartyPluginRoot {
|
||||
plugin_id: plugin.config_name.clone(),
|
||||
plugin_root: plugin.root.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
plugin_roots.sort_unstable_by(|a, b| a.plugin_root.cmp(&b.plugin_root));
|
||||
plugin_roots
|
||||
}
|
||||
|
||||
pub fn effective_mcp_servers(&self) -> HashMap<String, M> {
|
||||
let mut mcp_servers = HashMap::new();
|
||||
for plugin in self.plugins.iter().filter(|plugin| plugin.is_active()) {
|
||||
@@ -219,6 +245,7 @@ mod tests {
|
||||
config_name: config_name.to_string(),
|
||||
manifest_name: None,
|
||||
manifest_description: None,
|
||||
is_first_party: false,
|
||||
root: test_path(config_name),
|
||||
enabled: true,
|
||||
skill_roots,
|
||||
@@ -249,4 +276,38 @@ mod tests {
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_first_party_plugin_roots_only_includes_active_openai_plugins() {
|
||||
let shared_root = test_path("shared-plugin");
|
||||
let mut first_party = loaded_plugin("first@test", Vec::new());
|
||||
first_party.root = shared_root.clone();
|
||||
first_party.is_first_party = true;
|
||||
let mut duplicate = loaded_plugin("duplicate@test", Vec::new());
|
||||
duplicate.root = shared_root.clone();
|
||||
duplicate.is_first_party = true;
|
||||
let third_party = loaded_plugin("third@test", Vec::new());
|
||||
let mut disabled = loaded_plugin("disabled@test", Vec::new());
|
||||
disabled.is_first_party = true;
|
||||
disabled.enabled = false;
|
||||
let mut broken = loaded_plugin("broken@test", Vec::new());
|
||||
broken.is_first_party = true;
|
||||
broken.error = Some("broken".to_string());
|
||||
|
||||
let outcome = PluginLoadOutcome::from_plugins(vec![
|
||||
first_party,
|
||||
duplicate,
|
||||
third_party,
|
||||
disabled,
|
||||
broken,
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
outcome.effective_first_party_plugin_roots(),
|
||||
vec![FirstPartyPluginRoot {
|
||||
plugin_id: "first@test".to_string(),
|
||||
plugin_root: shared_root,
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user