Skip to main content

pyro_artifacts/
artifacts.rs

1use cargo_toml::Dependency;
2use flate2::Compression;
3use flate2::read::GzDecoder;
4use flate2::write::GzEncoder;
5use pyro_spec::{InterfaceSpec, ModuleFunc};
6use sha2::{Digest, Sha256};
7use std::collections::{BTreeMap, HashMap};
8use std::future::Future;
9use std::io::{self, Read, Write};
10use std::ops::Deref;
11use std::path::Path;
12use tar::{Builder, Header};
13use tokio::fs;
14
15use crate::cargo::{CapabilityIdent, CapabilityManifest, ConfiguredCapability, ModuleManifest};
16
17pub enum CapBinary {
18    Pe(Vec<u8>),
19    MachO(Vec<u8>),
20    Elf(Vec<u8>),
21}
22
23impl Deref for CapBinary {
24    type Target = [u8];
25
26    fn deref(&self) -> &Self::Target {
27        match self {
28            CapBinary::Pe(items) => items,
29            CapBinary::MachO(items) => items,
30            CapBinary::Elf(items) => items,
31        }
32    }
33}
34
35pub struct CapabilityBinary {
36    pub ident: CapabilityIdent,
37    pub libs: Vec<CapBinary>,
38    pub interface: InterfaceSpec<'static>,
39}
40
41pub struct CapabilitySource {
42    pub manifest: CapabilityManifest,
43    pub cargo_toml: String,
44    pub cargo_lock: String,
45    pub src_lib_rs: String,
46}
47
48pub struct Interface {
49    pub manifest: CapabilityManifest,
50    pub src_lib_rs: String,
51    pub interface: InterfaceSpec<'static>,
52}
53
54#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
55pub struct ModuleDependencies {
56    pub dependencies: BTreeMap<String, Dependency>,
57    pub capabilities: Vec<CapabilityIdent>,
58}
59
60/// Identity for a named module (from Module.toml [module] section).
61#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct PlaybookIdent {
63    pub author: String,
64    pub package: String,
65    pub version: String,
66}
67
68#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
69pub struct PlaybookSource {
70    pub manifest: ModuleManifest,
71    pub source: String,
72}
73
74#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
75pub struct PlaybookSpec {
76    pub ident: PlaybookIdent,
77    pub hash: String,
78    pub func: ModuleFunc<'static>,
79    pub capabilities: Vec<CapabilityIdent>,
80    #[serde(default)]
81    pub interconnect: BTreeMap<String, PlaybookIdent>,
82}
83
84#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
85pub struct PlaybookBinary {
86    /// Named module identity (author/name/version), if this is a named module.
87    pub wasm: Vec<u8>,
88    pub spec: PlaybookSpec,
89    #[serde(default)]
90    pub configurations: Vec<ConfiguredCapability>,
91}
92
93#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
94pub enum Playbook {
95    Source(PlaybookSource),
96    Binary(PlaybookBinary),
97}
98
99#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
100#[serde(tag = "type", rename_all = "lowercase")]
101pub struct CapabilityConfig {
102    /// Per-class capability configuration. Keys are class names.
103    pub classes: HashMap<String, Option<serde_json::Value>>,
104}
105
106impl std::hash::Hash for CapabilityConfig {
107    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
108        let mut entries: Vec<_> = self.classes.iter().collect();
109        entries.sort_by_key(|(k, _)| *k);
110        for (k, v) in entries {
111            k.hash(state);
112            if let Some(val) = v {
113                if let Ok(json_str) = serde_json::to_string(val) {
114                    json_str.hash(state);
115                }
116            } else {
117                0.hash(state);
118            }
119        }
120    }
121}
122
123impl PlaybookSource {
124    pub fn ident(&self) -> PlaybookIdent {
125        PlaybookIdent {
126            author: self.manifest.module.author.clone(),
127            package: self.manifest.module.package.clone(),
128            version: self.manifest.module.version.clone(),
129        }
130    }
131
132    pub fn dependencies(&self) -> ModuleDependencies {
133        let mut resolved_capabilities = Vec::new();
134        for cap in self.manifest.capabilities.values() {
135            resolved_capabilities.push(CapabilityIdent {
136                author: cap.author.clone(),
137                package: cap.package.clone(),
138                version: cap.version.clone(),
139            });
140        }
141        ModuleDependencies {
142            dependencies: self.manifest.dependencies.clone(),
143            capabilities: resolved_capabilities,
144        }
145    }
146
147    pub fn configurations(&self) -> Vec<ConfiguredCapability> {
148        self.manifest.capabilities.values().cloned().collect()
149    }
150
151    /// Computes a deterministic hash of the module's source and dependencies.
152    pub fn hash(&self) -> String {
153        let mut resolved_capabilities = Vec::new();
154        for cap in self.manifest.capabilities.values() {
155            resolved_capabilities.push(CapabilityIdent {
156                author: cap.author.clone(),
157                package: cap.package.clone(),
158                version: cap.version.clone(),
159            });
160        }
161        let base_hash = Self::compute_hash(
162            &self.source,
163            &self.manifest.dependencies,
164            &resolved_capabilities,
165        );
166        let mut hasher = Sha256::new();
167        hasher.update(base_hash.as_bytes());
168        if let Ok(interconnect_json) = serde_json::to_string(&self.manifest.interconnect) {
169            hasher.update(interconnect_json.as_bytes());
170        }
171        format!("{:x}", hasher.finalize())
172    }
173
174    pub fn compute_hash(
175        code: &str,
176        dependencies: &std::collections::BTreeMap<String, cargo_toml::Dependency>,
177        capabilities: &[crate::cargo::CapabilityIdent],
178    ) -> String {
179        let mut hasher = Sha256::new();
180
181        hasher.update(code.as_bytes());
182
183        if let Ok(deps_json) = serde_json::to_string(dependencies) {
184            hasher.update(deps_json.as_bytes());
185        }
186
187        let mut sorted_caps = capabilities.to_vec();
188        sorted_caps.sort_by(|a, b| {
189            a.package
190                .cmp(&b.package)
191                .then_with(|| a.author.cmp(&b.author))
192                .then_with(|| a.version.cmp(&b.version))
193        });
194
195        if let Ok(caps_json) = serde_json::to_string(&sorted_caps) {
196            hasher.update(caps_json.as_bytes());
197        }
198
199        format!("{:x}", hasher.finalize())
200    }
201
202    pub fn new(
203        ident: PlaybookIdent,
204        dependencies: ModuleDependencies,
205        configurations: Vec<ConfiguredCapability>,
206        source: String,
207        interconnect: BTreeMap<String, PlaybookIdent>,
208    ) -> Self {
209        let mut capabilities_map: BTreeMap<String, ConfiguredCapability> = BTreeMap::new();
210        for cap in configurations {
211            capabilities_map.insert(cap.package.clone(), cap);
212        }
213        for cap in dependencies.capabilities.iter() {
214            if !capabilities_map.contains_key(&cap.package) {
215                capabilities_map.insert(
216                    cap.package.clone(),
217                    ConfiguredCapability {
218                        author: cap.author.clone(),
219                        package: cap.package.clone(),
220                        version: cap.version.clone(),
221                        configuration: CapabilityConfig {
222                            classes: std::collections::HashMap::new(),
223                        },
224                    },
225                );
226            }
227        }
228        let pyroduct_dep =
229            cargo_toml::Dependency::Inherited(cargo_toml::InheritedDependencyDetail {
230                workspace: true,
231                ..Default::default()
232            });
233        let manifest = ModuleManifest::<toml::Value> {
234            module: CapabilityIdent {
235                package: ident.package,
236                version: ident.version,
237                author: ident.author,
238            },
239            workspace: None,
240            pyroduct: pyroduct_dep,
241            capabilities: capabilities_map,
242            dependencies: dependencies.dependencies,
243            dev_dependencies: Default::default(),
244            build_dependencies: Default::default(),
245            target: Default::default(),
246            features: Default::default(),
247            patch: Default::default(),
248            lib: None,
249            profile: Default::default(),
250            badges: Default::default(),
251            bin: Vec::new(),
252            bench: Vec::new(),
253            test: Vec::new(),
254            example: Vec::new(),
255            lints: Default::default(),
256            interconnect,
257        };
258        Self { manifest, source }
259    }
260}
261
262impl PlaybookBinary {
263    pub fn hash(&self) -> String {
264        self.spec.hash.clone()
265    }
266}
267
268impl Playbook {
269    pub fn hash(&self) -> String {
270        match self {
271            Playbook::Source(m) => m.hash(),
272            Playbook::Binary(m) => m.hash(),
273        }
274    }
275}
276
277pub enum Artifacts {
278    CapabilityBinary(CapabilityBinary),
279    CapabilitySource(CapabilitySource),
280    Interface(Interface),
281    Playbook(Playbook),
282}
283
284impl From<CapabilityBinary> for Artifacts {
285    fn from(value: CapabilityBinary) -> Self {
286        Artifacts::CapabilityBinary(value)
287    }
288}
289
290impl From<CapabilitySource> for Artifacts {
291    fn from(value: CapabilitySource) -> Self {
292        Artifacts::CapabilitySource(value)
293    }
294}
295
296impl From<Interface> for Artifacts {
297    fn from(value: Interface) -> Self {
298        Artifacts::Interface(value)
299    }
300}
301
302impl From<Playbook> for Artifacts {
303    fn from(value: Playbook) -> Self {
304        Artifacts::Playbook(value)
305    }
306}
307
308impl From<PlaybookBinary> for Artifacts {
309    fn from(value: PlaybookBinary) -> Self {
310        Artifacts::Playbook(Playbook::Binary(value))
311    }
312}
313
314impl From<PlaybookSource> for Artifacts {
315    fn from(value: PlaybookSource) -> Self {
316        Artifacts::Playbook(Playbook::Source(value))
317    }
318}
319
320/// The common trait for all artifact types.
321/// Requires Sized so we can return Self for the constructors.
322pub trait Artifact: Sized {
323    fn write_to_directory(&self, path: &Path) -> impl Future<Output = io::Result<()>> + Send;
324    fn to_tarball(&self) -> Result<Vec<u8>, io::Error>;
325
326    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error>;
327    fn from_dir(path: &Path) -> impl Future<Output = Result<Self, io::Error>> + Send;
328}
329
330// --- Helper for appending files to a tarball ---
331pub(crate) fn append_file<W: Write>(
332    tar: &mut Builder<W>,
333    name: &str,
334    data: &[u8],
335) -> Result<(), io::Error> {
336    let mut header = Header::new_gnu();
337    header.set_size(data.len() as u64);
338    header.set_mode(0o644);
339    header.set_cksum();
340    tar.append_data(&mut header, name, data)
341}
342
343// ==========================================
344// Trait Implementations for Concrete Structs
345// ==========================================
346
347impl Artifact for CapabilityBinary {
348    #[tracing::instrument(skip(self, path), fields(path = %path.display(), ident = ?self.ident))]
349    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
350        tracing::debug!("Writing CapabilityBinary to directory");
351        fs::create_dir_all(path).await?;
352        for lib in &self.libs {
353            match lib {
354                CapBinary::Pe(bytes) => fs::write(path.join("lib.dll"), bytes).await?,
355                CapBinary::MachO(bytes) => fs::write(path.join("lib.dylib"), bytes).await?,
356                CapBinary::Elf(bytes) => fs::write(path.join("lib.so"), bytes).await?,
357            }
358        }
359        fs::write(
360            path.join("ident.json"),
361            serde_json::to_string(&self.ident).map_err(|e| {
362                let err = io::Error::new(io::ErrorKind::InvalidData, e);
363                tracing::error!(error = ?err, "Failed to serialize ident");
364                err
365            })?,
366        )
367        .await?;
368        let interface_json = serde_json::to_string_pretty(&self.interface).map_err(|e| {
369            let err = io::Error::new(io::ErrorKind::InvalidData, e);
370            tracing::error!(error = ?err, "Failed to serialize interface");
371            err
372        })?;
373        fs::write(path.join("interface.json"), interface_json).await?;
374        Ok(())
375    }
376
377    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
378        let encoder = GzEncoder::new(Vec::new(), Compression::default());
379        let mut tar = Builder::new(encoder);
380
381        for lib in &self.libs {
382            match lib {
383                CapBinary::Pe(bytes) => append_file(&mut tar, "lib.dll", bytes)?,
384                CapBinary::MachO(bytes) => append_file(&mut tar, "lib.dylib", bytes)?,
385                CapBinary::Elf(bytes) => append_file(&mut tar, "lib.so", bytes)?,
386            }
387        }
388        append_file(
389            &mut tar,
390            "ident.json",
391            serde_json::to_string(&self.ident)
392                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
393                .as_bytes(),
394        )?;
395        append_file(
396            &mut tar,
397            "interface.json",
398            serde_json::to_string_pretty(&self.interface)
399                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
400                .as_bytes(),
401        )?;
402
403        tar.into_inner()?.finish()
404    }
405
406    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
407        let tar = GzDecoder::new(bytes);
408        let mut archive = tar::Archive::new(tar);
409
410        let mut libs = Vec::new();
411        let mut ident = None;
412        let mut interface = None;
413
414        for file in archive.entries()? {
415            let mut file = file?;
416            let path = file.path()?.to_path_buf();
417            let mut content = Vec::new();
418            file.read_to_end(&mut content)?;
419
420            match path.to_string_lossy().as_ref() {
421                "lib.dll" => libs.push(CapBinary::Pe(content)),
422                "lib.dylib" => libs.push(CapBinary::MachO(content)),
423                "lib.so" => libs.push(CapBinary::Elf(content)),
424                "ident.json" => {
425                    ident = serde_json::from_slice(&content).map_err(|e| {
426                        io::Error::new(
427                            io::ErrorKind::InvalidData,
428                            format!("Unable to deserialize manifest: {}", e),
429                        )
430                    })?;
431                }
432                "interface.json" => {
433                    interface = serde_json::from_slice(&content).map_err(|e| {
434                        io::Error::new(
435                            io::ErrorKind::InvalidData,
436                            format!("Unable to deserialize interface: {}", e),
437                        )
438                    })?;
439                }
440                _ => {}
441            }
442        }
443
444        if libs.is_empty() {
445            return Err(io::Error::new(io::ErrorKind::NotFound, "Missing library"));
446        }
447
448        Ok(CapabilityBinary {
449            ident: ident
450                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing ident.json"))?,
451            libs,
452            interface: interface.ok_or_else(|| {
453                io::Error::new(io::ErrorKind::InvalidData, "Missing interface.json")
454            })?,
455        })
456    }
457
458    #[tracing::instrument(skip(path), fields(path = %path.display()))]
459    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
460        tracing::debug!("Loading CapabilityBinary from directory");
461        let mut libs = Vec::new();
462        if let Ok(bytes) = fs::read(path.join("lib.dll")).await {
463            libs.push(CapBinary::Pe(bytes));
464        }
465        if let Ok(bytes) = fs::read(path.join("lib.dylib")).await {
466            libs.push(CapBinary::MachO(bytes));
467        }
468        if let Ok(bytes) = fs::read(path.join("lib.so")).await {
469            libs.push(CapBinary::Elf(bytes));
470        }
471
472        if libs.is_empty() {
473            let err = io::Error::new(io::ErrorKind::NotFound, "Missing capability library");
474            tracing::error!(error = ?err, "Failed to load capability library");
475            return Err(err);
476        }
477
478        let ident_string = fs::read(path.join("ident.json")).await?;
479        let ident = serde_json::from_slice(&ident_string).map_err(|e| {
480            let err = io::Error::new(
481                io::ErrorKind::InvalidData,
482                format!("Unable to deserialize ident: {}", e),
483            );
484            tracing::error!(error = ?err, "Failed to deserialize ident.json");
485            err
486        })?;
487
488        let interface_string = fs::read(path.join("interface.json")).await?;
489        let interface = serde_json::from_slice(&interface_string).map_err(|e| {
490            let err = io::Error::new(
491                io::ErrorKind::InvalidData,
492                format!("Unable to deserialize interface: {}", e),
493            );
494            tracing::error!(error = ?err, "Failed to deserialize interface.json");
495            err
496        })?;
497
498        Ok(CapabilityBinary {
499            libs,
500            ident,
501            interface,
502        })
503    }
504}
505
506impl Artifact for CapabilitySource {
507    #[tracing::instrument(skip(self, path), fields(path = %path.display(), manifest = ?self.manifest.capability))]
508    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
509        tracing::debug!("Writing CapabilitySource to directory");
510        fs::create_dir_all(path).await?;
511        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
512            let err = io::Error::new(
513                io::ErrorKind::InvalidData,
514                format!("Unable to serialize manifest: {}", e),
515            );
516            tracing::error!(error = ?err, "Failed to serialize Capability.toml");
517            err
518        })?;
519        fs::write(path.join("Capability.toml"), &manifest).await?;
520        fs::write(path.join("Cargo.toml"), &self.cargo_toml).await?;
521        fs::write(path.join("Cargo.lock"), &self.cargo_lock).await?;
522
523        let src_dir = path.join("src");
524        fs::create_dir_all(&src_dir).await?;
525        fs::write(src_dir.join("lib.rs"), &self.src_lib_rs).await?;
526        Ok(())
527    }
528
529    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
530        let encoder = GzEncoder::new(Vec::new(), Compression::default());
531        let mut tar = Builder::new(encoder);
532
533        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
534            io::Error::new(
535                io::ErrorKind::InvalidData,
536                format!("Unable to serialize manifest: {}", e),
537            )
538        })?;
539        append_file(&mut tar, "Capability.toml", manifest.as_bytes())?;
540        append_file(&mut tar, "Cargo.toml", self.cargo_toml.as_bytes())?;
541        append_file(&mut tar, "Cargo.lock", self.cargo_lock.as_bytes())?;
542        append_file(&mut tar, "src/lib.rs", self.src_lib_rs.as_bytes())?;
543
544        tar.into_inner()?.finish()
545    }
546
547    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
548        let tar = GzDecoder::new(bytes);
549        let mut archive = tar::Archive::new(tar);
550
551        let mut manifest = None;
552        let mut cargo_toml = None;
553        let mut cargo_lock = None;
554        let mut src_lib_rs = None;
555
556        for file in archive.entries()? {
557            let mut file = file?;
558            let path = file.path()?.to_path_buf();
559            let mut content = Vec::new();
560            file.read_to_end(&mut content)?;
561
562            match path.to_string_lossy().as_ref() {
563                "Capability.toml" => {
564                    manifest = toml::from_slice(&content).map_err(|e| {
565                        io::Error::new(
566                            io::ErrorKind::InvalidData,
567                            format!("Unable to deserialize manifest: {}", e),
568                        )
569                    })?;
570                }
571                "Cargo.toml" => cargo_toml = String::from_utf8(content).ok(),
572                "Cargo.lock" => cargo_lock = String::from_utf8(content).ok(),
573                "src/lib.rs" => src_lib_rs = String::from_utf8(content).ok(),
574                _ => {}
575            }
576        }
577
578        Ok(CapabilitySource {
579            manifest: manifest.ok_or_else(|| {
580                io::Error::new(io::ErrorKind::InvalidData, "Missing Capability.toml")
581            })?,
582            cargo_toml: cargo_toml
583                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing Cargo.toml"))?,
584            cargo_lock: cargo_lock
585                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing Cargo.lock"))?,
586            src_lib_rs: src_lib_rs
587                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing src/lib.rs"))?,
588        })
589    }
590
591    #[tracing::instrument(skip(path), fields(path = %path.display()))]
592    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
593        tracing::debug!("Loading CapabilitySource from directory");
594        let manifest_string = fs::read(path.join("Capability.toml")).await?;
595        let manifest = toml::from_slice(&manifest_string).map_err(|e| {
596            let err = io::Error::new(
597                io::ErrorKind::InvalidData,
598                format!("Unable to deserialize manifest: {}", e),
599            );
600            tracing::error!(error = ?err, "Failed to deserialize Capability.toml");
601            err
602        })?;
603
604        Ok(CapabilitySource {
605            manifest,
606            cargo_toml: fs::read_to_string(path.join("Cargo.toml")).await?,
607            cargo_lock: fs::read_to_string(path.join("Cargo.lock")).await?,
608            src_lib_rs: fs::read_to_string(path.join("src").join("lib.rs")).await?,
609        })
610    }
611}
612
613impl Artifact for Interface {
614    #[tracing::instrument(skip(self, path), fields(path = %path.display(), manifest = ?self.manifest.capability))]
615    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
616        tracing::debug!("Writing Interface to directory");
617        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
618            let err = io::Error::new(
619                io::ErrorKind::InvalidData,
620                format!("Unable to serialize manifest: {}", e),
621            );
622            tracing::error!(error = ?err, "Failed to serialize Capability.toml");
623            err
624        })?;
625        fs::create_dir_all(path).await?;
626        fs::write(path.join("Capability.toml"), &manifest).await?;
627        let interface_str = serde_json::to_string_pretty(&self.interface).map_err(|e| {
628            let err = io::Error::new(
629                io::ErrorKind::InvalidData,
630                format!("Unable to serialize interface: {}", e),
631            );
632            tracing::error!(error = ?err, "Failed to serialize interface.json");
633            err
634        })?;
635        fs::write(path.join("interface.json"), &interface_str).await?;
636
637        let src_dir = path.join("src");
638        fs::create_dir_all(&src_dir).await?;
639        fs::write(src_dir.join("lib.rs"), &self.src_lib_rs).await?;
640        Ok(())
641    }
642
643    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
644        let encoder = GzEncoder::new(Vec::new(), Compression::default());
645        let mut tar = Builder::new(encoder);
646        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
647            io::Error::new(
648                io::ErrorKind::InvalidData,
649                format!("Unable to serialize manifest: {}", e),
650            )
651        })?;
652        let interface = serde_json::to_string_pretty(&self.interface).map_err(|e| {
653            io::Error::new(
654                io::ErrorKind::InvalidData,
655                format!("Unable to serialize interface: {}", e),
656            )
657        })?;
658
659        append_file(&mut tar, "Capability.toml", manifest.as_bytes())?;
660        append_file(&mut tar, "interface.json", interface.as_bytes())?;
661        append_file(&mut tar, "src/lib.rs", self.src_lib_rs.as_bytes())?;
662
663        tar.into_inner()?.finish()
664    }
665
666    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
667        let tar = GzDecoder::new(bytes);
668        let mut archive = tar::Archive::new(tar);
669
670        let mut manifest = None;
671        let mut src_lib_rs = None;
672        let mut interface = None;
673
674        for file in archive.entries()? {
675            let mut file = file?;
676            let path = file.path()?.to_path_buf();
677            let mut content = Vec::new();
678            file.read_to_end(&mut content)?;
679
680            match path.to_string_lossy().as_ref() {
681                "Capability.toml" => {
682                    manifest = toml::from_slice(&content).map_err(|e| {
683                        io::Error::new(
684                            io::ErrorKind::InvalidData,
685                            format!("Unable to deserialize manifest: {}", e),
686                        )
687                    })?;
688                }
689                "interface.json" => {
690                    interface = serde_json::from_slice(&content).map_err(|e| {
691                        io::Error::new(
692                            io::ErrorKind::InvalidData,
693                            format!("Unable to deserialize interface: {}", e),
694                        )
695                    })?;
696                }
697                "src/lib.rs" => src_lib_rs = String::from_utf8(content).ok(),
698                _ => {}
699            }
700        }
701
702        Ok(Interface {
703            manifest: manifest.ok_or_else(|| {
704                io::Error::new(io::ErrorKind::InvalidData, "Missing Capability.toml")
705            })?,
706            src_lib_rs: src_lib_rs
707                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing src/lib.rs"))?,
708            interface: interface.ok_or_else(|| {
709                io::Error::new(io::ErrorKind::InvalidData, "Missing interface.json")
710            })?,
711        })
712    }
713
714    #[tracing::instrument(skip(path), fields(path = %path.display()))]
715    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
716        tracing::debug!("Loading Interface from directory");
717        let manifest_string = fs::read(path.join("Capability.toml")).await?;
718        let manifest = toml::from_slice(&manifest_string).map_err(|e| {
719            let err = io::Error::new(
720                io::ErrorKind::InvalidData,
721                format!("Unable to deserialize manifest: {}", e),
722            );
723            tracing::error!(error = ?err, "Failed to deserialize Capability.toml");
724            err
725        })?;
726
727        let interface_string = fs::read(path.join("interface.json")).await?;
728        let interface = toml::from_slice(&interface_string).map_err(|e| {
729            let err = io::Error::new(
730                io::ErrorKind::InvalidData,
731                format!("Unable to deserialize interface: {}", e),
732            );
733            tracing::error!(error = ?err, "Failed to deserialize interface.json");
734            err
735        })?;
736
737        Ok(Interface {
738            manifest,
739            src_lib_rs: fs::read_to_string(path.join("src").join("lib.rs")).await?,
740            interface,
741        })
742    }
743}
744
745impl Artifact for PlaybookSource {
746    #[tracing::instrument(skip(self, path), fields(path = %path.display(), ident = ?self.ident()))]
747    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
748        tracing::debug!("Writing PlaybookSource to directory");
749        fs::create_dir_all(path).await?;
750
751        let toml_str = toml::to_string_pretty(&self.manifest).map_err(|e| {
752            let err = io::Error::new(
753                io::ErrorKind::InvalidData,
754                format!("Unable to serialize Module.toml: {}", e),
755            );
756            tracing::error!(error = ?err, "Failed to serialize Module.toml");
757            err
758        })?;
759
760        fs::write(path.join("Module.toml"), &toml_str).await?;
761        fs::write(path.join("source.rs"), &self.source).await?;
762        Ok(())
763    }
764
765    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
766        let encoder = GzEncoder::new(Vec::new(), Compression::default());
767        let mut tar = Builder::new(encoder);
768
769        let toml_str = toml::to_string_pretty(&self.manifest).map_err(|e| {
770            io::Error::new(
771                io::ErrorKind::InvalidData,
772                format!("Unable to serialize Module.toml: {}", e),
773            )
774        })?;
775
776        append_file(&mut tar, "Module.toml", toml_str.as_bytes())?;
777        append_file(&mut tar, "source.rs", self.source.as_bytes())?;
778        tar.into_inner()?.finish()
779    }
780
781    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
782        let tar = GzDecoder::new(bytes);
783        let mut archive = tar::Archive::new(tar);
784        let mut source = None;
785
786        // Backward compatibility JSON parsing fields
787        let mut legacy_ident = None;
788        let mut legacy_dependencies = None;
789        let mut legacy_configurations = None;
790
791        // Module.toml parsed fields
792        let mut module_toml: Option<ModuleManifest<toml::Value>> = None;
793
794        for file in archive.entries()? {
795            let mut file = file?;
796            let path = file.path()?.to_path_buf();
797            let mut content = Vec::new();
798            file.read_to_end(&mut content)?;
799
800            match path.to_string_lossy().as_ref() {
801                "Module.toml" => {
802                    module_toml = Some(toml::from_slice(&content).map_err(|e| {
803                        io::Error::new(
804                            io::ErrorKind::InvalidData,
805                            format!("Unable to deserialize Module.toml: {}", e),
806                        )
807                    })?);
808                }
809                "source.rs" => source = String::from_utf8(content).ok(),
810                // Fallbacks
811                "ident.json" => {
812                    legacy_ident = Some(
813                        serde_json::from_slice::<PlaybookIdent>(&content).map_err(|e| {
814                            io::Error::new(
815                                io::ErrorKind::InvalidData,
816                                format!("Unable to deserialize ident: {}", e),
817                            )
818                        })?,
819                    );
820                }
821                "dependencies.json" => {
822                    legacy_dependencies = Some(
823                        serde_json::from_slice::<ModuleDependencies>(&content).map_err(|e| {
824                            io::Error::new(
825                                io::ErrorKind::InvalidData,
826                                format!("Unable to deserialize dependencies: {}", e),
827                            )
828                        })?,
829                    );
830                }
831                "configurations.json" => {
832                    legacy_configurations = Some(
833                        serde_json::from_slice::<Vec<ConfiguredCapability>>(&content).map_err(
834                            |e| {
835                                io::Error::new(
836                                    io::ErrorKind::InvalidData,
837                                    format!("Unable to deserialize configurations: {}", e),
838                                )
839                            },
840                        )?,
841                    );
842                }
843                _ => {}
844            }
845        }
846
847        let source =
848            source.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing source.rs"))?;
849
850        if let Some(mt) = module_toml {
851            Ok(PlaybookSource {
852                manifest: mt,
853                source,
854            })
855        } else {
856            let mut capabilities_map: BTreeMap<String, ConfiguredCapability> = BTreeMap::new();
857            let legacy_config = legacy_configurations.unwrap_or_default();
858            for cap in legacy_config {
859                capabilities_map.insert(cap.package.clone(), cap);
860            }
861            let legacy_dep = legacy_dependencies.ok_or_else(|| {
862                io::Error::new(
863                    io::ErrorKind::NotFound,
864                    "Missing Module.toml or dependencies.json",
865                )
866            })?;
867            let legacy_id = legacy_ident.ok_or_else(|| {
868                io::Error::new(io::ErrorKind::NotFound, "Missing Module.toml or ident.json")
869            })?;
870
871            let manifest = ModuleManifest::<toml::Value> {
872                module: CapabilityIdent {
873                    package: legacy_id.package,
874                    version: legacy_id.version,
875                    author: legacy_id.author,
876                },
877                workspace: None,
878                pyroduct: cargo_toml::Dependency::Inherited(
879                    cargo_toml::InheritedDependencyDetail {
880                        workspace: true,
881                        ..Default::default()
882                    },
883                ),
884                capabilities: capabilities_map,
885                dependencies: legacy_dep.dependencies,
886                dev_dependencies: Default::default(),
887                build_dependencies: Default::default(),
888                target: Default::default(),
889                features: Default::default(),
890                patch: Default::default(),
891                lib: None,
892                profile: Default::default(),
893                badges: Default::default(),
894                bin: Vec::new(),
895                bench: Vec::new(),
896                test: Vec::new(),
897                example: Vec::new(),
898                lints: Default::default(),
899                interconnect: BTreeMap::new(),
900            };
901            Ok(PlaybookSource { manifest, source })
902        }
903    }
904
905    #[tracing::instrument(skip(path), fields(path = %path.display()))]
906    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
907        tracing::debug!("Loading PlaybookSource from directory");
908        let source = fs::read_to_string(path.join("source.rs")).await?;
909
910        if fs::try_exists(path.join("Module.toml"))
911            .await
912            .unwrap_or(false)
913        {
914            let toml_bytes = fs::read(path.join("Module.toml")).await?;
915            let mt: ModuleManifest<toml::Value> = toml::from_slice(&toml_bytes).map_err(|e| {
916                let err = io::Error::new(
917                    io::ErrorKind::InvalidData,
918                    format!("Failed to parse Module.toml: {e}"),
919                );
920                tracing::error!(error = ?err);
921                err
922            })?;
923            Ok(PlaybookSource {
924                manifest: mt,
925                source,
926            })
927        } else {
928            let ident_string = fs::read(path.join("ident.json")).await?;
929            let ident: PlaybookIdent = serde_json::from_slice(&ident_string).map_err(|e| {
930                let err = io::Error::new(
931                    io::ErrorKind::InvalidData,
932                    format!("Unable to deserialize ident: {}", e),
933                );
934                tracing::error!(error = ?err, "Failed to deserialize ident.json");
935                err
936            })?;
937            let dependencies_string = fs::read(path.join("dependencies.json")).await?;
938            let dependencies: ModuleDependencies = serde_json::from_slice(&dependencies_string)
939                .map_err(|e| {
940                    let err = io::Error::new(
941                        io::ErrorKind::InvalidData,
942                        format!("Unable to deserialize dependencies: {}", e),
943                    );
944                    tracing::error!(error = ?err, "Failed to deserialize dependencies.json");
945                    err
946                })?;
947            let configurations: Vec<ConfiguredCapability> =
948                if fs::try_exists(path.join("configurations.json"))
949                    .await
950                    .unwrap_or(false)
951                {
952                    let configurations_string = fs::read(path.join("configurations.json")).await?;
953                    serde_json::from_slice(&configurations_string).map_err(|e| {
954                        let err = io::Error::new(
955                            io::ErrorKind::InvalidData,
956                            format!("Unable to deserialize configurations: {}", e),
957                        );
958                        tracing::error!(error = ?err, "Failed to deserialize configurations.json");
959                        err
960                    })?
961                } else {
962                    Vec::new()
963                };
964
965            let mut capabilities_map: BTreeMap<String, ConfiguredCapability> = BTreeMap::new();
966            for cap in configurations {
967                capabilities_map.insert(cap.package.clone(), cap);
968            }
969
970            let manifest = ModuleManifest::<toml::Value> {
971                module: CapabilityIdent {
972                    package: ident.package,
973                    version: ident.version,
974                    author: ident.author,
975                },
976                workspace: None,
977                pyroduct: cargo_toml::Dependency::Inherited(
978                    cargo_toml::InheritedDependencyDetail {
979                        workspace: true,
980                        ..Default::default()
981                    },
982                ),
983                capabilities: capabilities_map,
984                dependencies: dependencies.dependencies,
985                dev_dependencies: Default::default(),
986                build_dependencies: Default::default(),
987                target: Default::default(),
988                features: Default::default(),
989                patch: Default::default(),
990                lib: None,
991                profile: Default::default(),
992                badges: Default::default(),
993                bin: Vec::new(),
994                bench: Vec::new(),
995                test: Vec::new(),
996                example: Vec::new(),
997                lints: Default::default(),
998                interconnect: BTreeMap::new(),
999            };
1000            Ok(PlaybookSource { manifest, source })
1001        }
1002    }
1003}
1004
1005impl Artifact for PlaybookBinary {
1006    #[tracing::instrument(skip(self, path), fields(path = %path.display()))]
1007    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
1008        tracing::debug!("Writing PlaybookBinary to directory");
1009        fs::create_dir_all(path).await?;
1010        fs::write(path.join("mod.wasm"), &self.wasm).await?;
1011        let spec = serde_json::to_string_pretty(&self.spec).map_err(|e| {
1012            let err = io::Error::new(
1013                io::ErrorKind::InvalidData,
1014                format!("Unable to serialize spec: {}", e),
1015            );
1016            tracing::error!(error = ?err, "Failed to serialize spec.json");
1017            err
1018        })?;
1019        fs::write(path.join("spec.json"), spec).await?;
1020
1021        let mut capabilities_map = BTreeMap::new();
1022        for cap in &self.configurations {
1023            capabilities_map.insert(cap.package.clone(), cap.clone());
1024        }
1025        let pyroduct_dep =
1026            cargo_toml::Dependency::Inherited(cargo_toml::InheritedDependencyDetail {
1027                workspace: true,
1028                ..Default::default()
1029            });
1030        let manifest = ModuleManifest::<toml::Value> {
1031            module: CapabilityIdent {
1032                package: self.spec.ident.package.clone(),
1033                version: self.spec.ident.version.clone(),
1034                author: self.spec.ident.author.clone(),
1035            },
1036            workspace: None,
1037            pyroduct: pyroduct_dep,
1038            capabilities: capabilities_map,
1039            dependencies: BTreeMap::new(),
1040            dev_dependencies: Default::default(),
1041            build_dependencies: Default::default(),
1042            target: Default::default(),
1043            features: Default::default(),
1044            patch: Default::default(),
1045            lib: None,
1046            profile: Default::default(),
1047            badges: Default::default(),
1048            bin: Vec::new(),
1049            bench: Vec::new(),
1050            test: Vec::new(),
1051            example: Vec::new(),
1052            lints: Default::default(),
1053            interconnect: self.spec.interconnect.clone(),
1054        };
1055        let toml_str = toml::to_string_pretty(&manifest).map_err(|e| {
1056            let err = io::Error::new(
1057                io::ErrorKind::InvalidData,
1058                format!("Unable to serialize Module.toml: {}", e),
1059            );
1060            tracing::error!(error = ?err, "Failed to serialize Module.toml");
1061            err
1062        })?;
1063        fs::write(path.join("Module.toml"), &toml_str).await?;
1064        Ok(())
1065    }
1066
1067    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
1068        let encoder = GzEncoder::new(Vec::new(), Compression::default());
1069        let mut tar = Builder::new(encoder);
1070        append_file(&mut tar, "mod.wasm", &self.wasm)?;
1071        let spec = serde_json::to_string_pretty(&self.spec).map_err(|e| {
1072            io::Error::new(
1073                io::ErrorKind::InvalidData,
1074                format!("Unable to serialize spec: {}", e),
1075            )
1076        })?;
1077        append_file(&mut tar, "spec.json", spec.as_bytes())?;
1078
1079        let mut capabilities_map = BTreeMap::new();
1080        for cap in &self.configurations {
1081            capabilities_map.insert(cap.package.clone(), cap.clone());
1082        }
1083        let pyroduct_dep =
1084            cargo_toml::Dependency::Inherited(cargo_toml::InheritedDependencyDetail {
1085                workspace: true,
1086                ..Default::default()
1087            });
1088        let manifest = ModuleManifest::<toml::Value> {
1089            module: CapabilityIdent {
1090                package: self.spec.ident.package.clone(),
1091                version: self.spec.ident.version.clone(),
1092                author: self.spec.ident.author.clone(),
1093            },
1094            workspace: None,
1095            pyroduct: pyroduct_dep,
1096            capabilities: capabilities_map,
1097            dependencies: BTreeMap::new(),
1098            dev_dependencies: Default::default(),
1099            build_dependencies: Default::default(),
1100            target: Default::default(),
1101            features: Default::default(),
1102            patch: Default::default(),
1103            lib: None,
1104            profile: Default::default(),
1105            badges: Default::default(),
1106            bin: Vec::new(),
1107            bench: Vec::new(),
1108            test: Vec::new(),
1109            example: Vec::new(),
1110            lints: Default::default(),
1111            interconnect: self.spec.interconnect.clone(),
1112        };
1113        let toml_str = toml::to_string_pretty(&manifest).map_err(|e| {
1114            io::Error::new(
1115                io::ErrorKind::InvalidData,
1116                format!("Unable to serialize Module.toml: {}", e),
1117            )
1118        })?;
1119        append_file(&mut tar, "Module.toml", toml_str.as_bytes())?;
1120        tar.into_inner()?.finish()
1121    }
1122
1123    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
1124        let tar = GzDecoder::new(bytes);
1125        let mut archive = tar::Archive::new(tar);
1126        let mut wasm = None;
1127        let mut spec: Option<PlaybookSpec> = None;
1128        let mut configurations = None;
1129        let mut legacy_configurations = None;
1130
1131        for file in archive.entries()? {
1132            let mut file = file?;
1133            let path = file.path()?.to_path_buf();
1134            let mut content = Vec::new();
1135            file.read_to_end(&mut content)?;
1136
1137            match path.to_string_lossy().as_ref() {
1138                "mod.wasm" => wasm = Some(content),
1139                "spec.json" => {
1140                    spec = serde_json::from_slice(&content).map_err(|e| {
1141                        io::Error::new(
1142                            io::ErrorKind::InvalidData,
1143                            format!("Unable to deserialize spec: {}", e),
1144                        )
1145                    })?;
1146                }
1147                "Module.toml" => {
1148                    let mt: ModuleManifest<toml::Value> =
1149                        toml::from_slice(&content).map_err(|e| {
1150                            io::Error::new(
1151                                io::ErrorKind::InvalidData,
1152                                format!("Unable to deserialize Module.toml: {}", e),
1153                            )
1154                        })?;
1155                    configurations = Some(mt.capabilities.into_values().collect());
1156                }
1157                "configurations.json" => {
1158                    legacy_configurations =
1159                        Some(serde_json::from_slice(&content).map_err(|e| {
1160                            io::Error::new(
1161                                io::ErrorKind::InvalidData,
1162                                format!("Unable to deserialize configurations: {}", e),
1163                            )
1164                        })?);
1165                }
1166                _ => {}
1167            }
1168        }
1169
1170        let configurations = configurations.or(legacy_configurations).unwrap_or_default();
1171
1172        Ok(PlaybookBinary {
1173            wasm: wasm
1174                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing mod.wasm"))?,
1175            spec: spec
1176                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing spec.json"))?,
1177            configurations,
1178        })
1179    }
1180
1181    #[tracing::instrument(skip(path), fields(path = %path.display()))]
1182    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
1183        tracing::debug!("Loading PlaybookBinary from directory");
1184        let spec_string = fs::read(path.join("spec.json")).await?;
1185        let spec: PlaybookSpec = serde_json::from_slice(&spec_string).map_err(|e| {
1186            let err = io::Error::new(
1187                io::ErrorKind::InvalidData,
1188                format!("Unable to deserialize spec: {}", e),
1189            );
1190            tracing::error!(error = ?err, "Failed to deserialize spec.json");
1191            err
1192        })?;
1193
1194        let configurations = if fs::try_exists(path.join("Module.toml"))
1195            .await
1196            .unwrap_or(false)
1197        {
1198            let toml_bytes = fs::read(path.join("Module.toml")).await?;
1199            let mt: ModuleManifest<toml::Value> = toml::from_slice(&toml_bytes).map_err(|e| {
1200                let err = io::Error::new(
1201                    io::ErrorKind::InvalidData,
1202                    format!("Unable to deserialize Module.toml: {}", e),
1203                );
1204                tracing::error!(error = ?err, "Failed to deserialize Module.toml");
1205                err
1206            })?;
1207            mt.capabilities.into_values().collect()
1208        } else if fs::try_exists(path.join("configurations.json"))
1209            .await
1210            .unwrap_or(false)
1211        {
1212            let configurations_string = fs::read(path.join("configurations.json")).await?;
1213            serde_json::from_slice(&configurations_string).map_err(|e| {
1214                let err = io::Error::new(
1215                    io::ErrorKind::InvalidData,
1216                    format!("Unable to deserialize configurations: {}", e),
1217                );
1218                tracing::error!(error = ?err, "Failed to deserialize configurations.json");
1219                err
1220            })?
1221        } else {
1222            Vec::new()
1223        };
1224
1225        Ok(PlaybookBinary {
1226            wasm: fs::read(path.join("mod.wasm")).await?,
1227            spec,
1228            configurations,
1229        })
1230    }
1231}
1232
1233impl Artifact for Playbook {
1234    #[tracing::instrument(skip(self, path), fields(path = %path.display()))]
1235    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
1236        tracing::debug!("Writing Playbook to directory");
1237        match self {
1238            Playbook::Source(module_source) => module_source.write_to_directory(path).await,
1239            Playbook::Binary(module_binary) => module_binary.write_to_directory(path).await,
1240        }
1241    }
1242
1243    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
1244        match self {
1245            Playbook::Source(module_source) => module_source.to_tarball(),
1246            Playbook::Binary(module_binary) => module_binary.to_tarball(),
1247        }
1248    }
1249
1250    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
1251        // Peek at the filenames inside the tarball to determine if it's source or binary.
1252        let tar = GzDecoder::new(bytes);
1253        let mut archive = tar::Archive::new(tar);
1254
1255        let mut has_source_rs = false;
1256        let mut has_wasm = false;
1257
1258        for file in archive.entries()? {
1259            let file = file?;
1260            let path_str = file.path()?.to_string_lossy().into_owned();
1261
1262            match path_str.as_ref() {
1263                "source.rs" => has_source_rs = true,
1264                "mod.wasm" => has_wasm = true,
1265                _ => {}
1266            }
1267        }
1268
1269        if has_source_rs {
1270            Ok(Playbook::Source(PlaybookSource::from_tarball(bytes)?))
1271        } else if has_wasm {
1272            Ok(Playbook::Binary(PlaybookBinary::from_tarball(bytes)?))
1273        } else {
1274            Err(io::Error::new(
1275                io::ErrorKind::InvalidData,
1276                "Unknown module format in tarball: missing 'source.rs' or 'mod.wasm'",
1277            ))
1278        }
1279    }
1280
1281    #[tracing::instrument(skip(path), fields(path = %path.display()))]
1282    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
1283        tracing::debug!("Loading Playbook from directory");
1284        if fs::try_exists(path.join("source.rs"))
1285            .await
1286            .unwrap_or(false)
1287        {
1288            Ok(Playbook::Source(PlaybookSource::from_dir(path).await?))
1289        } else if fs::try_exists(path.join("mod.wasm")).await.unwrap_or(false) {
1290            Ok(Playbook::Binary(PlaybookBinary::from_dir(path).await?))
1291        } else {
1292            Err(io::Error::new(
1293                io::ErrorKind::InvalidData,
1294                "Unknown module format in directory: missing 'source.rs' or 'mod.wasm'",
1295            ))
1296        }
1297    }
1298}
1299
1300impl Artifact for Artifacts {
1301    #[tracing::instrument(skip(self, path), fields(path = %path.display()))]
1302    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
1303        tracing::debug!("Writing Artifacts to directory");
1304        match self {
1305            Artifacts::CapabilityBinary(c) => c.write_to_directory(path).await,
1306            Artifacts::CapabilitySource(c) => c.write_to_directory(path).await,
1307            Artifacts::Interface(i) => i.write_to_directory(path).await,
1308            Artifacts::Playbook(m) => m.write_to_directory(path).await,
1309        }
1310    }
1311
1312    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
1313        match self {
1314            Artifacts::CapabilityBinary(c) => c.to_tarball(),
1315            Artifacts::CapabilitySource(c) => c.to_tarball(),
1316            Artifacts::Interface(i) => i.to_tarball(),
1317            Artifacts::Playbook(m) => m.to_tarball(),
1318        }
1319    }
1320
1321    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
1322        // Peek at the filenames inside the tarball to determine what artifact this is.
1323        let tar = GzDecoder::new(bytes);
1324        let mut archive = tar::Archive::new(tar);
1325
1326        let mut has_source_rs = false;
1327        let mut has_wasm = false;
1328        let mut has_cap_toml = false;
1329        let mut has_lib = false;
1330
1331        for file in archive.entries()? {
1332            let file = file?;
1333            let path_str = file.path()?.to_string_lossy().into_owned();
1334
1335            match path_str.as_ref() {
1336                "source.rs" => has_source_rs = true,
1337                "mod.wasm" => has_wasm = true,
1338                "Capability.toml" => has_cap_toml = true,
1339                "lib.dll" | "lib.dylib" | "lib.so" => has_lib = true,
1340                _ => {}
1341            }
1342        }
1343
1344        if has_source_rs || has_wasm {
1345            // Let the Playbook implementation handle the Source vs Binary split
1346            Ok(Artifacts::Playbook(Playbook::from_tarball(bytes)?))
1347        } else if has_cap_toml {
1348            if has_lib {
1349                // This shouldn't happen based on the split, but for backward compatibility or weird cases
1350                Ok(Artifacts::CapabilitySource(CapabilitySource::from_tarball(
1351                    bytes,
1352                )?))
1353            } else {
1354                Ok(Artifacts::Interface(Interface::from_tarball(bytes)?))
1355            }
1356        } else if has_lib {
1357            Ok(Artifacts::CapabilityBinary(CapabilityBinary::from_tarball(
1358                bytes,
1359            )?))
1360        } else {
1361            Err(io::Error::new(
1362                io::ErrorKind::InvalidData,
1363                "Unknown artifact format in tarball",
1364            ))
1365        }
1366    }
1367
1368    #[tracing::instrument(skip(path), fields(path = %path.display()))]
1369    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
1370        tracing::debug!("Loading Artifacts from directory");
1371        let has_source_rs = fs::try_exists(path.join("source.rs"))
1372            .await
1373            .unwrap_or(false);
1374        let has_wasm = fs::try_exists(path.join("mod.wasm")).await.unwrap_or(false);
1375
1376        if has_source_rs || has_wasm {
1377            // Let the Playbook implementation handle the Source vs Binary split
1378            Ok(Artifacts::Playbook(Playbook::from_dir(path).await?))
1379        } else if fs::try_exists(path.join("Capability.toml"))
1380            .await
1381            .unwrap_or(false)
1382        {
1383            let has_dll = fs::try_exists(path.join("lib.dll")).await.unwrap_or(false);
1384            let has_dylib = fs::try_exists(path.join("lib.dylib"))
1385                .await
1386                .unwrap_or(false);
1387            let has_so = fs::try_exists(path.join("lib.so")).await.unwrap_or(false);
1388
1389            if has_dll || has_dylib || has_so {
1390                // This shouldn't happen based on the split, but for backward compatibility
1391                Ok(Artifacts::CapabilitySource(
1392                    CapabilitySource::from_dir(path).await?,
1393                ))
1394            } else {
1395                Ok(Artifacts::Interface(Interface::from_dir(path).await?))
1396            }
1397        } else if fs::try_exists(path.join("lib.dll")).await.unwrap_or(false)
1398            || fs::try_exists(path.join("lib.dylib"))
1399                .await
1400                .unwrap_or(false)
1401            || fs::try_exists(path.join("lib.so")).await.unwrap_or(false)
1402        {
1403            Ok(Artifacts::CapabilityBinary(
1404                CapabilityBinary::from_dir(path).await?,
1405            ))
1406        } else {
1407            Err(io::Error::new(
1408                io::ErrorKind::InvalidData,
1409                "Unknown artifact format in directory",
1410            ))
1411        }
1412    }
1413}