agentmux_srv/main.rs
1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4mod adoption;
5mod agents;
6mod ambient;
7mod backend;
8mod boot_timing;
9mod bootstrap;
10mod broker;
11mod config;
12mod event_log;
13mod headless;
14mod identity;
15mod migrations;
16mod persist;
17mod persist_subscriber;
18mod proc_name;
19mod reducer;
20mod registry;
21mod sagas;
22mod server;
23#[cfg(target_os = "windows")]
24mod srv_ipc;
25mod srv_info;
26mod state;
27mod drone;
28mod messaging;
29mod muxbus;
30mod util;
31#[cfg(windows)]
32mod crash_monitor;
33#[cfg(test)]
34mod test_support;
35
36use std::future::IntoFuture;
37use std::sync::Arc;
38
39use server::build_routers;
40
41#[tokio::main]
42async fn main() {
43 // -2. Document reading child: srv re-runs itself to parse one attached
44 // document (PDF, Word, Excel, PowerPoint, ODF, RTF) out of process,
45 // since parsers can crash, recurse or run long on hostile input and
46 // the parent kills the child at its deadline
47 // (SPEC_AGENT_PANE_FILE_ATTACHMENTS_2026_09_26.md §6). Nothing else
48 // may start in this mode.
49 {
50 let args: Vec<String> = std::env::args().collect();
51 if args.get(1).map(String::as_str) == Some(backend::attachments::extract::CHILD_ARG) {
52 proc_name::set(proc_name::DOC_CHILD);
53 std::process::exit(backend::attachments::extract::run_child(&args[2..]));
54 }
55 }
56 // -1. Crash monitor branch — must be checked before any other initialization.
57 if bootstrap::maybe_run_crash_monitor() {
58 return;
59 }
60 // Linux: a stable name for ps/top instead of the versioned file name's
61 // first 15 bytes. This runs on the main thread (tokio's block_on).
62 proc_name::set(proc_name::MAIN);
63
64 // Before anything changes the environment (srv_info.rs).
65 srv_info::capture_home_dir();
66
67 // 0. Headless (no launcher/host): prepare the env the launcher would have
68 // provided, and don't tie srv's life to a parent or stdin. Otherwise start
69 // the parent process watcher BEFORE tokio runtime does real work
70 // (Linux/macOS only). docs/specs/SPEC_SRV_HEADLESS_MODE_2026_09_26.md.
71 let headless = headless::requested();
72 if headless {
73 match headless::prepare_env() {
74 Ok(Some(key_file)) => eprintln!("agentmux-srv --headless: auth key written to {}", key_file.display()),
75 Ok(None) => {}
76 Err(e) => {
77 eprintln!("agentmux-srv --headless: {e}");
78 std::process::exit(1);
79 }
80 }
81 } else {
82 bootstrap::install_process_watchers();
83 }
84
85 // 0b. Attach out-of-process crash dump handler (Windows only).
86 // _crash_guard must stay alive — dropping it uninstalls the VEH handler.
87 #[cfg(windows)]
88 let _crash_guard = bootstrap::install_crash_guard();
89
90 // 0c. Anchor the uptime clock before anything else can take time. Must be
91 // neither a wall-clock stamp (moves backwards on a clock step) nor an
92 // `Instant` (stops while suspended) — see `suspend_aware_now_ms` in
93 // `backend::sysinfo` for both bugs and the per-platform sources.
94 backend::sysinfo::mark_process_start();
95
96 // 1. Init tracing (stderr + rolling file)
97 let _log_guard = bootstrap::init_logging();
98
99 // 1a. Start the boot-path stopwatch. Logged as one summary line at
100 // ESTART below — see `boot_timing`'s module doc for why these numbers
101 // go to the log rather than the splash.
102 boot_timing::init();
103
104 // 1b. Direct-launch PATH fallback.
105 bootstrap::enrich_path();
106
107 // 2. Parse CLI args and build config (dispatches `migrate` subcommand internally).
108 let config = bootstrap::load_config();
109 let version = config.version.to_string();
110 let build_time = config.build_time.to_string();
111
112 // 4. Initialize backend: data dir, in-process migrations, open every store,
113 // attach shared/global registries, run one-time seed/repair passes.
114 let stores = boot_timing::time("open_stores_and_migrate", || {
115 bootstrap::open_stores_and_migrate(&config, &version, &build_time)
116 });
117
118 // Event infrastructure + every background task that doesn't need AppState yet.
119 let bg = bootstrap::spawn_background_subsystems(
120 &stores.mstore,
121 &stores.filestore,
122 &stores.id_store,
123 &stores.identity_store,
124 );
125
126 // Widget packages: scan ~/.agentmux/widgets, apply this instance's
127 // approvals, merge their widget-bar entries, watch the folder
128 // (SPEC_USER_WIDGETS_AND_WIDGET_API_2026_10_09.md §8.1). After the user's
129 // widgets.json loaded above, whose v1 module entries it lists too.
130 backend::widget_packages::start(
131 &config.data_home,
132 config.auth_key.clone(),
133 bg.fs_watch_pool.clone(),
134 bg.config_watcher.clone(),
135 bg.event_bus.clone(),
136 bg.broker.clone(),
137 );
138
139 // 5. Bind TCP listeners, bring up LAN discovery / LSP supervisor / process tracker.
140 let net = boot_timing::time_async(
141 "bind_listeners_and_network",
142 bootstrap::bind_listeners_and_network(&config, &bg.event_bus, &bg.broker, &version),
143 )
144 .await;
145
146 // Phase E.2 — srv reducer plumbing (state, event bus, event log, persist subscriber).
147 let reducer = bootstrap::spawn_reducer_plumbing(&stores.mstore, &bg.event_bus, &bg.subagent_watcher).await;
148
149 let state = bootstrap::build_app_state(&config, version.clone(), stores, bg, &net, &reducer);
150
151 // Upgrade reactive message delivery now that AppState exists, so agents on a
152 // SubprocessController (every container agent, plus codex/gemini/qwen/kimi/
153 // muxcode/antigravity on the host) can actually receive inter-agent
154 // messages instead of having them dropped on a PTY fallback they reject.
155 bootstrap::install_agent_turn_delivery(&state);
156
157 // Cron fires through the same in-process inject path (identity M4c-3);
158 // before `cron_scheduler.start()` below, so no fire takes the HTTP
159 // fallback.
160 bootstrap::install_cron_delivery(&state);
161
162 // Durable jekt: replay messages held for an agent that was not running
163 // (SPEC_DURABLE_JEKT_DELIVERY_2026_09_24.md).
164 server::jekt_held::install(&state);
165
166 // The helper install asks first (SPEC_REMOTES_PANE_2026_10_05.md §4.9).
167 server::app_api::connections::install_helper_consent(&state);
168
169 // Now that AppState exists, wire up close-on-exit so a shell pane can
170 // actually close itself when its process exits — see
171 // `bootstrap::install_close_on_exit_handler`'s doc comment.
172 bootstrap::install_close_on_exit_handler(&state);
173
174 // Backstop for agent blocks left in a tab but in no pane — see
175 // `sagas::orphan_reaper`'s module doc.
176 sagas::orphan_reaper::install(&state);
177
178 // Out-of-band native-memory write detection (fast fs-watch path + slow
179 // reconciliation-sweep path) — see
180 // docs/specs/SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md §4.5.
181 backend::native_memory_drift::spawn(
182 state.fs_watch_pool.clone(),
183 state.mstore.clone(),
184 state.id_store.clone(),
185 state.broker.clone(),
186 );
187
188 // Retention/GC for native-memory version history — see
189 // docs/specs/SPEC_MEMORY_VERSION_CONTROL_AND_ARMORY_AUDIT_2026_08_19.md §7.1.
190 backend::native_memory_retention::spawn(state.id_store.clone());
191
192 // Start persistent cron scheduler — load enabled jobs from DB, fire any
193 // that missed their window (FIRE_ONCE_NOW), schedule all for future fires.
194 state.cron_scheduler.start().await;
195
196 // Saga durability PR 2 — resume-on-startup. Walk any sagas the
197 // durable log says are unresolved (running / compensating /
198 // failed) from a prior srv-process run, dispatch their inverse
199 // commands, and mark them compensated. Runs AFTER reducer
200 // bootstrap + persist subscriber spawn so the recovery's reducer
201 // dispatches operate against fully-populated state, BUT BEFORE
202 // the API server starts accepting requests so resumed
203 // compensation can't interleave with new sagas.
204 //
205 // Failure here is non-fatal: the saga log read might be transient,
206 // and starting up without recovery beats refusing to start.
207 // Operator can still inspect via `--diag sagas` (PR 2 part 2).
208 let resumed = boot_timing::time_async(
209 "saga_resume_on_startup",
210 sagas::recovery::compensate_unresolved(&state),
211 )
212 .await
213 .unwrap_or_else(|e| {
214 tracing::error!(
215 "[saga] resume-on-startup failed: {} — continuing; operator review needed",
216 e
217 );
218 0
219 });
220 if resumed > 0 {
221 tracing::info!(
222 "[saga] resume-on-startup compensated {} unresolved saga(s) from prior run",
223 resumed
224 );
225 }
226
227 // Phase E.1b — srv pipe IPC server (Windows only; conditional on
228 // AGENTMUX_SRV_PIPE_PATH). Bind happens BEFORE the AGENTMUXSRV-ESTART
229 // line so the launcher knows the pipe is ready when host starts.
230 #[cfg(target_os = "windows")]
231 bootstrap::bind_srv_pipe_ipc(
232 &version,
233 Arc::clone(&state.srv_state),
234 state.srv_events_tx.clone(),
235 Arc::clone(&reducer.srv_event_log),
236 );
237
238 // 6. Emit AGENTMUXSRV-ESTART on stderr (exact format from cmd/server/main-server.go:617)
239 bootstrap::emit_estart(
240 net.ws_addr.port(),
241 net.web_addr.port(),
242 &version,
243 &build_time,
244 &config.instance_id,
245 );
246
247 boot_timing::log_summary();
248
249 // 7. Build router and serve on both listeners
250 // Clone Arcs that are needed after `state` is moved into build_routers.
251 let state_for_exit = state.clone();
252 let wal_mstore = Arc::clone(&state.mstore);
253 let wal_filestore = Arc::clone(&state.filestore);
254 let config_watcher_for_lan = Arc::clone(&state.config_watcher);
255 let dev_proxy_registry = state.dev_proxy.clone();
256 let fleet_feed = Arc::clone(&state.fleet_feed);
257 let fleet_source = backend::fleet_source::FleetSource::live(
258 state.reactive_handler,
259 Arc::clone(&state.mstore),
260 Arc::clone(&state.config_watcher),
261 state.local_web_url.clone(),
262 );
263 let presence_id_store = Arc::clone(&state.id_store);
264 let presence_config = Arc::clone(&state.config_watcher);
265 let viewer = Arc::clone(&state.viewer);
266 let viewer_router = server::build_viewer_router(state.clone());
267 // Keeps the host's copy of which panes agents own current (for its popup
268 // decisions; docs/specs/SPEC_BROWSER_PANE_NATIVE_POPUPS_AGENT_DRIVEN_2026_10_08.md §3).
269 server::browser_host_sync::spawn(state.clone());
270 // Overlap notes (an agent editing a file another agent is also changing
271 // is told who): the progress watcher's edits are checked from here on.
272 backend::work_facts::overlap_notes::install(
273 Arc::clone(&state.mstore),
274 Arc::clone(&state.identity_store),
275 Arc::clone(&state.config_watcher),
276 state.http_client.clone(),
277 state.local_web_url.clone(),
278 );
279 // An agent's AgentMux MCP server that didn't connect is noticed and
280 // repaired (`agent:mcpselfheal`).
281 backend::mcp_health::install(Arc::clone(&state.config_watcher));
282 let routers = build_routers(state);
283 let router = routers.full;
284
285 // Native dev-proxy (docs/specs/SPEC_NATIVE_CONTAINER_DEV_PROXY_2026_09_19.md)
286 // — a small standalone HTTP server on its own fixed port, separate from
287 // the web/ws listeners above (Host-header routing to container-internal
288 // addresses, not part of AgentMux's own API surface). Detached: a bind
289 // failure or later error is logged inside `serve` itself and never
290 // fatal to srv startup, same posture as `lan_listeners`' reconcile loop.
291 tokio::spawn(backend::dev_proxy::serve(dev_proxy_registry));
292
293 // Hand the LAN listener supervisor its router now that one exists —
294 // `build_routers` consumes `AppState`, which owns the supervisor, so this
295 // cannot happen at construction time. Then honor the current setting and
296 // start the self-healing sweep, which also picks up interface changes
297 // (DHCP renewal, Wi-Fi↔Ethernet handoff, VPN up/down) without a restart.
298 // See `backend::lan_listeners`.
299 net.lan_listeners.set_router(routers.lan);
300 // The viewer listener (backend::viewer) comes up beside the LAN
301 // listeners, on its own port of the same range.
302 net.lan_listeners
303 .set_viewer(viewer_router, viewer, backend::lan_ports::LAN_PORT_RANGE);
304 net.lan_listeners
305 .apply(config_watcher_for_lan.get_settings().network_lan_discovery);
306 Arc::clone(&net.lan_listeners).spawn_reconcile_loop();
307
308 let web_server = axum::serve(net.web_listener, router.clone());
309 let ws_server = axum::serve(net.ws_listener, router);
310
311 // 8 & 9. Spawn stdin watch thread + SIGINT/SIGTERM handler (graceful shutdown).
312 let stdin_token = bootstrap::install_shutdown_handlers(!headless);
313
314 // Periodic WAL checkpoint — prevents unbounded WAL file growth during
315 // long-running sessions.
316 bootstrap::spawn_wal_checkpoint_loop(stdin_token.clone(), wal_mstore, wal_filestore);
317
318 // The fleet feed's change detection (`backend::fleet_feed`): compares the
319 // reachable agents, their kinds and states and this machine's channel
320 // count once a second, stops with the same token.
321 Arc::clone(&fleet_feed).spawn_change_detection(move || fleet_source.observe(), stdin_token.clone());
322
323 // The cloud presence record, published from the fleet feed's snapshot
324 // while signed in (`muxbus::wan_presence`). Here rather than beside
325 // `wan_publish::spawn` because the feed exists only once AppState does.
326 // A headless srv doesn't publish unless forced (`wan_presence::policy`).
327 muxbus::wan_presence::spawn(fleet_feed, presence_id_store, presence_config, headless, stdin_token.clone());
328
329 // Run both servers until shutdown
330 tokio::select! {
331 result = web_server.into_future() => {
332 if let Err(e) = result {
333 tracing::error!("web server error: {}", e);
334 }
335 }
336 result = ws_server.into_future() => {
337 if let Err(e) = result {
338 tracing::error!("ws server error: {}", e);
339 }
340 }
341 _ = stdin_token.cancelled() => {
342 tracing::info!("shutdown signal received, exiting");
343 }
344 }
345
346 // This host's other LAN channels stop listing this one as a sibling.
347 net.lan_discovery.withdraw_instance_record();
348
349 // Shutdown cleanup: close every agent through the one teardown
350 // (SPEC_AGENT_TEARDOWN_SINGLE_PATH_2026_10_01.md, `Policy::app_exit()`),
351 // so each gets to end its turn and exit and nothing it started (`task dev`
352 // → task.exe/node, `Shell()` sessions) orphans on srv exit. Capped at
353 // `agent_teardown::APP_EXIT_CAP`; the launcher's backstop takes whatever
354 // is left. Beside it, the cloud presence goodbye (at most 2 s, so it
355 // never outlasts the teardown's own cap): devices show this computer as
356 // offline at once instead of minutes later.
357 let (closed, ()) = tokio::join!(
358 sagas::agent_teardown::app_exit(&state_for_exit),
359 muxbus::wan_presence::goodbye_on_shutdown(),
360 );
361 tracing::info!(agents = closed, "shutdown: agents closed");
362}