use crate::{
Daemon, DaemonConfig,
daemon::event::{DaemonEvent, DaemonEventSender},
ext::hub::DownloadRegistry,
hook::{
self, DaemonHook,
system::{memory::Memory, task::TaskSet},
},
service::ServiceManager,
};
use anyhow::Result;
use model::ProviderRegistry;
use std::{path::Path, sync::Arc};
use tokio::sync::{Mutex, RwLock};
use wcore::{AgentConfig, Runtime, ToolRequest};
const SYSTEM_AGENT: &str = include_str!("../../prompts/walrus.md");
const SKILL_MASTER_AGENT: &str = include_str!("../../prompts/skill-master.md");
impl Daemon {
pub(crate) async fn build(
config: &DaemonConfig,
config_dir: &Path,
event_tx: DaemonEventSender,
) -> Result<(Self, Option<ServiceManager>)> {
let (runtime, service_manager) = Self::build_runtime(config, config_dir, &event_tx).await?;
Ok((
Self {
runtime: Arc::new(RwLock::new(Arc::new(runtime))),
config_dir: config_dir.to_path_buf(),
event_tx,
},
service_manager,
))
}
pub async fn reload(&self) -> Result<()> {
let mut config = DaemonConfig::load(&self.config_dir.join("walrus.toml"))?;
config.services.clear();
let (new_runtime, _) =
Self::build_runtime(&config, &self.config_dir, &self.event_tx).await?;
*self.runtime.write().await = Arc::new(new_runtime);
tracing::info!("daemon reloaded");
Ok(())
}
async fn build_runtime(
config: &DaemonConfig,
config_dir: &Path,
event_tx: &DaemonEventSender,
) -> Result<(
Runtime<ProviderRegistry, DaemonHook>,
Option<ServiceManager>,
)> {
let manager = Self::build_providers(config)?;
let (hook, service_manager) = Self::build_hook(config, config_dir, event_tx).await?;
let tool_tx = Self::build_tool_sender(event_tx);
let mut runtime = Runtime::new(manager, hook, Some(tool_tx)).await;
Self::load_agents(&mut runtime, config_dir, config)?;
Ok((runtime, service_manager))
}
fn build_providers(config: &DaemonConfig) -> Result<ProviderRegistry> {
let active_model = config
.system
.walrus
.model
.clone()
.ok_or_else(|| anyhow::anyhow!("system.walrus.model is required in walrus.toml"))?;
let registry = ProviderRegistry::from_providers(active_model.clone(), &config.provider)?;
tracing::info!(
"provider registry initialized — active model: {}",
registry.active_model_name().unwrap_or_default()
);
Ok(registry)
}
async fn build_hook(
config: &DaemonConfig,
config_dir: &Path,
event_tx: &DaemonEventSender,
) -> Result<(DaemonHook, Option<ServiceManager>)> {
let downloads = Arc::new(Mutex::new(DownloadRegistry::new()));
let skills_dir = config_dir.join(wcore::paths::SKILLS_DIR);
let skills = hook::skill::SkillHandler::load(skills_dir).unwrap_or_else(|e| {
tracing::warn!("failed to load skills: {e}");
hook::skill::SkillHandler::default()
});
let mcp_servers = config.mcps.values().cloned().collect::<Vec<_>>();
let mcp_handler = hook::mcp::McpHandler::load(&mcp_servers).await;
let tasks = Arc::new(Mutex::new(TaskSet::new()));
let sandboxed = detect_sandbox();
if sandboxed {
tracing::info!("sandbox mode active — OS tools bypass permission check");
}
let (registry, service_manager) = if config.services.is_empty() {
(None, None)
} else {
let daemon_socket = wcore::paths::SOCKET_PATH.to_path_buf();
let mut sm = ServiceManager::new(&config.services, config_dir, daemon_socket);
sm.spawn_all().await?;
let registry = sm.handshake_all().await;
(Some(Arc::new(registry)), Some(sm))
};
let memory = Some(Memory::open(
config_dir.join("memory"),
config.system.memory.clone(),
Box::new(crate::hook::system::memory::storage::FsStorage),
));
Ok((
DaemonHook::new(
skills,
mcp_handler,
tasks,
downloads,
config.permissions.clone(),
sandboxed,
memory,
registry,
event_tx.clone(),
),
service_manager,
))
}
fn build_tool_sender(event_tx: &DaemonEventSender) -> wcore::ToolSender {
let (tool_tx, mut tool_rx) = tokio::sync::mpsc::unbounded_channel::<ToolRequest>();
let event_tx = event_tx.clone();
tokio::spawn(async move {
while let Some(req) = tool_rx.recv().await {
if event_tx.send(DaemonEvent::ToolCall(req)).is_err() {
break;
}
}
});
tool_tx
}
fn load_agents(
runtime: &mut Runtime<ProviderRegistry, DaemonHook>,
config_dir: &Path,
config: &DaemonConfig,
) -> Result<()> {
let prompts = crate::config::load_agents_dir(&config_dir.join(wcore::paths::AGENTS_DIR))?;
let prompt_map: std::collections::BTreeMap<String, String> = prompts.into_iter().collect();
let mut walrus_config = config.system.walrus.clone();
walrus_config.name = wcore::paths::DEFAULT_AGENT.to_owned();
walrus_config.system_prompt = runtime
.hook
.memory
.as_ref()
.map(|m| m.build_soul())
.unwrap_or_else(|| SYSTEM_AGENT.to_owned());
runtime.add_agent(walrus_config);
let mut skill_master = AgentConfig::new("skill-master");
skill_master.system_prompt = SKILL_MASTER_AGENT.to_owned();
skill_master.description = "Interactive skill recorder".to_owned();
skill_master.thinking = config.system.walrus.thinking;
runtime.add_agent(skill_master);
for (name, agent_config) in &config.agents {
let Some(prompt) = prompt_map.get(name) else {
tracing::warn!("agent '{name}' in TOML has no matching .md file, skipping");
continue;
};
let mut agent = agent_config.clone();
agent.name = name.clone();
agent.system_prompt = prompt.clone();
tracing::info!("registered agent '{name}' (thinking={})", agent.thinking);
runtime.add_agent(agent);
}
let default_think = config.system.walrus.thinking;
for (stem, prompt) in &prompt_map {
if config.agents.contains_key(stem) {
continue;
}
let mut agent = AgentConfig::new(stem.as_str());
agent.system_prompt = prompt.clone();
agent.thinking = default_think;
tracing::info!("registered agent '{stem}' (defaults, thinking={default_think})");
runtime.add_agent(agent);
}
for agent_config in runtime.agents() {
runtime
.hook
.register_scope(agent_config.name.clone(), &agent_config);
}
Ok(())
}
}
fn detect_sandbox() -> bool {
std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.is_ok_and(|u| u == "walrus")
}