Skip to main content

hatchery_node/
kit.rs

1//! Standard node kit cores: dig2browser (`d2b`), mail4agent (`m4a`), the four
2//! session-restore crates (`session-restore`) and kernel WireGuard
3//! (`wireguard`). Each core is compiled only with its own feature; the default
4//! build (`kit`) enables all of them.
5
6use std::sync::OnceLock;
7
8/// Names of the cores linked into this build, in owner order.
9pub fn linked_core_names() -> &'static [&'static str] {
10    static NAMES: OnceLock<Vec<&'static str>> = OnceLock::new();
11    NAMES.get_or_init(|| {
12        let mut names: Vec<&'static str> = Vec::new();
13        #[cfg(feature = "d2b")]
14        names.push("dig2browser");
15        #[cfg(feature = "m4a")]
16        names.push("mail4agent");
17        #[cfg(feature = "session-restore")]
18        names.push("session-restore");
19        #[cfg(feature = "wireguard")]
20        names.push("wireguard");
21        names
22    })
23}
24
25/// Touches a real symbol in each enabled core so the node binary links them.
26/// Does not start a browser, a provider CLI, or a tunnel, and does not read
27/// a secret. The mailbox is started separately by [`start_mailbox`].
28pub fn force_link() -> usize {
29    let mut acc: usize = 0;
30    #[cfg(feature = "d2b")]
31    {
32        acc = acc.wrapping_add(dig2browser::browser_stream::LOCAL_PAGE_HTML.len());
33    }
34    #[cfg(feature = "m4a")]
35    {
36        let mail: fn() -> Result<(), mail4agent::MainError> = mail4agent::run;
37        acc = acc.wrapping_add(mail as usize);
38    }
39    #[cfg(feature = "session-restore")]
40    {
41        let claude = std::mem::size_of::<claude_session_restore::transcript::SessionEvent>();
42        let codex = codex_session_restore::MAX_HEAD_BYTES;
43        let grok: fn() = grok_session_restore::main;
44        let kimi: fn() = kimi_session_restore::main;
45        acc = acc
46            .wrapping_add(claude)
47            .wrapping_add(codex)
48            .wrapping_add(grok as usize)
49            .wrapping_add(kimi as usize);
50    }
51    #[cfg(feature = "wireguard")]
52    {
53        let wireguard: fn(
54            &crate::kit_wireguard::NodeWgPeerConfig,
55        ) -> Result<std::net::IpAddr, crate::kit_wireguard::NodeWgPeerError> =
56            crate::kit_wireguard::bring_up_node_wireguard;
57        acc = acc
58            .wrapping_add(wireguard as usize)
59            .wrapping_add(crate::kit_wireguard::LINK_TAG.len());
60    }
61    acc
62}
63
64/// Start the mail4agent mailbox on a background thread (`m4a` only).
65/// Same daemon the standalone `mail4agent` binary runs.
66#[cfg(feature = "m4a")]
67///
68/// The daemon's own failure (for example a bootstrap refusal) is logged and
69/// visible through [`MailboxHandle::is_finished`]; it is never swallowed.
70pub fn start_mailbox() -> Result<MailboxHandle, mail4agent::MainError> {
71    let join = std::thread::Builder::new()
72        .name("mail4agent".to_owned())
73        .spawn(|| {
74            let result = mail4agent::run();
75            if let Err(error) = &result {
76                tracing::error!(%error, "mail4agent mailbox stopped");
77            }
78            result
79        })
80        .map_err(mail4agent::MainError::Runtime)?;
81    Ok(MailboxHandle { join })
82}
83
84/// Handle of the background mailbox thread.
85#[cfg(feature = "m4a")]
86pub struct MailboxHandle {
87    join: std::thread::JoinHandle<Result<(), mail4agent::MainError>>,
88}
89
90#[cfg(feature = "m4a")]
91impl MailboxHandle {
92    /// True once the mailbox thread has returned (it only returns on failure).
93    pub fn is_finished(&self) -> bool {
94        self.join.is_finished()
95    }
96
97    /// Block until the mailbox thread ends and return its result.
98    pub fn join(self) -> Result<(), mail4agent::MainError> {
99        self.join.join().unwrap_or_else(|_| {
100            Err(mail4agent::MainError::Runtime(std::io::Error::other("mail4agent thread panicked")))
101        })
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[cfg(all(feature = "d2b", feature = "m4a", feature = "session-restore", feature = "wireguard"))]
110    #[test]
111    fn kit_names_the_four_cores() {
112        assert_eq!(
113            linked_core_names(),
114            ["dig2browser", "mail4agent", "session-restore", "wireguard"]
115        );
116        assert!(force_link() > 0);
117    }
118
119    #[test]
120    fn linked_names_match_enabled_features() {
121        let expected = cfg!(feature = "d2b") as usize
122            + cfg!(feature = "m4a") as usize
123            + cfg!(feature = "session-restore") as usize
124            + cfg!(feature = "wireguard") as usize;
125        assert_eq!(linked_core_names().len(), expected);
126    }
127
128    #[cfg(feature = "m4a")]
129    #[test]
130    fn kit_exposes_mailbox_automaton_entry() {
131        let start: fn() -> Result<MailboxHandle, mail4agent::MainError> = start_mailbox;
132        assert!(start as usize != 0);
133    }
134}