mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Support portable Agent Plugins throughout installation (#36544)
## Why Agent Plugins use a schema-declared root `plugin.json` and can have dotted names or versions that do not fit Codex's directory-safe version format. The packaging and installation paths still assumed the legacy manifest layout and identifier rules. ## What changed - Recognize valid root Agent Plugin manifests when discovering, packing, and installing plugins, while leaving unrelated root manifests on the legacy path. - Accept safe dotted plugin names, default missing Agent Plugin versions to `1.0.0`, and derive stable directory-safe versions when necessary without rewriting the portable manifest. - Skip legacy command migration for Agent Plugins and reject symlinks or other unsupported file types while copying plugin sources. ## Testing Add coverage for portable bundle round trips, manifest discovery, dotted names, version handling, command preservation, and symlink rejection. GitOrigin-RevId: 61476c4c4100495842253d8b429c0b896490962d
This commit is contained in:
@@ -52,13 +52,32 @@ pub fn validate_plugin_segment(segment: &str, kind: &str) -> Result<(), String>
|
||||
if segment.is_empty() {
|
||||
return Err(format!("invalid {kind}: must not be empty"));
|
||||
}
|
||||
if !segment
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
|
||||
let allow_dots = kind == "plugin name";
|
||||
if allow_dots && matches!(segment, "." | "..") {
|
||||
return Err(format!("invalid {kind}: path traversal is not allowed"));
|
||||
}
|
||||
if allow_dots && (segment.starts_with('.') || segment.ends_with('.') || segment.contains(".."))
|
||||
{
|
||||
return Err(format!(
|
||||
"invalid {kind}: only ASCII letters, digits, `_`, and `-` are allowed"
|
||||
"invalid {kind}: dots must separate non-empty name segments"
|
||||
));
|
||||
}
|
||||
if !segment
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') || allow_dots && ch == '.')
|
||||
{
|
||||
let allowed_characters = if allow_dots {
|
||||
"ASCII letters, digits, `.`, `_`, and `-`"
|
||||
} else {
|
||||
"ASCII letters, digits, `_`, and `-`"
|
||||
};
|
||||
return Err(format!(
|
||||
"invalid {kind}: only {allowed_characters} are allowed"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "plugin_id_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
34
codex-rs/plugin/src/plugin_id_tests.rs
Normal file
34
codex-rs/plugin/src/plugin_id_tests.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use super::PluginId;
|
||||
|
||||
#[test]
|
||||
fn accepts_dotted_plugin_names() {
|
||||
let plugin_id =
|
||||
PluginId::new("acme.tools".to_string(), "marketplace".to_string()).expect("plugin id");
|
||||
|
||||
assert_eq!(plugin_id.as_key(), "acme.tools@marketplace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marketplace_names_preserve_legacy_character_set() {
|
||||
PluginId::new("acme".to_string(), "marketplace_name-1".to_string()).expect("marketplace name");
|
||||
|
||||
let err = PluginId::new("acme".to_string(), "market.place".to_string())
|
||||
.expect_err("dotted marketplace name");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"invalid marketplace name: only ASCII letters, digits, `_`, and `-` are allowed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dot_path_segments() {
|
||||
assert!(PluginId::new(".".to_string(), "marketplace".to_string()).is_err());
|
||||
assert!(PluginId::new("plugin".to_string(), "..".to_string()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_dots_that_can_alias_path_segments() {
|
||||
for plugin_name in [".plugin", "plugin.", "plugin..name"] {
|
||||
assert!(PluginId::new(plugin_name.to_string(), "marketplace".to_string()).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user