Skip to main content

hatchery_node/
lib.rs

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