use super::evolution::EvolutionEvent;
use super::mcp::McpRequirement;
use super::types::{Category, ContentMode, HostId, Priority, Provenance, TriggerKind, TrustLevel};
use crate::deps::ProgramDep;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "lowercase")]
pub enum SkillScope {
#[default]
User,
Project,
Fleet,
Team,
Enterprise,
}
impl SkillScope {
pub fn is_user(&self) -> bool {
matches!(self, SkillScope::User)
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
#[default]
Indexed,
OnDemand,
}
impl Visibility {
pub fn is_indexed(&self) -> bool {
matches!(self, Visibility::Indexed)
}
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(default)]
pub struct GovernanceRef {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub org_id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub constitution_hash: String,
}
pub fn scope_visible(
scope: SkillScope,
skill_fleet: Option<&str>,
skill_project: Option<&str>,
skill_team: Option<&str>, active_fleet: Option<&str>,
active_project: Option<&str>,
active_team: Option<&str>, ) -> bool {
match scope {
SkillScope::User => true,
SkillScope::Enterprise => true,
SkillScope::Project => skill_project.is_some() && active_project == skill_project,
SkillScope::Fleet => skill_fleet.is_some() && active_fleet == skill_fleet,
SkillScope::Team => skill_team.is_some() && active_team == skill_team,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
#[serde(flatten)]
pub manifest: SkillManifest,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_sha256: Option<String>,
#[serde(default)]
pub trust_level: TrustLevel,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities_declared: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publisher_signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SkillManifest {
pub name: String,
pub version: String,
pub publisher: String,
pub description: String,
pub category: Category,
#[serde(default, skip_serializing_if = "SkillScope::is_user")]
pub scope: SkillScope,
#[serde(default, skip_serializing_if = "Visibility::is_indexed")]
pub visibility: Visibility,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fleet: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub team: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub governance: Option<GovernanceRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(default)]
pub provenance: Provenance,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hosts: Vec<HostId>,
pub content: Content,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires: Vec<Requirement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub triggers: Vec<Trigger>,
#[serde(default)]
pub priority: Priority,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evolution_log: Vec<EvolutionEvent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub transfer_chain: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mcp_requirements: Vec<McpRequirement>,
#[serde(default)]
pub updated_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_programs: Vec<ProgramDep>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Content {
pub r#abstract: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub procedure: Option<Procedure>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
impl Content {
pub fn mode(&self) -> Option<ContentMode> {
match (
self.context.is_some(),
self.procedure.is_some(),
self.command.is_some(),
self.note.is_some(),
) {
(true, false, false, false) => Some(ContentMode::Context),
(false, true, false, false) => Some(ContentMode::Workflow),
(false, false, true, false) => Some(ContentMode::Command),
(false, false, false, true) => Some(ContentMode::Note),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Procedure {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub variables: Vec<Variable>,
pub steps: Vec<ProcedureStep>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct RetryConfig {
pub max_retries: u32,
#[serde(default)]
pub backoff_secs: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum FailureAction {
Skip,
#[default]
Abort,
Retry,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
pub struct Variable {
pub name: String,
#[serde(rename = "type", default)]
pub var_type: VarType,
#[serde(default)]
pub required: bool,
#[serde(
default,
alias = "default_value",
skip_serializing_if = "Option::is_none"
)]
pub default: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub choices: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum VarType {
#[default]
String,
Path,
Url,
Number,
Bool,
Array,
}
impl std::fmt::Display for VarType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VarType::String => write!(f, "string"),
VarType::Path => write!(f, "path"),
VarType::Url => write!(f, "url"),
VarType::Number => write!(f, "number"),
VarType::Bool => write!(f, "bool"),
VarType::Array => write!(f, "array"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ProcedureStep {
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub depends_on: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(default)]
pub on_failure: FailureAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry: Option<RetryConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_secs: Option<u64>,
#[serde(default)]
pub needs_approval: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegate_to: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk: Option<crate::hitl::RiskTier>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Trigger {
#[serde(rename = "type")]
pub kind: TriggerKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
}
impl Trigger {
pub fn exact_keyword(&self) -> Option<&str> {
if matches!(self.kind, TriggerKind::Keyword) {
self.pattern.as_deref()
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Requirement {
pub name: String,
#[serde(default = "default_any_version")]
pub version: String,
}
fn default_any_version() -> String {
"*".to_string()
}
#[cfg(test)]
mod tests;