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}