use std::collections::{BTreeMap, BTreeSet}; use config_source::{ ConfigSchemaContribution, MarkdownDocumentProjection, VirtualPath, project_markdown_document, }; use serde::Deserialize; use worker::skill::{ SkillActivationResponse, SkillCatalogEntry, SkillCatalogResponse, SkillDetailResponse, SkillDiagnostic, SkillDiagnosticSeverity, SkillProvenance, SkillResourceRef, SkillSourceKind, }; use crate::config_source::{ WorkspaceConfigSchemaProvider, WorkspaceConfigState, evaluate_workspace_config_state, }; const BUILTIN_SKILL_ID: &str = "agent-skills"; const BUILTIN_SKILL_SOURCE: &str = include_str!("../../../resources/skills/agent-skills/SKILL.md"); const BUILTIN_SKILL_VIRTUAL_PATH: &str = "builtin/skills/agent-skills/SKILL.md"; const SKILL_SCHEMA_PROVIDER_ID: &str = "builtin:skills"; const SKILL_SCHEMA_NAMESPACE: &str = "skills"; const SKILL_SCHEMA_VERSION: &str = "1"; const SKILL_CATALOG_AUTHORITY: &str = "workspace-config-skills-v1"; /// Skill documents are values imported from `SKILL.md`. Known Agent Skills /// frontmatter is typed while extension keys remain concrete values. pub const SKILL_DOCUMENT_SCHEMA_SOURCE: &str = r#"{ frontmatter = { name = String; description = String; license = String default ""; compatibility = String default ""; metadata = {...String} default {}; ...Unknown }; content = String; }"#; const SKILL_CONFIG_SCHEMA_SOURCE: &str = r#"{ skills = {...{ frontmatter = { name = String; description = String; license = String default ""; compatibility = String default ""; metadata = {...String} default {}; ...Unknown }; content = String; }} default {}; }"#; #[derive(Debug, Clone, Copy)] pub struct SkillConfigSchemaProvider; impl WorkspaceConfigSchemaProvider for SkillConfigSchemaProvider { fn contribution(&self) -> crate::Result { ConfigSchemaContribution::new( SKILL_SCHEMA_PROVIDER_ID, SKILL_SCHEMA_NAMESPACE, SKILL_SCHEMA_VERSION, SKILL_CONFIG_SCHEMA_SOURCE, ) .map_err(|error| crate::Error::Config(error.to_string())) } } #[derive(Debug, thiserror::Error)] pub enum SkillError { #[error("unknown Skill `{0}`")] NotFound(String), #[error("Skill `{0}` has blocking diagnostics")] InvalidSkill(String), #[error("failed to evaluate the active Workspace config revision: {0}")] Evaluation(String), #[error("the active Workspace config projection is missing its root value")] MissingProjection, #[error("the active Workspace config Skill projection is invalid: {0}")] InvalidProjection(String), } #[derive(Debug, Clone)] struct ParsedSkill { name: String, description: String, allowed_tools: Vec, body: String, provenance: SkillProvenance, overrides: Vec, resources: Vec, diagnostics: Vec, } #[derive(Debug, Deserialize)] struct WorkspaceSkillProjection { #[serde(default)] skills: BTreeMap, } pub fn catalog(state: &WorkspaceConfigState) -> Result { let entries = merged_skills(state)? .into_values() .map(|skill| skill.catalog_entry()) .collect(); Ok(SkillCatalogResponse { authority: SKILL_CATALOG_AUTHORITY.to_string(), entries, diagnostics: Vec::new(), }) } pub fn lint(state: &WorkspaceConfigState) -> Result { catalog(state) } pub fn detail(state: &WorkspaceConfigState, name: &str) -> Result { let skill = merged_skills(state)? .remove(name) .ok_or_else(|| SkillError::NotFound(name.to_string()))?; Ok(SkillDetailResponse { name: skill.name, description: skill.description, provenance: skill.provenance, overrides: skill.overrides, diagnostics: skill.diagnostics, body: skill.body, allowed_tools: skill.allowed_tools, allowed_tools_status: "experimental_hint_only".to_string(), resources: skill.resources, }) } pub fn activation( state: &WorkspaceConfigState, name: &str, ) -> Result { let skill = merged_skills(state)? .remove(name) .ok_or_else(|| SkillError::NotFound(name.to_string()))?; if skill.has_errors() { return Err(SkillError::InvalidSkill(name.to_string())); } Ok(SkillActivationResponse { name: skill.name, provenance: skill.provenance, diagnostics: skill.diagnostics, body: skill.body, }) } fn merged_skills( state: &WorkspaceConfigState, ) -> Result, SkillError> { let mut merged = BTreeMap::new(); let builtin_projection = project_markdown_document(BUILTIN_SKILL_SOURCE) .expect("embedded built-in Skill Markdown is valid"); let builtin = parse_skill( BUILTIN_SKILL_ID, builtin_projection, SkillProvenance { kind: SkillSourceKind::Builtin, id: format!("builtin:{BUILTIN_SKILL_ID}"), virtual_path: Some(BUILTIN_SKILL_VIRTUAL_PATH.to_string()), revision: None, source_digest: Some(config_source::digest_bytes(BUILTIN_SKILL_SOURCE.as_bytes())), tree_digest: None, }, Vec::new(), None, ); merged.insert(builtin.name.clone(), builtin); let evaluation = evaluate_workspace_config_state(state, state.contract.schema_bundle.clone()) .map_err(|error| SkillError::Evaluation(error.to_string()))?; let projection = evaluation .projections .first() .ok_or(SkillError::MissingProjection)?; let workspace = serde_json::from_value::(projection.data_json.clone()) .map_err(|error| SkillError::InvalidProjection(error.to_string()))?; for (config_key, document) in workspace.skills { let name = string_field(&document.frontmatter, "name") .unwrap_or(&config_key) .to_string(); let canonical_path = format!("skills/{name}/SKILL.md"); let mut source_diagnostic = None; let source_entry = VirtualPath::parse(&canonical_path) .ok() .and_then(|path| state.snapshot.entries.get(&path)); if let Some(entry) = source_entry { match project_markdown_document(&entry.content) { Ok(expected) if normalize_document(expected.clone()) == document => {} Ok(_) => { source_diagnostic = Some(SkillDiagnostic::error( "skill_source_mismatch", format!( "Skill `{name}` must be the imported value of `{canonical_path}` in the active config revision" ), Some(format!("workspace:{name}")), )); } Err(message) => { source_diagnostic = Some(SkillDiagnostic::error( "invalid_skill_markdown", format!("{canonical_path}: {message}"), Some(format!("workspace:{name}")), )); } } } else { source_diagnostic = Some(SkillDiagnostic::error( "missing_skill_source", format!("Skill `{name}` requires `{canonical_path}` in the active config revision"), Some(format!("workspace:{name}")), )); } let source_digest = source_entry .map(|entry| entry.content_digest.clone()) .unwrap_or_else(|| config_source::digest_bytes(canonical_path.as_bytes())); let resources = workspace_resources(state, &name); let mut skill = parse_skill( &name, document, SkillProvenance { kind: SkillSourceKind::Workspace, id: format!("workspace:{name}"), virtual_path: Some(canonical_path), revision: Some(state.snapshot.revision), source_digest: Some(source_digest), tree_digest: Some(state.snapshot.digest.clone()), }, resources, source_diagnostic, ); if let Some(overridden) = merged.insert(name, skill.clone()) { skill.overrides.push(overridden.provenance); merged.insert(skill.name.clone(), skill); } } Ok(merged) } fn normalize_document(mut document: MarkdownDocumentProjection) -> MarkdownDocumentProjection { document .frontmatter .entry("license") .or_insert_with(|| serde_json::Value::String(String::new())); document .frontmatter .entry("compatibility") .or_insert_with(|| serde_json::Value::String(String::new())); document .frontmatter .entry("metadata") .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); document } fn parse_skill( fallback_name: &str, document: MarkdownDocumentProjection, provenance: SkillProvenance, resources: Vec, source_diagnostic: Option, ) -> ParsedSkill { let mut diagnostics = Vec::new(); if let Some(diagnostic) = source_diagnostic { diagnostics.push(diagnostic); } let frontmatter = document.frontmatter; let name = string_field(&frontmatter, "name").unwrap_or(fallback_name); if !valid_skill_name(name) { diagnostics.push(SkillDiagnostic::error( "invalid_skill_name", "frontmatter `name` must be a lowercase kebab-case Skill id", Some(provenance.id.clone()), )); } if name != fallback_name { diagnostics.push(SkillDiagnostic::error( "skill_name_mismatch", format!("frontmatter name `{name}` must match Skill id `{fallback_name}`"), Some(provenance.id.clone()), )); } let description = string_field(&frontmatter, "description") .unwrap_or_default() .trim() .to_string(); if description.is_empty() { diagnostics.push(SkillDiagnostic::error( "missing_description", "frontmatter `description` must be a non-empty string", Some(provenance.id.clone()), )); } for field in [ "profile", "system_prompt", "prompt", "plugins", "plugin", "model_invokation", "model_invocation", "user_invocable", "graph", "invocation", ] { if frontmatter.contains_key(field) { diagnostics.push(SkillDiagnostic::error( "workflow_authority", format!("frontmatter `{field}` is workflow/profile authority and is not allowed"), Some(provenance.id.clone()), )); } } let allowed_tools = frontmatter .get("allowed-tools") .map(parse_allowed_tools) .unwrap_or_default(); if !allowed_tools.is_empty() { diagnostics.push(SkillDiagnostic::warning( "allowed_tools_hint", "frontmatter `allowed-tools` is an instruction hint only and does not grant tools", Some(provenance.id.clone()), )); } ParsedSkill { name: fallback_name.to_string(), description, allowed_tools, body: document.content, provenance, overrides: Vec::new(), resources, diagnostics, } } fn valid_skill_name(name: &str) -> bool { !name.is_empty() && !name.starts_with('-') && !name.ends_with('-') && !name.contains("--") && name .bytes() .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') } fn string_field<'a>( frontmatter: &'a serde_json::Map, field: &str, ) -> Option<&'a str> { frontmatter.get(field).and_then(serde_json::Value::as_str) } fn parse_allowed_tools(value: &serde_json::Value) -> Vec { match value { serde_json::Value::String(value) => value .split(|character: char| character == ',' || character.is_whitespace()) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .collect(), serde_json::Value::Array(values) => values .iter() .filter_map(serde_json::Value::as_str) .map(ToOwned::to_owned) .collect(), _ => Vec::new(), } } fn workspace_resources(state: &WorkspaceConfigState, name: &str) -> Vec { let mut resources = Vec::new(); for (kind, directory) in [ ("reference", "references"), ("asset", "assets"), ("script", "scripts"), ] { let prefix = format!("skills/{name}/{directory}/"); for child in resource_children(state, &prefix) { resources.push(SkillResourceRef { kind: kind.to_string(), name: child, supported: kind == "reference", diagnostic: (kind != "reference").then(|| { format!("{kind} resources are catalogued but are not loaded automatically") }), }); } } resources } fn resource_children(state: &WorkspaceConfigState, prefix: &str) -> Vec { let mut children = BTreeSet::new(); for path in state.snapshot.entries.keys() { let Some(relative) = path.as_str().strip_prefix(prefix) else { continue; }; if relative.is_empty() { continue; } let child = relative.split('/').next().unwrap_or(relative); children.insert(format!("{prefix}{child}")); } children.into_iter().collect() } impl ParsedSkill { fn has_errors(&self) -> bool { self.diagnostics .iter() .any(|diagnostic| diagnostic.severity == SkillDiagnosticSeverity::Error) } fn catalog_entry(&self) -> SkillCatalogEntry { SkillCatalogEntry { name: self.name.clone(), description: self.description.clone(), provenance: self.provenance.clone(), overrides: self.overrides.clone(), diagnostics: self.diagnostics.clone(), } } } #[cfg(test)] mod tests { use config_source::{ ConfigContentType, ConfigEntry, ConfigTreeSnapshot, ToolchainContract, WorkspaceConfigSchemaBundle, }; use super::*; fn state(main: &str, markdown: &str, name: &str) -> WorkspaceConfigState { let skill_path = format!("skills/{name}/SKILL.md"); let reference_path = format!("skills/{name}/references/checklist.md"); let tree = ConfigTreeSnapshot::from_entries( 9, [ ConfigEntry::new( VirtualPath::parse("main.dcdl").unwrap(), ConfigContentType::Decodal, main, ) .unwrap(), ConfigEntry::new( VirtualPath::parse(&skill_path).unwrap(), ConfigContentType::Text, markdown, ) .unwrap(), ConfigEntry::new( VirtualPath::parse(&reference_path).unwrap(), ConfigContentType::Text, "checklist", ) .unwrap(), ], ) .unwrap(); let bundle = WorkspaceConfigSchemaBundle::compose([SkillConfigSchemaProvider .contribution() .unwrap()]) .unwrap(); WorkspaceConfigState { snapshot: tree, contract: ToolchainContract::with_schema_bundle( 1, vec![VirtualPath::parse("main.dcdl").unwrap()], 1, bundle, ), projection_digest: "projection".to_string(), } } fn main_source(name: &str) -> String { let config_key = name.replace('-', "_"); format!( r#"{{ skills = {{ {config_key} = import "./skills/{name}/SKILL.md" as {}; }}; }}"#, SKILL_DOCUMENT_SCHEMA_SOURCE ) } #[test] fn workspace_skill_projection_keeps_extensions_and_uses_virtual_resources() { let markdown = concat!( "---\n", "name: debug-rust\n", "description: Debug Rust failures\n", "custom-authority: no\n", "allowed-tools: Read Grep\n", "metadata:\n owner: platform\n", "---\n", "# Debug Rust\n", ); let state = state(&main_source("debug-rust"), markdown, "debug-rust"); let evaluation = evaluate_workspace_config_state(&state, state.contract.schema_bundle.clone()).unwrap(); assert_eq!( evaluation.projections[0].data_json["skills"]["debug_rust"]["frontmatter"]["custom-authority"], "no" ); let catalog = catalog(&state).unwrap(); let item = catalog .entries .iter() .find(|item| item.name == "debug-rust") .unwrap(); assert_eq!(item.provenance.kind, SkillSourceKind::Workspace); assert_eq!(item.provenance.revision, Some(9)); assert!( item.diagnostics .iter() .all(|diagnostic| diagnostic.severity != SkillDiagnosticSeverity::Error) ); let detail = detail(&state, "debug-rust").unwrap(); assert_eq!(detail.body, "# Debug Rust\n"); assert_eq!(detail.allowed_tools, vec!["Read", "Grep"]); assert_eq!( detail.resources[0].name, "skills/debug-rust/references/checklist.md" ); assert_eq!( activation(&state, "debug-rust").unwrap().body, "# Debug Rust\n" ); } #[test] fn workspace_override_replaces_builtin_deterministically() { let markdown = concat!( "---\nname: agent-skills\n", "description: Workspace override\n", "---\n# Workspace agent skills\n", ); let state = state(&main_source("agent-skills"), markdown, "agent-skills"); let catalog = catalog(&state).unwrap(); assert_eq!( catalog .entries .iter() .filter(|item| item.name == "agent-skills") .count(), 1 ); let item = catalog .entries .iter() .find(|item| item.name == "agent-skills") .unwrap(); assert_eq!(item.description, "Workspace override"); assert_eq!(item.provenance.kind, SkillSourceKind::Workspace); assert_eq!(item.overrides[0].kind, SkillSourceKind::Builtin); } #[test] fn inline_skill_value_cannot_claim_canonical_source_identity() { let main = r#"{ skills = { debug_rust = { frontmatter = { name = "debug-rust"; description = "Inline authority"; }; content = "inline"; }; }; }"#; let markdown = "---\nname: debug-rust\ndescription: File authority\n---\nfile\n"; let state = state(main, markdown, "debug-rust"); let item = catalog(&state) .unwrap() .entries .into_iter() .find(|item| item.name == "debug-rust") .unwrap(); assert!( item.diagnostics .iter() .any(|diagnostic| diagnostic.code == "skill_source_mismatch") ); assert!(matches!( activation(&state, "debug-rust"), Err(SkillError::InvalidSkill(_)) )); } #[test] fn schema_accepts_unknown_extensions_but_keeps_skill_documents_typed() { let contribution = SkillConfigSchemaProvider.contribution().unwrap(); assert_eq!(contribution.namespace, "skills"); assert!(contribution.source.contains("...Unknown")); assert!(contribution.source.contains("metadata = {...String}")); } }