mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Support plugin agent roles
This commit is contained in:
@@ -57,6 +57,7 @@ use tracing::instrument;
|
||||
use tracing::warn;
|
||||
|
||||
const DEFAULT_SKILLS_DIR_NAME: &str = "skills";
|
||||
const DEFAULT_AGENTS_DIR_NAME: &str = "agents";
|
||||
const DEFAULT_HOOKS_CONFIG_FILE: &str = "hooks/hooks.json";
|
||||
const DEFAULT_MCP_CONFIG_FILE: &str = ".mcp.json";
|
||||
const DEFAULT_APP_CONFIG_FILE: &str = ".app.json";
|
||||
@@ -764,6 +765,7 @@ async fn load_plugin(
|
||||
manifest_description: None,
|
||||
root,
|
||||
enabled: plugin.enabled,
|
||||
agent_roots: Vec::new(),
|
||||
skill_roots: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: false,
|
||||
@@ -812,6 +814,7 @@ async fn load_plugin(
|
||||
} => {
|
||||
loaded_plugin.manifest_name = Some(manifest.display_name().to_string());
|
||||
loaded_plugin.manifest_description = manifest.description.clone();
|
||||
loaded_plugin.agent_roots = plugin_agent_roots(&plugin_root, manifest_paths);
|
||||
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
|
||||
let resolved_skills = load_plugin_skills(
|
||||
&plugin_root,
|
||||
@@ -972,6 +975,28 @@ fn plugin_skill_roots(
|
||||
paths
|
||||
}
|
||||
|
||||
fn plugin_agent_roots(
|
||||
plugin_root: &AbsolutePathBuf,
|
||||
manifest_paths: &PluginManifestPaths,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let mut paths = default_agent_roots(plugin_root);
|
||||
if let Some(path) = &manifest_paths.agents {
|
||||
paths.push(path.clone());
|
||||
}
|
||||
paths.sort_unstable();
|
||||
paths.dedup();
|
||||
paths
|
||||
}
|
||||
|
||||
fn default_agent_roots(plugin_root: &AbsolutePathBuf) -> Vec<AbsolutePathBuf> {
|
||||
let agents_dir = plugin_root.join(DEFAULT_AGENTS_DIR_NAME);
|
||||
if agents_dir.is_dir() {
|
||||
vec![agents_dir]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_skill_roots(plugin_root: &AbsolutePathBuf) -> Vec<AbsolutePathBuf> {
|
||||
let skills_dir = plugin_root.join(DEFAULT_SKILLS_DIR_NAME);
|
||||
if skills_dir.is_dir() {
|
||||
|
||||
@@ -6,6 +6,9 @@ use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginLoadOutcome;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_plugins::PluginAgentRoot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -66,6 +69,80 @@ fn configured_plugins_from_stack_merges_user_layers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_discovers_default_and_manifest_agent_roots() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
let plugin_root = temp_dir.path().join("plugins/cache/test/agented/local");
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"agented","agents":"./extra-agents"}"#,
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join("agents/researcher.toml"),
|
||||
r#"name = "researcher"
|
||||
description = "Research role"
|
||||
developer_instructions = "Research carefully"
|
||||
"#,
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join("extra-agents/reviewer.toml"),
|
||||
r#"name = "reviewer"
|
||||
description = "Review role"
|
||||
developer_instructions = "Review carefully"
|
||||
"#,
|
||||
);
|
||||
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![user_layer(
|
||||
user_config_path(&temp_dir, "config.toml"),
|
||||
r#"
|
||||
[plugins."agented@test"]
|
||||
enabled = true
|
||||
"#,
|
||||
)],
|
||||
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,
|
||||
HashMap::new(),
|
||||
&store,
|
||||
Some(Product::Codex),
|
||||
/*prefer_remote_curated_conflicts*/ false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let plugin = plugins
|
||||
.iter()
|
||||
.find(|plugin| plugin.config_name == "agented@test")
|
||||
.expect("agented plugin should load");
|
||||
assert_eq!(
|
||||
plugin.agent_roots,
|
||||
vec![
|
||||
plugin_root.join("agents").abs(),
|
||||
plugin_root.join("extra-agents").abs(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
PluginLoadOutcome::from_plugins(plugins).effective_plugin_agent_roots(),
|
||||
vec![
|
||||
PluginAgentRoot {
|
||||
path: plugin_root.join("agents").abs(),
|
||||
plugin_id: "agented@test".to_string(),
|
||||
plugin_root: plugin_root.abs(),
|
||||
},
|
||||
PluginAgentRoot {
|
||||
path: plugin_root.join("extra-agents").abs(),
|
||||
plugin_id: "agented@test".to_string(),
|
||||
plugin_root: plugin_root.abs(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hooks_only_scope_shares_plugin_resolution_without_loading_other_capabilities() {
|
||||
let temp_dir = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -864,6 +864,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
),
|
||||
root: AbsolutePathBuf::try_from(plugin_root.clone()).unwrap(),
|
||||
enabled: true,
|
||||
agent_roots: Vec::new(),
|
||||
skill_roots: vec![plugin_root.join("skills").abs()],
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: true,
|
||||
@@ -2145,6 +2146,7 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions
|
||||
manifest_description: None,
|
||||
root: AbsolutePathBuf::try_from(plugin_root).unwrap(),
|
||||
enabled: false,
|
||||
agent_roots: Vec::new(),
|
||||
skill_roots: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: false,
|
||||
@@ -2320,6 +2322,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
manifest_description: None,
|
||||
root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(),
|
||||
enabled: true,
|
||||
agent_roots: Vec::new(),
|
||||
skill_roots: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: false,
|
||||
|
||||
@@ -33,6 +33,8 @@ struct RawPluginManifest {
|
||||
// Keep manifest paths as raw strings so we can validate the required `./...` syntax before
|
||||
// resolving them under the plugin root.
|
||||
#[serde(default)]
|
||||
agents: Option<RawPluginManifestPaths>,
|
||||
#[serde(default)]
|
||||
skills: Option<RawPluginManifestPaths>,
|
||||
#[serde(default)]
|
||||
mcp_servers: Option<RawPluginManifestMcpServers>,
|
||||
@@ -162,6 +164,7 @@ pub(crate) fn parse_plugin_manifest_uri(
|
||||
version,
|
||||
description,
|
||||
keywords,
|
||||
agents,
|
||||
skills,
|
||||
mcp_servers,
|
||||
apps,
|
||||
@@ -258,6 +261,7 @@ pub(crate) fn parse_plugin_manifest_uri(
|
||||
description,
|
||||
keywords,
|
||||
paths: codex_plugin::manifest::PluginManifestPaths {
|
||||
agents: resolve_manifest_paths(plugin_root, "agents", agents.as_ref()),
|
||||
skills: resolve_manifest_paths(plugin_root, "skills", skills.as_ref()),
|
||||
mcp_servers: resolve_manifest_mcp_servers(plugin_root, mcp_servers),
|
||||
apps: resolve_manifest_path(plugin_root, "apps", apps.as_deref()),
|
||||
|
||||
Reference in New Issue
Block a user