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