1use anyhow::Result;
6use std::net::SocketAddr;
7use std::sync::Arc;
8use tokio::sync::mpsc;
9
10use crate::events::ActionExecutionError;
11use crate::llm::OllamaClient;
12use crate::protocol::metadata::DevelopmentState;
13use crate::state::app_state::AppState;
14use crate::state::ServerId;
15
16pub(crate) fn min_stability_refusal(
24 kind: &str,
25 protocol: &str,
26 actual: DevelopmentState,
27 min: DevelopmentState,
28) -> String {
29 format!(
30 "Cannot start {protocol} {kind}: its stability is {} but --min-stability requires at least \
31 {}. Raise the protocol's maturity or lower --min-stability.",
32 actual.as_str(),
33 min.as_str()
34 )
35}
36
37fn is_bound_addr(addr: &SocketAddr) -> bool {
47 addr.port() != 0
48}
49
50fn is_addr_in_use_error(err: &anyhow::Error) -> bool {
52 if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
53 io_err.kind() == std::io::ErrorKind::AddrInUse
54 } else {
55 false
56 }
57}
58
59pub async fn start_server_by_id(
61 state: &AppState,
62 server_id: ServerId,
63 llm_client: &OllamaClient,
64 status_tx: &mpsc::UnboundedSender<String>,
65) -> Result<(), ActionExecutionError> {
66 let server = match state
68 .with_server_mut(server_id, |current| {
69 crate::protocol::StartupParams::preflight_owned(&mut current.startup_params)?;
70 Ok::<_, crate::protocol::StartupParamError>(current.clone())
71 })
72 .await
73 {
74 Some(Ok(current)) => current,
75 Some(Err(error)) => return Err(ActionExecutionError::Fatal(error.into())),
76 None => {
77 let _ = status_tx.send(format!("[ERROR] Server #{} not found", server_id.as_u32()));
78 return Ok(());
79 }
80 };
81
82 let listen_addr: SocketAddr = format!("127.0.0.1:{}", server.port)
84 .parse()
85 .map_err(|e| ActionExecutionError::Fatal(anyhow::anyhow!("Invalid address: {}", e)))?;
86
87 let protocol_name = server.protocol_name.clone();
88
89 use crate::state::server::ServerStatus;
91
92 let protocol = crate::protocol::server_registry::registry()
94 .resolve(&protocol_name)
95 .map_err(|e| anyhow::anyhow!("{e}"))?;
96
97 let metadata = protocol.metadata();
99
100 if let Some(min) = state.get_min_stability().await {
105 if metadata.state < min {
106 let full_error = min_stability_refusal("server", &protocol_name, metadata.state, min);
107 state
108 .update_server_status(server_id, ServerStatus::Error(full_error.clone()))
109 .await;
110 let _ = status_tx.send(format!("[ERROR] {}", full_error));
111 let _ = status_tx.send("__UPDATE_UI__".to_string());
112 return Err(ActionExecutionError::Fatal(anyhow::anyhow!(full_error)));
113 }
114 }
115
116 let system_caps = state.get_system_capabilities().await;
117 let privilege_requirement =
119 protocol.startup_privilege_requirement(server.startup_params.as_ref());
120
121 let privilege_met = privilege_requirement.is_met_by(&system_caps);
130
131 let requires_privileges = match &privilege_requirement {
132 crate::protocol::metadata::PrivilegeRequirement::PrivilegedPort(_) => {
133 server.port > 0 && server.port < 1024 && !privilege_met
136 }
137 _ => !privilege_met,
139 };
140
141 if requires_privileges {
142 let error_msg = format!(
143 "Cannot start {} server on port {}: {}. Current capabilities: {}",
144 protocol_name,
145 server.port,
146 privilege_requirement.description(),
147 system_caps.description()
148 );
149
150 let suggestion = if cfg!(target_os = "linux") {
152 match &privilege_requirement {
153 crate::protocol::metadata::PrivilegeRequirement::PrivilegedPort(port) => {
154 format!("\nSuggestion: Run as root (sudo) or use a port >= 1024 (e.g., {}, {}, {})",
155 port + 8000, port + 10000, 8080)
156 }
157 crate::protocol::metadata::PrivilegeRequirement::RawSockets => {
158 "\nSuggestion: Run as root or grant CAP_NET_RAW: sudo setcap cap_net_raw+ep /path/to/netget".to_string()
159 }
160 crate::protocol::metadata::PrivilegeRequirement::Root => {
161 "\nSuggestion: Run as root (sudo netget ...)".to_string()
162 }
163 _ => String::new(),
164 }
165 } else if cfg!(target_os = "macos") {
166 "\nSuggestion: Run as root (sudo netget ...)".to_string()
167 } else {
168 "\nSuggestion: Run as Administrator".to_string()
169 };
170
171 let full_error = format!("{}{}", error_msg, suggestion);
172
173 state
174 .update_server_status(server_id, ServerStatus::Error(full_error.clone()))
175 .await;
176 let _ = status_tx.send(format!("[ERROR] {}", full_error));
177 let _ = status_tx.send("__UPDATE_UI__".to_string());
178 return Err(ActionExecutionError::PrivilegeDenied {
179 requirement: privilege_requirement.description(),
180 message: full_error,
181 });
182 }
183
184 let blocker = crate::protocol::dependencies::startup_blocker(
199 &protocol.startup_dependencies(server.startup_params.as_ref()),
200 &system_caps,
201 );
202
203 if let Some(dep) = blocker {
204 let full_error = format!(
205 "Cannot start {} server on port {}: {}. {}",
206 protocol_name,
207 server.port,
208 dep.description(),
209 dep.installation_hint()
210 );
211
212 state
213 .update_server_status(server_id, ServerStatus::Error(full_error.clone()))
214 .await;
215 let _ = status_tx.send(format!("[ERROR] {}", full_error));
216 let _ = status_tx.send("__UPDATE_UI__".to_string());
217 return Err(ActionExecutionError::PrivilegeDenied {
218 requirement: dep.name(),
219 message: full_error,
220 });
221 }
222
223 let startup_params = if let Some(params_json) = server.startup_params.clone() {
228 let schema = protocol.get_startup_parameters();
230 match crate::protocol::StartupParams::new_validated(params_json, schema) {
232 Ok(p) => Some(p),
233 Err(e) => {
234 let msg = format!("Invalid startup_params for {}: {}", protocol_name, e);
235 state
236 .update_server_status(server_id, ServerStatus::Error(msg.clone()))
237 .await;
238 let _ = status_tx.send(format!("[ERROR] {}", msg));
239 let _ = status_tx.send("__UPDATE_UI__".to_string());
240 return Err(ActionExecutionError::Fatal(anyhow::anyhow!(msg)));
241 }
242 }
243 } else {
244 None
245 };
246
247 #[allow(deprecated)]
251 let spawn_ctx = crate::protocol::SpawnContext {
252 listen_addr,
253 mac_address: None,
254 interface: None,
255 host: None,
256 port: None,
257 llm_client: llm_client.clone().with_status_tx(status_tx.clone()),
260 state: Arc::new(state.clone()),
261 status_tx: status_tx.clone(),
262 server_id,
263 startup_params,
264 };
265
266 match protocol.spawn(spawn_ctx).await {
268 Ok(actual_addr) => {
269 let bound = is_bound_addr(&actual_addr);
270
271 let msg = if bound {
273 format!(
274 "[SERVER] Starting server #{} ({}) on {}",
275 server_id.as_u32(),
276 protocol_name,
277 actual_addr
278 )
279 } else {
280 format!(
281 "[SERVER] Starting server #{} ({}) (no listening socket)",
282 server_id.as_u32(),
283 protocol_name
284 )
285 };
286 let _ = status_tx.send(msg);
287
288 if bound {
292 state.update_server_local_addr(server_id, actual_addr).await;
293 }
294 state
295 .update_server_status(server_id, ServerStatus::Running)
296 .await;
297 if bound && (server.port == 0 || server.port != actual_addr.port()) {
299 let update_msg = format!(
300 "[SERVER] Server #{} ({}) listening on {}",
301 server_id.as_u32(),
302 protocol_name,
303 actual_addr
304 );
305 let _ = status_tx.send(update_msg);
306 }
307 let _ = status_tx.send("__UPDATE_UI__".to_string());
309 }
310 Err(e) => {
311 if is_addr_in_use_error(&e) {
313 let _ = status_tx.send(format!(
315 "[INFO] Port {} is already in use for {} server, will retry with LLM suggestion",
316 server.port,
317 protocol_name
318 ));
319 return Err(ActionExecutionError::PortConflict {
320 port: server.port,
321 protocol: protocol_name.clone(),
322 underlying_error: e.to_string(),
323 });
324 }
325
326 state
328 .update_server_status(server_id, ServerStatus::Error(e.to_string()))
329 .await;
330 let _ = status_tx.send(format!(
331 "[ERROR] Failed to start {} server #{}: {}",
332 protocol_name,
333 server_id.as_u32(),
334 e
335 ));
336 let _ = status_tx.send("__UPDATE_UI__".to_string());
337 return Err(ActionExecutionError::Fatal(e));
338 }
339 }
340
341 Ok(())
342}
343
344#[allow(clippy::too_many_arguments)]
347pub fn legacy_bind_ip(host: Option<&str>) -> Result<std::net::IpAddr> {
351 use std::net::{IpAddr, Ipv4Addr};
352 match host.map(str::trim) {
353 None | Some("") | Some("localhost") => Ok(IpAddr::V4(Ipv4Addr::LOCALHOST)),
354 Some(h) => h
355 .trim_start_matches('[')
356 .trim_end_matches(']')
357 .parse()
358 .map_err(|_| anyhow::anyhow!("host {h:?} is not an IP address (or \"localhost\")")),
359 }
360}
361
362pub async fn start_server_from_action(
363 state: &AppState,
364 mac_address: Option<String>,
366 interface: Option<String>,
367 host: Option<String>,
368 port: Option<u16>,
369 protocol: &str,
370 send_first: bool,
371 initial_memory: Option<String>,
372 instruction: String,
373 mut startup_params: Option<serde_json::Value>,
374 event_handlers: Option<Vec<serde_json::Value>>,
375 scheduled_tasks: Option<Vec<crate::llm::actions::common::ServerTaskDefinition>>,
376 feedback_instructions: Option<String>,
377 status_tx: mpsc::UnboundedSender<String>,
378) -> Result<ServerId> {
379 use crate::state::server::ServerStatus;
380 crate::protocol::StartupParams::preflight_owned(&mut startup_params)?;
381 let prepared_tasks = crate::state::task::prepare_tasks(
382 scheduled_tasks.as_deref(),
383 crate::state::task::TaskScope::Global,
384 )?;
385
386 let registry = crate::protocol::server_registry::registry();
390 let protocol_impl = registry
391 .resolve(protocol)
392 .map_err(|e| anyhow::anyhow!("{e}"))?;
393
394 if let Some(min) = state.get_min_stability().await {
398 let actual = protocol_impl.metadata().state;
399 if actual < min {
400 let msg = min_stability_refusal("server", protocol, actual, min);
401 let _ = status_tx.send(format!("[ERROR] {}", msg));
402 return Err(anyhow::anyhow!(msg));
403 }
404 }
405
406 let port = match port {
415 Some(explicit) => Some(explicit),
416 None => {
417 let caps = state.get_system_capabilities().await;
418 match crate::protocol::default_port::default_port_for_server(
419 protocol_impl.as_ref(),
420 host.as_deref(),
421 &caps,
422 ) {
423 Some(default) => {
424 let _ = status_tx.send(format!(
425 "[INFO] {} server: no port given; {}",
426 protocol,
427 default.describe()
428 ));
429 Some(default.port)
430 }
431 None => None,
432 }
433 }
434 };
435
436 let declares_send_first = protocol_impl
448 .get_startup_parameters()
449 .iter()
450 .any(|p| p.name == "send_first");
451
452 let mut startup_params = if send_first && declares_send_first {
453 let mut params = startup_params.unwrap_or_else(|| serde_json::json!({}));
454 match params.as_object_mut() {
455 Some(map) => {
456 map.entry("send_first")
457 .or_insert(serde_json::Value::Bool(true));
458 Some(params)
459 }
460 None => {
461 return Err(anyhow::anyhow!(
462 "startup_params must be a JSON object, got: {}",
463 crate::utils::redact::redact_sensitive(¶ms)
464 ));
465 }
466 }
467 } else {
468 if send_first && !declares_send_first {
469 let _ = status_tx.send(format!(
470 "[WARN] Protocol '{}' does not support send_first; ignoring it",
471 protocol
472 ));
473 tracing::warn!(
474 "send_first requested for protocol '{}', which declares no send_first startup parameter - ignoring",
475 protocol
476 );
477 }
478 startup_params
479 };
480
481 crate::protocol::StartupParams::preflight_owned(&mut startup_params)?;
482
483 let startup_params_obj = match startup_params.clone() {
490 Some(params_json) => {
491 let schema = protocol_impl.get_startup_parameters();
492 crate::cli::management::validate_server_startup_params(
493 ¶ms_json,
494 &schema,
495 protocol,
496 )?;
497 Some(
498 crate::protocol::StartupParams::new_validated(params_json, schema).map_err(
499 |e| anyhow::anyhow!("Invalid startup_params for {}: {}", protocol, e),
500 )?,
501 )
502 }
503 None => {
504 crate::protocol::StartupParams::new_validated(
505 serde_json::json!({}),
506 protocol_impl.get_startup_parameters(),
507 )?;
508 None
509 }
510 };
511
512 let (final_mac, final_interface, final_host, final_port, _use_new_path, listen_addr) =
518 if let Some(defaults) = protocol_impl.default_binding() {
519 let (mac, iface, host_str, port_num) =
521 defaults.apply(mac_address.clone(), interface.clone(), host.clone(), port);
522
523 let final_port_num = port_num;
537
538 let legacy_addr = match (&host_str, final_port_num) {
541 (Some(h), Some(p)) => format!("{}:{}", h, p)
542 .parse()
543 .unwrap_or_else(|_| "127.0.0.1:0".parse().unwrap()),
544 _ => "127.0.0.1:0".parse().unwrap(),
545 };
546
547 (mac, iface, host_str, final_port_num, true, legacy_addr)
548 } else {
549 let port_value = port.ok_or_else(|| {
552 anyhow::anyhow!(
553 "Protocol '{}' requires 'port' parameter (unmigrated protocol)",
554 protocol
555 )
556 })?;
557
558 let actual_port = port_value;
562
563 let ip = legacy_bind_ip(host.as_deref())?;
567 let listen_addr = SocketAddr::new(ip, actual_port);
568
569 (
570 None,
571 None,
572 Some(ip.to_string()),
573 Some(actual_port),
574 false,
575 listen_addr,
576 )
577 };
578
579 let privilege_requirement =
582 protocol_impl.startup_privilege_requirement(startup_params.as_ref());
583 let system_caps = state.get_system_capabilities().await;
584
585 let privilege_met = privilege_requirement.is_met_by(&system_caps);
592
593 let requires_privileges = match &privilege_requirement {
594 crate::protocol::metadata::PrivilegeRequirement::PrivilegedPort(_) => {
595 let privileged_port = match final_port {
599 Some(p) => p > 0 && p < 1024,
600 None => false, };
602 privileged_port && !privilege_met
603 }
604 _ => !privilege_met,
606 };
607
608 if requires_privileges {
609 let error_msg = format!(
610 "Cannot start {} server: {}. Current capabilities: {}",
611 protocol,
612 privilege_requirement.description(),
613 system_caps.description()
614 );
615 return Err(anyhow::anyhow!(error_msg));
616 }
617
618 if let Some(dep) = crate::protocol::dependencies::startup_blocker(
628 &protocol_impl.startup_dependencies(startup_params.as_ref()),
629 &system_caps,
630 ) {
631 let msg = format!(
632 "Cannot start {} server: {}. {}",
633 protocol,
634 dep.description(),
635 dep.installation_hint()
636 );
637 let _ = status_tx.send(format!("[ERROR] {}", msg));
638 return Err(anyhow::anyhow!(msg));
639 }
640
641 let parsed_handlers = event_handlers
642 .map(crate::events::handler::EventHandler::parse_event_handlers)
643 .transpose()
644 .map_err(|error| anyhow::anyhow!("Invalid event handler configuration: {}", error))?;
645
646 let display_port = final_port.unwrap_or(0);
650
651 let server = crate::state::server::ServerInstance {
652 id: ServerId::new(0), port: display_port,
654 protocol_name: protocol.to_string(),
655 instruction: instruction.clone(),
656 memory: String::new(),
657 status: ServerStatus::Starting,
658 connections: Default::default(),
659 local_addr: None,
660 created_at: crate::utils::clock::Instant::now(),
661 status_changed_at: crate::utils::clock::Instant::now(),
662 startup_params: startup_params.clone(),
663 event_handler_config: None,
664 protocol_data: serde_json::Value::Null,
665 log_files: Default::default(),
666 feedback_instructions,
667 feedback_buffer: Vec::new(),
668 last_feedback_processed: None,
669 recent_connections: Default::default(),
670 connection_opened_at: Default::default(),
671 };
672
673 let server_id = state.add_server(server).await;
674 let mut startup_guard = super::startup_guard::StartupGuard::server(state, server_id);
675
676 if let Some(mem) = initial_memory {
678 state.set_memory(server_id, mem).await;
679 }
680
681 if let Some(config) = parsed_handlers {
682 state
683 .with_server_mut(server_id, |server| {
684 server.event_handler_config = Some(config)
685 })
686 .await;
687 let _ = status_tx.send("[INFO] Event handler configuration applied".to_string());
688 }
689
690 for (task_def, mut task) in scheduled_tasks
692 .unwrap_or_default()
693 .into_iter()
694 .zip(prepared_tasks)
695 {
696 task.scope = crate::state::task::TaskScope::Server(server_id);
697 let task_name = task.name.clone();
698 let task_id_num = state.add_task(task).await;
699 if task_def.recurring {
705 let interval = task_def.interval_secs.unwrap_or(60);
706 let max_info = match task_def.max_executions {
707 Some(max) => format!(" (max {} executions)", max),
708 None => String::new(),
709 };
710 let msg = format!(
711 "[TASK] Scheduled recurring task '{}' (ID: {}) to execute every {}s{}",
712 task_name, task_id_num, interval, max_info
713 );
714 tracing::info!("{}", msg);
715 let _ = status_tx.send(msg);
716 } else {
717 let msg = format!(
718 "[TASK] Scheduled one-shot task '{}' (ID: {}) to execute in {}s",
719 task_name,
720 task_id_num,
721 task_def.delay_secs.unwrap_or(0)
722 );
723 tracing::info!("{}", msg);
724 let _ = status_tx.send(msg);
725 }
726 }
727
728 let llm_client = if let Some(client) = state.get_llm_client().await {
734 client
735 } else {
736 #[cfg(not(target_arch = "wasm32"))]
737 {
738 let ollama_url = state.get_ollama_url().await;
739 OllamaClient::new(ollama_url)
740 }
741 #[cfg(target_arch = "wasm32")]
742 {
743 anyhow::bail!("no LLM client is configured; the browser build installs one at startup")
744 }
745 }
746 .with_status_tx(status_tx.clone());
749
750 #[allow(deprecated)]
751 let spawn_ctx = crate::protocol::SpawnContext {
752 listen_addr,
753 mac_address: final_mac,
754 interface: final_interface,
755 host: final_host,
756 port: final_port,
757 llm_client,
758 state: Arc::new(state.clone()),
759 status_tx: status_tx.clone(),
760 server_id,
761 startup_params: startup_params_obj,
762 };
763
764 match protocol_impl.spawn(spawn_ctx).await {
766 Ok(actual_addr) => {
767 let bound = is_bound_addr(&actual_addr);
768
769 let msg = if bound {
771 format!(
772 "[SERVER] Starting server #{} ({}) on {}",
773 server_id.as_u32(),
774 protocol,
775 actual_addr
776 )
777 } else {
778 format!(
779 "[SERVER] Starting server #{} ({}) (no listening socket)",
780 server_id.as_u32(),
781 protocol
782 )
783 };
784 let _ = status_tx.send(msg);
785
786 if bound {
789 state.update_server_local_addr(server_id, actual_addr).await;
790 }
791 state
792 .update_server_status(server_id, ServerStatus::Running)
793 .await;
794
795 if bound
797 && (final_port.unwrap_or(0) == 0 || final_port.unwrap_or(0) != actual_addr.port())
798 {
799 let update_msg = format!(
800 "[SERVER] Server #{} ({}) listening on {}",
801 server_id.as_u32(),
802 protocol,
803 actual_addr
804 );
805 let _ = status_tx.send(update_msg);
806 }
807
808 startup_guard.disarm();
809 Ok(server_id)
810 }
811 Err(e) => {
812 state
820 .update_server_status(server_id, ServerStatus::Error(e.to_string()))
821 .await;
822 state.remove_server(server_id).await;
823 startup_guard.disarm();
824 let _ = status_tx.send(format!(
825 "[ERROR] Server #{} ({}) failed to start: {}",
826 server_id.as_u32(),
827 protocol,
828 e
829 ));
830 let _ = status_tx.send("__UPDATE_UI__".to_string());
831 Err(e)
832 }
833 }
834}