mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
## What changed - Build executor skill catalogs from discovery V2 inventories without reopening host paths or falling back to legacy discovery. - Merge Cloud and executor skills by remote plugin identity and skill name, retaining Cloud metadata and accepted provider read routes. Apply plugin selection and disablement before merging. - Share a stable catalog across each step's rendering, list/read tools, and explicit and implicit skill invocations. - Preserve unmatched executor entries when Cloud catalogs are unavailable or partial; only complete Cloud catalogs disable unmatched remote entries. - Bind discovery caches to captured environments and executor sessions, and refresh MCP when V2 inventories change. Preserve pending invalidations when forced MCP refreshes fail. ## Testing Add regression coverage for V2 inventory sharing and refresh, environment isolation, catalog merging, partial Cloud results, disabled plugins, and catalog consistency across steps and read routes. GitOrigin-RevId: 842bdcbc9e19364dd69a9abf9e48330b7425a76c
145 lines
4.1 KiB
Rust
145 lines
4.1 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use codex_protocol::user_input::UserInput;
|
|
use codex_skills::extract_tool_mentions;
|
|
|
|
use crate::catalog::SkillAuthority;
|
|
use crate::catalog::SkillCatalog;
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::catalog::SkillPackageId;
|
|
|
|
const SKILL_PATH_PREFIX: &str = "skill://";
|
|
|
|
#[tracing::instrument(
|
|
level = "trace",
|
|
skip_all,
|
|
fields(
|
|
input_count = inputs.len(),
|
|
catalog_entry_count = catalog.entries.len()
|
|
)
|
|
)]
|
|
pub(crate) fn collect_explicit_skill_mentions(
|
|
inputs: &[UserInput],
|
|
catalog: &SkillCatalog,
|
|
) -> Vec<SkillCatalogEntry> {
|
|
let mut selected = Vec::new();
|
|
let mut seen = HashSet::new();
|
|
let mut blocked_plain_names = HashSet::new();
|
|
|
|
for input in inputs {
|
|
match input {
|
|
UserInput::Skill { name, path } => {
|
|
blocked_plain_names.insert(name.clone());
|
|
select_by_path(catalog, &path.to_string_lossy(), &mut seen, &mut selected);
|
|
}
|
|
UserInput::Mention { name, path } if path_is_skill(path) => {
|
|
blocked_plain_names.insert(name.clone());
|
|
select_by_path(catalog, path, &mut seen, &mut selected);
|
|
}
|
|
UserInput::Text { .. }
|
|
| UserInput::Image { .. }
|
|
| UserInput::LocalImage { .. }
|
|
| UserInput::Audio { .. }
|
|
| UserInput::LocalAudio { .. } => {}
|
|
UserInput::Mention { .. } => {}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
for input in inputs {
|
|
let UserInput::Text { text, .. } = input else {
|
|
continue;
|
|
};
|
|
|
|
let mentions = extract_tool_mentions(text);
|
|
for path in mentions.paths() {
|
|
if path_is_skill(path) {
|
|
select_by_path(
|
|
catalog,
|
|
normalize_skill_path(path),
|
|
&mut seen,
|
|
&mut selected,
|
|
);
|
|
}
|
|
}
|
|
for name in mentions.plain_names() {
|
|
if blocked_plain_names.contains(name) {
|
|
continue;
|
|
}
|
|
if let Some(entry) = catalog
|
|
.entries
|
|
.iter()
|
|
.find(|entry| entry.enabled && entry.name == name)
|
|
{
|
|
push_selected(entry, &mut seen, &mut selected);
|
|
}
|
|
}
|
|
}
|
|
|
|
selected
|
|
}
|
|
|
|
fn select_by_path(
|
|
catalog: &SkillCatalog,
|
|
path: &str,
|
|
seen: &mut HashSet<SkillCatalogEntryKey>,
|
|
selected: &mut Vec<SkillCatalogEntry>,
|
|
) {
|
|
let normalized_path = normalize_skill_path(path);
|
|
for entry in catalog.entries.iter().filter(|entry| entry.enabled) {
|
|
if entry_matches_path(entry, normalized_path) {
|
|
push_selected(entry, seen, selected);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn push_selected(
|
|
entry: &SkillCatalogEntry,
|
|
seen: &mut HashSet<SkillCatalogEntryKey>,
|
|
selected: &mut Vec<SkillCatalogEntry>,
|
|
) {
|
|
let key = SkillCatalogEntryKey::from(entry);
|
|
if seen.insert(key) {
|
|
selected.push(entry.clone());
|
|
}
|
|
}
|
|
|
|
fn entry_matches_path(entry: &SkillCatalogEntry, path: &str) -> bool {
|
|
entry.source_locations().any(|source| {
|
|
source.enabled
|
|
&& (source.main_prompt.as_str() == path
|
|
|| source.package.0 == path
|
|
|| source
|
|
.display_path
|
|
.as_deref()
|
|
.is_some_and(|display_path| normalize_skill_path(display_path) == path))
|
|
})
|
|
}
|
|
|
|
fn path_is_skill(path: &str) -> bool {
|
|
path.starts_with(SKILL_PATH_PREFIX)
|
|
|| path
|
|
.rsplit(['/', '\\'])
|
|
.next()
|
|
.is_some_and(|file_name| file_name.eq_ignore_ascii_case("SKILL.md"))
|
|
}
|
|
|
|
fn normalize_skill_path(path: &str) -> &str {
|
|
path.strip_prefix(SKILL_PATH_PREFIX).unwrap_or(path)
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
struct SkillCatalogEntryKey {
|
|
authority: SkillAuthority,
|
|
package: SkillPackageId,
|
|
}
|
|
|
|
impl From<&SkillCatalogEntry> for SkillCatalogEntryKey {
|
|
fn from(entry: &SkillCatalogEntry) -> Self {
|
|
Self {
|
|
authority: entry.authority.clone(),
|
|
package: entry.id.clone(),
|
|
}
|
|
}
|
|
}
|