systemprompt_agent/services/agent_orchestration/process/
mod.rs1mod command;
11mod signals;
12
13use std::fs;
14use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
15use systemprompt_models::paths::BuildPaths;
16use systemprompt_models::{AppPaths, Config};
17
18use crate::services::agent_orchestration::{OrchestrationError, OrchestrationResult};
19
20pub use signals::{
21 force_kill_process, kill_process, kill_process_verified, process_exists, terminate_gracefully,
22 terminate_gracefully_verified, terminate_process,
23};
24
25pub fn spawn_detached(paths: &AppPaths, agent_name: &str, port: u16) -> OrchestrationResult<u32> {
26 let binary_path = BuildPaths::resolve_self().map_err(|e| {
27 OrchestrationError::ProcessSpawnFailed(format!("Failed to resolve running binary: {e}"))
28 })?;
29
30 let config = Config::get().map_err(|e| {
31 OrchestrationError::ProcessSpawnFailed(format!("Failed to get config: {e}"))
32 })?;
33
34 let secrets = SecretsBootstrap::get().map_err(|e| {
35 OrchestrationError::ProcessSpawnFailed(format!("Failed to get secrets: {e}"))
36 })?;
37
38 let profile_path = ProfileBootstrap::get_path().map_err(|e| {
39 OrchestrationError::ProcessSpawnFailed(format!("Failed to get profile path: {e}"))
40 })?;
41
42 let log_file = command::prepare_agent_log_file(agent_name, &paths.system().logs())?;
43
44 let mut cmd = command::build_agent_command(command::BuildAgentCommandParams {
45 binary_path: &binary_path,
46 agent_name,
47 port,
48 profile_path,
49 secrets,
50 config,
51 log_file,
52 });
53
54 let child = cmd.spawn().map_err(|e| {
55 OrchestrationError::ProcessSpawnFailed(format!("Failed to spawn {agent_name}: {e}"))
56 })?;
57
58 let pid = child.id();
59 #[expect(
60 clippy::mem_forget,
61 reason = "detached agent process: skip Child's drop-time wait so the spawned agent keeps \
62 running after this fn returns"
63 )]
64 std::mem::forget(child);
65
66 if !signals::verify_process_started(pid) {
67 return Err(OrchestrationError::ProcessSpawnFailed(format!(
68 "Agent {} (PID {}) died immediately after spawn",
69 agent_name, pid
70 )));
71 }
72
73 tracing::debug!(pid = %pid, agent_name = %agent_name, "Detached process spawned");
74 Ok(pid)
75}
76
77pub fn is_port_in_use(port: u16) -> bool {
78 use std::net::TcpListener;
79 TcpListener::bind(format!("127.0.0.1:{port}")).is_err()
80}
81
82pub fn validate_agent_binary() -> crate::error::AgentResult<()> {
83 let binary_path = BuildPaths::resolve_self()
84 .map_err(|e| crate::error::AgentError::Validation(e.to_string()))?;
85
86 let metadata = fs::metadata(&binary_path).map_err(crate::error::AgentError::Io)?;
87
88 if !metadata.is_file() {
89 return Err(crate::error::AgentError::Validation(format!(
90 "Agent binary is not a file: {}",
91 binary_path.display()
92 )));
93 }
94
95 #[cfg(unix)]
96 {
97 use std::os::unix::fs::PermissionsExt;
98 let permissions = metadata.permissions();
99 if permissions.mode() & 0o111 == 0 {
100 return Err(crate::error::AgentError::Validation(format!(
101 "Agent binary is not executable: {}",
102 binary_path.display()
103 )));
104 }
105 }
106
107 Ok(())
108}