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::{self, DaemonHook, task::TaskRegistry},
13    service::ServiceManager,
14};
15use anyhow::Result;
16use compact_str::CompactString;
17use model::ProviderManager;
18use std::{path::Path, sync::Arc};
19use tokio::sync::{Mutex, RwLock};
20use wcore::{AgentConfig, Runtime, ToolRequest};
21
22const SYSTEM_AGENT: &str = include_str!("../../prompts/walrus.md");
23
24impl Daemon {
25    /// Build a fully-configured [`Daemon`] from the given config, config
26    /// directory, and event sender. Returns the daemon and an optional
27    /// ServiceManager for lifecycle management of child services.
28    pub(crate) async fn build(
29        config: &DaemonConfig,
30        config_dir: &Path,
31        event_tx: DaemonEventSender,
32    ) -> Result<(Self, Option<ServiceManager>)> {
33        let (runtime, service_manager) = Self::build_runtime(config, config_dir, &event_tx).await?;
34        Ok((
35            Self {
36                runtime: Arc::new(RwLock::new(Arc::new(runtime))),
37                config_dir: config_dir.to_path_buf(),
38                event_tx,
39                agents_config: config.agents.clone(),
40            },
41            service_manager,
42        ))
43    }
44
45    /// Rebuild the runtime from disk and swap it in atomically.
46    ///
47    /// In-flight requests that already hold a reference to the old runtime
48    /// complete normally. New requests after the swap see the new runtime.
49    /// Note: reload does not restart managed services — that requires a
50    /// full daemon restart. Services field is cleared to avoid re-spawning.
51    pub async fn reload(&self) -> Result<()> {
52        let mut config = DaemonConfig::load(&self.config_dir.join("walrus.toml"))?;
53        config.services.clear();
54        let (new_runtime, _) =
55            Self::build_runtime(&config, &self.config_dir, &self.event_tx).await?;
56        *self.runtime.write().await = Arc::new(new_runtime);
57        tracing::info!("daemon reloaded");
58        Ok(())
59    }
60
61    /// Construct a fresh [`Runtime`] from config. Used by both [`build`] and [`reload`].
62    /// Returns the runtime and an optional ServiceManager for child service lifecycle.
63    async fn build_runtime(
64        config: &DaemonConfig,
65        config_dir: &Path,
66        event_tx: &DaemonEventSender,
67    ) -> Result<(Runtime<ProviderManager, DaemonHook>, Option<ServiceManager>)> {
68        let manager = Self::build_providers(config).await?;
69        let (hook, service_manager) =
70            Self::build_hook(config, config_dir, event_tx, &manager).await?;
71        let tool_tx = Self::build_tool_sender(event_tx);
72        let mut runtime = Runtime::new(manager, hook, Some(tool_tx)).await;
73        // Set compact hook on runtime for auto-compaction.
74        if let Some(ref registry) = runtime.hook.registry {
75            runtime.set_compact_hook(Arc::clone(registry) as Arc<dyn wcore::CompactHook>);
76        }
77        Self::load_agents(&mut runtime, config_dir, config)?;
78        Ok((runtime, service_manager))
79    }
80
81    /// Construct the provider manager from config.
82    ///
83    /// Builds remote providers from config and sets the active model.
84    async fn build_providers(config: &DaemonConfig) -> Result<ProviderManager> {
85        let active_model = config
86            .walrus
87            .model
88            .clone()
89            .ok_or_else(|| anyhow::anyhow!("walrus.model is required in walrus.toml"))?;
90        let manager = ProviderManager::from_providers(active_model, &config.provider).await?;
91
92        tracing::info!(
93            "provider manager initialized — active model: {}",
94            manager.active_model_name().unwrap_or_default()
95        );
96        Ok(manager)
97    }
98
99    /// Build the daemon hook with all backends (skills, MCP, tasks, downloads).
100    /// Memory is handled by an external extension service.
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        manager: &ProviderManager,
107    ) -> Result<(DaemonHook, Option<ServiceManager>)> {
108        let downloads = Arc::new(Mutex::new(DownloadRegistry::new()));
109
110        let skills_dir = config_dir.join(wcore::paths::SKILLS_DIR);
111        let skills = hook::skill::SkillHandler::load(skills_dir).unwrap_or_else(|e| {
112            tracing::warn!("failed to load skills: {e}");
113            hook::skill::SkillHandler::default()
114        });
115
116        let mcp_servers = config.mcps.values().cloned().collect::<Vec<_>>();
117        let mcp_handler = hook::mcp::McpHandler::load(&mcp_servers).await;
118
119        let tasks = Arc::new(Mutex::new(TaskRegistry::new(
120            config.tasks.max_concurrent,
121            config.tasks.viewable_window,
122            std::time::Duration::from_secs(config.tasks.task_timeout),
123            event_tx.clone(),
124        )));
125
126        let sandboxed = detect_sandbox();
127        if sandboxed {
128            tracing::info!("sandbox mode active — OS tools bypass permission check");
129        }
130
131        // Spawn and handshake managed services.
132        let (registry, service_manager) = if config.services.is_empty() {
133            (None, None)
134        } else {
135            let daemon_socket = wcore::paths::SOCKET_PATH.to_path_buf();
136            let mut sm = ServiceManager::new(&config.services, config_dir, daemon_socket);
137            sm.spawn_all().await?;
138            let mut registry = sm.handshake_all().await;
139            // Set model for Infer fulfillment before wrapping in Arc.
140            registry.set_model(manager.clone());
141            (Some(Arc::new(registry)), Some(sm))
142        };
143
144        Ok((
145            DaemonHook::new(
146                skills,
147                mcp_handler,
148                tasks,
149                downloads,
150                config.permissions.clone(),
151                sandboxed,
152                registry,
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<ProviderManager, 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.
191        let mut walrus_config = config.walrus.clone();
192        walrus_config.name = CompactString::from("walrus");
193        walrus_config.system_prompt = SYSTEM_AGENT.to_owned();
194        runtime.add_agent(walrus_config);
195
196        // Sub-agents from TOML — each must have a matching .md file.
197        for (name, agent_config) in &config.agents {
198            let Some(prompt) = prompt_map.get(name) else {
199                tracing::warn!("agent '{name}' in TOML has no matching .md file, skipping");
200                continue;
201            };
202            let mut agent = agent_config.clone();
203            agent.name = CompactString::from(name.as_str());
204            agent.system_prompt = prompt.clone();
205            tracing::info!("registered agent '{name}' (thinking={})", agent.thinking);
206            runtime.add_agent(agent);
207        }
208
209        // Also register agents that have .md files but no TOML entry (defaults).
210        let default_think = config.walrus.thinking;
211        for (stem, prompt) in &prompt_map {
212            if config.agents.contains_key(stem) {
213                continue;
214            }
215            let mut agent = AgentConfig::new(stem.as_str());
216            agent.system_prompt = prompt.clone();
217            agent.thinking = default_think;
218            tracing::info!("registered agent '{stem}' (defaults, thinking={default_think})");
219            runtime.add_agent(agent);
220        }
221
222        // Populate per-agent scope maps for dispatch enforcement.
223        for agent_config in runtime.agents() {
224            runtime
225                .hook
226                .register_scope(agent_config.name.clone(), &agent_config);
227        }
228
229        Ok(())
230    }
231}
232
233/// Detect sandbox mode by checking if the current process is running as
234/// a user named `walrus`.
235fn detect_sandbox() -> bool {
236    std::env::var("USER")
237        .or_else(|_| std::env::var("LOGNAME"))
238        .is_ok_and(|u| u == "walrus")
239}