1use std::collections::BTreeMap;
9
10use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
11use base64::Engine;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14
15use crate::lockfile::Integrity;
16use crate::manifest::PackageType;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SkillFile {
22 pub relative_path: String,
23 pub contents: Vec<u8>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Skill {
31 pub owner: String,
32 pub name: String,
33 pub version: String,
34 pub files: Vec<SkillFile>,
35 pub integrity: Integrity,
36}
37
38impl Skill {
39 #[must_use]
42 pub fn id(&self) -> String {
43 format!("{}/{}", self.owner, self.name)
44 }
45
46 #[must_use]
48 pub fn lockfile_key(&self) -> String {
49 format!(
50 "{}/{}/{}@{}",
51 PackageType::Skills.as_str(),
52 self.owner,
53 self.name,
54 self.version
55 )
56 }
57
58 #[must_use]
60 pub fn computed_integrity(&self) -> Integrity {
61 compute_integrity(&self.files)
62 }
63
64 #[must_use]
66 pub fn integrity_matches(&self) -> bool {
67 self.computed_integrity() == self.integrity
68 }
69}
70
71#[must_use]
74pub fn compute_integrity(files: &[SkillFile]) -> Integrity {
75 let mut sorted: Vec<&SkillFile> = files.iter().collect();
76 sorted.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
77
78 let mut hasher = Sha256::new();
79 for f in sorted {
80 hasher.update(f.relative_path.as_bytes());
81 hasher.update([0u8]);
82 hasher.update((f.contents.len() as u64).to_le_bytes());
83 hasher.update(&f.contents);
84 }
85 let digest = hasher.finalize();
86 let b64 = BASE64_STANDARD.encode(digest);
87 Integrity::parse(format!("sha256-{b64}"))
88 .expect("base64 of sha256 always matches integrity regex")
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(tag = "type", rename_all = "lowercase")]
99pub enum McpTransport {
100 Stdio {
103 command: String,
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 args: Vec<String>,
106 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
107 env: BTreeMap<String, String>,
108 },
109 Http {
111 url: String,
112 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
113 headers: BTreeMap<String, String>,
114 },
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct McpServer {
121 pub id: String,
124 pub version: String,
125 pub transport: McpTransport,
126}
127
128impl McpServer {
129 #[must_use]
131 pub fn lockfile_key(&self) -> String {
132 format!("{}/{}@{}", PackageType::Mcp.as_str(), self.id, self.version)
133 }
134
135 #[must_use]
138 pub fn computed_integrity(&self) -> Integrity {
139 let bytes = serde_json::to_vec(&self.transport)
140 .expect("McpTransport with String keys serializes infallibly");
141 let mut hasher = Sha256::new();
142 hasher.update(self.id.as_bytes());
143 hasher.update([0u8]);
144 hasher.update(self.version.as_bytes());
145 hasher.update([0u8]);
146 hasher.update(&bytes);
147 let digest = hasher.finalize();
148 let b64 = BASE64_STANDARD.encode(digest);
149 Integrity::parse(format!("sha256-{b64}"))
150 .expect("base64 of sha256 always matches integrity regex")
151 }
152
153 #[must_use]
156 pub fn short_name(&self) -> String {
157 self.id
158 .rsplit('/')
159 .next()
160 .unwrap_or(&self.id)
161 .to_lowercase()
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Subagent {
171 pub id: String,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct Prompt {
176 pub id: String,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct Command {
181 pub id: String,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Hook {
186 pub id: String,
187}