fix(plugins): support root local marketplace plugins (#28771)

## Summary
- allow local marketplace `source.path: "."` and `source.path: "./"` to
resolve to the marketplace root
- keep `""` invalid and preserve rejection of non-root paths without
`./` plus non-normal/traversal paths
- add focused regression coverage for repo-root plugin layouts and
rejected local paths

## Tests
- `RUSTUP_TOOLCHAIN=stable just fmt`
- `RUSTUP_TOOLCHAIN=stable just test -p codex-core-plugins`
- `RUSTUP_TOOLCHAIN=stable just fix -p codex-core-plugins`

Note: plain pinned-toolchain `just fmt` was blocked locally by a rustup
`clippy` component conflict, so validation used the working stable 1.95
toolchain fallback.
This commit is contained in:
Casey Chow
2026-06-17 17:06:42 -04:00
committed by GitHub
Unverified
parent a0586ad12d
commit a760b63f83
2 changed files with 128 additions and 32 deletions
+14 -8
View File
@@ -544,20 +544,26 @@ fn resolve_local_plugin_source_path(
marketplace_path: &AbsolutePathBuf,
path: &str,
) -> Result<AbsolutePathBuf, MarketplaceError> {
let Some(path) = path.strip_prefix("./") else {
match path {
"" => {
return Err(MarketplaceError::InvalidMarketplaceFile {
path: marketplace_path.to_path_buf(),
message: "local plugin source path must not be empty".to_string(),
});
}
"." | "./" => return marketplace_root_dir(marketplace_path),
_ => {}
}
// Non-root local sources must keep the explicit `./` prefix and remain normalized.
let Some(relative_path) = path.strip_prefix("./") else {
return Err(MarketplaceError::InvalidMarketplaceFile {
path: marketplace_path.to_path_buf(),
message: "local plugin source path must start with `./`".to_string(),
});
};
if path.is_empty() {
return Err(MarketplaceError::InvalidMarketplaceFile {
path: marketplace_path.to_path_buf(),
message: "local plugin source path must not be empty".to_string(),
});
}
let relative_source_path = Path::new(path);
let relative_source_path = Path::new(relative_path);
if relative_source_path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
+114 -24
View File
@@ -420,6 +420,92 @@ fn list_marketplaces_supports_alternate_manifest_layout() {
);
}
#[test]
fn list_marketplaces_supports_repo_root_local_plugin_sources() {
for path in [".", "./"] {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join(".codex-plugin")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
format!(
r#"{{
"name": "repo-root-marketplace",
"plugins": [
{{
"name": "repo-root-plugin",
"source": {{
"source": "local",
"path": "{path}"
}}
}}
]
}}"#
),
)
.unwrap();
fs::write(
repo_root.join(".codex-plugin/plugin.json"),
r#"{
"name":"repo-root-plugin",
"interface": {
"displayName": "Repo Root Plugin"
}
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
/*home_dir*/ None,
)
.unwrap()
.marketplaces;
assert_eq!(
marketplaces,
vec![Marketplace {
name: "repo-root-marketplace".to_string(),
path: AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"))
.unwrap(),
interface: None,
plugins: vec![MarketplacePlugin {
name: "repo-root-plugin".to_string(),
local_version: None,
source: MarketplacePluginSource::Local {
path: AbsolutePathBuf::try_from(repo_root).unwrap(),
},
policy: MarketplacePluginPolicy {
installation: MarketplacePluginInstallPolicy::Available,
authentication: MarketplacePluginAuthPolicy::OnInstall,
products: None,
},
interface: Some(PluginManifestInterface {
display_name: Some("Repo Root Plugin".to_string()),
short_description: None,
long_description: None,
developer_name: None,
category: None,
capabilities: Vec::new(),
website_url: None,
privacy_policy_url: None,
terms_of_service_url: None,
default_prompt: None,
brand_color: None,
composer_icon: None,
logo: None,
screenshots: Vec::new(),
}),
keywords: Vec::new(),
}],
}]
);
}
}
#[test]
fn list_marketplaces_includes_plugins_without_discoverable_manifest() {
let tmp = tempdir().unwrap();
@@ -1423,37 +1509,41 @@ fn list_marketplaces_ignores_plugin_interface_assets_without_dot_slash() {
#[test]
fn find_marketplace_plugin_skips_invalid_local_paths() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
for path in ["", "plugin-1", "././", "./plugins/../", "../plugin-1"] {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
format!(
r#"{{
"name": "codex-curated",
"plugins": [
{
{{
"name": "local-plugin",
"source": {
"source": {{
"source": "local",
"path": "../plugin-1"
}
}
"path": "{path}"
}}
}}
]
}"#,
)
.unwrap();
}}"#
),
)
.unwrap();
let err = find_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap_err();
let err = find_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap_err();
assert_eq!(
err.to_string(),
"plugin `local-plugin` was not found in marketplace `codex-curated`"
);
assert_eq!(
err.to_string(),
"plugin `local-plugin` was not found in marketplace `codex-curated`"
);
}
}
#[test]