supercode_harness/
harness_command.rs1use std::process::Command;
20
21pub const HERMES_BIN_ENV: &str = "SUPERCODE_HERMES_BIN";
23
24pub const OPENCLAW_BIN_ENV: &str = "SUPERCODE_OPENCLAW_BIN";
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub(crate) enum Arg {
30 Plain(String),
31 Secret,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub(crate) struct HarnessCommand {
37 pub(crate) program: String,
38 pub(crate) args: Vec<Arg>,
40 pub(crate) secrets: Vec<String>,
42 pub(crate) env: Vec<(String, String)>,
43}
44
45impl HarnessCommand {
46 pub(crate) fn new(program: impl Into<String>) -> Self {
47 Self {
48 program: program.into(),
49 args: Vec::new(),
50 secrets: Vec::new(),
51 env: Vec::new(),
52 }
53 }
54
55 pub(crate) fn arg(&mut self, value: impl Into<String>) -> &mut Self {
56 self.args.push(Arg::Plain(value.into()));
57 self
58 }
59
60 pub(crate) fn args<I: IntoIterator<Item = S>, S: Into<String>>(
61 &mut self,
62 values: I,
63 ) -> &mut Self {
64 for value in values {
65 self.arg(value);
66 }
67 self
68 }
69
70 pub(crate) fn secret(&mut self, value: impl Into<String>) -> &mut Self {
72 self.args.push(Arg::Secret);
73 self.secrets.push(value.into());
74 self
75 }
76
77 pub(crate) fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
78 self.env.push((key.into(), value.into()));
79 self
80 }
81
82 pub(crate) fn narrate(&self) -> String {
84 let mut line = shell_quote(&self.program);
85 for arg in &self.args {
86 line.push(' ');
87 match arg {
88 Arg::Plain(value) => line.push_str(&shell_quote(value)),
89 Arg::Secret => line.push_str("<redacted>"),
90 }
91 }
92 line
93 }
94
95 pub(crate) fn run(&self) -> Result<String, String> {
98 let mut secrets = self.secrets.iter();
99 let mut command = Command::new(&self.program);
100 for arg in &self.args {
101 match arg {
102 Arg::Plain(value) => command.arg(value),
103 Arg::Secret => command.arg(secrets.next().expect("one secret per Arg::Secret")),
104 };
105 }
106 for (key, value) in &self.env {
107 command.env(key, value);
108 }
109 command.stdin(std::process::Stdio::null());
110 let output = command
111 .output()
112 .map_err(|error| format!("`{}` could not be executed: {error}", self.narrate()))?;
113 if output.status.success() {
114 return Ok(String::from_utf8_lossy(&output.stdout).into_owned());
115 }
116 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
117 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
118 let detail = if stderr.is_empty() { stdout } else { stderr };
119 Err(format!(
120 "`{}` failed ({}): {}",
121 self.narrate(),
122 output.status,
123 if detail.is_empty() {
124 "the harness printed nothing".to_string()
125 } else {
126 detail
127 }
128 ))
129 }
130}
131
132pub(crate) fn shell_quote(value: &str) -> String {
133 if !value.is_empty()
134 && value
135 .chars()
136 .all(|c| c.is_ascii_alphanumeric() || "-_./:@=+,".contains(c))
137 {
138 return value.to_string();
139 }
140 format!("'{}'", value.replace('\'', "'\\''"))
141}
142
143pub(crate) fn harness_program(harness: &str) -> Result<String, Option<String>> {
154 let variable = match harness {
155 crate::HarnessId::HERMES => HERMES_BIN_ENV,
156 crate::HarnessId::OPENCLAW => OPENCLAW_BIN_ENV,
157 _ => return Err(None),
158 };
159 if let Some(over) = std::env::var_os(variable) {
160 let over = over.to_string_lossy().trim().to_string();
161 if !over.is_empty() {
162 return Ok(over);
163 }
164 }
165 let registry = crate::harness_support_registry();
166 let program = registry
167 .harnesses
168 .iter()
169 .find(|descriptor| descriptor.id.as_str() == harness)
170 .and_then(|descriptor| descriptor.runtime.default_launch.as_ref())
171 .map(|launch| launch.program.clone())
172 .ok_or_else(|| {
173 Some(format!(
174 "the registry has no launch for `{harness}`, so its CLI cannot be located"
175 ))
176 })?;
177 Ok(program.strip_suffix("-acp").unwrap_or(&program).to_string())
178}