Download codex-rs/core-plugins/src/loader_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 32.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/loader_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core-plugins/src/loader_tests.rs
-
curl -L -o loader_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core-plugins/src/loader_tests.rs
32.1 kB
| use super::*; | |
| use crate::manifest::load_plugin_manifest; | |
| use crate::manifest::load_plugin_manifest_with_format; | |
| use crate::test_support::test_skill_root_loader; | |
| use crate::test_support::write_file; | |
| use codex_config::ConfigLayerEntry; | |
| use codex_config::ConfigLayerSource; | |
| use codex_config::ConfigRequirements; | |
| use codex_config::ConfigRequirementsToml; | |
| use codex_plugin::PluginId; | |
| use codex_utils_plugins::AGENT_PLUGIN_SCHEMA_URI; | |
| use pretty_assertions::assert_eq; | |
| use tempfile::TempDir; | |
| fn user_config_path(temp_dir: &TempDir, file_name: &str) -> AbsolutePathBuf { | |
| AbsolutePathBuf::from_absolute_path(temp_dir.path().join(file_name)) | |
| .expect("test user config path should be absolute") | |
| } | |
| fn user_layer(path: AbsolutePathBuf, config: &str) -> ConfigLayerEntry { | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::User { | |
| file: path, | |
| profile: None, | |
| }, | |
| toml::from_str(config).expect("user config toml"), | |
| ) | |
| } | |
| async fn ema_policy_overlays_native_and_agent_plugins_without_changing_endpoints() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let policy_config = r#" | |
| [plugins."sample@test".mcp_servers.example] | |
| enabled = true | |
| [plugins."sample@test".mcp_servers.example.ema_auth] | |
| url = "https://resource.example/mcp" | |
| client_id = "resource-client" | |
| authorization_server_issuer = "https://as.example" | |
| resource = "https://resource.example" | |
| scopes = ["tools"] | |
| "#; | |
| let stack = ConfigLayerStack::new( | |
| vec![ConfigLayerEntry::new( | |
| ConfigLayerSource::EnterpriseManaged { | |
| id: "ema-policy".to_string(), | |
| name: "EMA policy".to_string(), | |
| }, | |
| toml::from_str(policy_config).expect("managed policy toml"), | |
| )], | |
| ConfigRequirements::default(), | |
| ConfigRequirementsToml::default(), | |
| ) | |
| .expect("valid trusted policy stack"); | |
| let policies = configured_plugins_from_stack(&stack, temp_dir.path()) | |
| .remove("sample@test") | |
| .expect("configured plugin") | |
| .mcp_servers; | |
| for (name, manifest_path, manifest, mcp_path, mcp) in [ | |
| ( | |
| "native", | |
| ".codex-plugin/plugin.json", | |
| r#"{"name":"native"}"#, | |
| ".mcp.json", | |
| r#"{"mcpServers":{"example":{"url":"https://resource.example/mcp"}}}"#, | |
| ), | |
| ( | |
| "agent", | |
| "plugin.json", | |
| r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json","name":"agent"}"#, | |
| "mcp.json", | |
| r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"example":{"type":"streamable-http","url":"https://resource.example/mcp"}}}"#, | |
| ), | |
| ] { | |
| for endpoint in [ | |
| "https://resource.example/mcp", | |
| "https://resource.example/mcp/other", | |
| "https://resource.example/mcp?tenant=other", | |
| "https://other.example/mcp", | |
| ] { | |
| let root = temp_dir.path().join(name); | |
| write_file(&root.join(manifest_path), manifest); | |
| write_file( | |
| &root.join(mcp_path), | |
| &mcp.replace("https://resource.example/mcp", endpoint), | |
| ); | |
| let declared = load_plugin_mcp_servers(&root, /*auth_mode*/ None).await; | |
| let transport = declared["example"].transport.clone(); | |
| let installed = load_plugin_mcp_servers_with_policy( | |
| &root, | |
| /*auth_mode*/ None, | |
| Some(&policies), | |
| ) | |
| .await; | |
| let mut selected = declared; | |
| apply_configured_plugin_mcp_server_policies(&policies, &mut selected); | |
| assert_eq!(installed, selected); | |
| let server = &selected["example"]; | |
| assert_eq!(server.transport, transport); | |
| assert_eq!(server.enabled, endpoint == "https://resource.example/mcp"); | |
| assert_eq!(server.auth, codex_config::McpServerAuth::EmaAuth); | |
| assert_eq!( | |
| server.oauth, | |
| Some(codex_config::McpServerOAuthConfig { | |
| client_id: Some("resource-client".into()), | |
| authorization_server_issuer: Some("https://as.example".into()), | |
| ema_registration_error: (endpoint != "https://resource.example/mcp") | |
| .then_some("plugin endpoint does not match its EMA registration"), | |
| ..Default::default() | |
| }) | |
| ); | |
| assert_eq!( | |
| server.oauth_resource.as_deref(), | |
| Some("https://resource.example") | |
| ); | |
| assert_eq!( | |
| server.scopes.as_deref(), | |
| Some(["tools".to_string()].as_slice()) | |
| ); | |
| assert_eq!(server.oauth_idp(), None); | |
| } | |
| } | |
| } | |
| async fn agent_plugin_overlay_apps_are_not_runtime_active() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugin"); | |
| write_file( | |
| &plugin_root.join("plugin.json"), | |
| &format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"plugin"}}"#), | |
| ); | |
| write_file( | |
| &plugin_root.join(".codex-plugin/plugin.json"), | |
| r#"{"name":"plugin","apps":"./.app.json"}"#, | |
| ); | |
| write_file( | |
| &plugin_root.join(".app.json"), | |
| r#"{"apps":{"example":{"id":"connector_example"}}}"#, | |
| ); | |
| assert!(load_plugin_apps(&plugin_root).await.is_empty()); | |
| } | |
| async fn agent_plugin_codex_mcp_overlay_only_forwards_matching_stdio_server_env_vars() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugin"); | |
| write_file( | |
| &plugin_root.join("plugin.json"), | |
| &format!( | |
| r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"plugin","extensions":{{"com.openai":{{"interface":{{"displayName":"Portable"}}}}}}}}"# | |
| ), | |
| ); | |
| write_file( | |
| &plugin_root.join("mcp.json"), | |
| r#"{ | |
| "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", | |
| "mcpServers": { | |
| "shared": { | |
| "type": "stdio", | |
| "command": "portable-server", | |
| "args": ["portable"], | |
| "env": { | |
| "DB_PASSWORD": "${DB_PASSWORD}", | |
| "API_TOKEN": "portable-token", | |
| "REMOTE_ONLY": "${REMOTE_ONLY}", | |
| "UNLISTED": "${UNLISTED}" | |
| } | |
| }, | |
| "portable-only": {"type": "stdio", "command": "portable-only"} | |
| } | |
| }"#, | |
| ); | |
| let mut expected = load_plugin_mcp_servers(&plugin_root, /*auth_mode*/ None).await; | |
| let Some(McpServerConfig { | |
| transport: McpServerTransportConfig::Stdio { env, env_vars, .. }, | |
| .. | |
| }) = expected.get_mut("shared") | |
| else { | |
| panic!("expected portable stdio server"); | |
| }; | |
| env.as_mut() | |
| .expect("portable environment") | |
| .remove("DB_PASSWORD"); | |
| env_vars.extend(["DB_PASSWORD".into(), "API_TOKEN".into()]); | |
| write_file( | |
| &plugin_root.join(".codex-plugin/plugin.json"), | |
| r#"{"name":"legacy-plugin"}"#, | |
| ); | |
| write_file( | |
| &plugin_root.join(".mcp.json"), | |
| r#"{ | |
| "mcpServers": { | |
| "shared": { | |
| "command": "legacy-server", | |
| "args": ["legacy"], | |
| "env_vars": ["DB_PASSWORD", "API_TOKEN", {"name": "REMOTE_ONLY", "source": "remote"}] | |
| }, | |
| "legacy-only": {"command": "legacy-only", "env_vars": ["UNLISTED"]} | |
| } | |
| }"#, | |
| ); | |
| assert_eq!( | |
| load_plugin_mcp_servers(&plugin_root, /*auth_mode*/ None).await, | |
| expected | |
| ); | |
| } | |
| async fn agent_plugin_codex_mcp_overlay_supports_inline_legacy_servers_without_portable_env() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugin"); | |
| write_file( | |
| &plugin_root.join("plugin.json"), | |
| &format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"plugin"}}"#), | |
| ); | |
| write_file( | |
| &plugin_root.join("mcp.json"), | |
| r#"{ | |
| "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", | |
| "mcpServers": { | |
| "shared": { | |
| "type": "stdio", | |
| "command": "portable-server" | |
| } | |
| } | |
| }"#, | |
| ); | |
| let mut expected = load_plugin_mcp_servers(&plugin_root, /*auth_mode*/ None).await; | |
| let Some(McpServerConfig { | |
| transport: McpServerTransportConfig::Stdio { env_vars, .. }, | |
| .. | |
| }) = expected.get_mut("shared") | |
| else { | |
| panic!("expected portable stdio server"); | |
| }; | |
| env_vars.push("TOKEN".into()); | |
| write_file( | |
| &plugin_root.join(".codex-plugin/plugin.json"), | |
| r#"{ | |
| "name": "legacy-plugin", | |
| "mcpServers": { | |
| "shared": {"command": "legacy-server", "env_vars": ["TOKEN"]} | |
| } | |
| }"#, | |
| ); | |
| assert_eq!( | |
| load_plugin_mcp_servers(&plugin_root, /*auth_mode*/ None).await, | |
| expected | |
| ); | |
| } | |
| async fn agent_plugin_mcp_rejects_config_symlink_outside_plugin_root() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugin"); | |
| let outside_config = temp_dir.path().join("outside-mcp.json"); | |
| fs::create_dir_all(&plugin_root).expect("create plugin root"); | |
| fs::write( | |
| plugin_root.join("plugin.json"), | |
| format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"plugin"}}"#), | |
| ) | |
| .expect("write Agent Plugins manifest"); | |
| fs::write( | |
| &outside_config, | |
| r#"{"$schema":"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json","mcpServers":{"outside":{"type":"stdio","command":"echo"}}}"#, | |
| ) | |
| .expect("write outside MCP config"); | |
| std::os::unix::fs::symlink(&outside_config, plugin_root.join("mcp.json")) | |
| .expect("create MCP symlink"); | |
| let config_path = AbsolutePathBuf::from_absolute_path(plugin_root.join("mcp.json")) | |
| .expect("absolute MCP path"); | |
| let discovered = load_mcp_servers_from_file( | |
| &plugin_root, | |
| /*plugin_data_root*/ None, | |
| PluginManifestFormat::AgentPlugin, | |
| &config_path, | |
| ) | |
| .await; | |
| assert!(discovered.mcp_servers.is_empty()); | |
| } | |
| async fn agent_plugin_mcp_rejects_present_nonregular_config() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugin"); | |
| let config_path = plugin_root.join("mcp.json"); | |
| fs::create_dir_all(&config_path).expect("create nonregular MCP config"); | |
| let discovered = load_mcp_servers_from_file( | |
| &plugin_root, | |
| /*plugin_data_root*/ None, | |
| PluginManifestFormat::AgentPlugin, | |
| &AbsolutePathBuf::from_absolute_path(config_path).expect("absolute MCP path"), | |
| ) | |
| .await; | |
| assert!(discovered.mcp_servers.is_empty()); | |
| } | |
| async fn legacy_manifest_can_point_at_root_mcp_json() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugin"); | |
| fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create manifest directory"); | |
| fs::write( | |
| plugin_root.join(".codex-plugin/plugin.json"), | |
| r#"{"name":"plugin","mcpServers":"./mcp.json"}"#, | |
| ) | |
| .expect("write legacy manifest"); | |
| fs::write( | |
| plugin_root.join("mcp.json"), | |
| r#"{"mcpServers":{"legacy":{"command":"echo"}}}"#, | |
| ) | |
| .expect("write legacy MCP config"); | |
| let manifest = load_plugin_manifest(&plugin_root).expect("load legacy manifest"); | |
| let discovered = load_plugin_mcp_servers_from_manifest_with_format( | |
| &plugin_root, | |
| &manifest.paths, | |
| /*plugin_policy*/ None, | |
| /*plugin_data_root*/ None, | |
| PluginManifestFormat::Legacy, | |
| ) | |
| .await; | |
| assert_eq!( | |
| discovered.keys().collect::<Vec<_>>(), | |
| vec![&"legacy".to_string()] | |
| ); | |
| } | |
| async fn installed_agent_plugin_uses_isolated_data_root_for_stdio_mcp() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugins/cache/c/a-b/local"); | |
| write_file( | |
| &plugin_root.join("plugin.json"), | |
| &format!(r#"{{"$schema":"{AGENT_PLUGIN_SCHEMA_URI}","name":"a-b"}}"#), | |
| ); | |
| write_file( | |
| &plugin_root.join("mcp.json"), | |
| r#"{ | |
| "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", | |
| "mcpServers": { | |
| "example": { | |
| "type": "stdio", | |
| "command": "echo" | |
| } | |
| } | |
| }"#, | |
| ); | |
| let stack = ConfigLayerStack::new( | |
| vec![user_layer( | |
| user_config_path(&temp_dir, "config.toml"), | |
| "[plugins.\"a-b@c\"]\nenabled = true\n", | |
| )], | |
| ConfigRequirements::default(), | |
| ConfigRequirementsToml::default(), | |
| ) | |
| .expect("valid config layer stack"); | |
| let store = PluginStore::new(temp_dir.path().to_path_buf()); | |
| let plugins = load_plugins_from_layer_stack( | |
| &stack, | |
| RemoteInstalledPluginsSnapshot::default(), | |
| &store, | |
| /*plugin_skill_snapshots*/ None, | |
| Some(Product::Codex), | |
| /*remote_global_catalog_active*/ false, | |
| test_skill_root_loader().as_ref(), | |
| &BTreeSet::new(), | |
| ) | |
| .await; | |
| let expected_data_root = temp_dir | |
| .path() | |
| .join("plugins") | |
| .join("data") | |
| .join("agent-plugins") | |
| .join("6920dd17774030852d11d1b94758fcaae4f894c7b2f36301ed174bc3b33e0743"); | |
| let expected_data_root = AbsolutePathBuf::from_absolute_path(expected_data_root) | |
| .expect("absolute Agent Plugin data root") | |
| .canonicalize() | |
| .expect("canonical Agent Plugin data root"); | |
| let server = plugins | |
| .first() | |
| .and_then(|plugin| plugin.mcp_servers.get("example")) | |
| .expect("Agent plugin stdio MCP server"); | |
| let McpServerTransportConfig::Stdio { env, .. } = &server.transport else { | |
| panic!("expected stdio MCP server"); | |
| }; | |
| assert_eq!( | |
| env.as_ref() | |
| .and_then(|env| env.get("PLUGIN_DATA")) | |
| .map(String::as_str), | |
| expected_data_root.as_path().to_str() | |
| ); | |
| assert!(expected_data_root.as_path().is_dir()); | |
| } | |
| fn configured_plugins_from_stack_merges_enabled_effective_layers() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let stack = ConfigLayerStack::new( | |
| vec![ | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::System { | |
| file: user_config_path(&temp_dir, "system.toml"), | |
| }, | |
| toml::from_str("[plugins.system]\nenabled = true\n").expect("system config toml"), | |
| ), | |
| user_layer( | |
| user_config_path(&temp_dir, "config.toml"), | |
| "[plugins.base]\nenabled = true\n", | |
| ), | |
| user_layer( | |
| user_config_path(&temp_dir, "work.config.toml"), | |
| "[plugins.profile]\nenabled = false\n", | |
| ), | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::Project { | |
| dot_codex_folder: user_config_path(&temp_dir, "project/.codex"), | |
| }, | |
| toml::from_str( | |
| "[plugins.profile]\nenabled = true\n[plugins.profile.mcp_servers.example]\nenabled = false\n", | |
| ) | |
| .expect("project config toml"), | |
| ), | |
| ConfigLayerEntry::new_disabled( | |
| ConfigLayerSource::Project { | |
| dot_codex_folder: user_config_path(&temp_dir, "project/untrusted/.codex"), | |
| }, | |
| toml::from_str("[plugins.untrusted]\nenabled = true\n") | |
| .expect("untrusted project config toml"), | |
| "project is untrusted", | |
| ), | |
| ], | |
| ConfigRequirements::default(), | |
| ConfigRequirementsToml::default(), | |
| ) | |
| .expect("valid config layer stack"); | |
| let project_mcp_servers = HashMap::from([( | |
| "example".to_string(), | |
| PluginMcpServerConfig { | |
| enabled: false, | |
| ..PluginMcpServerConfig::default() | |
| }, | |
| )]); | |
| let plugins = configured_plugins_from_stack(&stack, temp_dir.path()); | |
| assert_eq!( | |
| plugins, | |
| HashMap::from([ | |
| ( | |
| "base".to_string(), | |
| PluginConfig { | |
| enabled: true, | |
| mcp_servers: HashMap::new(), | |
| }, | |
| ), | |
| ( | |
| "profile".to_string(), | |
| PluginConfig { | |
| enabled: true, | |
| mcp_servers: project_mcp_servers.clone(), | |
| }, | |
| ), | |
| ( | |
| "system".to_string(), | |
| PluginConfig { | |
| enabled: true, | |
| mcp_servers: HashMap::new(), | |
| }, | |
| ), | |
| ]) | |
| ); | |
| assert_eq!( | |
| configured_plugin_mcp_server_policies(&stack).get("profile"), | |
| Some(&project_mcp_servers) | |
| ); | |
| } | |
| async fn hooks_only_scope_shares_plugin_resolution_without_loading_other_capabilities() { | |
| let temp_dir = TempDir::new().expect("tempdir"); | |
| let plugin_root = temp_dir.path().join("plugins/cache/test/valid/local"); | |
| write_file( | |
| &plugin_root.join(".codex-plugin/plugin.json"), | |
| r#"{"name":"valid"}"#, | |
| ); | |
| write_file( | |
| &plugin_root.join("skills/example/SKILL.md"), | |
| "---\nname: example\ndescription: example skill\n---\n", | |
| ); | |
| write_file( | |
| &plugin_root.join(".mcp.json"), | |
| r#"{"mcpServers":{"example":{"command":"echo"}}}"#, | |
| ); | |
| write_file( | |
| &plugin_root.join(".app.json"), | |
| r#"{"apps":{"example":{"id":"connector_example"}}}"#, | |
| ); | |
| write_file( | |
| &plugin_root.join("hooks/hooks.json"), | |
| r#"{ | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "echo startup" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| }"#, | |
| ); | |
| let disabled_root = temp_dir.path().join("plugins/cache/test/disabled/local"); | |
| write_file( | |
| &disabled_root.join(".codex-plugin/plugin.json"), | |
| r#"{"name":"disabled"}"#, | |
| ); | |
| write_file( | |
| &disabled_root.join("hooks/hooks.json"), | |
| r#"{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"echo disabled"}]}]}}"#, | |
| ); | |
| let malformed_root = temp_dir.path().join("plugins/cache/test/malformed/local"); | |
| write_file( | |
| &malformed_root.join(".codex-plugin/plugin.json"), | |
| "not valid json", | |
| ); | |
| let warning_root = temp_dir.path().join("plugins/cache/test/warning/local"); | |
| write_file( | |
| &warning_root.join(".codex-plugin/plugin.json"), | |
| r#"{"name":"warning"}"#, | |
| ); | |
| write_file(&warning_root.join("hooks/hooks.json"), "not valid json"); | |
| let stack = ConfigLayerStack::new( | |
| vec![user_layer( | |
| user_config_path(&temp_dir, "config.toml"), | |
| r#" | |
| [plugins."valid@test"] | |
| enabled = true | |
| [plugins."disabled@test"] | |
| enabled = false | |
| [plugins.invalid] | |
| enabled = true | |
| [plugins."malformed@test"] | |
| enabled = true | |
| [plugins."missing@test"] | |
| enabled = true | |
| [plugins."warning@test"] | |
| enabled = true | |
| "#, | |
| )], | |
| ConfigRequirements::default(), | |
| ConfigRequirementsToml::default(), | |
| ) | |
| .expect("valid config layer stack"); | |
| let store = PluginStore::new(temp_dir.path().to_path_buf()); | |
| let full = load_plugins_from_layer_stack( | |
| &stack, | |
| RemoteInstalledPluginsSnapshot::default(), | |
| &store, | |
| /*plugin_skill_snapshots*/ None, | |
| Some(Product::Codex), | |
| /*remote_global_catalog_active*/ false, | |
| test_skill_root_loader().as_ref(), | |
| &BTreeSet::new(), | |
| ) | |
| .await; | |
| let hooks_only = load_plugins_from_layer_stack_with_scope( | |
| &stack, | |
| HashMap::new(), | |
| &BTreeSet::new(), | |
| &store, | |
| /*remote_global_catalog_active*/ false, | |
| PluginLoadScope::HooksOnly, | |
| ) | |
| .await; | |
| let validation_state = |plugins: &[LoadedPlugin<McpServerConfig>]| { | |
| plugins | |
| .iter() | |
| .map(|plugin| { | |
| ( | |
| plugin.config_name.clone(), | |
| plugin.enabled, | |
| plugin.root.clone(), | |
| plugin.error.clone(), | |
| plugin.hook_sources.clone(), | |
| plugin.hook_load_warnings.clone(), | |
| ) | |
| }) | |
| .collect::<Vec<_>>() | |
| }; | |
| assert_eq!(validation_state(&hooks_only), validation_state(&full)); | |
| let full_valid = full | |
| .iter() | |
| .find(|plugin| plugin.config_name == "valid@test") | |
| .expect("full load should include valid plugin"); | |
| assert!(full_valid.manifest_name.is_some()); | |
| assert!(!full_valid.skill_roots.is_empty()); | |
| assert!(!full_valid.mcp_servers.is_empty()); | |
| assert!(!full_valid.apps.is_empty()); | |
| let hooks_only_valid = hooks_only | |
| .iter() | |
| .find(|plugin| plugin.config_name == "valid@test") | |
| .expect("hooks-only load should include valid plugin"); | |
| assert_eq!(hooks_only_valid.manifest_name, None); | |
| assert!(hooks_only_valid.skill_roots.is_empty()); | |
| assert!(hooks_only_valid.mcp_servers.is_empty()); | |
| assert!(hooks_only_valid.apps.is_empty()); | |
| } | |
| fn curated_plugin_cache_version_shortens_full_git_sha() { | |
| assert_eq!( | |
| curated_plugin_cache_version("0123456789abcdef0123456789abcdef01234567"), | |
| "01234567" | |
| ); | |
| } | |
| fn curated_plugin_cache_version_preserves_non_git_sha_versions() { | |
| assert_eq!( | |
| curated_plugin_cache_version("export-backup"), | |
| "export-backup" | |
| ); | |
| assert_eq!(curated_plugin_cache_version("0123456"), "0123456"); | |
| } | |
| fn plugin_id() -> PluginId { | |
| PluginId::parse("demo-plugin@test-marketplace").expect("plugin id") | |
| } | |
| fn plugin_root() -> (tempfile::TempDir, AbsolutePathBuf) { | |
| let tmp = tempfile::tempdir().expect("tempdir"); | |
| let plugin_root = | |
| AbsolutePathBuf::try_from(tmp.path().join("demo-plugin")).expect("plugin root"); | |
| fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create manifest dir"); | |
| fs::create_dir_all(plugin_root.join("hooks")).expect("create hooks dir"); | |
| (tmp, plugin_root) | |
| } | |
| fn write_manifest(plugin_root: &AbsolutePathBuf, manifest: &str) { | |
| fs::write(plugin_root.join(".codex-plugin/plugin.json"), manifest).expect("write manifest"); | |
| } | |
| fn write_hook_file(plugin_root: &AbsolutePathBuf, relative_path: &str, event: &str, command: &str) { | |
| fs::write( | |
| plugin_root.join(relative_path), | |
| format!( | |
| r#"{{ | |
| "hooks": {{ | |
| "{event}": [ | |
| {{ | |
| "hooks": [{{ "type": "command", "command": "{command}" }}] | |
| }} | |
| ] | |
| }} | |
| }}"# | |
| ), | |
| ) | |
| .expect("write hooks"); | |
| } | |
| fn load_sources(plugin_root: &AbsolutePathBuf) -> (Vec<PluginHookSource>, Vec<String>) { | |
| let loaded_manifest = | |
| load_plugin_manifest_with_format(plugin_root.as_path()).expect("manifest"); | |
| let plugin_data_root = AbsolutePathBuf::try_from( | |
| plugin_root | |
| .as_path() | |
| .parent() | |
| .expect("plugin root parent") | |
| .join("plugin-data"), | |
| ) | |
| .expect("plugin data root"); | |
| load_plugin_hooks( | |
| plugin_root, | |
| &plugin_id(), | |
| &plugin_data_root, | |
| &loaded_manifest.manifest.paths, | |
| ) | |
| } | |
| fn assert_sources(sources: &[PluginHookSource], expected_relative_paths: &[&str]) { | |
| assert_eq!( | |
| sources | |
| .iter() | |
| .map(|source| source.plugin_id.clone()) | |
| .collect::<Vec<_>>(), | |
| vec![plugin_id(); expected_relative_paths.len()] | |
| ); | |
| assert_eq!( | |
| sources | |
| .iter() | |
| .map(|source| source.source_relative_path.as_str()) | |
| .collect::<Vec<_>>(), | |
| expected_relative_paths | |
| ); | |
| assert_eq!( | |
| sources | |
| .iter() | |
| .map(|source| source.hooks.handler_count()) | |
| .collect::<Vec<_>>(), | |
| vec![1; expected_relative_paths.len()] | |
| ); | |
| } | |
| fn load_plugin_hooks_discovers_default_hooks_file() { | |
| let (_tmp, plugin_root) = plugin_root(); | |
| write_manifest(&plugin_root, r#"{ "name": "demo-plugin" }"#); | |
| fs::write( | |
| plugin_root.join("hooks/hooks.json"), | |
| r#"{ | |
| "hooks": { | |
| "PreToolUse": [ | |
| { | |
| "matcher": "Bash", | |
| "hooks": [{ "type": "command", "command": "echo default" }] | |
| } | |
| ] | |
| } | |
| }"#, | |
| ) | |
| .expect("write hooks"); | |
| let (sources, warnings) = load_sources(&plugin_root); | |
| assert_eq!(warnings, Vec::<String>::new()); | |
| assert_sources(&sources, &["hooks/hooks.json"]); | |
| } | |
| fn load_plugin_hooks_supports_manifest_hook_path() { | |
| let (_tmp, plugin_root) = plugin_root(); | |
| write_manifest( | |
| &plugin_root, | |
| r#"{ | |
| "name": "demo-plugin", | |
| "hooks": "./hooks/one.json" | |
| }"#, | |
| ); | |
| write_hook_file(&plugin_root, "hooks/one.json", "PreToolUse", "echo one"); | |
| let (sources, warnings) = load_sources(&plugin_root); | |
| assert_eq!(warnings, Vec::<String>::new()); | |
| assert_sources(&sources, &["hooks/one.json"]); | |
| } | |
| fn load_plugin_hooks_manifest_paths_replace_default_hooks_file() { | |
| let (_tmp, plugin_root) = plugin_root(); | |
| write_manifest( | |
| &plugin_root, | |
| r#"{ | |
| "name": "demo-plugin", | |
| "hooks": ["./hooks/one.json", "./hooks/two.json"] | |
| }"#, | |
| ); | |
| write_hook_file( | |
| &plugin_root, | |
| "hooks/hooks.json", | |
| "PreToolUse", | |
| "echo ignored", | |
| ); | |
| write_hook_file(&plugin_root, "hooks/one.json", "PreToolUse", "echo one"); | |
| write_hook_file(&plugin_root, "hooks/two.json", "PostToolUse", "echo two"); | |
| let (sources, warnings) = load_sources(&plugin_root); | |
| assert_eq!(warnings, Vec::<String>::new()); | |
| assert_sources(&sources, &["hooks/one.json", "hooks/two.json"]); | |
| } | |
| fn load_plugin_hooks_supports_inline_manifest_hooks() { | |
| let (_tmp, plugin_root) = plugin_root(); | |
| write_manifest( | |
| &plugin_root, | |
| r#"{ | |
| "name": "demo-plugin", | |
| "hooks": { | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "matcher": "startup", | |
| "hooks": [{ "type": "command", "command": "echo inline" }] | |
| } | |
| ] | |
| } | |
| } | |
| }"#, | |
| ); | |
| let (sources, warnings) = load_sources(&plugin_root); | |
| assert_eq!(warnings, Vec::<String>::new()); | |
| assert_sources(&sources, &["plugin.json#hooks[0]"]); | |
| } | |
| fn load_plugin_hooks_reports_invalid_hook_file() { | |
| let (_tmp, plugin_root) = plugin_root(); | |
| write_manifest(&plugin_root, r#"{ "name": "demo-plugin" }"#); | |
| fs::write(plugin_root.join("hooks/hooks.json"), "{ not-json").expect("write invalid hooks"); | |
| let (sources, warnings) = load_sources(&plugin_root); | |
| assert_eq!(sources, Vec::<PluginHookSource>::new()); | |
| assert_eq!( | |
| warnings, | |
| vec![format!( | |
| "failed to parse plugin hooks config {}: key must be a string at line 1 column 3", | |
| plugin_root.join("hooks/hooks.json").display() | |
| )] | |
| ); | |
| } | |
| fn load_plugin_hooks_supports_inline_manifest_hook_list() { | |
| let (_tmp, plugin_root) = plugin_root(); | |
| write_manifest( | |
| &plugin_root, | |
| r#"{ | |
| "name": "demo-plugin", | |
| "hooks": [ | |
| { | |
| "hooks": { | |
| "SessionStart": [ | |
| { | |
| "hooks": [{ "type": "command", "command": "echo inline one" }] | |
| } | |
| ] | |
| } | |
| }, | |
| { | |
| "hooks": { | |
| "Stop": [ | |
| { | |
| "hooks": [{ "type": "command", "command": "echo inline two" }] | |
| } | |
| ] | |
| } | |
| } | |
| ] | |
| }"#, | |
| ); | |
| let (sources, warnings) = load_sources(&plugin_root); | |
| assert_eq!(warnings, Vec::<String>::new()); | |
| assert_sources(&sources, &["plugin.json#hooks[0]", "plugin.json#hooks[1]"]); | |
| } | |
| fn materialize_git_subdir_uses_sparse_checkout() { | |
| let run_git = |args: &[&str], cwd| super::run_git(args, cwd, PluginGitMode::Manual); | |
| let run_git_output = | |
| |args: &[&str], cwd| super::run_git_output(args, cwd, PluginGitMode::Manual); | |
| let codex_home = tempfile::tempdir().expect("create codex home"); | |
| let repo = tempfile::tempdir().expect("create git repo"); | |
| let plugin_dir = repo.path().join("plugins/toolkit"); | |
| fs::create_dir_all(&plugin_dir).expect("create plugin directory"); | |
| fs::create_dir_all(repo.path().join("plugins/other")).expect("create other plugin"); | |
| fs::write(plugin_dir.join("marker.txt"), "toolkit").expect("write plugin marker"); | |
| fs::write(repo.path().join("plugins/other/marker.txt"), "other").expect("write other marker"); | |
| fs::write(repo.path().join("root.txt"), "root").expect("write root marker"); | |
| run_git(&["init"], Some(repo.path())).expect("init git repo"); | |
| run_git( | |
| &["config", "user.email", "test@example.com"], | |
| Some(repo.path()), | |
| ) | |
| .expect("configure git email"); | |
| run_git(&["config", "user.name", "Test User"], Some(repo.path())).expect("configure git name"); | |
| run_git(&["add", "."], Some(repo.path())).expect("stage git repo"); | |
| run_git(&["commit", "-m", "init"], Some(repo.path())).expect("commit git repo"); | |
| let sha = run_git_output(&["rev-parse", "HEAD"], Some(repo.path())).expect("resolve commit"); | |
| let materialized = materialize_marketplace_plugin_source( | |
| codex_home.path(), | |
| &MarketplacePluginSource::Git { | |
| url: repo.path().display().to_string(), | |
| path: Some("plugins/toolkit".to_string()), | |
| ref_name: None, | |
| sha: Some(sha), | |
| }, | |
| ) | |
| .expect("materialize git source"); | |
| assert_eq!( | |
| plugin_dir.file_name(), | |
| materialized.path.as_path().file_name() | |
| ); | |
| assert!(materialized.path.as_path().join("marker.txt").is_file()); | |
| let checkout_root = materialized | |
| .path | |
| .as_path() | |
| .parent() | |
| .and_then(Path::parent) | |
| .expect("materialized path should be nested under checkout root"); | |
| assert!(!checkout_root.join("root.txt").exists()); | |
| assert!(!checkout_root.join("plugins/other/marker.txt").exists()); | |
| } | |
| fn materialize_git_source_rejects_sha_that_resolves_to_hostile_default_branch() { | |
| let run_git = |args: &[&str], cwd| super::run_git(args, cwd, PluginGitMode::Manual); | |
| let run_git_output = | |
| |args: &[&str], cwd| super::run_git_output(args, cwd, PluginGitMode::Manual); | |
| let codex_home = tempfile::tempdir().expect("create codex home"); | |
| let repo = tempfile::tempdir().expect("create git repo"); | |
| run_git(&["init"], Some(repo.path())).expect("init git repo"); | |
| run_git( | |
| &["config", "user.email", "test@example.com"], | |
| Some(repo.path()), | |
| ) | |
| .expect("configure git email"); | |
| run_git(&["config", "user.name", "Test User"], Some(repo.path())).expect("configure git name"); | |
| fs::write(repo.path().join("marker.txt"), "benign").expect("write benign marker"); | |
| run_git(&["add", "."], Some(repo.path())).expect("stage git repo"); | |
| run_git(&["commit", "-m", "benign"], Some(repo.path())).expect("commit benign revision"); | |
| let benign_sha = | |
| run_git_output(&["rev-parse", "HEAD"], Some(repo.path())).expect("resolve commit A"); | |
| fs::write(repo.path().join("marker.txt"), "malicious").expect("write malicious marker"); | |
| run_git(&["add", "."], Some(repo.path())).expect("stage malicious revision"); | |
| run_git(&["commit", "-m", "malicious"], Some(repo.path())).expect("commit malicious revision"); | |
| let malicious_sha = | |
| run_git_output(&["rev-parse", "HEAD"], Some(repo.path())).expect("resolve commit B"); | |
| run_git(&["branch", "-m", &benign_sha], Some(repo.path())) | |
| .expect("name default branch after commit A"); | |
| let err = materialize_marketplace_plugin_source( | |
| codex_home.path(), | |
| &MarketplacePluginSource::Git { | |
| url: repo.path().display().to_string(), | |
| path: None, | |
| ref_name: None, | |
| sha: Some(benign_sha.clone()), | |
| }, | |
| ) | |
| .expect_err("hostile default branch must not satisfy SHA pinning"); | |
| assert_eq!( | |
| err, | |
| format!("checked out Git SHA {malicious_sha} does not match requested SHA {benign_sha}") | |
| ); | |
| } | |