Skip to main content

systemprompt_agent/services/agent_orchestration/process/
command.rs

1//! Build the `Command` used to spawn a detached agent subprocess and rotate its
2//! log file.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use crate::services::shared::Result;
8use std::fs::{self, File};
9use std::path::{Path, PathBuf};
10use std::process::Command;
11use systemprompt_models::{CliPaths, Config, Secrets};
12
13use crate::services::agent_orchestration::{OrchestrationError, OrchestrationResult};
14
15const MAX_LOG_SIZE: u64 = 10 * 1024 * 1024;
16
17pub fn rotate_log_if_needed(log_path: &Path) -> Result<()> {
18    if let Ok(metadata) = fs::metadata(log_path)
19        && metadata.len() > MAX_LOG_SIZE
20    {
21        let backup_path = log_path.with_extension("log.old");
22        fs::rename(log_path, &backup_path)?;
23    }
24    Ok(())
25}
26
27pub fn prepare_agent_log_file(agent_name: &str, log_dir: &Path) -> OrchestrationResult<File> {
28    if let Err(e) = fs::create_dir_all(log_dir) {
29        tracing::error!(
30            error = %e,
31            path = %log_dir.display(),
32            "Failed to create agent log directory - agent may fail to start"
33        );
34    }
35
36    let log_file_path = log_dir.join(format!("agent-{}.log", agent_name));
37    if let Err(e) = rotate_log_if_needed(&log_file_path) {
38        tracing::warn!(
39            error = %e,
40            path = %log_file_path.display(),
41            "Failed to rotate agent log file"
42        );
43    }
44
45    fs::OpenOptions::new()
46        .create(true)
47        .append(true)
48        .open(&log_file_path)
49        .map_err(|e| {
50            OrchestrationError::ProcessSpawnFailed(format!(
51                "Failed to create log file {}: {}",
52                log_file_path.display(),
53                e
54            ))
55        })
56}
57
58#[derive(Debug)]
59pub struct BuildAgentCommandParams<'a> {
60    pub binary_path: &'a PathBuf,
61    pub agent_name: &'a str,
62    pub port: u16,
63    pub profile_path: &'a str,
64    pub secrets: &'a Secrets,
65    pub config: &'a Config,
66    pub log_file: File,
67}
68
69pub fn build_agent_command(params: BuildAgentCommandParams<'_>) -> Command {
70    let BuildAgentCommandParams {
71        binary_path,
72        agent_name,
73        port,
74        profile_path,
75        secrets,
76        config,
77        log_file,
78    } = params;
79    let mut command = Command::new(binary_path);
80    for arg in CliPaths::agent_run_args() {
81        command.arg(arg);
82    }
83    command
84        .arg("--agent-name")
85        .arg(agent_name)
86        .arg("--port")
87        .arg(port.to_string())
88        .env_clear();
89    if let Ok(path) = std::env::var("PATH") {
90        command.env("PATH", path);
91    }
92    if let Ok(home) = std::env::var("HOME") {
93        command.env("HOME", home);
94    }
95    // Why: SSRF guard allowlist (see
96    // systemprompt_models::net::TRUSTED_HTTP_HOSTS_ENV). The agent child
97    // re-validates outbound URLs when it loads the profile catalog, so the
98    // operator's process-wide trust assertion must travel with it — env_clear
99    // would otherwise leave the child running with an empty allowlist and
100    // reject sealed-network hostnames the parent already accepted.
101    if let Ok(trusted) = std::env::var(systemprompt_models::net::TRUSTED_HTTP_HOSTS_ENV) {
102        command.env(systemprompt_models::net::TRUSTED_HTTP_HOSTS_ENV, trusted);
103    }
104    command
105        .env("SYSTEMPROMPT_PROFILE", profile_path)
106        .env(systemprompt_models::subprocess::SUBPROCESS_MARKER_ENV, "1")
107        .env(systemprompt_models::subprocess::AGENT_NAME_ENV, agent_name)
108        .env("AGENT_PORT", port.to_string())
109        .env("DATABASE_TYPE", &config.database_type)
110        .stdout(std::process::Stdio::null())
111        .stderr(std::process::Stdio::from(log_file))
112        .stdin(std::process::Stdio::null());
113
114    for (k, v) in secrets.to_subprocess_env() {
115        command.env(k, v);
116    }
117
118    if let Ok(fly_app) = std::env::var("FLY_APP_NAME") {
119        command.env("FLY_APP_NAME", fly_app);
120    }
121
122    place_in_own_process_group(&mut command);
123
124    command
125}
126
127#[cfg(unix)]
128fn place_in_own_process_group(command: &mut Command) {
129    use std::os::unix::process::CommandExt;
130    // Why: pgid 0 makes the child its own group leader (pgid == pid), so the
131    // supervisor can signal the whole group on shutdown and reach any a2a
132    // children the agent spawns, not just the agent itself.
133    command.process_group(0);
134}
135
136#[cfg(windows)]
137fn place_in_own_process_group(command: &mut Command) {
138    use std::os::windows::process::CommandExt;
139    const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
140    command.creation_flags(CREATE_NEW_PROCESS_GROUP);
141}