mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add batched executor capability discovery (#33852)
## Why Selected capability roots can contribute plugins, MCP servers, connectors, and skills. Discovering each contribution separately requires repeated access to the executor filesystem. ## What changed - Add the `capabilityRoots/discoverV1` exec-server RPC to scan selected roots and materialize recognized plugin manifests, configuration files, skill instructions, and skill metadata in one bounded request. - Add the opt-in `executor_capability_discovery` feature, with a thread-scoped cache and per-step snapshot shared by MCP and skill discovery. - Parse MCP, connector, and skill contributions from the materialized snapshot, including serving cached skill instructions without another filesystem read. ## Testing - Cover discovery limits, manifest precedence, root-local failures, cache reuse, plugin contributions, and parity with the existing environment skill loader. GitOrigin-RevId: f98fd2321cafb58c596db02da1f83c09d8eb375d
This commit is contained in:
1 parent
caa6e554d6
commit
08e30a2e4e
47 files changed
+1957
-47
No files matched your search
@@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
use codex_exec_server::CapabilityRootDiscovery;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_protocol::protocol::Product;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -45,6 +47,19 @@ pub struct EnvironmentSkillMetadata {
|
||||
pub policy: Option<SkillPolicy>,
|
||||
}
|
||||
|
||||
/// Parsed executor skill plus the instructions already materialized by capability discovery.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EnvironmentSkillSnapshot {
|
||||
pub metadata: EnvironmentSkillMetadata,
|
||||
pub instructions: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvironmentSkillSnapshotOutcome {
|
||||
pub skills: Vec<EnvironmentSkillSnapshot>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl EnvironmentSkillMetadata {
|
||||
pub fn allows_implicit_invocation(&self) -> bool {
|
||||
self.policy
|
||||
@@ -212,6 +227,130 @@ pub async fn load_environment_skills_from_root(
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Parses an executor-produced manifest bundle without issuing additional filesystem requests.
|
||||
pub fn load_environment_skills_from_discovery(
|
||||
discovery: &CapabilityRootDiscovery,
|
||||
restriction_product: Option<Product>,
|
||||
) -> EnvironmentSkillSnapshotOutcome {
|
||||
let mut outcome = EnvironmentSkillSnapshotOutcome {
|
||||
warnings: discovery.warnings.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(error) = &discovery.error {
|
||||
outcome.warnings.push(error.clone());
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let mut plugin_namespaces = HashMap::new();
|
||||
for (plugin_root, name) in discovery.namespace_manifests.iter().filter_map(|manifest| {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ManifestName {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
let plugin_root = manifest.path.parent()?.parent()?;
|
||||
let parsed = serde_json::from_str::<ManifestName>(&manifest.contents).ok()?;
|
||||
let name = if parsed.name.trim().is_empty() {
|
||||
plugin_root.basename()?
|
||||
} else {
|
||||
parsed.name
|
||||
};
|
||||
Some((plugin_root, name))
|
||||
}) {
|
||||
// Exec-server orders manifests by the same precedence as local discovery. Preserve the
|
||||
// first manifest if an older or alternate server returns duplicates for one plugin root.
|
||||
plugin_namespaces.entry(plugin_root).or_insert(name);
|
||||
}
|
||||
|
||||
for skill in &discovery.skills {
|
||||
let plugin_namespace =
|
||||
nearest_plugin_namespace(&skill.instructions.path, &plugin_namespaces);
|
||||
let ParsedSkillFrontmatter {
|
||||
name: base_name,
|
||||
description,
|
||||
short_description,
|
||||
} = match parse_skill_frontmatter_metadata_inner(&skill.instructions.contents, || {
|
||||
default_skill_name(&skill.instructions.path)
|
||||
}) {
|
||||
Ok(frontmatter) => frontmatter,
|
||||
Err(error) => {
|
||||
outcome.warnings.push(format!(
|
||||
"Failed to load environment skill at {}: {error}",
|
||||
skill.instructions.path
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let name = plugin_namespace
|
||||
.map(|namespace| format!("{namespace}:{base_name}"))
|
||||
.unwrap_or(base_name);
|
||||
if let Err(error) = validate_len(&name, MAX_QUALIFIED_NAME_LEN, "qualified name") {
|
||||
outcome.warnings.push(format!(
|
||||
"Failed to load environment skill at {}: {error}",
|
||||
skill.instructions.path
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let (dependencies, policy) = skill
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| {
|
||||
serde_yaml::from_str::<SkillMetadataFile>(&metadata.contents)
|
||||
.map_err(|error| {
|
||||
tracing::warn!(
|
||||
path = %metadata.path,
|
||||
"ignoring invalid discovered skill metadata: {error}"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.map(|metadata| {
|
||||
(
|
||||
resolve_dependencies(metadata.dependencies),
|
||||
resolve_policy(metadata.policy),
|
||||
)
|
||||
})
|
||||
.unwrap_or((None, None));
|
||||
let metadata = EnvironmentSkillMetadata {
|
||||
path_to_skills_md: skill.instructions.path.clone(),
|
||||
name,
|
||||
description,
|
||||
short_description,
|
||||
dependencies,
|
||||
policy,
|
||||
};
|
||||
if metadata.matches_product_restriction(restriction_product) {
|
||||
outcome.skills.push(EnvironmentSkillSnapshot {
|
||||
metadata,
|
||||
instructions: skill.instructions.contents.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
outcome.skills.sort_by(|left, right| {
|
||||
left.metadata.name.cmp(&right.metadata.name).then_with(|| {
|
||||
left.metadata
|
||||
.path_to_skills_md
|
||||
.to_string()
|
||||
.cmp(&right.metadata.path_to_skills_md.to_string())
|
||||
})
|
||||
});
|
||||
outcome
|
||||
}
|
||||
|
||||
fn nearest_plugin_namespace<'a>(
|
||||
skill_path: &PathUri,
|
||||
plugin_namespaces: &'a HashMap<PathUri, String>,
|
||||
) -> Option<&'a str> {
|
||||
let mut ancestor = skill_path.parent();
|
||||
while let Some(path) = ancestor {
|
||||
if let Some(namespace) = plugin_namespaces.get(&path) {
|
||||
return Some(namespace);
|
||||
}
|
||||
ancestor = path.parent();
|
||||
}
|
||||
None
|
||||
}
|
||||
async fn read_skill_contents(
|
||||
file_system: &dyn ExecutorFileSystem,
|
||||
skill_path: &PathUri,
|
||||
|
||||
Reference in new issue
Block a user