1use std::path::{Path, PathBuf};
16
17use serde::{Deserialize, Serialize};
18
19use crate::error::{KernelError, Result};
20use crate::module::{ModuleKind, ModuleMetadata, ModuleState};
21use crate::registry::StateRegistry;
22use crate::Kernel;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ModuleManifest {
27 pub id: String,
29 pub name: String,
31 pub version: String,
33 pub kind: ModuleKind,
35 pub description: Option<String>,
37 pub spec_kind: Option<String>,
39 pub binary: Option<String>,
41 pub skill: Option<String>,
43 pub mcp: Option<String>,
45 #[serde(default)]
47 pub base_url: Option<String>,
48 #[serde(default)]
50 pub operations: Vec<String>,
51 #[serde(default)]
55 pub capabilities: Vec<String>,
56}
57
58impl ModuleManifest {
59 pub fn from_json(raw: &str) -> Result<Self> {
61 Ok(serde_json::from_str(raw)?)
62 }
63
64 pub fn load(path: &Path) -> Result<Self> {
67 let manifest_path = if path.is_dir() {
68 path.join("module.json")
69 } else {
70 path.to_path_buf()
71 };
72 let raw = std::fs::read_to_string(&manifest_path).map_err(|e| {
73 KernelError::Other(anyhow::anyhow!(
74 "failed to read manifest {}: {e}",
75 manifest_path.display()
76 ))
77 })?;
78 Self::from_json(&raw)
79 }
80
81 pub fn metadata(&self) -> ModuleMetadata {
83 ModuleMetadata {
84 id: self.id.clone(),
85 name: self.name.clone(),
86 version: self.version.clone(),
87 kind: self.kind,
88 description: self.description.clone(),
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
96pub struct ResolvedManifest {
97 pub manifest: ModuleManifest,
99 pub binary_path: Option<PathBuf>,
101 pub skill_path: Option<PathBuf>,
103 pub mcp_path: Option<PathBuf>,
105}
106
107impl ResolvedManifest {
108 pub fn resolve(manifest: ModuleManifest, base_dir: &Path) -> Self {
110 let join = |relative: &Option<String>| relative.as_deref().map(|s| base_dir.join(s));
111 Self {
112 binary_path: join(&manifest.binary),
113 skill_path: join(&manifest.skill),
114 mcp_path: join(&manifest.mcp),
115 manifest,
116 }
117 }
118}
119
120impl Kernel {
121 pub async fn register_module_from_manifest(&self, path: &Path) -> Result<ResolvedManifest> {
128 let manifest = ModuleManifest::load(path)?;
129 let base_dir = if path.is_dir() {
130 path.to_path_buf()
131 } else {
132 path.parent()
133 .unwrap_or_else(|| Path::new("."))
134 .to_path_buf()
135 };
136 let resolved = ResolvedManifest::resolve(manifest, &base_dir);
137 let metadata = resolved.manifest.metadata();
138 self.registry()
139 .upsert_module(&metadata, ModuleState::Loaded)
140 .await?;
141 tracing::info!(
142 id = %metadata.id,
143 kind = ?metadata.kind,
144 "registered module from manifest"
145 );
146 Ok(resolved)
147 }
148}
149
150pub async fn register_in_registry(
153 registry: &StateRegistry,
154 manifest: &ModuleManifest,
155) -> Result<()> {
156 registry
157 .upsert_module(&manifest.metadata(), ModuleState::Loaded)
158 .await
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 const SAMPLE: &str = r#"
166 {
167 "id": "petstore",
168 "name": "Pet Store",
169 "version": "1.0.0",
170 "kind": "native",
171 "description": "Generated client for the Pet Store API.",
172 "spec_kind": "openapi",
173 "binary": "petstore-cli",
174 "skill": "SKILL.md",
175 "mcp": "mcp.json",
176 "base_url": "https://petstore.example.com/v1",
177 "operations": ["list_pets", "create_pet", "get_pet"]
178 }"#;
179
180 #[test]
181 fn parses_sample_manifest() {
182 let m = ModuleManifest::from_json(SAMPLE).unwrap();
183 assert_eq!(m.id, "petstore");
184 assert_eq!(m.kind, ModuleKind::Native);
185 assert_eq!(m.operations.len(), 3);
186 }
187
188 #[tokio::test]
189 async fn registers_manifest_in_registry() {
190 let kernel = Kernel::in_memory().await.unwrap();
191 let dir = tempfile::tempdir().unwrap();
192 let manifest_path = dir.path().join("module.json");
193 std::fs::write(&manifest_path, SAMPLE).unwrap();
194
195 let resolved = kernel
196 .register_module_from_manifest(&manifest_path)
197 .await
198 .unwrap();
199 assert_eq!(resolved.manifest.id, "petstore");
200 assert_eq!(
201 resolved.binary_path.as_ref().unwrap(),
202 &dir.path().join("petstore-cli")
203 );
204
205 let rec = kernel
206 .registry()
207 .get_module("petstore")
208 .await
209 .unwrap()
210 .expect("record");
211 assert_eq!(rec.state, ModuleState::Loaded);
212 assert_eq!(rec.version, "1.0.0");
213 }
214}