Skip to main content

pakx_core/
install.rs

1//! Resolved install payloads — what an adapter actually writes to disk.
2//!
3//! These types are produced by the resolver (Step 6+) from a manifest
4//! `DepSpec` plus registry metadata, and consumed by adapter trait methods.
5//! `Skill` and `McpServer` are fleshed out for v0.1; the other primitives
6//! are opaque markers that get full schemas as each adapter step lands.
7
8use 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/// One file inside a skill bundle (e.g. `SKILL.md`, `reference/usage.md`).
19/// The path is *relative* to the skill's install root.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SkillFile {
22    pub relative_path: String,
23    pub contents: Vec<u8>,
24}
25
26/// A resolved skill ready to install. Identity is `owner/name@version`;
27/// `integrity` covers the concatenated, sorted file contents (see
28/// [`compute_integrity`]).
29#[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    /// Canonical `<owner>/<name>` identifier used in lockfile keys and
40    /// install paths.
41    #[must_use]
42    pub fn id(&self) -> String {
43        format!("{}/{}", self.owner, self.name)
44    }
45
46    /// Lockfile entry key for this skill.
47    #[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    /// Recompute integrity from this skill's files.
59    #[must_use]
60    pub fn computed_integrity(&self) -> Integrity {
61        compute_integrity(&self.files)
62    }
63
64    /// Convenience: `self.computed_integrity() == self.integrity`.
65    #[must_use]
66    pub fn integrity_matches(&self) -> bool {
67        self.computed_integrity() == self.integrity
68    }
69}
70
71/// Compute an [`Integrity`] over a slice of skill files. Stable across runs
72/// and machines because files are sorted by relative path first.
73#[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// ---------------------------------------------------------------------------
92// MCP server payload + transport
93// ---------------------------------------------------------------------------
94
95/// How an MCP server is launched / reached. `BTreeMap` keeps env + headers
96/// in deterministic order for hashing and write-out.
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(tag = "type", rename_all = "lowercase")]
99pub enum McpTransport {
100    /// Locally-spawned subprocess that speaks MCP over stdio. By far the
101    /// most common transport today (npm/pypi/binary packages).
102    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    /// Hosted MCP server reachable over HTTP/SSE.
110    Http {
111        url: String,
112        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
113        headers: BTreeMap<String, String>,
114    },
115}
116
117/// A resolved MCP server ready to install. The adapter writes the transport
118/// into the agent's MCP config (e.g. `.mcp.json` for Claude Code).
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct McpServer {
121    /// Canonical id from the source registry, e.g.
122    /// `io.github.modelcontextprotocol/server-filesystem`.
123    pub id: String,
124    pub version: String,
125    pub transport: McpTransport,
126}
127
128impl McpServer {
129    /// Lockfile entry key.
130    #[must_use]
131    pub fn lockfile_key(&self) -> String {
132        format!("{}/{}@{}", PackageType::Mcp.as_str(), self.id, self.version)
133    }
134
135    /// Sha256 over the canonical JSON of the transport config. Used as the
136    /// lockfile integrity and as the on-disk-drift detector.
137    #[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    /// Short name used as the key inside agent-side config files
154    /// (`.mcp.json` etc.). Last `/`-separated segment of the id, lowercased.
155    #[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// ---------------------------------------------------------------------------
166// Opaque markers for primitives not yet implemented
167// ---------------------------------------------------------------------------
168
169#[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}