Skip to main content

supercode_harness/
teams.rs

1//! Where supercode-teams lives on this box, and the service unit that keeps
2//! its node up (`docs/architecture/teams.md`).
3//!
4//! supercode does not implement teams; the `sdk/teams` package does. This
5//! module holds the two facts the Rust CLI needs about it:
6//!
7//! * **where its Node entry is** — [`teams_entry`], resolved exactly the way
8//!   [`crate::orchestrator::daemon_entry`] resolves the orchestrator's:
9//!   `SUPERCODE_TEAMS_ENTRY` first, then the checkout the running binary sits
10//!   in, then the current directory, then the globally installed
11//!   `@volter-ai-dev/supercode-teams` package (`npm root -g`).
12//! * **what a service unit for its node would say** — [`service_unit`] renders
13//!   the launchd plist / systemd unit that runs `node <entry> node start
14//!   --listen 127.0.0.1:0`, written under `<home>/service/`;
15//!   [`install_service`] and [`uninstall_service`] drive `launchctl` /
16//!   `systemctl --user` over it.
17//!
18//! Everything else about teams — its keys, grants, log, machines — is the Node
19//! package's own state, written by its own CLI. There is no second writer of
20//! that home in this binary.
21
22use std::path::{Path, PathBuf};
23
24use crate::orchestrator::{absolute_program, ServiceState, ServiceUnit};
25
26/// The teams CLI entry inside the `sdk/teams` package.
27pub const TEAMS_ENTRY: &str = "bin/teams.mjs";
28
29/// The npm name the `sdk/teams` package is published under.
30pub const TEAMS_PACKAGE: &str = "@volter-ai-dev/supercode-teams";
31
32/// Directory the rendered service unit is written into, relative to the home.
33pub const SERVICE_DIR: &str = "service";
34
35/// launchd label / systemd unit name for this Machine's teams node.
36pub const SERVICE_NAME: &str = "dev.volter.supercode-teams-node";
37
38/// Why a teams verb could not do its work.
39#[derive(Debug, thiserror::Error)]
40pub enum TeamsError {
41    /// The Node teams entry could not be located.
42    #[error("no teams entry found (looked for `sdk/teams/{TEAMS_ENTRY}` under: {searched}); install it with `npm install -g {TEAMS_PACKAGE}`")]
43    NoEntry {
44        /// The candidate paths that were searched, joined.
45        searched: String,
46    },
47    /// A service manager refused, or there is none on this platform.
48    #[error("teams service: {action} failed: {detail}")]
49    Service {
50        /// What was attempted (`install`, `uninstall`).
51        action: &'static str,
52        /// What the service manager (or this module) said about it.
53        detail: String,
54    },
55    /// A file under the teams home could not be written or removed.
56    #[error("teams file `{}`: {source}", path.display())]
57    File {
58        /// The path involved.
59        path: PathBuf,
60        /// The underlying I/O failure.
61        source: std::io::Error,
62    },
63}
64
65/// The teams home: `SUPERCODE_TEAMS_HOME`, else `<SUPERCODE_HOME>/teams`.
66///
67/// The same precedence `sdk/teams/home.mjs` uses, so a unit installed from
68/// here serves the home the Node CLI reads.
69pub fn teams_home() -> PathBuf {
70    if let Ok(home) = std::env::var("SUPERCODE_TEAMS_HOME") {
71        if !home.is_empty() {
72            return PathBuf::from(home);
73        }
74    }
75    crate::agent::global_instructions_dir().join("teams")
76}
77
78/// Locate the Node teams entry (`sdk/teams/bin/teams.mjs`).
79///
80/// Candidates, in order: `SUPERCODE_TEAMS_ENTRY` (an explicit override, which
81/// is also how a test points at a fake), the repo checkout the running binary
82/// sits in, the current directory, the workspace this crate was built from,
83/// and the globally installed npm package — an installed binary has no
84/// checkout, so `npm install -g @volter-ai-dev/supercode-teams` is how a
85/// Machine gets its node.
86pub fn teams_entry() -> Result<PathBuf, TeamsError> {
87    let mut searched = Vec::new();
88    if let Some(explicit) = std::env::var_os("SUPERCODE_TEAMS_ENTRY") {
89        let path = PathBuf::from(explicit);
90        if path.is_file() {
91            return Ok(path);
92        }
93        searched.push(path.display().to_string());
94    }
95    let mut roots: Vec<PathBuf> = Vec::new();
96    if let Ok(exe) = std::env::current_exe() {
97        // target/<profile>/supercode → the workspace root is two levels up.
98        roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
99    }
100    if let Ok(cwd) = std::env::current_dir() {
101        roots.push(cwd);
102    }
103    // A locally built binary's target directory can live anywhere (a shared
104    // cargo build dir, another volume), so the checkout it was built from is
105    // the last candidate. On an installed binary this path simply does not
106    // exist and is skipped like any other miss.
107    if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
108        roots.push(workspace.to_path_buf());
109    }
110    for root in roots {
111        let candidate = root.join("sdk/teams").join(TEAMS_ENTRY);
112        if candidate.is_file() {
113            return Ok(candidate);
114        }
115        searched.push(candidate.display().to_string());
116    }
117    if let Some(global) = global_npm_root() {
118        let candidate = global.join(TEAMS_PACKAGE).join(TEAMS_ENTRY);
119        if candidate.is_file() {
120            return Ok(candidate);
121        }
122        searched.push(candidate.display().to_string());
123    }
124    Err(TeamsError::NoEntry {
125        searched: searched.join(", "),
126    })
127}
128
129/// Where npm installs global packages (`npm root -g`), when npm is present.
130fn global_npm_root() -> Option<PathBuf> {
131    let output = std::process::Command::new("npm")
132        .args(["root", "-g"])
133        .stdin(std::process::Stdio::null())
134        .stderr(std::process::Stdio::null())
135        .output()
136        .ok()?;
137    if !output.status.success() {
138        return None;
139    }
140    let text = String::from_utf8_lossy(&output.stdout);
141    let root = text.trim();
142    if root.is_empty() {
143        return None;
144    }
145    Some(PathBuf::from(root))
146}
147
148/// Render the per-platform service unit for this Machine's teams node.
149///
150/// The node takes no `--root`: it serves the home its own environment
151/// resolves (`SUPERCODE_TEAMS_HOME`, else `<SUPERCODE_HOME>/teams`), so the
152/// unit names the listen address and nothing else. A port of `0` means the
153/// node picks one and publishes it in `<home>/node.json`.
154pub fn service_unit(home: &Path, entry: &Path, node: &str) -> ServiceUnit {
155    let home_display = home.display().to_string();
156    let entry_display = entry.display().to_string();
157    if cfg!(target_os = "macos") {
158        let path = home.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.plist"));
159        let text = format!(
160            r#"<?xml version="1.0" encoding="UTF-8"?>
161<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
162<plist version="1.0">
163<dict>
164  <key>Label</key><string>{SERVICE_NAME}</string>
165  <key>ProgramArguments</key>
166  <array>
167    <string>{node}</string>
168    <string>{entry_display}</string>
169    <string>node</string>
170    <string>start</string>
171    <string>--listen</string>
172    <string>127.0.0.1:0</string>
173  </array>
174  <key>EnvironmentVariables</key>
175  <dict>
176    <key>SUPERCODE_TEAMS_HOME</key><string>{home_display}</string>
177  </dict>
178  <key>RunAtLoad</key><true/>
179  <key>KeepAlive</key><true/>
180  <key>StandardOutPath</key><string>{home_display}/service/teams-node.out.log</string>
181  <key>StandardErrorPath</key><string>{home_display}/service/teams-node.err.log</string>
182</dict>
183</plist>
184"#
185        );
186        let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
187        ServiceUnit {
188            kind: "launchd",
189            path,
190            text,
191            install_command: install,
192        }
193    } else {
194        let path = home
195            .join(SERVICE_DIR)
196            .join(format!("{SERVICE_NAME}.service"));
197        let text = format!(
198            "[Unit]\n\
199             Description=supercode teams node ({home_display})\n\
200             After=network.target\n\
201             \n\
202             [Service]\n\
203             Environment=SUPERCODE_TEAMS_HOME={home_display}\n\
204             ExecStart={node} {entry_display} node start --listen 127.0.0.1:0\n\
205             Restart=on-failure\n\
206             KillSignal=SIGTERM\n\
207             \n\
208             [Install]\n\
209             WantedBy=default.target\n"
210        );
211        let install = format!(
212            "systemctl --user link {} && systemctl --user enable --now {SERVICE_NAME}",
213            path.display()
214        );
215        ServiceUnit {
216            kind: "systemd",
217            path,
218            text,
219            install_command: install,
220        }
221    }
222}
223
224/// Write a rendered unit under `<home>/service/`.
225pub fn write_unit(unit: &ServiceUnit) -> Result<(), TeamsError> {
226    if let Some(parent) = unit.path.parent() {
227        std::fs::create_dir_all(parent).map_err(|source| TeamsError::File {
228            path: unit.path.clone(),
229            source,
230        })?;
231    }
232    std::fs::write(&unit.path, &unit.text).map_err(|source| TeamsError::File {
233        path: unit.path.clone(),
234        source,
235    })
236}
237
238/// Run a service-manager command and return (success, stdout+stderr).
239fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
240    let output = std::process::Command::new(program).args(args).output()?;
241    let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
242    text.push_str(&String::from_utf8_lossy(&output.stderr));
243    Ok((output.status.success(), text.trim().to_string()))
244}
245
246#[cfg(target_os = "macos")]
247fn gui_domain() -> String {
248    // SAFETY: `getuid` reads this process's own real user id and cannot fail.
249    format!("gui/{}", unsafe { libc::getuid() })
250}
251
252/// What the platform's service manager says about the teams node unit.
253///
254/// Never starts or installs anything.
255pub fn service_status() -> ServiceState {
256    platform_status()
257}
258
259#[cfg(target_os = "macos")]
260fn platform_status() -> ServiceState {
261    let label = SERVICE_NAME.to_string();
262    let target = format!("{}/{SERVICE_NAME}", gui_domain());
263    match run_tool("launchctl", &["print", &target]) {
264        Ok((true, text)) => ServiceState {
265            kind: "launchd",
266            label,
267            installed: true,
268            pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
269            detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
270        },
271        Ok((false, _)) => ServiceState {
272            kind: "launchd",
273            label,
274            installed: false,
275            pid: None,
276            detail: format!("not bootstrapped in {}", gui_domain()),
277        },
278        Err(error) => ServiceState {
279            kind: "launchd",
280            label,
281            installed: false,
282            pid: None,
283            detail: format!("launchctl unavailable: {error}"),
284        },
285    }
286}
287
288#[cfg(all(unix, not(target_os = "macos")))]
289fn platform_status() -> ServiceState {
290    let label = SERVICE_NAME.to_string();
291    match run_tool("systemctl", &["--user", "is-active", SERVICE_NAME]) {
292        Ok((active, text)) => {
293            let known = run_tool("systemctl", &["--user", "is-enabled", SERVICE_NAME])
294                .map(|(ok, _)| ok)
295                .unwrap_or(false);
296            ServiceState {
297                kind: "systemd",
298                label,
299                installed: active || known,
300                pid: None,
301                detail: if text.is_empty() {
302                    "unknown".into()
303                } else {
304                    text
305                },
306            }
307        }
308        Err(error) => ServiceState {
309            kind: "systemd",
310            label,
311            installed: false,
312            pid: None,
313            detail: format!("systemctl unavailable: {error}"),
314        },
315    }
316}
317
318#[cfg(not(unix))]
319fn platform_status() -> ServiceState {
320    ServiceState {
321        kind: "none",
322        label: SERVICE_NAME.to_string(),
323        installed: false,
324        pid: None,
325        detail: "no service manager on this platform".into(),
326    }
327}
328
329/// `key = value` out of a service manager's block output.
330#[cfg(target_os = "macos")]
331fn field_of(text: &str, key: &str) -> Option<String> {
332    text.lines()
333        .find_map(|line| line.trim().strip_prefix(key))
334        .map(|value| value.trim().to_string())
335}
336
337/// The file name the unit takes on this platform.
338fn unit_file_name() -> String {
339    if cfg!(target_os = "macos") {
340        format!("{SERVICE_NAME}.plist")
341    } else {
342        format!("{SERVICE_NAME}.service")
343    }
344}
345
346/// Render the unit, hand it to the platform's service manager, and start it.
347///
348/// Refuses a label the manager already holds rather than replacing it: two
349/// homes share one label, so an install that silently took it over would point
350/// a running node at a different folder.
351pub fn install_service(
352    home: &Path,
353    entry: &Path,
354    node: &str,
355) -> Result<(ServiceUnit, ServiceState), TeamsError> {
356    let existing = service_status();
357    if existing.installed {
358        return Err(TeamsError::Service {
359            action: "install",
360            detail: format!(
361                "`{}` is already installed ({}); `supercode teams node uninstall` first",
362                existing.label, existing.detail
363            ),
364        });
365    }
366    let unit = service_unit(home, entry, &absolute_program(node));
367    write_unit(&unit)?;
368    platform_install(&unit)?;
369    Ok((unit, service_status()))
370}
371
372#[cfg(target_os = "macos")]
373fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
374    let path = unit.path.display().to_string();
375    let (ok, text) =
376        run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
377            TeamsError::Service {
378                action: "install",
379                detail: format!("launchctl: {error}"),
380            }
381        })?;
382    if !ok {
383        return Err(TeamsError::Service {
384            action: "install",
385            detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
386        });
387    }
388    Ok(())
389}
390
391/// Untested on this box (the receipt is macOS); these are the commands
392/// `service_unit` prints as its `install_command`.
393#[cfg(all(unix, not(target_os = "macos")))]
394fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
395    let path = unit.path.display().to_string();
396    for args in [
397        vec!["--user", "link", path.as_str()],
398        vec!["--user", "enable", "--now", SERVICE_NAME],
399    ] {
400        let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
401            action: "install",
402            detail: format!("systemctl: {error}"),
403        })?;
404        if !ok {
405            return Err(TeamsError::Service {
406                action: "install",
407                detail: format!("systemctl {}: {text}", args.join(" ")),
408            });
409        }
410    }
411    Ok(())
412}
413
414#[cfg(not(unix))]
415fn platform_install(_unit: &ServiceUnit) -> Result<(), TeamsError> {
416    Err(TeamsError::Service {
417        action: "install",
418        detail: "no service manager on this platform".into(),
419    })
420}
421
422/// Stop and unregister the unit, and remove the rendered file.
423///
424/// Idempotent: a unit the manager does not hold is not an error, because the
425/// state the operator asked for is the state they get.
426pub fn uninstall_service(home: &Path) -> Result<ServiceState, TeamsError> {
427    platform_uninstall()?;
428    let unit_path = home.join(SERVICE_DIR).join(unit_file_name());
429    match std::fs::remove_file(&unit_path) {
430        Ok(()) => {}
431        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
432        Err(source) => {
433            return Err(TeamsError::File {
434                path: unit_path,
435                source,
436            })
437        }
438    }
439    // `launchctl bootout` returns before the job is torn down, so the state
440    // this reports is the settled one, not the manager mid-teardown.
441    let mut state = service_status();
442    for _ in 0..40 {
443        if !state.installed {
444            break;
445        }
446        std::thread::sleep(std::time::Duration::from_millis(100));
447        state = service_status();
448    }
449    Ok(state)
450}
451
452#[cfg(target_os = "macos")]
453fn platform_uninstall() -> Result<(), TeamsError> {
454    let target = format!("{}/{SERVICE_NAME}", gui_domain());
455    let (ok, text) =
456        run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
457            action: "uninstall",
458            detail: format!("launchctl: {error}"),
459        })?;
460    // `bootout` on a label nobody holds says so and exits non-zero.
461    if !ok && !text.contains("No such process") && !text.contains("not find") {
462        return Err(TeamsError::Service {
463            action: "uninstall",
464            detail: format!("launchctl bootout {target}: {text}"),
465        });
466    }
467    Ok(())
468}
469
470#[cfg(all(unix, not(target_os = "macos")))]
471fn platform_uninstall() -> Result<(), TeamsError> {
472    let _ = run_tool("systemctl", &["--user", "disable", "--now", SERVICE_NAME]);
473    Ok(())
474}
475
476#[cfg(not(unix))]
477fn platform_uninstall() -> Result<(), TeamsError> {
478    Ok(())
479}