1use super::evolution::EvolutionEvent;
4use super::mcp::McpRequirement;
5use super::types::{Category, ContentMode, HostId, Priority, Provenance, TriggerKind, TrustLevel};
6use crate::deps::ProgramDep;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(
12 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
13)]
14#[serde(rename_all = "lowercase")]
15pub enum SkillScope {
16 #[default]
18 User,
19 Project,
21 Fleet,
23 Team,
25 Enterprise,
27}
28
29impl SkillScope {
30 pub fn is_user(&self) -> bool {
32 matches!(self, SkillScope::User)
33 }
34}
35
36#[derive(
39 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
40)]
41#[serde(rename_all = "snake_case")]
42pub enum Visibility {
43 #[default]
45 Indexed,
46 OnDemand,
49}
50
51impl Visibility {
52 pub fn is_indexed(&self) -> bool {
54 matches!(self, Visibility::Indexed)
55 }
56}
57
58#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
65#[serde(default)]
66pub struct GovernanceRef {
67 #[serde(default, skip_serializing_if = "String::is_empty")]
68 pub org_id: String,
69 #[serde(default, skip_serializing_if = "String::is_empty")]
70 pub constitution_hash: String,
71}
72
73pub fn scope_visible(
80 scope: SkillScope,
81 skill_fleet: Option<&str>,
82 skill_project: Option<&str>,
83 skill_team: Option<&str>, active_fleet: Option<&str>,
85 active_project: Option<&str>,
86 active_team: Option<&str>, ) -> bool {
88 match scope {
89 SkillScope::User => true,
90 SkillScope::Enterprise => true,
91 SkillScope::Project => skill_project.is_some() && active_project == skill_project,
92 SkillScope::Fleet => skill_fleet.is_some() && active_fleet == skill_fleet,
93 SkillScope::Team => skill_team.is_some() && active_team == skill_team,
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Skill {
101 #[serde(flatten)]
102 pub manifest: SkillManifest,
103
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub content_sha256: Option<String>,
107
108 #[serde(default)]
110 pub trust_level: TrustLevel,
111
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub capabilities_declared: Vec<String>,
115
116 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub publisher_signature: Option<String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
125pub struct SkillManifest {
126 pub name: String,
127 pub version: String,
128 pub publisher: String,
129 pub description: String,
130 pub category: Category,
131
132 #[serde(default, skip_serializing_if = "SkillScope::is_user")]
135 pub scope: SkillScope,
136
137 #[serde(default, skip_serializing_if = "Visibility::is_indexed")]
140 pub visibility: Visibility,
141
142 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub origin: Option<String>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub origin_version: Option<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub origin_hash: Option<String>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub fleet: Option<String>,
157
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub team: Option<String>,
161
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub governance: Option<GovernanceRef>,
165
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub project: Option<String>,
169
170 #[serde(default)]
173 pub provenance: Provenance,
174
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub hosts: Vec<HostId>,
177
178 pub content: Content,
179
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
181 pub requires: Vec<Requirement>,
182
183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub tags: Vec<String>,
185
186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub triggers: Vec<Trigger>,
188
189 #[serde(default)]
190 pub priority: Priority,
191
192 #[serde(default, skip_serializing_if = "Vec::is_empty")]
194 pub evolution_log: Vec<EvolutionEvent>,
195
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
200 pub transfer_chain: Vec<String>,
201
202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
208 pub mcp_requirements: Vec<McpRequirement>,
209
210 #[serde(default)]
214 pub updated_at: DateTime<Utc>,
215
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
219 pub requires_programs: Vec<ProgramDep>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
223pub struct Content {
224 pub r#abstract: String,
226
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub context: Option<String>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub procedure: Option<Procedure>,
233
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub command: Option<String>,
236
237 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub note: Option<String>,
241}
242
243impl Content {
244 pub fn mode(&self) -> Option<ContentMode> {
245 match (
246 self.context.is_some(),
247 self.procedure.is_some(),
248 self.command.is_some(),
249 self.note.is_some(),
250 ) {
251 (true, false, false, false) => Some(ContentMode::Context),
252 (false, true, false, false) => Some(ContentMode::Workflow),
253 (false, false, true, false) => Some(ContentMode::Command),
254 (false, false, false, true) => Some(ContentMode::Note),
255 _ => None,
256 }
257 }
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
261pub struct Procedure {
262 #[serde(default, skip_serializing_if = "Vec::is_empty")]
263 pub variables: Vec<Variable>,
264 pub steps: Vec<ProcedureStep>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
269pub struct RetryConfig {
270 pub max_retries: u32,
271 #[serde(default)]
272 pub backoff_secs: Option<u64>,
273}
274
275#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
277#[serde(rename_all = "lowercase")]
278pub enum FailureAction {
279 Skip,
281 #[default]
283 Abort,
284 Retry,
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
289pub struct Variable {
290 pub name: String,
291 #[serde(rename = "type", default)]
292 pub var_type: VarType,
293 #[serde(default)]
294 pub required: bool,
295 #[serde(
299 default,
300 alias = "default_value",
301 skip_serializing_if = "Option::is_none"
302 )]
303 pub default: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub description: Option<String>,
306 #[serde(default, skip_serializing_if = "Vec::is_empty")]
308 pub choices: Vec<String>,
309}
310
311#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
314#[serde(rename_all = "lowercase")]
315pub enum VarType {
316 #[default]
317 String,
318 Path,
319 Url,
320 Number,
321 Bool,
322 Array,
324}
325
326impl std::fmt::Display for VarType {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 match self {
329 VarType::String => write!(f, "string"),
330 VarType::Path => write!(f, "path"),
331 VarType::Url => write!(f, "url"),
332 VarType::Number => write!(f, "number"),
333 VarType::Bool => write!(f, "bool"),
334 VarType::Array => write!(f, "array"),
335 }
336 }
337}
338
339#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
340pub struct ProcedureStep {
341 pub description: String,
342
343 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub tool: Option<String>,
347
348 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub intent: Option<String>,
354
355 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub tool_hint: Option<String>,
360
361 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub id: Option<String>,
367
368 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub depends_on: Vec<String>,
372
373 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub command: Option<String>,
378
379 #[serde(default)]
380 pub on_failure: FailureAction,
381
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub retry: Option<RetryConfig>,
384
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub timeout_secs: Option<u64>,
387
388 #[serde(default)]
392 pub needs_approval: bool,
393
394 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub delegate_to: Option<String>,
401
402 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub risk: Option<crate::hitl::RiskTier>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
409pub struct Trigger {
410 #[serde(rename = "type")]
411 pub kind: TriggerKind,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub pattern: Option<String>,
414}
415
416impl Trigger {
417 pub fn exact_keyword(&self) -> Option<&str> {
419 if matches!(self.kind, TriggerKind::Keyword) {
420 self.pattern.as_deref()
421 } else {
422 None
423 }
424 }
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
428pub struct Requirement {
429 pub name: String,
430 #[serde(default = "default_any_version")]
431 pub version: String,
432}
433
434fn default_any_version() -> String {
435 "*".to_string()
436}
437
438#[cfg(test)]
439mod tests;