Extract runtime skill source boundary

This commit is contained in:
jif-oai
2026-06-25 13:05:05 +01:00
parent 6ed45b053c
commit 6c9dde77d0
4 changed files with 462 additions and 291 deletions
+1
View File
@@ -7,6 +7,7 @@ pub mod model;
pub mod remote;
pub mod render;
mod root_loader;
pub mod runtime;
pub mod service;
mod skill_instructions;
pub mod system;
+371
View File
@@ -0,0 +1,371 @@
use std::any::Any;
use std::fmt;
use std::future::Future;
use std::hash::Hash;
use std::hash::Hasher;
use std::pin::Pin;
use std::sync::Arc;
use codex_utils_path_uri::PathUri;
use crate::model::SkillDependencies;
/// Source authority that owns a skill package and must be used to read it.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SkillSourceKind {
/// Codex-hosted skills, including bundled, user, repo, plugin-installed,
/// and downloaded/materialized remote skills.
Host,
/// Skills owned by an execution environment.
Executor,
/// Skills owned by the orchestrator rather than an execution environment.
Orchestrator,
/// A source kind for providers that do not fit an existing transport category.
Custom(String),
}
impl SkillSourceKind {
pub fn custom(kind: impl Into<String>) -> Self {
Self::Custom(kind.into())
}
fn as_str(&self) -> &str {
match self {
Self::Host => "host",
Self::Executor => "executor",
Self::Orchestrator => "orchestrator",
Self::Custom(kind) => kind,
}
}
}
impl fmt::Display for SkillSourceKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(formatter)
}
}
/// Opaque authority identity for list/read routing.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SkillAuthority {
pub kind: SkillSourceKind,
pub id: String,
}
impl SkillAuthority {
pub fn new(kind: SkillSourceKind, id: impl Into<String>) -> Self {
Self {
kind,
id: id.into(),
}
}
}
/// Opaque package id. Callers should not parse local paths out of this value.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SkillPackageId(pub String);
/// Opaque resource id inside a skill package, optionally bound to the
/// environment path that owns its contents.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SkillResourceId {
id: String,
environment_path: Option<EnvironmentSkillResource>,
}
impl SkillResourceId {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
environment_path: None,
}
}
pub fn environment(
id: impl Into<String>,
environment_id: impl Into<String>,
path: PathUri,
) -> Self {
Self {
id: id.into(),
environment_path: Some(EnvironmentSkillResource {
environment_id: environment_id.into(),
path,
}),
}
}
pub fn as_str(&self) -> &str {
&self.id
}
pub fn environment_path(&self) -> Option<(&str, &PathUri)> {
self.environment_path
.as_ref()
.map(|resource| (resource.environment_id.as_str(), &resource.path))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct EnvironmentSkillResource {
environment_id: String,
path: PathUri,
}
/// Metadata shown in the always-visible skills catalog.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillCatalogEntry {
pub id: SkillPackageId,
pub authority: SkillAuthority,
pub name: String,
pub description: String,
pub short_description: Option<String>,
pub main_prompt: SkillResourceId,
pub display_path: Option<String>,
pub dependencies: Option<SkillDependencies>,
pub enabled: bool,
pub prompt_visible: bool,
}
impl SkillCatalogEntry {
pub fn new(
id: SkillPackageId,
authority: SkillAuthority,
name: impl Into<String>,
description: impl Into<String>,
main_prompt: SkillResourceId,
) -> Self {
Self {
id,
authority,
name: name.into(),
description: description.into(),
short_description: None,
main_prompt,
display_path: None,
dependencies: None,
enabled: true,
prompt_visible: true,
}
}
pub fn with_short_description(mut self, short_description: Option<String>) -> Self {
self.short_description = short_description;
self
}
pub fn with_display_path(mut self, display_path: impl Into<String>) -> Self {
self.display_path = Some(display_path.into());
self
}
pub fn with_dependencies(mut self, dependencies: Option<SkillDependencies>) -> Self {
self.dependencies = dependencies;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn hidden_from_prompt(mut self) -> Self {
self.prompt_visible = false;
self
}
pub fn rendered_path(&self) -> &str {
self.display_path
.as_deref()
.unwrap_or_else(|| self.main_prompt.as_str())
}
}
/// Merged catalog for one model step.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SkillCatalog {
pub entries: Vec<SkillCatalogEntry>,
pub warnings: Vec<String>,
}
impl SkillCatalog {
pub fn extend(&mut self, other: SkillCatalog) {
for entry in other.entries {
self.push_entry(entry);
}
self.warnings.extend(other.warnings);
}
pub fn push_entry(&mut self, entry: SkillCatalogEntry) {
if self
.entries
.iter()
.any(|existing| existing.authority == entry.authority && existing.id == entry.id)
{
return;
}
self.entries.push(entry);
}
}
/// A request to read one resource from the source that listed it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillReadRequest {
pub authority: SkillAuthority,
pub package: SkillPackageId,
pub resource: SkillResourceId,
}
/// Contents returned after resolving a skill resource through its owner.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillReadResult {
pub resource: SkillResourceId,
pub contents: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillSourceError {
pub message: String,
}
impl SkillSourceError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for SkillSourceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(formatter)
}
}
impl std::error::Error for SkillSourceError {}
pub type SkillSourceResult<T> = Result<T, SkillSourceError>;
/// Boxed future used to keep heterogeneous runtime skill sources object-safe.
pub type SkillSourceFuture<'a, T> = Pin<Box<dyn Future<Output = SkillSourceResult<T>> + Send + 'a>>;
/// Opaque identity of the runtime owner captured by a skill source.
#[derive(Clone)]
pub struct SkillSourceIdentity(Arc<dyn Any + Send + Sync>);
impl SkillSourceIdentity {
pub fn from_owner<T>(owner: Arc<T>) -> Self
where
T: Any + Send + Sync,
{
Self(owner)
}
}
impl fmt::Debug for SkillSourceIdentity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple("SkillSourceIdentity").finish()
}
}
impl PartialEq for SkillSourceIdentity {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for SkillSourceIdentity {}
impl Hash for SkillSourceIdentity {
fn hash<H: Hasher>(&self, state: &mut H) {
(Arc::as_ptr(&self.0) as *const ()).hash(state);
}
}
/// One bound source of runtime skills.
///
/// Implementations capture the authority needed to list and read their skills,
/// such as a host snapshot, one executor filesystem, or an orchestrator client.
/// A resource returned by [`SkillSource::list`] must be read through the same
/// source instead of being converted into an ambient local path.
pub trait SkillSource: Send + Sync {
/// Returns the authority category owned by this source.
fn kind(&self) -> SkillSourceKind;
/// Returns the stable identity of the runtime owner captured by this source.
fn identity(&self) -> SkillSourceIdentity;
/// Lists the skills available from the authority captured by this source.
fn list(&self) -> SkillSourceFuture<'_, SkillCatalog>;
/// Reads a resource previously listed by this source.
fn read(&self, request: SkillReadRequest) -> SkillSourceFuture<'_, SkillReadResult>;
}
#[derive(Clone)]
struct RegisteredSkillSource {
label: String,
source: Arc<dyn SkillSource>,
}
/// Bound skill sources used to build and read one runtime catalog.
#[derive(Clone, Default)]
pub struct SkillSources {
sources: Vec<RegisteredSkillSource>,
}
impl SkillSources {
pub fn new() -> Self {
Self::default()
}
pub fn with_source(mut self, label: impl Into<String>, source: Arc<dyn SkillSource>) -> Self {
self.sources.push(RegisteredSkillSource {
label: label.into(),
source,
});
self
}
pub async fn list(&self) -> SkillCatalog {
let mut catalog = SkillCatalog::default();
for source in &self.sources {
match source.source.list().await {
Ok(source_catalog) => catalog.extend(source_catalog),
Err(err) => catalog.warnings.push(format!(
"{} skills unavailable: {}",
source.label, err.message
)),
}
}
catalog
}
pub async fn list_kind(&self, kind: &SkillSourceKind) -> SkillSourceResult<SkillCatalog> {
let mut catalog = SkillCatalog::default();
for source in self
.sources
.iter()
.filter(|source| source.source.kind() == *kind)
{
let source_catalog = source.source.list().await.map_err(|err| {
SkillSourceError::new(format!(
"{} skills unavailable: {}",
source.label, err.message
))
})?;
catalog.extend(source_catalog);
}
Ok(catalog)
}
}
impl fmt::Debug for SkillSources {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_list()
.entries(self.sources.iter().map(|source| &source.label))
.finish()
}
}
+9 -232
View File
@@ -1,212 +1,12 @@
use codex_core_skills::model::SkillDependencies;
use codex_utils_path_uri::PathUri;
/// Source authority that owns a skill package and must be used to read it.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SkillSourceKind {
/// Codex-hosted skills, including bundled, user, repo, plugin-installed,
/// and downloaded/materialized remote skills.
Host,
/// Skills owned by an execution environment.
Executor,
/// Skills owned by the orchestrator rather than an execution environment.
Orchestrator,
/// Extension-private source kind for future providers that do not fit an
/// existing transport category.
Custom(String),
}
impl SkillSourceKind {
pub fn custom(kind: impl Into<String>) -> Self {
Self::Custom(kind.into())
}
fn as_str(&self) -> &str {
match self {
Self::Host => "host",
Self::Executor => "executor",
Self::Orchestrator => "orchestrator",
Self::Custom(kind) => kind,
}
}
}
impl std::fmt::Display for SkillSourceKind {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(formatter)
}
}
/// Opaque authority identity for list/read routing.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SkillAuthority {
pub kind: SkillSourceKind,
pub id: String,
}
impl SkillAuthority {
pub fn new(kind: SkillSourceKind, id: impl Into<String>) -> Self {
Self {
kind,
id: id.into(),
}
}
}
/// Opaque package id. Callers should not parse local paths out of this value.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SkillPackageId(pub String);
/// Opaque resource id inside a skill package, optionally bound to the
/// environment path that owns its contents.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SkillResourceId {
id: String,
environment_path: Option<EnvironmentSkillResource>,
}
impl SkillResourceId {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
environment_path: None,
}
}
pub fn environment(
id: impl Into<String>,
environment_id: impl Into<String>,
path: PathUri,
) -> Self {
Self {
id: id.into(),
environment_path: Some(EnvironmentSkillResource {
environment_id: environment_id.into(),
path,
}),
}
}
pub fn as_str(&self) -> &str {
&self.id
}
pub(crate) fn environment_path(&self) -> Option<(&str, &PathUri)> {
self.environment_path
.as_ref()
.map(|resource| (resource.environment_id.as_str(), &resource.path))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct EnvironmentSkillResource {
environment_id: String,
path: PathUri,
}
/// Metadata shown in the always-visible skills catalog.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillCatalogEntry {
pub id: SkillPackageId,
pub authority: SkillAuthority,
pub name: String,
pub description: String,
pub short_description: Option<String>,
pub main_prompt: SkillResourceId,
pub display_path: Option<String>,
pub dependencies: Option<SkillDependencies>,
pub enabled: bool,
pub prompt_visible: bool,
}
impl SkillCatalogEntry {
pub fn new(
id: SkillPackageId,
authority: SkillAuthority,
name: impl Into<String>,
description: impl Into<String>,
main_prompt: SkillResourceId,
) -> Self {
Self {
id,
authority,
name: name.into(),
description: description.into(),
short_description: None,
main_prompt,
display_path: None,
dependencies: None,
enabled: true,
prompt_visible: true,
}
}
pub fn with_short_description(mut self, short_description: Option<String>) -> Self {
self.short_description = short_description;
self
}
pub fn with_display_path(mut self, display_path: impl Into<String>) -> Self {
self.display_path = Some(display_path.into());
self
}
pub fn with_dependencies(mut self, dependencies: Option<SkillDependencies>) -> Self {
self.dependencies = dependencies;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn hidden_from_prompt(mut self) -> Self {
self.prompt_visible = false;
self
}
pub(crate) fn rendered_path(&self) -> &str {
self.display_path
.as_deref()
.unwrap_or_else(|| self.main_prompt.as_str())
}
}
/// Merged catalog for one turn.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SkillCatalog {
pub entries: Vec<SkillCatalogEntry>,
pub warnings: Vec<String>,
}
impl SkillCatalog {
pub fn extend(&mut self, other: SkillCatalog) {
for entry in other.entries {
self.push_entry(entry);
}
self.warnings.extend(other.warnings);
}
pub fn push_entry(&mut self, entry: SkillCatalogEntry) {
if self
.entries
.iter()
.any(|existing| existing.authority == entry.authority && existing.id == entry.id)
{
return;
}
self.entries.push(entry);
}
}
/// Contents returned after resolving a skill resource through its owner.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillReadResult {
pub resource: SkillResourceId,
pub contents: String,
}
pub use codex_core_skills::runtime::SkillAuthority;
pub use codex_core_skills::runtime::SkillCatalog;
pub use codex_core_skills::runtime::SkillCatalogEntry;
pub use codex_core_skills::runtime::SkillPackageId;
pub use codex_core_skills::runtime::SkillReadResult;
pub use codex_core_skills::runtime::SkillResourceId;
pub use codex_core_skills::runtime::SkillSourceError as SkillProviderError;
pub use codex_core_skills::runtime::SkillSourceKind;
pub use codex_core_skills::runtime::SkillSourceResult as SkillProviderResult;
/// Search results for a package whose files are not readable through ordinary
/// executor filesystem access.
@@ -221,26 +21,3 @@ pub struct SkillSearchMatch {
pub title: String,
pub snippet: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SkillProviderError {
pub message: String,
}
impl SkillProviderError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for SkillProviderError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.message.fmt(formatter)
}
}
impl std::error::Error for SkillProviderError {}
pub type SkillProviderResult<T> = Result<T, SkillProviderError>;
+81 -59
View File
@@ -1,6 +1,13 @@
use std::fmt;
use std::sync::Arc;
use codex_core_skills::runtime::SkillReadRequest as RuntimeSkillReadRequest;
use codex_core_skills::runtime::SkillSource;
use codex_core_skills::runtime::SkillSourceFuture;
use codex_core_skills::runtime::SkillSourceIdentity;
use codex_core_skills::runtime::SkillSources;
use codex_mcp::McpResourceClient;
use crate::catalog::SkillCatalog;
use crate::catalog::SkillProviderError;
use crate::catalog::SkillProviderResult;
@@ -17,6 +24,7 @@ pub struct SkillProviderSource {
kind: SkillSourceKind,
label: String,
provider: Arc<dyn SkillProvider>,
identity: SkillSourceIdentity,
}
impl SkillProviderSource {
@@ -25,10 +33,12 @@ impl SkillProviderSource {
label: impl Into<String>,
provider: Arc<dyn SkillProvider>,
) -> Self {
let identity = SkillSourceIdentity::from_owner(Arc::new(Arc::clone(&provider)));
Self {
kind,
label: label.into(),
provider,
identity,
}
}
@@ -56,6 +66,17 @@ impl SkillProviderSource {
fn owns_kind(&self, kind: &SkillSourceKind) -> bool {
&self.kind == kind
}
fn bind(&self, query: SkillListQuery) -> Arc<dyn SkillSource> {
Arc::new(BoundSkillProvider {
kind: self.kind.clone(),
provider: Arc::clone(&self.provider),
identity: self.identity.clone(),
host_snapshot: query.host_snapshot.clone(),
mcp_resources: query.mcp_resources.clone(),
query: Some(query),
})
}
}
impl fmt::Debug for SkillProviderSource {
@@ -108,74 +129,48 @@ impl SkillProviders {
}
pub(crate) async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
self.list_matching(&query, |source| source.should_list(&query))
.await
self.sources_for_turn(query).list().await
}
pub(crate) async fn list_orchestrator_for_turn(
&self,
query: SkillListQuery,
) -> SkillProviderResult<SkillCatalog> {
let mut catalog = SkillCatalog::default();
for source in self
.sources
.iter()
.filter(|source| source.kind == SkillSourceKind::Orchestrator)
{
let source_catalog = source.provider.list(query.clone()).await.map_err(|err| {
SkillProviderError::new(format!(
"{} skills unavailable: {}",
source.label, err.message
))
})?;
catalog.extend(source_catalog);
}
Ok(catalog)
self.sources_for_turn(query)
.list_kind(&SkillSourceKind::Orchestrator)
.await
}
async fn list_matching(
&self,
query: &SkillListQuery,
should_list: impl Fn(&SkillProviderSource) -> bool,
) -> SkillCatalog {
let mut catalog = SkillCatalog::default();
for source in self.sources.iter().filter(|source| should_list(source)) {
extend_catalog(
&mut catalog,
source.provider.list(query.clone()).await,
source.label.as_str(),
);
}
catalog
fn sources_for_turn(&self, query: SkillListQuery) -> SkillSources {
self.sources
.iter()
.filter(|source| source.should_list(&query))
.fold(SkillSources::new(), |sources, source| {
sources.with_source(source.label.clone(), source.bind(query.clone()))
})
}
pub(crate) async fn read(
&self,
request: SkillReadRequest,
) -> Result<SkillReadResult, SkillProviderError> {
let mut last_error = None;
for source in self
let mut sources = 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!(
.filter(|source| source.owns_kind(&request.authority.kind));
let Some(source) = sources.next() else {
return Err(SkillProviderError::new(format!(
"{} skill provider is not configured",
request.authority.kind
))),
)));
};
if sources.next().is_some() {
return Err(SkillProviderError::new(format!(
"{} skill authority is ambiguous",
request.authority.kind
)));
}
source.provider.read(request).await
}
pub async fn search(
@@ -204,15 +199,42 @@ impl SkillProviders {
}
}
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)),
struct BoundSkillProvider {
kind: SkillSourceKind,
provider: Arc<dyn SkillProvider>,
identity: SkillSourceIdentity,
host_snapshot: Option<Arc<codex_core_skills::HostSkillsSnapshot>>,
mcp_resources: Option<Arc<McpResourceClient>>,
query: Option<SkillListQuery>,
}
impl SkillSource for BoundSkillProvider {
fn kind(&self) -> SkillSourceKind {
self.kind.clone()
}
fn identity(&self) -> SkillSourceIdentity {
self.identity.clone()
}
fn list(&self) -> SkillSourceFuture<'_, SkillCatalog> {
let Some(query) = self.query.clone() else {
return Box::pin(async {
Err(SkillProviderError::new(
"skill source was not bound for catalog listing",
))
});
};
self.provider.list(query)
}
fn read(&self, request: RuntimeSkillReadRequest) -> SkillSourceFuture<'_, SkillReadResult> {
self.provider.read(SkillReadRequest {
authority: request.authority,
package: request.package,
resource: request.resource,
host_snapshot: self.host_snapshot.clone(),
mcp_resources: self.mcp_resources.clone(),
})
}
}