Skip to main content

hatchery_node/
lib.rs

1//! Native Gate4Agent node server and runtime.
2
3mod bundle_catalog;
4
5mod context_pack;
6
7mod harness_mcp_proxy;
8
9mod bundle_provider;
10
11mod git_worktree;
12pub mod plugins;
13pub mod provider_inventory;
14pub mod session_signals;
15
16mod host_directory_browser;
17
18mod environment_profiles;
19
20mod session_environment;
21
22mod worktree_service;
23
24pub(crate) use gate4agent_node_core::session_registry;
25
26mod standalone_workspace;
27
28#[cfg(windows)]
29mod workspace_file_windows;
30
31#[cfg(unix)]
32mod workspace_file_unix;
33
34#[cfg(feature = "dig2-station-probe")]
35mod dig2_station_lease;
36#[cfg(feature = "dig2-station-probe")]
37mod dig2_station_probe;
38mod kit;
39#[cfg(feature = "wireguard")]
40pub(crate) use gate4agent_node_core::kit_wireguard;
41mod network_allowlist_catalog;
42pub(crate) use gate4agent_node_core::platform;
43pub mod enroll_client;
44pub mod uplink_journal;
45pub mod restream_session;
46pub mod uplink_calls;
47pub mod machine_env;
48pub use machine_env::collect as collect_machine_environment;
49pub mod restream_g4a_bridge;
50#[cfg(feature = "d2b")]
51pub mod browser_capture;
52pub mod m4a_accounts;
53#[cfg(unix)]
54pub mod keystore_proxy;
55/// 0.2.15 name of [`keystore_proxy`].
56pub mod m4a_signer_proxy {
57    pub use super::keystore_proxy::*;
58}
59pub mod door;
60#[cfg(feature = "local-server")]
61pub mod local_server;
62pub mod wake_pipeline;
63#[cfg(unix)]
64pub mod isolation;
65pub use kit::{force_link, linked_core_names};
66#[cfg(feature = "m4a")]
67pub use kit::start_mailbox;
68#[cfg(feature = "m4a")]
69pub use kit::MailboxHandle;
70#[cfg(feature = "wireguard")]
71pub use kit_wireguard::{
72    bring_up_node_wireguard, node_wg_from_env, NodeWgPeerConfig, NodeWgPeerError,
73    LINK_TAG as WIREGUARD_LINK_TAG, WG_ADDRESS_ENV, WG_INTERFACE_ENV, WG_KEEPALIVE_ENV,
74    WG_LISTEN_PORT_ENV, WG_PEER_ADDRESS_ENV, WG_PEER_ENDPOINT_ENV, WG_PEER_KEY_ENV,
75    WG_PRIVATE_KEY_ENV,
76};
77mod provider_runtime;
78mod server;
79pub(crate) use gate4agent_node_core::spawn_spec;
80
81#[cfg(feature = "fixture")]
82pub use bundle_catalog::protect_bundle_source_tree_fixture;
83pub use bundle_catalog::{
84    BundleCatalog, BundleCatalogError, NodeBundle, NodeBundleError, NodeBundleFile,
85    MAX_BUNDLE_CATALOG_ENTRIES, MAX_BUNDLE_FILES, MAX_BUNDLE_FILE_BYTES, MAX_BUNDLE_PATH_BYTES,
86    MAX_BUNDLE_TOTAL_BYTES,
87};
88pub use environment_profiles::{
89    NodeEnvironmentProfile, NodeEnvironmentProfileError, MAX_NODE_ENVIRONMENT_PROFILES,
90};
91pub use gate4agent_node_protocol::WorktreeServiceMode;
92pub use gate4agent_runtime_native::{
93    orca_home_roots, HistorySourceLayout, NativeHistoryConfig, NativeHistoryRoot,
94};
95pub use network_allowlist_catalog::{
96    claude_allowed_domains_from_permits, claude_bash_sandbox_network_os_supported,
97    claude_settings_network_overlay_args, claude_station_network_settings_json,
98    codex_network_access_config_overlay, load_network_allowlist_catalog_file,
99    parse_network_allowlist_catalog_text, permit_peer_to_allowed_domain,
100    provider_native_mapping_supported, resolve_network_allowlist_catalog,
101    resolve_provider_native_launch_overlay, NetworkAllowlistCatalog, NetworkAllowlistCatalogError,
102    NetworkAllowlistEntry, NetworkPermitProtocol, NetworkPermitSketch, ProviderNativeNetworkSketch,
103    CLAUDE_STATION_NETWORK_SETTINGS_FILE, MAX_NETWORK_ALLOWLIST_CATALOG_ENTRIES,
104    MAX_NETWORK_PERMITS_PER_ENTRY, NETWORK_ALLOWLIST_CATALOG_ENV,
105    NETWORK_ALLOWLIST_CATALOG_SCHEMA_VERSION,
106};
107#[cfg(feature = "fixture")]
108pub use server::SpawnManagedWorktreeV2FailureProbe;
109pub use server::node_wake::{NodeLetter, NodeWake};
110pub use server::{
111    default_node_endpoint, default_state_path, NodeServer, NodeServerConfig, NodeServerError,
112    NodeShutdownHandle, WorkspaceConfig,
113};
114pub use session_environment::{
115    NodeSecretReference, NodeSecretResolveError, NodeSecretResolver, NodeSecretValue,
116    NodeSecretValueError, NodeSessionEnvironmentMutation, NodeSessionFile,
117    NodeSessionMaterializationProfile, NodeSessionMaterializationProfileError,
118    NodeSessionPathBinding, NodeSessionPathClass, MAX_NODE_SECRET_REFERENCE_BYTES,
119    MAX_NODE_SECRET_VALUE_BYTES, MAX_SESSION_ENVIRONMENT_ENTRIES,
120    MAX_SESSION_MATERIALIZATION_FILES, MAX_SESSION_MATERIALIZATION_FILE_BYTES,
121    MAX_SESSION_MATERIALIZATION_RELATIVE_PATH_BYTES,
122};
123pub use spawn_spec::{
124    SpawnProfileRegistry, SpawnProfileRegistryError, DEFAULT_SPAWN_PROFILE_ID, MAX_SPAWN_PROFILES,
125};
126pub use worktree_service::ManagedWorktreeProfile;
127
128#[cfg(windows)]
129pub use server::DEFAULT_NODE_ENDPOINT;
130
131pub use gate4agent_node_protocol as protocol;
132
133#[cfg(test)]
134mod kit_manifest {
135    #[test]
136    fn default_features_are_the_full_kit() {
137        let manifest = include_str!("../Cargo.toml");
138        let features = manifest
139            .split("[features]")
140            .nth(1)
141            .expect("features section")
142            .split("\n[")
143            .next()
144            .expect("features body");
145        assert!(
146            features.contains("default = [\"kit\"]"),
147            "default build must include the kit"
148        );
149        let kit = feature_array(features, "kit");
150        for core in ["m4a", "d2b", "session-restore", "wireguard", "memory"] {
151            assert!(
152                kit.iter().any(|item| item == core),
153                "kit feature missing {core}: {kit:?}"
154            );
155        }
156        assert!(
157            !features.contains("bare = "),
158            "there is no light mode"
159        );
160        let m4a = feature_array(features, "m4a");
161        assert_eq!(m4a, ["dep:mail4agent"]);
162        let d2b = feature_array(features, "d2b");
163        // The browser stream (websocket + frame encoding) rides with dig2browser.
164        assert_eq!(d2b, ["dep:dig2browser", "dep:tokio-tungstenite", "dep:futures-util", "dep:image"]);
165        let wg = feature_array(features, "wireguard");
166        assert_eq!(wg, ["gate4agent-node-core/wireguard"]);
167        let code = manifest
168            .lines()
169            .map(|line| line.split('#').next().unwrap_or(""))
170            .collect::<Vec<_>>()
171            .join("\n");
172        assert!(
173            !code.contains("hatchery-tui") && !code.contains("hatchery_tui"),
174            "node must not depend on the hatchery TUI"
175        );
176        assert!(manifest.contains("dig2browser = { version = \"0.5.3\""));
177        assert!(manifest.contains("mail4agent = { version = \"0.4.10\""));
178        assert!(!manifest.contains("[patch.crates-io]"), "published state has no patch table");
179        assert!(!manifest.contains("../../../"), "no path dependency leaves the workspace");
180        assert!(manifest.contains("claude-session-restore = { version = \"0.1.3\""));
181        assert!(manifest.contains("codex-session-restore = { version = \"0.1.3\""));
182        assert!(manifest.contains("grok-session-restore"));
183        assert!(manifest.contains("kimi-session-restore"));
184    }
185
186    fn feature_array(features: &str, name: &str) -> Vec<String> {
187        let line_start = features
188            .find(&format!("{name} = "))
189            .unwrap_or_else(|| panic!("missing feature {name}"));
190        let rest = &features[line_start + name.len() + 3..];
191        if rest.starts_with("[]") {
192            return Vec::new();
193        }
194        assert!(rest.starts_with("["), "{name} must be an array");
195        let end = rest.find(']').unwrap_or_else(|| panic!("{name} array"));
196        rest[1..end]
197            .split(',')
198            .map(|item| item.trim().trim_matches('"').to_owned())
199            .filter(|item| !item.is_empty())
200            .collect()
201    }
202}
203
204
205#[cfg(test)]
206mod deploy_artifacts {
207    //! Static checks for the three kit deploy modes. No Docker build, no
208    //! QEMU boot, no network. Lives in `--lib` so the ordinary node test
209    //! cycle refuses missing or gutted deploy entry points.
210
211    #[test]
212    fn service_unit_is_mode1_kit_entry() {
213        let unit = include_str!("../deploy/node/gate4agent-node.service");
214        assert!(unit.contains("ExecStart=/usr/local/bin/gate4agent-node"));
215        assert!(unit.contains("gate4agent node (standard kit)"));
216        assert!(
217            !unit.contains("INSTALL_PROVIDER_CLIS"),
218            "mode 1 unit must not install provider CLIs"
219        );
220        assert!(
221            unit.contains("GATE4AGENT_WG_INTERFACE") || unit.contains("WireGuard"),
222            "unit must document optional WG env"
223        );
224    }
225
226    #[test]
227    fn dockerfile_and_build_script_are_mode2() {
228        let dockerfile = include_str!("../deploy/node/Dockerfile");
229        assert!(dockerfile.contains("cargo build --locked --release -p hatchery-node"));
230        assert!(dockerfile.contains("ENTRYPOINT [\"gate4agent-node\"]"));
231        assert!(!dockerfile.contains("COPY mail4agent"));
232        assert!(!dockerfile.contains("COPY gate4agent"), "crates.io only: no sibling checkouts");
233        assert!(
234            !dockerfile.contains("GATE4AGENT_NODE_TOKEN"),
235            "image must not bake a node token"
236        );
237        let build = include_str!("../deploy/node/docker-build.sh");
238        assert!(build.contains("docker build"));
239        assert!(build.contains("gate4agent-node:kit"));
240        assert!(!build.contains("copy_tree \"$WS/mail4agent\""));
241    }
242
243    #[test]
244    fn qemu_guest_script_is_mode3_underlay_only() {
245        let script = include_str!("../deploy/node/qemu-guest.sh");
246        assert!(script.contains("Mode 3"));
247        assert!(script.contains("QEMU_NET"));
248        assert!(script.contains("tap") && script.contains("user"));
249        assert!(
250            script.contains("Kernel WireGuard is not a QEMU network"),
251            "script must keep WG out of the QEMU NIC story"
252        );
253        assert!(script.contains("GATE4AGENT_WG_"));
254        assert!(script.contains("gate4agent-node"));
255        // Guest boot stays opt-in: this test only reads the script text.
256        assert!(
257            !script.contains("cargo test"),
258            "qemu guest script must not be wired into cargo test"
259        );
260    }
261
262    #[test]
263    fn deploy_readme_names_three_modes() {
264        let readme = include_str!("../deploy/node/README.md");
265        assert!(readme.contains("Mode 1 — service"));
266        assert!(readme.contains("Mode 2 — container"));
267        assert!(readme.contains("Mode 3 — QEMU guest"));
268        assert!(readme.contains("no light mode"));
269    }
270}