mod gate;
pub mod provider;
mod registrar;
pub use gate::execute_tool;
pub use provider::build_provider_chain;
pub use registrar::ToolFilter;
use std::sync::Arc;
use tracing::{info, warn};
use crate::bus::MessageBus;
use crate::config::{Config, MemoryBackend};
use crate::cron::CronService;
use crate::hands::HandManifest;
use crate::hooks::HookEngine;
use crate::memory::factory::create_searcher_with_provider;
use crate::memory::longterm::LongTermMemory;
use crate::providers::LLMProvider;
use crate::runtime::{create_runtime, ContainerRuntime, NativeRuntime};
use crate::safety::taint::TaintEngine;
use crate::safety::SafetyLayer;
use crate::tools::mcp::client::McpClient;
use crate::tools::ToolRegistry;
use crate::utils::metrics::MetricsCollector;
pub struct ZeptoKernel {
pub config: Arc<Config>,
pub provider: Option<Arc<dyn LLMProvider>>,
pub tools: ToolRegistry,
pub safety: Option<SafetyLayer>,
pub metrics: Arc<MetricsCollector>,
pub hooks: Arc<HookEngine>,
pub mcp_clients: Vec<Arc<McpClient>>,
pub ltm: Option<Arc<tokio::sync::Mutex<LongTermMemory>>>,
pub taint: Option<Arc<std::sync::RwLock<TaintEngine>>>,
}
impl ZeptoKernel {
pub async fn boot(
config: Config,
bus: Arc<MessageBus>,
template: Option<&crate::config::templates::AgentTemplate>,
hand: Option<&HandManifest>,
) -> anyhow::Result<Self> {
let filter = ToolFilter::from_config(&config, template, hand);
let provider: Option<Arc<dyn LLMProvider>> =
if let Some((chain, names)) = provider::build_provider_chain(&config).await {
let chain_label = names.join(" -> ");
info!(
provider_chain = %chain_label,
"Assembled provider chain"
);
Some(chain)
} else {
None
};
let safety = if config.safety.enabled {
Some(SafetyLayer::new(config.safety.clone()))
} else {
None
};
let taint = if config.safety.enabled && config.safety.taint.enabled {
Some(Arc::new(std::sync::RwLock::new(TaintEngine::new(
config.safety.taint.clone(),
))))
} else {
None
};
let metrics = Arc::new(MetricsCollector::new());
let hooks = Arc::new(HookEngine::new(config.hooks.clone()));
let embedding_provider: Option<Arc<dyn LLMProvider>> =
if matches!(config.memory.backend, MemoryBackend::Embedding) {
provider::build_runtime_provider_chain(&config)
.await
.map(|(chain, _)| Arc::from(chain))
} else {
None
};
let memory_searcher = create_searcher_with_provider(&config.memory, embedding_provider);
let ltm: Option<Arc<tokio::sync::Mutex<LongTermMemory>>> =
if !matches!(config.memory.backend, MemoryBackend::Disabled) {
let ltm_path = Config::dir().join("memory").join("longterm.json");
match LongTermMemory::with_path_and_searcher(ltm_path, memory_searcher.clone()) {
Ok(ltm) => Some(Arc::new(tokio::sync::Mutex::new(ltm))),
Err(e) => {
warn!("Failed to load long-term memory: {}", e);
None
}
}
} else {
None
};
let runtime: Arc<dyn ContainerRuntime> = match create_runtime(&config.runtime).await {
Ok(r) => {
info!("Using {} runtime for shell commands", r.name());
r
}
Err(e) => {
if config.runtime.allow_fallback_to_native {
warn!(
"Failed to create configured runtime: {}. Falling back to native.",
e
);
Arc::new(NativeRuntime::new())
} else {
return Err(anyhow::anyhow!(
"Configured runtime '{:?}' unavailable: {}. \
Enable runtime.allow_fallback_to_native to opt in to native fallback.",
config.runtime.runtime_type,
e
));
}
}
};
let cron_store_path = Config::dir().join("cron").join("jobs.json");
let cron_service = Arc::new(CronService::with_jitter(
cron_store_path,
bus.clone(),
config.routines.jitter_ms,
));
cron_service.start(&config.routines.on_miss).await?;
let mut tools = ToolRegistry::new();
let deps = registrar::ToolDeps {
runtime,
bus,
cron_service,
memory_searcher,
shared_ltm: ltm.clone(),
template: template.cloned(),
};
let (mcp_clients, external_tool_names) =
registrar::register_all_tools(&mut tools, &config, &filter, &deps).await?;
if let Some(ref taint_lock) = taint {
if let Ok(mut engine) = taint_lock.write() {
engine.register_external_tools(external_tool_names);
}
}
info!("Kernel boot: {} tools registered", tools.len());
Ok(ZeptoKernel {
config: Arc::new(config),
provider,
tools,
safety,
metrics,
hooks,
mcp_clients,
ltm,
taint,
})
}
pub fn tool_definitions(&self) -> Vec<crate::providers::ToolDefinition> {
self.tools.definitions()
}
pub fn provider(&self) -> Option<Arc<dyn LLMProvider>> {
self.provider.as_ref().map(Arc::clone)
}
pub async fn shutdown(&self) {
for client in &self.mcp_clients {
let _ = client.shutdown().await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::EchoTool;
use crate::utils::metrics::MetricsCollector;
fn test_kernel() -> ZeptoKernel {
let config = Config::default();
let mut tools = ToolRegistry::new();
tools.register(Box::new(EchoTool));
ZeptoKernel {
config: Arc::new(config.clone()),
provider: Some(Arc::new(crate::providers::ClaudeProvider::new("test-key"))),
tools,
safety: if config.safety.enabled {
Some(SafetyLayer::new(config.safety.clone()))
} else {
None
},
metrics: Arc::new(MetricsCollector::new()),
hooks: Arc::new(HookEngine::new(config.hooks.clone())),
mcp_clients: vec![],
ltm: None,
taint: if config.safety.enabled && config.safety.taint.enabled {
Some(Arc::new(std::sync::RwLock::new(TaintEngine::new(
config.safety.taint.clone(),
))))
} else {
None
},
}
}
#[test]
fn test_kernel_struct_holds_all_subsystems() {
let kernel = test_kernel();
assert!(!kernel.tools.is_empty());
assert!(kernel.tools.has("echo"));
assert!(kernel.mcp_clients.is_empty());
assert!(kernel.ltm.is_none());
}
#[test]
fn test_kernel_tool_definitions_delegates_to_registry() {
let kernel = test_kernel();
let defs = kernel.tool_definitions();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].name, "echo");
}
#[test]
fn test_kernel_provider_returns_arc_clone() {
let kernel = test_kernel();
let p1 = kernel.provider().expect("test kernel has provider");
let p2 = kernel.provider().expect("test kernel has provider");
assert_eq!(p1.name(), p2.name());
}
#[test]
fn test_kernel_safety_none_when_disabled() {
let mut config = Config::default();
config.safety.enabled = false;
let kernel = ZeptoKernel {
config: Arc::new(config.clone()),
provider: Some(Arc::new(crate::providers::ClaudeProvider::new("test-key"))),
tools: ToolRegistry::new(),
safety: None,
metrics: Arc::new(MetricsCollector::new()),
hooks: Arc::new(HookEngine::new(config.hooks.clone())),
mcp_clients: vec![],
ltm: None,
taint: None,
};
assert!(kernel.safety.is_none());
}
#[test]
fn test_kernel_safety_some_when_enabled() {
let mut config = Config::default();
config.safety.enabled = true;
let kernel = ZeptoKernel {
config: Arc::new(config.clone()),
provider: Some(Arc::new(crate::providers::ClaudeProvider::new("test-key"))),
tools: ToolRegistry::new(),
safety: Some(SafetyLayer::new(config.safety.clone())),
metrics: Arc::new(MetricsCollector::new()),
hooks: Arc::new(HookEngine::new(config.hooks.clone())),
mcp_clients: vec![],
ltm: None,
taint: Some(Arc::new(std::sync::RwLock::new(TaintEngine::new(
config.safety.taint.clone(),
)))),
};
assert!(kernel.safety.is_some());
}
#[tokio::test]
async fn test_kernel_shutdown_empty_clients() {
let kernel = test_kernel();
kernel.shutdown().await;
}
}