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