walrus_daemon/daemon/
builder.rs1use 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 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 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 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 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 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 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 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 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 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 fn load_agents(
182 runtime: &mut Runtime<ProviderManager, DaemonHook>,
183 config_dir: &Path,
184 config: &DaemonConfig,
185 ) -> Result<()> {
186 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 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 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 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 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
233fn detect_sandbox() -> bool {
236 std::env::var("USER")
237 .or_else(|_| std::env::var("LOGNAME"))
238 .is_ok_and(|u| u == "walrus")
239}