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