fix: harden plugin feature gating (#15020)

1. Use requirement-resolved config.features as the plugin gate.
2. Guard plugin/list, plugin/read, and related flows behind that gate.
3. Skip bad marketplace.json files instead of failing the whole list.
4. Simplify plugin state and caching.
This commit is contained in:
xl-openai
2026-03-18 10:11:43 -07:00
committed by GitHub
parent 606d85055f
commit 580f32ad2a
40 changed files with 926 additions and 52 deletions

View File

@@ -2272,6 +2272,7 @@ pub enum SessionSource {
VSCode,
Exec,
Mcp,
Custom(String),
SubAgent(SubAgentSource),
#[serde(other)]
Unknown,
@@ -2302,6 +2303,7 @@ impl fmt::Display for SessionSource {
SessionSource::VSCode => f.write_str("vscode"),
SessionSource::Exec => f.write_str("exec"),
SessionSource::Mcp => f.write_str("mcp"),
SessionSource::Custom(source) => f.write_str(source),
SessionSource::SubAgent(sub_source) => write!(f, "subagent_{sub_source}"),
SessionSource::Unknown => f.write_str("unknown"),
}
@@ -2309,6 +2311,23 @@ impl fmt::Display for SessionSource {
}
impl SessionSource {
pub fn from_startup_arg(value: &str) -> Result<Self, &'static str> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err("session source must not be empty");
}
let normalized = trimmed.to_ascii_lowercase();
Ok(match normalized.as_str() {
"cli" => SessionSource::Cli,
"vscode" => SessionSource::VSCode,
"exec" => SessionSource::Exec,
"mcp" | "appserver" | "app-server" | "app_server" => SessionSource::Mcp,
"unknown" => SessionSource::Unknown,
_ => SessionSource::Custom(trimmed.to_string()),
})
}
pub fn get_nickname(&self) -> Option<String> {
match self {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
@@ -2332,6 +2351,25 @@ impl SessionSource {
_ => None,
}
}
pub fn restriction_product(&self) -> Option<Product> {
match self {
SessionSource::Custom(source) => Product::from_session_source_name(source),
SessionSource::Cli
| SessionSource::VSCode
| SessionSource::Exec
| SessionSource::Mcp
| SessionSource::SubAgent(_)
| SessionSource::Unknown => Some(Product::Codex),
}
}
pub fn matches_product_restriction(&self, products: &[Product]) -> bool {
products.is_empty()
|| self
.restriction_product()
.is_some_and(|product| products.contains(&product))
}
}
impl fmt::Display for SubAgentSource {
@@ -2923,6 +2961,18 @@ pub enum Product {
#[serde(alias = "ATLAS")]
Atlas,
}
impl Product {
pub fn from_session_source_name(value: &str) -> Option<Self> {
let normalized = value.trim().to_ascii_lowercase();
match normalized.as_str() {
"chatgpt" => Some(Self::Chatgpt),
"codex" => Some(Self::Codex),
"atlas" => Some(Self::Atlas),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
@@ -3423,6 +3473,92 @@ mod tests {
.any(|root| root.is_path_writable(path))
}
#[test]
fn session_source_from_startup_arg_maps_known_values() {
assert_eq!(
SessionSource::from_startup_arg("vscode").unwrap(),
SessionSource::VSCode
);
assert_eq!(
SessionSource::from_startup_arg("app-server").unwrap(),
SessionSource::Mcp
);
}
#[test]
fn session_source_from_startup_arg_preserves_custom_values() {
assert_eq!(
SessionSource::from_startup_arg("atlas").unwrap(),
SessionSource::Custom("atlas".to_string())
);
}
#[test]
fn session_source_restriction_product_defaults_non_custom_sources_to_codex() {
assert_eq!(
SessionSource::Cli.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::VSCode.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Exec.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Mcp.restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::SubAgent(SubAgentSource::Review).restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Unknown.restriction_product(),
Some(Product::Codex)
);
}
#[test]
fn session_source_restriction_product_maps_custom_sources_to_products() {
assert_eq!(
SessionSource::Custom("chatgpt".to_string()).restriction_product(),
Some(Product::Chatgpt)
);
assert_eq!(
SessionSource::Custom("ATLAS".to_string()).restriction_product(),
Some(Product::Atlas)
);
assert_eq!(
SessionSource::Custom("codex".to_string()).restriction_product(),
Some(Product::Codex)
);
assert_eq!(
SessionSource::Custom("atlas-dev".to_string()).restriction_product(),
None
);
}
#[test]
fn session_source_matches_product_restriction() {
assert!(
SessionSource::Custom("chatgpt".to_string())
.matches_product_restriction(&[Product::Chatgpt])
);
assert!(
!SessionSource::Custom("chatgpt".to_string())
.matches_product_restriction(&[Product::Codex])
);
assert!(SessionSource::VSCode.matches_product_restriction(&[Product::Codex]));
assert!(
!SessionSource::Custom("atlas-dev".to_string())
.matches_product_restriction(&[Product::Atlas])
);
assert!(SessionSource::Custom("atlas-dev".to_string()).matches_product_restriction(&[]));
}
fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec<PathBuf> {
let mut paths = vec![cwd.to_path_buf()];
paths.extend(