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