Skip to main content

rskit_skill/
manifest.rs

1//! Skill manifest schema and validation.
2
3use rskit_validation::Validator;
4use rskit_validation::input::validate_safe_path;
5use serde::{Deserialize, Serialize};
6
7use crate::SkillError;
8
9/// Skill safety order. Informational in manifests; effective safety is computed from tools.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
11#[serde(rename_all = "kebab-case")]
12pub enum Safety {
13    /// Read-only skill intent.
14    #[default]
15    ReadOnly,
16    /// Mutating skill intent.
17    Mutating,
18    /// Destructive skill intent.
19    Destructive,
20}
21
22/// Locked skill manifest.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct Manifest {
26    /// Canonical manifest schema version.
27    #[serde(rename = "schema_version")]
28    pub schema_version: String,
29    /// Stable skill name.
30    pub name: String,
31    /// Semantic version.
32    pub version: String,
33    /// Human-readable description.
34    pub description: String,
35    /// Optional license expression.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub license: Option<String>,
38    /// Optional authors.
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub authors: Vec<String>,
41    /// Referenced tools, resources, prompts, and MCP servers.
42    pub references: References,
43    /// Activation requirements.
44    #[serde(default)]
45    pub requires: Requires,
46    /// Human approval checkpoints independent from tool sensitive invocations.
47    #[serde(
48        default,
49        rename = "human_approval",
50        skip_serializing_if = "Vec::is_empty"
51    )]
52    pub human_approval: Vec<HumanApprovalStep>,
53    /// Optional budgets requested by the skill.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub budgets: Option<Budgets>,
56    /// Optional model routing hints.
57    #[serde(
58        default,
59        rename = "model_hints",
60        skip_serializing_if = "Option::is_none"
61    )]
62    pub model_hints: Option<ModelHints>,
63    /// Progressive disclosure text.
64    #[serde(
65        default,
66        rename = "progressive_disclosure",
67        skip_serializing_if = "Option::is_none"
68    )]
69    pub progressive_disclosure: Option<ProgressiveDisclosure>,
70    /// Inert script assets.
71    #[serde(default, skip_serializing_if = "Vec::is_empty")]
72    pub scripts: Vec<ScriptAsset>,
73    /// Optional signature metadata.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub signature: Option<Signature>,
76    /// Informational declared safety; does not grant authority.
77    pub safety: Safety,
78}
79
80impl Manifest {
81    /// Validate required string fields.
82    pub fn validate(&self) -> Result<(), SkillError> {
83        Validator::new()
84            .required("schema_version", &self.schema_version)
85            .required("name", &self.name)
86            .required("version", &self.version)
87            .required("description", &self.description)
88            .validate()
89            .map_err(|error| SkillError::InvalidManifest(error.to_string()))?;
90
91        for script in &self.scripts {
92            validate_safe_path(&script.path)
93                .map_err(|error| SkillError::InvalidManifest(error.to_string()))?;
94        }
95        Ok(())
96    }
97}
98
99/// Prompt reference with explicit version.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct PromptRef {
103    /// Prompt name.
104    pub name: String,
105    /// Prompt version.
106    pub version: String,
107}
108
109/// References to executable and context-bearing registrations by name/pattern.
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct References {
113    /// Tool names referenced by the skill.
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub tools: Vec<String>,
116    /// Prompt names and versions referenced by the skill.
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub prompts: Vec<PromptRef>,
119    /// Resource URI patterns referenced by the skill.
120    #[serde(default, skip_serializing_if = "Vec::is_empty")]
121    pub resources: Vec<String>,
122    /// MCP server names referenced by the skill.
123    #[serde(default, rename = "mcp_servers", skip_serializing_if = "Vec::is_empty")]
124    pub mcp_servers: Vec<String>,
125}
126
127/// Activation preconditions.
128#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129#[serde(deny_unknown_fields)]
130pub struct Requires {
131    /// Scopes the principal must hold to activate the skill. These never grant executable authority.
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub scopes: Vec<String>,
134    /// Capability gates such as network or filesystem.
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub capabilities: Vec<String>,
137}
138
139/// Human approval checkpoint.
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141#[serde(deny_unknown_fields)]
142pub struct HumanApprovalStep {
143    /// Workflow step name.
144    pub step: String,
145    /// Human-readable condition.
146    pub when: String,
147    /// Why approval is required.
148    pub rationale: String,
149}
150
151/// Skill-requested budget limits.
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153#[serde(deny_unknown_fields)]
154pub struct Budgets {
155    /// Maximum tokens.
156    #[serde(
157        default,
158        rename = "max_tokens",
159        skip_serializing_if = "Option::is_none"
160    )]
161    pub max_tokens: Option<u64>,
162    /// Maximum calls.
163    #[serde(default, rename = "max_calls", skip_serializing_if = "Option::is_none")]
164    pub max_calls: Option<u32>,
165    /// Maximum cost.
166    #[serde(default, rename = "max_cost", skip_serializing_if = "Option::is_none")]
167    pub max_cost: Option<MaxCost>,
168    /// ISO 8601 wall-clock duration.
169    #[serde(
170        default,
171        rename = "wall_clock",
172        skip_serializing_if = "Option::is_none"
173    )]
174    pub wall_clock: Option<String>,
175}
176
177/// Maximum cost budget.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(deny_unknown_fields)]
180pub struct MaxCost {
181    /// Decimal amount.
182    pub amount: f64,
183    /// Currency code.
184    pub currency: String,
185}
186
187/// Optional model hints.
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189#[serde(deny_unknown_fields)]
190pub struct ModelHints {
191    /// Ordered list of preferred model identifiers.
192    #[serde(default, skip_serializing_if = "Vec::is_empty")]
193    pub preferred: Vec<String>,
194    /// Model identifiers to reject.
195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
196    pub reject: Vec<String>,
197}
198
199/// Signature metadata carried by the manifest.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(deny_unknown_fields)]
202pub struct Signature {
203    /// Signature algorithm or verifier hint.
204    pub algorithm: String,
205    /// Signature value.
206    pub value: String,
207    /// Verifier key identifier.
208    #[serde(rename = "key_id")]
209    pub key_id: String,
210}
211
212/// Progressive disclosure copy.
213#[derive(Debug, Clone, Default, Serialize, Deserialize)]
214#[serde(deny_unknown_fields)]
215pub struct ProgressiveDisclosure {
216    /// Short summary.
217    pub summary: String,
218    /// Detailed disclosure text.
219    pub detail: String,
220}
221
222/// Inert script asset metadata.
223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
224#[serde(deny_unknown_fields)]
225pub struct ScriptAsset {
226    /// Script path relative to pack root.
227    pub path: String,
228    /// Script description.
229    pub description: String,
230}