walrus_daemon/daemon/
builder.rs1use 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 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 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 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 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 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 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 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<ProviderRegistry, 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.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 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 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 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 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
245fn detect_sandbox() -> bool {
248 std::env::var("USER")
249 .or_else(|_| std::env::var("LOGNAME"))
250 .is_ok_and(|u| u == "walrus")
251}