ironclaw 0.22.0

Secure personal AI assistant that protects your data and expands its capabilities on the fly
Documentation
//! Embedded registry catalog compiled into the binary at build time.
//!
//! When IronClaw is distributed as a pre-built binary without a source tree,
//! the `registry/` directory is unavailable. This module provides the same
//! manifest data via `include_str!` from a JSON blob generated by `build.rs`.

use std::collections::HashMap;
use std::sync::OnceLock;

use crate::registry::manifest::{BundleDefinition, BundlesFile, ExtensionManifest};

/// Raw JSON generated by build.rs from `registry/{tools,channels}/*.json` and `_bundles.json`.
const EMBEDDED_CATALOG: &str = include_str!(concat!(env!("OUT_DIR"), "/embedded_catalog.json"));

/// Intermediate deserialization shape matching the build.rs output.
#[derive(serde::Deserialize)]
struct EmbeddedCatalogRaw {
    #[serde(default)]
    tools: Vec<ExtensionManifest>,
    #[serde(default)]
    channels: Vec<ExtensionManifest>,
    #[serde(default)]
    mcp_servers: Vec<ExtensionManifest>,
    #[serde(default)]
    bundles: BundlesFile,
}

/// Parsed catalog cached across calls.
struct ParsedCatalog {
    manifests: HashMap<String, ExtensionManifest>,
    bundles: HashMap<String, BundleDefinition>,
}

fn parsed_catalog() -> &'static ParsedCatalog {
    static CACHE: OnceLock<ParsedCatalog> = OnceLock::new();
    CACHE.get_or_init(|| {
        let raw: EmbeddedCatalogRaw = match serde_json::from_str(EMBEDDED_CATALOG) {
            Ok(v) => v,
            Err(e) => {
                tracing::warn!("Failed to parse embedded catalog: {}", e);
                return ParsedCatalog {
                    manifests: HashMap::new(),
                    bundles: HashMap::new(),
                };
            }
        };

        let mut manifests = HashMap::new();
        for m in raw.tools {
            let key = format!("tools/{}", m.name);
            manifests.insert(key, m);
        }
        for m in raw.channels {
            let key = format!("channels/{}", m.name);
            manifests.insert(key, m);
        }
        for m in raw.mcp_servers {
            let key = format!("mcp-servers/{}", m.name);
            manifests.insert(key, m);
        }

        ParsedCatalog {
            manifests,
            bundles: raw.bundles.bundles,
        }
    })
}

/// Load all embedded extension manifests, keyed by `"tools/<name>"` or `"channels/<name>"`.
pub fn load_embedded() -> HashMap<String, ExtensionManifest> {
    parsed_catalog().manifests.clone()
}

/// Load embedded bundle definitions.
pub fn load_embedded_bundles() -> HashMap<String, BundleDefinition> {
    parsed_catalog().bundles.clone()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_load_embedded_parses() {
        let manifests = load_embedded();
        // Should have at least the manifests from registry/ if built from the repo
        // (empty is also valid for minimal builds without registry/)
        assert!(
            manifests.is_empty() || manifests.contains_key("tools/github"),
            "Expected either empty catalog or github tool, got {} entries",
            manifests.len()
        );
    }

    #[test]
    fn test_load_embedded_bundles_parses() {
        let bundles = load_embedded_bundles();
        assert!(
            bundles.is_empty() || bundles.contains_key("default"),
            "Expected either empty bundles or 'default' bundle"
        );
    }
}