Skip to main content

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}