mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why We're moving exec-server to use PathUri for its internal path representations. ## What Move `ExecutorFileSystem` APIs to use `PathUri` instead of `AbsolutePathBuf`. Future changes will convert higher-level parts of exec-server.
242 lines
7.5 KiB
Rust
242 lines
7.5 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::fmt;
|
|
use std::io;
|
|
use std::sync::Arc;
|
|
|
|
use codex_exec_server::ExecutorFileSystem;
|
|
use codex_exec_server::LOCAL_FS;
|
|
use codex_protocol::protocol::Product;
|
|
use codex_protocol::protocol::SkillScope;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::PathUri;
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct SkillMetadata {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub short_description: Option<String>,
|
|
pub interface: Option<SkillInterface>,
|
|
pub dependencies: Option<SkillDependencies>,
|
|
pub policy: Option<SkillPolicy>,
|
|
/// Path to the SKILLS.md file that declares this skill.
|
|
pub path_to_skills_md: AbsolutePathBuf,
|
|
pub scope: SkillScope,
|
|
pub plugin_id: Option<String>,
|
|
}
|
|
|
|
impl SkillMetadata {
|
|
pub fn allows_implicit_invocation(&self) -> bool {
|
|
self.policy
|
|
.as_ref()
|
|
.and_then(|policy| policy.allow_implicit_invocation)
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
pub fn matches_product_restriction_for_product(
|
|
&self,
|
|
restriction_product: Option<Product>,
|
|
) -> bool {
|
|
match &self.policy {
|
|
Some(policy) => {
|
|
policy.products.is_empty()
|
|
|| restriction_product.is_some_and(|product| {
|
|
product.matches_product_restriction(&policy.products)
|
|
})
|
|
}
|
|
None => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
pub struct SkillPolicy {
|
|
pub allow_implicit_invocation: Option<bool>,
|
|
// TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and
|
|
// storing this metadata.
|
|
pub products: Vec<Product>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SkillInterface {
|
|
pub display_name: Option<String>,
|
|
pub short_description: Option<String>,
|
|
pub icon_small: Option<AbsolutePathBuf>,
|
|
pub icon_large: Option<AbsolutePathBuf>,
|
|
pub brand_color: Option<String>,
|
|
pub default_prompt: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SkillDependencies {
|
|
pub tools: Vec<SkillToolDependency>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SkillToolDependency {
|
|
pub r#type: String,
|
|
pub value: String,
|
|
pub description: Option<String>,
|
|
pub transport: Option<String>,
|
|
pub command: Option<String>,
|
|
pub url: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SkillError {
|
|
pub path: AbsolutePathBuf,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SkillLoadOutcome {
|
|
pub skills: Vec<SkillMetadata>,
|
|
pub errors: Vec<SkillError>,
|
|
pub disabled_paths: HashSet<AbsolutePathBuf>,
|
|
pub(crate) skill_roots: Vec<AbsolutePathBuf>,
|
|
pub(crate) skill_root_by_path: Arc<HashMap<AbsolutePathBuf, AbsolutePathBuf>>,
|
|
pub(crate) file_systems_by_skill_path: SkillFileSystemsByPath,
|
|
pub(crate) implicit_skills_by_scripts_dir: Arc<HashMap<AbsolutePathBuf, SkillMetadata>>,
|
|
pub(crate) implicit_skills_by_doc_path: Arc<HashMap<AbsolutePathBuf, SkillMetadata>>,
|
|
}
|
|
|
|
impl SkillLoadOutcome {
|
|
pub fn is_skill_enabled(&self, skill: &SkillMetadata) -> bool {
|
|
!self.disabled_paths.contains(&skill.path_to_skills_md)
|
|
}
|
|
|
|
pub fn is_skill_allowed_for_implicit_invocation(&self, skill: &SkillMetadata) -> bool {
|
|
self.is_skill_enabled(skill) && skill.allows_implicit_invocation()
|
|
}
|
|
|
|
pub fn allowed_skills_for_implicit_invocation(&self) -> Vec<SkillMetadata> {
|
|
self.skills
|
|
.iter()
|
|
.filter(|skill| self.is_skill_allowed_for_implicit_invocation(skill))
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
pub fn skills_with_enabled(&self) -> impl Iterator<Item = (&SkillMetadata, bool)> {
|
|
self.skills
|
|
.iter()
|
|
.map(|skill| (skill, self.is_skill_enabled(skill)))
|
|
}
|
|
|
|
pub(crate) fn file_system_for_skill(
|
|
&self,
|
|
skill: &SkillMetadata,
|
|
) -> Option<Arc<dyn ExecutorFileSystem>> {
|
|
self.file_systems_by_skill_path
|
|
.get(&skill.path_to_skills_md)
|
|
}
|
|
}
|
|
|
|
/// Host-loaded skills for one turn, including the filesystem mapping needed to
|
|
/// read skill bodies through the environment that loaded them.
|
|
#[derive(Debug, Clone)]
|
|
pub struct HostLoadedSkills {
|
|
outcome: Arc<SkillLoadOutcome>,
|
|
}
|
|
|
|
impl HostLoadedSkills {
|
|
pub fn new(outcome: Arc<SkillLoadOutcome>) -> Self {
|
|
Self { outcome }
|
|
}
|
|
|
|
pub fn outcome(&self) -> &SkillLoadOutcome {
|
|
self.outcome.as_ref()
|
|
}
|
|
|
|
pub async fn read_skill_text(&self, skill: &SkillMetadata) -> io::Result<String> {
|
|
let fs = self
|
|
.outcome
|
|
.file_system_for_skill(skill)
|
|
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
|
|
let path = PathUri::from_abs_path(&skill.path_to_skills_md)?;
|
|
fs.read_file_text(&path, /*sandbox*/ None).await
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub(crate) struct SkillFileSystemsByPath {
|
|
values: Arc<HashMap<AbsolutePathBuf, Arc<dyn ExecutorFileSystem>>>,
|
|
}
|
|
|
|
impl SkillFileSystemsByPath {
|
|
pub(crate) fn new(values: HashMap<AbsolutePathBuf, Arc<dyn ExecutorFileSystem>>) -> Self {
|
|
Self {
|
|
values: Arc::new(values),
|
|
}
|
|
}
|
|
|
|
fn get(&self, path: &AbsolutePathBuf) -> Option<Arc<dyn ExecutorFileSystem>> {
|
|
self.values.get(path).map(Arc::clone)
|
|
}
|
|
|
|
fn retain_paths(&mut self, paths: &HashSet<AbsolutePathBuf>) {
|
|
self.values = Arc::new(
|
|
self.values
|
|
.iter()
|
|
.filter(|(path, _)| paths.contains(*path))
|
|
.map(|(path, fs)| (path.clone(), Arc::clone(fs)))
|
|
.collect(),
|
|
);
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for SkillFileSystemsByPath {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("SkillFileSystemsByPath")
|
|
.field("len", &self.values.len())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
pub fn filter_skill_load_outcome_for_product(
|
|
mut outcome: SkillLoadOutcome,
|
|
restriction_product: Option<Product>,
|
|
) -> SkillLoadOutcome {
|
|
outcome
|
|
.skills
|
|
.retain(|skill| skill.matches_product_restriction_for_product(restriction_product));
|
|
let retained_paths: HashSet<AbsolutePathBuf> = outcome
|
|
.skills
|
|
.iter()
|
|
.map(|skill| skill.path_to_skills_md.clone())
|
|
.collect();
|
|
outcome
|
|
.file_systems_by_skill_path
|
|
.retain_paths(&retained_paths);
|
|
outcome.skill_root_by_path = Arc::new(
|
|
outcome
|
|
.skill_root_by_path
|
|
.iter()
|
|
.filter(|(path, _)| retained_paths.contains(*path))
|
|
.map(|(path, root)| (path.clone(), root.clone()))
|
|
.collect(),
|
|
);
|
|
let retained_roots: HashSet<AbsolutePathBuf> =
|
|
outcome.skill_root_by_path.values().cloned().collect();
|
|
outcome
|
|
.skill_roots
|
|
.retain(|root| retained_roots.contains(root));
|
|
outcome.implicit_skills_by_scripts_dir = Arc::new(
|
|
outcome
|
|
.implicit_skills_by_scripts_dir
|
|
.iter()
|
|
.filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product))
|
|
.map(|(path, skill)| (path.clone(), skill.clone()))
|
|
.collect(),
|
|
);
|
|
outcome.implicit_skills_by_doc_path = Arc::new(
|
|
outcome
|
|
.implicit_skills_by_doc_path
|
|
.iter()
|
|
.filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product))
|
|
.map(|(path, skill)| (path.clone(), skill.clone()))
|
|
.collect(),
|
|
);
|
|
outcome
|
|
}
|