mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Implement v1 skills extension prompt injection (#26167)
## Why The skills extension needs a real turn-time path before host, executor, or remote skills can be routed through it. The previous code was mostly a placeholder catalog/provider sketch, so there was no bounded available-skills fragment, no source-owned `SKILL.md` read, and no place for warnings or per-turn selection state to live. This PR makes `ext/skills` the authority-preserving flow for listing candidate skills and injecting only explicitly selected main prompts, without adding more of that logic to `codex-core`. ## What changed - Expands catalog entries with `main_prompt`, display path, short description, dependency metadata, enabled/prompt visibility flags, and authority/package-aware read requests. - Replaces the placeholder `providers/*` modules with `SkillProviderSource` and `SkillProviders`, routing list/read/search calls by source kind and surfacing provider failures as warnings. - Adds bounded available-skills rendering and `SKILL.md` main-prompt truncation before the fragments enter model context. - Resolves explicit skill selections from structured `UserInput::Skill`, skill-file mentions, `skill://...` paths, and plain `$skill` text mentions, then reads selected prompts through their owning provider. - Stores mutable per-thread skills config and per-turn catalog/selection/warning state. - Adds `install_with_providers` so tests and future host wiring can supply concrete providers. ## Testing - Not run locally. - Added `codex-rs/ext/skills/tests/skills_extension.rs` coverage for available-catalog injection, selected prompt injection through the owning provider, and prompt-hidden skills that remain invokable.
This commit is contained in:
15 files changed
+1001
-335
No files matched your search
@@ -0,0 +1,129 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_core_skills::injection::extract_tool_mentions;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
use crate::catalog::SkillAuthority;
|
||||
use crate::catalog::SkillCatalog;
|
||||
use crate::catalog::SkillCatalogEntry;
|
||||
use crate::catalog::SkillPackageId;
|
||||
|
||||
const SKILL_PATH_PREFIX: &str = "skill://";
|
||||
|
||||
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::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.main_prompt.0 == path
|
||||
|| entry.id.0 == path
|
||||
|| entry
|
||||
.display_path
|
||||
.as_deref()
|
||||
.is_some_and(|display_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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user