Files
codex/codex-rs/ext/skills/src/sources.rs
T
jif 96d2d2f68c 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.
2026-06-03 16:24:16 +02:00

184 lines
5.1 KiB
Rust

use std::fmt;
use std::sync::Arc;
use crate::catalog::SkillCatalog;
use crate::catalog::SkillProviderError;
use crate::catalog::SkillReadResult;
use crate::catalog::SkillSearchResult;
use crate::catalog::SkillSourceKind;
use crate::provider::SkillListQuery;
use crate::provider::SkillProvider;
use crate::provider::SkillReadRequest;
use crate::provider::SkillSearchRequest;
#[derive(Clone)]
pub struct SkillProviderSource {
kind: SkillSourceKind,
label: String,
provider: Arc<dyn SkillProvider>,
}
impl SkillProviderSource {
pub fn new(
kind: SkillSourceKind,
label: impl Into<String>,
provider: Arc<dyn SkillProvider>,
) -> Self {
Self {
kind,
label: label.into(),
provider,
}
}
pub fn host(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self {
Self::new(SkillSourceKind::Host, label, provider)
}
pub fn executor(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self {
Self::new(SkillSourceKind::Executor, label, provider)
}
pub fn remote(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self {
Self::new(SkillSourceKind::Remote, label, provider)
}
fn should_list(&self, query: &SkillListQuery) -> bool {
match &self.kind {
SkillSourceKind::Host => query.include_host_skills,
SkillSourceKind::Executor => !query.executor_authorities.is_empty(),
SkillSourceKind::Remote => query.include_remote_skills,
SkillSourceKind::Custom(_) => true,
}
}
fn owns_kind(&self, kind: &SkillSourceKind) -> bool {
&self.kind == kind
}
}
impl fmt::Debug for SkillProviderSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SkillProviderSource")
.field("kind", &self.kind)
.field("label", &self.label)
.finish()
}
}
#[derive(Clone, Default, Debug)]
pub struct SkillProviders {
sources: Vec<SkillProviderSource>,
}
impl SkillProviders {
pub fn new() -> Self {
Self::default()
}
pub fn with_provider(mut self, source: SkillProviderSource) -> Self {
self.sources.push(source);
self
}
pub fn with_host_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self {
self.sources
.push(SkillProviderSource::host("host", provider));
self
}
pub fn with_executor_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self {
self.sources
.push(SkillProviderSource::executor("executor", provider));
self
}
pub fn with_remote_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self {
self.sources
.push(SkillProviderSource::remote("remote", provider));
self
}
pub(crate) async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
let mut catalog = SkillCatalog::default();
for source in self
.sources
.iter()
.filter(|source| source.should_list(&query))
{
extend_catalog(
&mut catalog,
source.provider.list(query.clone()).await,
source.label.as_str(),
);
}
catalog
}
pub(crate) async fn read(
&self,
request: SkillReadRequest,
) -> Result<SkillReadResult, SkillProviderError> {
let mut last_error = None;
for source in self
.sources
.iter()
.filter(|source| source.owns_kind(&request.authority.kind))
{
match source.provider.read(request.clone()).await {
Ok(result) => return Ok(result),
Err(err) => last_error = Some(err),
}
}
match last_error {
Some(err) => Err(err),
None => Err(SkillProviderError::new(format!(
"{} skill provider is not configured",
request.authority.kind
))),
}
}
pub async fn search(
&self,
request: SkillSearchRequest,
) -> Result<SkillSearchResult, SkillProviderError> {
let mut last_error = None;
for source in self
.sources
.iter()
.filter(|source| source.owns_kind(&request.authority.kind))
{
match source.provider.search(request.clone()).await {
Ok(result) => return Ok(result),
Err(err) => last_error = Some(err),
}
}
match last_error {
Some(err) => Err(err),
None => Err(SkillProviderError::new(format!(
"{} skill provider is not configured",
request.authority.kind
))),
}
}
}
fn extend_catalog(
catalog: &mut SkillCatalog,
result: Result<SkillCatalog, SkillProviderError>,
label: &str,
) {
match result {
Ok(source_catalog) => catalog.extend(source_catalog),
Err(err) => catalog
.warnings
.push(format!("{label} skills unavailable: {}", err.message)),
}
}