mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Restrict plugin migration to home scope (#39663)
## Why Plugin imports persist user-global enabled state. Repository-controlled settings must not be allowed to select executable plugin content for installation. ## What changed - Detect plugin migrations only from home-scoped external agent settings. - Reject plugin imports with a non-empty project `cwd` before loading Codex configuration, while continuing to treat an empty `cwd` as home scope. ## Testing - Cover repository-scoped detection with remote, installed, and project-relative marketplaces. - Verify that forged project-scoped imports fail without writing `config.toml`. GitOrigin-RevId: 53cb46ae049cb49283187312d475d40cf42e35ab
This commit is contained in:
1 parent
240bbfc14a
commit
97c82c0900
4 files changed
+74
-115
No files matched your search
@@ -327,7 +327,9 @@ impl ExternalAgentConfigService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.source.supports_plugin_migration(settings.as_ref()) {
|
// Plugin import persists user-global enabled state, so repository-controlled
|
||||||
|
// settings must never be treated as plugin installation authority.
|
||||||
|
if scope.is_home() && self.source.supports_plugin_migration(settings.as_ref()) {
|
||||||
match ConfigBuilder::default()
|
match ConfigBuilder::default()
|
||||||
.codex_home(self.codex_home.clone())
|
.codex_home(self.codex_home.clone())
|
||||||
.fallback_cwd(Some(self.codex_home.clone()))
|
.fallback_cwd(Some(self.codex_home.clone()))
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ impl ExternalAgentConfigService {
|
|||||||
&self,
|
&self,
|
||||||
cwd: Option<&Path>,
|
cwd: Option<&Path>,
|
||||||
) -> io::Result<BTreeMap<String, MarketplaceImportSource>> {
|
) -> io::Result<BTreeMap<String, MarketplaceImportSource>> {
|
||||||
let Some(scope) = MigrationScope::from_cwd(cwd)? else {
|
// Plugin installs are user-global. Only missing or empty `cwd` values
|
||||||
return Ok(BTreeMap::new());
|
// denote home scope; a project scope must not select executable content.
|
||||||
};
|
if cwd.is_some_and(|cwd| !cwd.as_os_str().is_empty()) {
|
||||||
let source_root = scope
|
return Err(invalid_data_error(
|
||||||
.repo_root()
|
"repository-scoped plugin migration is not allowed".to_string(),
|
||||||
.unwrap_or(self.external_agent_home.as_path());
|
));
|
||||||
|
}
|
||||||
|
let scope = MigrationScope::home();
|
||||||
let source_settings = self.source_settings(&scope);
|
let source_settings = self.source_settings(&scope);
|
||||||
self.source.marketplace_import_sources(
|
self.source.marketplace_import_sources(
|
||||||
self.external_agent_home.as_path(),
|
self.external_agent_home.as_path(),
|
||||||
source_root,
|
self.external_agent_home.as_path(),
|
||||||
&source_settings,
|
&source_settings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -86,11 +88,13 @@ impl ExternalAgentConfigService {
|
|||||||
cwd: Option<&Path>,
|
cwd: Option<&Path>,
|
||||||
details: Option<MigrationDetails>,
|
details: Option<MigrationDetails>,
|
||||||
) -> io::Result<PluginImportOutcome> {
|
) -> io::Result<PluginImportOutcome> {
|
||||||
|
let cwd = cwd.filter(|cwd| !cwd.as_os_str().is_empty());
|
||||||
let Some(MigrationDetails { plugins, .. }) = details else {
|
let Some(MigrationDetails { plugins, .. }) = details else {
|
||||||
return Err(invalid_data_error(
|
return Err(invalid_data_error(
|
||||||
"plugins migration item is missing details".to_string(),
|
"plugins migration item is missing details".to_string(),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
let import_sources = self.marketplace_import_sources(cwd)?;
|
||||||
let config = ConfigBuilder::default()
|
let config = ConfigBuilder::default()
|
||||||
.codex_home(self.codex_home.clone())
|
.codex_home(self.codex_home.clone())
|
||||||
.fallback_cwd(Some(
|
.fallback_cwd(Some(
|
||||||
@@ -120,7 +124,6 @@ impl ExternalAgentConfigService {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|marketplace| (marketplace.name, marketplace.path))
|
.map(|marketplace| (marketplace.name, marketplace.path))
|
||||||
.collect::<BTreeMap<_, _>>();
|
.collect::<BTreeMap<_, _>>();
|
||||||
let import_sources = self.marketplace_import_sources(cwd)?;
|
|
||||||
for plugin_group in plugins {
|
for plugin_group in plugins {
|
||||||
let marketplace_name = plugin_group.marketplace_name.clone();
|
let marketplace_name = plugin_group.marketplace_name.clone();
|
||||||
let plugin_names = plugin_group.plugin_names;
|
let plugin_names = plugin_group.plugin_names;
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ async fn detect_home_plugins_uses_local_settings_over_project_settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn detect_repo_skips_plugins_that_are_already_configured_in_codex() {
|
async fn detect_repo_skips_plugins_from_remote_marketplace() {
|
||||||
let root = TempDir::new().expect("create tempdir");
|
let root = TempDir::new().expect("create tempdir");
|
||||||
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
||||||
let codex_home = root.path().join(".codex");
|
let codex_home = root.path().join(".codex");
|
||||||
@@ -435,27 +435,7 @@ enabled = true
|
|||||||
.await
|
.await
|
||||||
.expect("detect");
|
.expect("detect");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||||
items,
|
|
||||||
vec![ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
|
||||||
description: format!(
|
|
||||||
"Migrate enabled plugins from {}",
|
|
||||||
repo_root
|
|
||||||
.join(EXTERNAL_AGENT_DIR)
|
|
||||||
.join("settings.json")
|
|
||||||
.display()
|
|
||||||
),
|
|
||||||
cwd: Some(repo_root),
|
|
||||||
details: Some(MigrationDetails {
|
|
||||||
plugins: vec![PluginsMigration {
|
|
||||||
marketplace_name: "acme-tools".to_string(),
|
|
||||||
plugin_names: vec!["deployer".to_string()],
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -559,7 +539,7 @@ async fn import_plugins_requires_details() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn detect_repo_does_not_skip_plugins_only_configured_in_project_codex() {
|
async fn detect_repo_skips_plugins_only_configured_in_project_codex() {
|
||||||
let root = TempDir::new().expect("create tempdir");
|
let root = TempDir::new().expect("create tempdir");
|
||||||
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
||||||
let codex_home = root.path().join(".codex");
|
let codex_home = root.path().join(".codex");
|
||||||
@@ -600,27 +580,7 @@ enabled = true
|
|||||||
.await
|
.await
|
||||||
.expect("detect");
|
.expect("detect");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||||
items,
|
|
||||||
vec![ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
|
||||||
description: format!(
|
|
||||||
"Migrate enabled plugins from {}",
|
|
||||||
repo_root
|
|
||||||
.join(EXTERNAL_AGENT_DIR)
|
|
||||||
.join("settings.json")
|
|
||||||
.display()
|
|
||||||
),
|
|
||||||
cwd: Some(repo_root),
|
|
||||||
details: Some(MigrationDetails {
|
|
||||||
plugins: vec![PluginsMigration {
|
|
||||||
marketplace_name: "acme-tools".to_string(),
|
|
||||||
plugin_names: vec!["formatter".to_string()],
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -681,7 +641,7 @@ async fn detect_home_skips_plugins_with_invalid_marketplace_source() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn detect_repo_filters_plugins_against_installed_marketplace() {
|
async fn detect_repo_skips_plugins_even_with_installed_marketplace() {
|
||||||
let root = TempDir::new().expect("create tempdir");
|
let root = TempDir::new().expect("create tempdir");
|
||||||
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
||||||
let codex_home = root.path().join(".codex");
|
let codex_home = root.path().join(".codex");
|
||||||
@@ -787,25 +747,5 @@ source = "owner/debug-marketplace"
|
|||||||
.await
|
.await
|
||||||
.expect("detect");
|
.expect("detect");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||||
items,
|
|
||||||
vec![ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
|
||||||
description: format!(
|
|
||||||
"Migrate enabled plugins from {}",
|
|
||||||
repo_root
|
|
||||||
.join(EXTERNAL_AGENT_DIR)
|
|
||||||
.join("settings.json")
|
|
||||||
.display()
|
|
||||||
),
|
|
||||||
cwd: Some(repo_root),
|
|
||||||
details: Some(MigrationDetails {
|
|
||||||
plugins: vec![PluginsMigration {
|
|
||||||
marketplace_name: "debug".to_string(),
|
|
||||||
plugin_names: vec!["available".to_string()],
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ async fn import_plugins_defers_marketplace_source_validation_to_add_marketplace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn import_plugins_supports_external_agent_plugin_marketplace_layout() {
|
async fn import_plugins_treats_empty_cwd_as_home_scope() {
|
||||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||||
let marketplace_root = external_agent_home.join("my-marketplace");
|
let marketplace_root = external_agent_home.join("my-marketplace");
|
||||||
let plugin_root = marketplace_root.join("plugins").join("cloudflare");
|
let plugin_root = marketplace_root.join("plugins").join("cloudflare");
|
||||||
@@ -139,7 +139,7 @@ async fn import_plugins_supports_external_agent_plugin_marketplace_layout() {
|
|||||||
|
|
||||||
let outcome = service_for_paths(external_agent_home, codex_home.clone())
|
let outcome = service_for_paths(external_agent_home, codex_home.clone())
|
||||||
.import_plugins(
|
.import_plugins(
|
||||||
/*cwd*/ None,
|
/*cwd*/ Some(std::path::Path::new("")),
|
||||||
Some(MigrationDetails {
|
Some(MigrationDetails {
|
||||||
plugins: vec![PluginsMigration {
|
plugins: vec![PluginsMigration {
|
||||||
marketplace_name: "my-plugins".to_string(),
|
marketplace_name: "my-plugins".to_string(),
|
||||||
@@ -529,7 +529,7 @@ async fn import_plugins_infers_external_official_marketplace_when_missing_from_s
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn detect_repo_supports_project_relative_external_agent_plugin_marketplace_path() {
|
async fn detect_repo_skips_project_relative_external_agent_plugin_marketplace_path() {
|
||||||
let root = TempDir::new().expect("create tempdir");
|
let root = TempDir::new().expect("create tempdir");
|
||||||
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
||||||
let codex_home = root.path().join(".codex");
|
let codex_home = root.path().join(".codex");
|
||||||
@@ -588,31 +588,11 @@ async fn detect_repo_supports_project_relative_external_agent_plugin_marketplace
|
|||||||
.await
|
.await
|
||||||
.expect("detect");
|
.expect("detect");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||||
items,
|
|
||||||
vec![ExternalAgentConfigMigrationItem {
|
|
||||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
|
||||||
description: format!(
|
|
||||||
"Migrate enabled plugins from {}",
|
|
||||||
repo_root
|
|
||||||
.join(EXTERNAL_AGENT_DIR)
|
|
||||||
.join("settings.json")
|
|
||||||
.display()
|
|
||||||
),
|
|
||||||
cwd: Some(repo_root),
|
|
||||||
details: Some(MigrationDetails {
|
|
||||||
plugins: vec![PluginsMigration {
|
|
||||||
marketplace_name: "my-plugins".to_string(),
|
|
||||||
plugin_names: vec!["cloudflare".to_string()],
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn import_plugins_supports_project_relative_external_agent_plugin_marketplace_path() {
|
async fn import_rejects_forged_project_relative_external_agent_plugin_item() {
|
||||||
let root = TempDir::new().expect("create tempdir");
|
let root = TempDir::new().expect("create tempdir");
|
||||||
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
||||||
let codex_home = root.path().join(".codex");
|
let codex_home = root.path().join(".codex");
|
||||||
@@ -663,32 +643,66 @@ async fn import_plugins_supports_project_relative_external_agent_plugin_marketpl
|
|||||||
.expect("write plugin manifest");
|
.expect("write plugin manifest");
|
||||||
|
|
||||||
let outcome = service_for_paths(external_agent_home, codex_home.clone())
|
let outcome = service_for_paths(external_agent_home, codex_home.clone())
|
||||||
.import_plugins(
|
.import(vec![ExternalAgentConfigMigrationItem {
|
||||||
Some(repo_root.as_path()),
|
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||||
Some(MigrationDetails {
|
description: String::new(),
|
||||||
|
cwd: Some(repo_root.clone()),
|
||||||
|
details: Some(MigrationDetails {
|
||||||
plugins: vec![PluginsMigration {
|
plugins: vec![PluginsMigration {
|
||||||
marketplace_name: "my-plugins".to_string(),
|
marketplace_name: "my-plugins".to_string(),
|
||||||
plugin_names: vec!["cloudflare".to_string()],
|
plugin_names: vec!["cloudflare".to_string()],
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
)
|
}])
|
||||||
.await
|
.await;
|
||||||
.expect("import plugins");
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
outcome,
|
outcome,
|
||||||
PluginImportOutcome {
|
ExternalAgentConfigImportOutcome {
|
||||||
succeeded_marketplaces: vec!["my-plugins".to_string()],
|
pending_plugin_imports: Vec::new(),
|
||||||
succeeded_plugin_ids: vec!["cloudflare@my-plugins".to_string()],
|
item_results: vec![ExternalAgentConfigImportItemResult {
|
||||||
failed_marketplaces: Vec::new(),
|
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||||
failed_plugin_ids: Vec::new(),
|
description: String::new(),
|
||||||
raw_errors: Vec::new(),
|
cwd: Some(repo_root.clone()),
|
||||||
|
success_count: 0,
|
||||||
|
error_count: 1,
|
||||||
|
successes: Vec::new(),
|
||||||
|
raw_errors: vec![ExternalAgentConfigImportRawError {
|
||||||
|
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||||
|
error_type: None,
|
||||||
|
sub_error_type: None,
|
||||||
|
failure_stage: "plugin_import".to_string(),
|
||||||
|
message: "repository-scoped plugin migration is not allowed".to_string(),
|
||||||
|
cwd: Some(repo_root),
|
||||||
|
source: None,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let config = fs::read_to_string(codex_home.join("config.toml")).expect("read config");
|
assert!(!codex_home.join("config.toml").exists());
|
||||||
assert!(config.contains(r#"[plugins."cloudflare@my-plugins"]"#));
|
}
|
||||||
assert!(config.contains("enabled = true"));
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn import_plugins_rejects_project_cwd_before_config_loading() {
|
||||||
|
let root = TempDir::new().expect("create tempdir");
|
||||||
|
let external_agent_home = root.path().join(EXTERNAL_AGENT_DIR);
|
||||||
|
let codex_home = root.path().join(".codex");
|
||||||
|
let repo_root = root.path().join("repo");
|
||||||
|
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||||
|
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||||
|
fs::write(codex_home.join("config.toml"), "not valid toml").expect("write invalid config");
|
||||||
|
|
||||||
|
let error = service_for_paths(external_agent_home, codex_home)
|
||||||
|
.import_plugins(Some(repo_root.as_path()), Some(github_plugin_details()))
|
||||||
|
.await
|
||||||
|
.expect_err("reject project cwd before config loading");
|
||||||
|
|
||||||
|
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"repository-scoped plugin migration is not allowed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user