Skip to main content

harness_core/
skill.rs

1//! `Skill` trait — strictly aligned with the
2//! [agentskills.io](https://agentskills.io/specification) specification.
3//!
4//! See DESIGN.md §6 for how this maps onto the public spec and which fields
5//! are framework extensions (anything under `metadata.harness.*`).
6
7use crate::{Context, World, error::SkillError};
8use serde::{Deserialize, Serialize};
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::path::PathBuf;
12
13/// The frontmatter of a `SKILL.md`, exactly as specified.
14///
15/// All fields except `name` and `description` are optional, mirroring the spec.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SkillManifest {
18    pub name: String,
19    pub description: String,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub license: Option<String>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub compatibility: Option<String>,
24    /// Free-form key-value map. Framework extensions live under `metadata.harness.*`.
25    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
26    pub metadata: BTreeMap<String, serde_json::Value>,
27    /// Space-separated tool patterns (e.g. "Bash(git:*) Read"). Experimental in the spec.
28    #[serde(
29        default,
30        skip_serializing_if = "Option::is_none",
31        rename = "allowed-tools"
32    )]
33    pub allowed_tools: Option<String>,
34}
35
36impl SkillManifest {
37    /// Read framework-specific extensions from `metadata.harness`.
38    pub fn harness_ext(&self) -> Option<HarnessExt> {
39        let v = self.metadata.get("harness")?;
40        serde_json::from_value::<HarnessExt>(v.clone()).ok()
41    }
42}
43
44/// Optional `metadata.harness.*` sub-tree. Spec-compliant: keys other agents
45/// don't recognise are simply ignored.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct HarnessExt {
48    #[serde(default)]
49    pub kind: Option<crate::Execution>,
50    #[serde(default)]
51    pub risk: Option<crate::ToolRisk>,
52    /// Rust function path; only meaningful for `#[skill]` macro-generated skills.
53    #[serde(default)]
54    pub entrypoint: Option<String>,
55    #[serde(default)]
56    pub schema_version: Option<String>,
57}
58
59/// A non-`SKILL.md` resource bundled with the skill (`scripts/*`, `references/*`, `assets/*`).
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Resource {
62    pub kind: ResourceKind,
63    pub path: PathBuf,
64    /// 1-line description used in progressive disclosure.
65    pub summary: Option<String>,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "lowercase")]
70#[non_exhaustive]
71pub enum ResourceKind {
72    Script,
73    Reference,
74    Asset,
75}
76
77/// Optional in-process handler attached to a skill (only `#[skill]`-generated skills carry one).
78pub type SkillHandler = std::sync::Arc<
79    dyn for<'a> Fn(
80            &'a mut Context,
81            &'a mut World,
82        ) -> futures::future::BoxFuture<'a, Result<(), SkillError>>
83        + Send
84        + Sync,
85>;
86
87pub trait Skill: Send + Sync + 'static {
88    fn manifest(&self) -> &SkillManifest;
89    /// The full Markdown body, loaded on demand (progressive disclosure tier 2).
90    fn body(&self) -> Cow<'_, str>;
91    fn resources(&self) -> &[Resource] {
92        &[]
93    }
94    fn handler(&self) -> Option<SkillHandler> {
95        None
96    }
97}
98
99/// `inventory` slot for compile-time skill registration via `#[skill]`.
100///
101/// Macro-generated skills `inventory::submit!` a `SkillEntry` here so that
102/// `harness::skills::all()` can enumerate them at runtime without any IoC container.
103pub struct SkillEntry {
104    pub factory: fn() -> std::sync::Arc<dyn Skill>,
105}
106
107inventory::collect!(SkillEntry);
108
109/// Enumerate every `#[skill]`-registered skill (filesystem-loaded skills are
110/// separate — see `harness-skills` for `scan_skills_root`).
111pub fn iter_macro_skills() -> impl Iterator<Item = std::sync::Arc<dyn Skill>> {
112    inventory::iter::<SkillEntry>().map(|e| (e.factory)())
113}