pub mod mcp;
#[cfg(test)]
mod mcp_tests;
mod static_engine;
pub mod wasm;
pub(crate) use mcp::McpHostEngine;
pub(crate) use static_engine::StaticEngine;
pub(crate) use wasm::WasmEngine;
use std::collections::HashMap;
use async_trait::async_trait;
use crate::context::CapsuleContext;
use crate::error::CapsuleResult;
use crate::manifest::{CapsuleManifest, EnvDef};
#[async_trait]
pub(crate) trait ExecutionEngine: Send + Sync {
async fn load(&mut self, ctx: &CapsuleContext) -> CapsuleResult<()>;
async fn unload(&mut self) -> CapsuleResult<()>;
fn take_inbound_rx(
&mut self,
) -> Option<tokio::sync::mpsc::Receiver<astrid_core::InboundMessage>> {
None
}
async fn wait_ready(&self, _timeout: std::time::Duration) -> crate::capsule::ReadyStatus {
crate::capsule::ReadyStatus::Ready
}
async fn invoke_interceptor(
&self,
_action: &str,
_payload: &[u8],
_caller: Option<&astrid_events::ipc::IpcMessage>,
) -> CapsuleResult<crate::capsule::InterceptResult> {
Err(crate::error::CapsuleError::NotSupported(
"interceptors not supported by this engine".into(),
))
}
fn check_health(&self) -> crate::capsule::CapsuleState {
crate::capsule::CapsuleState::Ready
}
}
pub(crate) fn build_onboarding_field(
key: &str,
def: &EnvDef,
) -> astrid_events::ipc::OnboardingField {
use astrid_events::ipc::OnboardingFieldType;
let field_type = if def.env_type == "secret" {
if !def.enum_values.is_empty() {
tracing::warn!(
key = %key,
"Secret field has enum_values - ignoring enum and using masked input"
);
}
OnboardingFieldType::Secret
} else if def.env_type == "array" {
OnboardingFieldType::Array
} else if def.enum_values.len() > 1 {
OnboardingFieldType::Enum(def.enum_values.clone())
} else {
OnboardingFieldType::Text
};
let mut default = def.default.as_ref().and_then(|d| match d {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Null => None,
other => Some(other.to_string()),
});
if def.enum_values.len() == 1 && default.is_none() {
default = Some(def.enum_values[0].clone());
}
let prompt = def
.request
.clone()
.unwrap_or_else(|| format!("Please enter value for {key}"));
astrid_events::ipc::OnboardingField {
key: key.to_string(),
prompt,
description: def.description.clone(),
field_type,
default,
placeholder: def.placeholder.clone(),
}
}
pub(crate) async fn resolve_env(
manifest: &CapsuleManifest,
ctx: &CapsuleContext,
reserved_keys: &[String],
source: &str,
) -> CapsuleResult<HashMap<String, String>> {
let mut resolved = HashMap::new();
let mut onboarding_fields = Vec::new();
for (key, def) in &manifest.env {
if reserved_keys.iter().any(|k| k == key) {
tracing::warn!(
capsule = %manifest.package.name,
key = %key,
"Capsule manifest [env] declares reserved key - ignoring"
);
continue;
}
if let Ok(Some(val_bytes)) = ctx.kv.get(key).await {
match String::from_utf8(val_bytes) {
Ok(val) => {
resolved.insert(key.clone(), val);
},
Err(e) => {
tracing::warn!(
capsule = %manifest.package.name,
key = %key,
error = %e,
"Value from KV store is not valid UTF-8; requiring onboarding"
);
onboarding_fields.push(build_onboarding_field(key, def));
},
}
} else if def.enum_values.len() > 1 {
onboarding_fields.push(build_onboarding_field(key, def));
} else if let Some(default_val) = &def.default {
let val = match default_val {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => String::new(),
other => other.to_string(),
};
resolved.insert(key.clone(), val);
} else {
onboarding_fields.push(build_onboarding_field(key, def));
}
}
if !onboarding_fields.is_empty() {
let missing_display: String = onboarding_fields
.iter()
.map(|f| f.key.as_str())
.collect::<Vec<_>>()
.join(", ");
tracing::info!(
capsule = %manifest.package.name,
missing = %missing_display,
"Capsule has unconfigured env fields — loading with empty defaults"
);
for field in &onboarding_fields {
resolved.entry(field.key.clone()).or_default();
}
let msg = astrid_events::ipc::IpcMessage::new(
"astrid.v1.onboarding.required",
astrid_events::ipc::IpcPayload::OnboardingRequired {
capsule_id: manifest.package.name.clone(),
fields: onboarding_fields,
},
uuid::Uuid::nil(),
);
let _ = ctx.event_bus.publish(astrid_events::AstridEvent::Ipc {
metadata: astrid_events::EventMetadata::new(source),
message: msg,
});
}
Ok(resolved)
}