use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
use astrid_core::UplinkProfile;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapsuleManifest {
pub package: PackageDef,
#[serde(default, rename = "component")]
pub components: Vec<ComponentDef>,
#[serde(default)]
pub dependencies: DependenciesDef,
#[serde(default)]
pub capabilities: CapabilitiesDef,
#[serde(default)]
pub env: HashMap<String, EnvDef>,
#[serde(default, rename = "context_file")]
pub context_files: Vec<ContextFileDef>,
#[serde(default, rename = "command")]
pub commands: Vec<CommandDef>,
#[serde(default, rename = "mcp_server")]
pub mcp_servers: Vec<McpServerDef>,
#[serde(default, rename = "skill")]
pub skills: Vec<SkillDef>,
#[serde(default, rename = "uplink")]
pub uplinks: Vec<UplinkDef>,
#[serde(default, rename = "llm_provider")]
pub llm_providers: Vec<LlmProviderDef>,
#[serde(default, rename = "interceptor")]
pub interceptors: Vec<InterceptorDef>,
#[serde(default, rename = "cron")]
pub cron_jobs: Vec<CronDef>,
#[serde(default, rename = "tool")]
pub tools: Vec<ToolDef>,
#[serde(default, rename = "topic")]
pub topics: Vec<TopicDef>,
#[serde(skip)]
#[doc(hidden)]
pub effective_provides_cache: OnceLock<Vec<String>>,
}
impl CapsuleManifest {
#[must_use]
pub fn effective_provides(&self) -> &[String] {
self.effective_provides_cache.get_or_init(|| {
if !self.dependencies.provides.is_empty() {
return self.dependencies.provides.clone();
}
let mut caps = Vec::new();
for topic in &self.capabilities.ipc_publish {
caps.push(format!("topic:{topic}"));
}
for tool in &self.tools {
caps.push(format!("tool:{}", tool.name));
}
for provider in &self.llm_providers {
caps.push(format!("llm:{}", provider.id));
}
for uplink in &self.uplinks {
caps.push(format!("uplink:{}", uplink.name));
}
caps
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DependenciesDef {
#[serde(default)]
pub provides: Vec<String>,
#[serde(default)]
pub requires: Vec<String>,
}
impl DependenciesDef {
#[must_use]
pub fn is_empty(&self) -> bool {
self.provides.is_empty() && self.requires.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageDef {
pub name: String,
pub version: String,
pub description: Option<String>,
#[serde(default)]
pub authors: Vec<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
pub license: Option<String>,
#[serde(rename = "license-file")]
pub license_file: Option<PathBuf>,
pub readme: Option<PathBuf>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub categories: Vec<String>,
#[serde(rename = "astrid-version")]
pub astrid_version: Option<String>,
pub publish: Option<bool>,
pub include: Option<Vec<String>>,
pub exclude: Option<Vec<String>>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentDef {
#[serde(default)]
pub id: String,
#[serde(rename = "file", alias = "entrypoint")]
pub path: PathBuf,
pub hash: Option<String>,
#[serde(default)]
pub r#type: String,
#[serde(default)]
pub link: Vec<String>,
#[serde(default)]
pub capabilities: Option<CapabilitiesDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CapabilitiesDef {
#[serde(default)]
pub uplink: bool,
#[serde(default)]
pub net: Vec<String>,
#[serde(default)]
pub kv: Vec<String>,
#[serde(default)]
pub fs_read: Vec<String>,
#[serde(default)]
pub fs_write: Vec<String>,
#[serde(default)]
pub host_process: Vec<String>,
#[serde(default)]
pub net_bind: Vec<String>,
#[serde(default)]
pub ipc_publish: Vec<String>,
#[serde(default)]
pub ipc_subscribe: Vec<String>,
#[serde(default)]
pub identity: Vec<String>,
#[serde(default)]
pub allow_prompt_injection: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvDef {
#[serde(rename = "type")]
pub env_type: String,
pub request: Option<String>,
pub description: Option<String>,
pub default: Option<serde_json::Value>,
#[serde(default)]
pub enum_values: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextFileDef {
pub name: String,
pub file: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandDef {
pub name: String,
pub description: Option<String>,
pub file: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerDef {
pub id: String,
pub description: Option<String>,
#[serde(rename = "type")]
pub server_type: Option<String>,
pub command: Option<String>,
#[serde(default)]
pub args: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillDef {
pub name: String,
pub description: Option<String>,
pub file: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UplinkDef {
pub name: String,
pub platform: String,
pub profile: UplinkProfile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmProviderDef {
pub id: String,
pub description: Option<String>,
#[serde(default)]
pub capabilities: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterceptorDef {
pub event: String,
pub action: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TopicDirection {
Publish,
Subscribe,
}
impl fmt::Display for TopicDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Publish => f.write_str("publish"),
Self::Subscribe => f.write_str("subscribe"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopicDef {
pub name: String,
pub direction: TopicDirection,
pub description: Option<String>,
pub schema: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CronDef {
pub name: String,
pub schedule: String,
pub action: String,
}