Skip to main content

heyo_sdk/
daemons.rs

1//! User-owned heyvmd daemons (Mac mini, lab box, anywhere the user runs the
2//! `heyvmd` networking sidecar) registered with the cloud. Mirrors
3//! `sdk-ts/src/daemons.ts`.
4//!
5//! Each daemon advertises an iroh tunnel back to the local heyvm HTTP API; the
6//! cloud proxies SDK calls into it, so once a daemon is registered the SDK
7//! surface (`Sandbox`, `commands().run`, `sandbox.shell()`, file I/O) works
8//! against sandboxes on that daemon exactly as it does for cloud-hosted ones.
9//!
10//! For a direct P2P data path to a daemon (bypassing the cloud), pair this with
11//! [`HeyoClient::connect_p2p`](crate::HeyoClient::connect_p2p) or
12//! [`P2pTunnel`](crate::P2pTunnel) using the daemon's connection ticket.
13//!
14//! ```no_run
15//! use heyo_sdk::{Daemons, HeyoClientOptions};
16//! # async fn run() -> Result<(), heyo_sdk::HeyoError> {
17//! let daemons = Daemons::list(HeyoClientOptions::default()).await?;
18//! if let Some(home) = daemons.iter().find(|d| d.name.as_deref() == Some("homelab")) {
19//!     println!("{} is {:?}", home.id, home.status);
20//! }
21//! # Ok(()) }
22//! ```
23
24use reqwest::Method;
25use serde::Deserialize;
26
27use crate::client::{HeyoClient, HeyoClientOptions, RequestOptions};
28use crate::commands::encode_path;
29use crate::errors::HeyoError;
30
31/// Status the cloud reports for a registered daemon. Derived from the cloud's
32/// stale-host watcher: `Online` means a fresh heartbeat within ~3 minutes,
33/// `Stale` means missed heartbeats, `Offline` means the cloud has flipped the
34/// row away from `available`.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum DaemonStatus {
38    Online,
39    Stale,
40    Offline,
41}
42
43/// A daemon the authenticated user has registered with the cloud.
44#[derive(Debug, Clone, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct DaemonInfo {
47    /// Stable id assigned when the daemon first registered (`hd-…`).
48    pub id: String,
49    /// Human-readable label set by the daemon (defaults to hostname).
50    #[serde(default)]
51    pub name: Option<String>,
52    pub status: DaemonStatus,
53    /// RFC 3339 timestamp of the most recent heartbeat.
54    pub last_seen_at: String,
55    /// RFC 3339 timestamp from initial registration.
56    pub created_at: String,
57}
58
59#[derive(Deserialize)]
60struct DaemonsEnvelope {
61    #[serde(default)]
62    daemons: Vec<DaemonInfo>,
63}
64
65/// Static API for managing the caller's registered daemons.
66pub struct Daemons;
67
68impl Daemons {
69    /// List every daemon the authenticated user has registered.
70    pub async fn list(client_options: HeyoClientOptions) -> Result<Vec<DaemonInfo>, HeyoError> {
71        let client = HeyoClient::new(client_options)?;
72        let env: DaemonsEnvelope = client
73            .request(Method::GET, "/me/daemons", None::<&()>, RequestOptions::default())
74            .await?;
75        Ok(env.daemons)
76    }
77
78    /// Fetch a single daemon. Returns [`HeyoError::NotFound`] if the caller owns
79    /// no such daemon.
80    pub async fn get(
81        id: &str,
82        client_options: HeyoClientOptions,
83    ) -> Result<DaemonInfo, HeyoError> {
84        let client = HeyoClient::new(client_options)?;
85        let path = format!("/me/daemons/{}", encode_path(id));
86        client
87            .request(Method::GET, &path, None::<&()>, RequestOptions::default())
88            .await
89    }
90
91    /// Unregister a daemon. Idempotent on 404 — passing an unknown id is a no-op,
92    /// so reconciliation loops can call this freely. Existing sandboxes the daemon
93    /// hosts are *not* deleted; they become unreachable until the daemon
94    /// re-registers (or another daemon claims the same id).
95    pub async fn delete(id: &str, client_options: HeyoClientOptions) -> Result<(), HeyoError> {
96        let client = HeyoClient::new(client_options)?;
97        let path = format!("/me/daemons/{}", encode_path(id));
98        match client
99            .request::<serde_json::Value>(
100                Method::DELETE,
101                &path,
102                None::<&()>,
103                RequestOptions::default(),
104            )
105            .await
106        {
107            Ok(_) => Ok(()),
108            // Swallow 404 — caller likely already removed it.
109            Err(HeyoError::NotFound(_)) => Ok(()),
110            Err(e) => Err(e),
111        }
112    }
113}