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