Skip to main content

walrus_daemon/daemon/
builder.rs

1//! Daemon construction and lifecycle methods.
2//!
3//! This module provides the [`Daemon`] builder and reload logic as private
4//! `impl Daemon` methods. [`Daemon::build`] constructs a fully-configured
5//! daemon from a [`DaemonConfig`]. [`Daemon::reload`] rebuilds the runtime
6//! in-place from disk without restarting transports.
7
8use crate::{
9    Daemon, DaemonConfig,
10    daemon::event::{DaemonEvent, DaemonEventSender},
11    ext::hub::DownloadRegistry,
12    hook::{
13        self, DaemonHook,
14        system::{memory::Memory, task::TaskSet},
15    },
16    service::ServiceManager,
17};
18use anyhow::Result;
19use model::ProviderRegistry;
20use std::{path::Path, sync::Arc};
21use tokio::sync::{Mutex, RwLock};
22use wcore::{AgentConfig, Runtime, ToolRequest};
23
24const SYSTEM_AGENT: &str = include_str!("../../prompts/walrus.md");
25const SKILL_MASTER_AGENT: &str = include_str!("../../prompts/skill-master.md");
26
27impl Daemon {
28    /// Build a fully-configured [`Daemon`] from the given config, config
29    /// directory, and event sender. Returns the daemon and an optional
30    /// ServiceManager for lifecycle management of child services.
31    pub(crate) async fn build(
32        config: &DaemonConfig,
33        config_dir: &Path,
34        event_tx: DaemonEventSender,
35    ) -> Result<(Self, Option<ServiceManager>)> {
36        let (runtime, service_manager) = Self::build_runtime(config, config_dir, &event_tx).await?;
37        Ok((
38            Self {
39                runtime: Arc::new(RwLock::new(Arc::new(runtime))),
40                config_dir: config_dir.to_path_buf(),
41                event_tx,
42            },
43            service_manager,
44        ))
45    }
46
47    /// Rebuild the runtime from disk and swap it in atomically.
48    ///
49    /// In-flight requests that already hold a reference to the old runtime
50    /// complete normally. New requests after the swap see the new runtime.
51    /// Note: reload does not restart managed services — that requires a
52    /// full daemon restart. Services field is cleared to avoid re-spawning.
53    pub async fn reload(&self) -> Result<()> {
54        let mut config = DaemonConfig::load(&self.config_dir.join("walrus.toml"))?;
55        config.services.clear();
56        let (new_runtime, _) =
57            Self::build_runtime(&config, &self.config_dir, &self.event_tx).await?;
58        *self.runtime.write().await = Arc::new(new_runtime);
59        tracing::info!("daemon reloaded");
60        Ok(())
61    }
62
63    /// Construct a fresh [`Runtime`] from config. Used by both [`build`] and [`reload`].
64    /// Returns the runtime and an optional ServiceManager for child service lifecycle.
65    async fn build_runtime(
66        config: &DaemonConfig,
67        config_dir: &Path,
68        event_tx: &DaemonEventSender,
69    ) -> Result<(
70        Runtime<ProviderRegistry, DaemonHook>,
71        Option<ServiceManager>,
72    )> {
73        let manager = Self::build_providers(config)?;
74        let (hook, service_manager) = Self::build_hook(config, config_dir, event_tx).await?;
75        let tool_tx = Self::build_tool_sender(event_tx);
76        let mut runtime = Runtime::new(manager, hook, Some(tool_tx)).await;
77        Self::load_agents(&mut runtime, config_dir, config)?;
78        Ok((runtime, service_manager))
79    }
80
81    /// Construct the provider registry from config.
82    ///
83    /// Builds remote providers from config and sets the active model.
84    fn build_providers(config: &DaemonConfig) -> Result<ProviderRegistry> {
85        let active_model = config
86            .system
87            .walrus
88            .model
89            .clone()
90            .ok_or_else(|| anyhow::anyhow!("system.walrus.model is required in walrus.toml"))?;
91        let registry = ProviderRegistry::from_providers(active_model.clone(), &config.provider)?;
92
93        tracing::info!(
94            "provider registry initialized — active model: {}",
95            registry.active_model_name().unwrap_or_default()
96        );
97        Ok(registry)
98    }
99
100    /// Build the daemon hook with all backends (skills, MCP, tasks, downloads, memory).
101    /// Returns the hook and an optional ServiceManager for child service lifecycle.
102    async fn build_hook(
103        config: &DaemonConfig,
104        config_dir: &Path,
105        event_tx: &DaemonEventSender,
106    ) -> Result<(DaemonHook, Option<ServiceManager>)> {
107        let downloads = Arc::new(Mutex::new(DownloadRegistry::new()));
108
109        let skills_dir = config_dir.join(wcore::paths::SKILLS_DIR);
110        let skills = hook::skill::SkillHandler::load(skills_dir).unwrap_or_else(|e| {
111            tracing::warn!("failed to load skills: {e}");
112            hook::skill::SkillHandler::default()
113        });
114
115        let mcp_servers = config.mcps.values().cloned().collect::<Vec<_>>();
116        let mcp_handler = hook::mcp::McpHandler::load(&mcp_servers).await;
117
118        let tasks = Arc::new(Mutex::new(TaskSet::new()));
119
120        let sandboxed = detect_sandbox();
121        if sandboxed {
122            tracing::info!("sandbox mode active — OS tools bypass permission check");
123        }
124
125        // Spawn and handshake managed services.
126        let (registry, service_manager) = if config.services.is_empty() {
127            (None, None)
128        } else {
129            let daemon_socket = wcore::paths::SOCKET_PATH.to_path_buf();
130            let mut sm = ServiceManager::new(&config.services, config_dir, daemon_socket);
131            sm.spawn_all().await?;
132            let registry = sm.handshake_all().await;
133            (Some(Arc::new(registry)), Some(sm))
134        };
135
136        let memory = Some(Memory::open(
137            config_dir.join("memory"),
138            config.system.memory.clone(),
139            Box::new(crate::hook::system::memory::storage::FsStorage),
140        ));
141
142        Ok((
143            DaemonHook::new(
144                skills,
145                mcp_handler,
146                tasks,
147                downloads,
148                config.permissions.clone(),
149                sandboxed,
150                memory,
151                registry,
152                event_tx.clone(),
153            ),
154            service_manager,
155        ))
156    }
157
158    /// Build a [`ToolSender`] that forwards [`ToolRequest`]s into the daemon
159    /// event loop as [`DaemonEvent::ToolCall`] variants.
160    ///
161    /// Spawns a lightweight bridge task relaying from the tool channel into
162    /// the main daemon event channel.
163    fn build_tool_sender(event_tx: &DaemonEventSender) -> wcore::ToolSender {
164        let (tool_tx, mut tool_rx) = tokio::sync::mpsc::unbounded_channel::<ToolRequest>();
165        let event_tx = event_tx.clone();
166        tokio::spawn(async move {
167            while let Some(req) = tool_rx.recv().await {
168                if event_tx.send(DaemonEvent::ToolCall(req)).is_err() {
169                    break;
170                }
171            }
172        });
173        tool_tx
174    }
175
176    /// Load agents and add them to the runtime.
177    ///
178    /// The built-in walrus agent is always registered first. Sub-agents are
179    /// loaded by iterating TOML `[agents.*]` entries and matching each to a
180    /// `.md` prompt file from the agents directory.
181    fn load_agents(
182        runtime: &mut Runtime<ProviderRegistry, DaemonHook>,
183        config_dir: &Path,
184        config: &DaemonConfig,
185    ) -> Result<()> {
186        // Load prompt files from disk: (filename_stem, text).
187        let prompts = crate::config::load_agents_dir(&config_dir.join(wcore::paths::AGENTS_DIR))?;
188        let prompt_map: std::collections::BTreeMap<String, String> = prompts.into_iter().collect();
189
190        // Built-in walrus agent. Read soul from memory (Walrus.md), fall back to compiled-in.
191        let mut walrus_config = config.system.walrus.clone();
192        walrus_config.name = wcore::paths::DEFAULT_AGENT.to_owned();
193        walrus_config.system_prompt = runtime
194            .hook
195            .memory
196            .as_ref()
197            .map(|m| m.build_soul())
198            .unwrap_or_else(|| SYSTEM_AGENT.to_owned());
199        runtime.add_agent(walrus_config);
200
201        // Built-in skill-master agent.
202        let mut skill_master = AgentConfig::new("skill-master");
203        skill_master.system_prompt = SKILL_MASTER_AGENT.to_owned();
204        skill_master.description = "Interactive skill recorder".to_owned();
205        skill_master.thinking = config.system.walrus.thinking;
206        runtime.add_agent(skill_master);
207
208        // Sub-agents from TOML — each must have a matching .md file.
209        for (name, agent_config) in &config.agents {
210            let Some(prompt) = prompt_map.get(name) else {
211                tracing::warn!("agent '{name}' in TOML has no matching .md file, skipping");
212                continue;
213            };
214            let mut agent = agent_config.clone();
215            agent.name = name.clone();
216            agent.system_prompt = prompt.clone();
217            tracing::info!("registered agent '{name}' (thinking={})", agent.thinking);
218            runtime.add_agent(agent);
219        }
220
221        // Also register agents that have .md files but no TOML entry (defaults).
222        let default_think = config.system.walrus.thinking;
223        for (stem, prompt) in &prompt_map {
224            if config.agents.contains_key(stem) {
225                continue;
226            }
227            let mut agent = AgentConfig::new(stem.as_str());
228            agent.system_prompt = prompt.clone();
229            agent.thinking = default_think;
230            tracing::info!("registered agent '{stem}' (defaults, thinking={default_think})");
231            runtime.add_agent(agent);
232        }
233
234        // Populate per-agent scope maps for dispatch enforcement.
235        for agent_config in runtime.agents() {
236            runtime
237                .hook
238                .register_scope(agent_config.name.clone(), &agent_config);
239        }
240
241        Ok(())
242    }
243}
244
245/// Detect sandbox mode by checking if the current process is running as
246/// a user named `walrus`.
247fn detect_sandbox() -> bool {
248    std::env::var("USER")
249        .or_else(|_| std::env::var("LOGNAME"))
250        .is_ok_and(|u| u == "walrus")
251}