Files
huskies/server/src/gateway/mod.rs
T

145 lines
5.3 KiB
Rust
Raw Normal View History

2026-04-29 00:29:54 +00:00
//! Multi-project gateway — entrypoint wiring and route tree.
//!
//! When `huskies --gateway` is used, the server starts in gateway mode.
//! Business logic lives in `service::gateway`, HTTP handlers in `http::gateway`.
//! This file contains only the `run` entrypoint and `build_gateway_route` wiring.
use crate::http::gateway::*;
2026-05-12 16:30:55 +00:00
use crate::rebuild::ShutdownReason;
2026-04-29 00:29:54 +00:00
use crate::service::gateway::{self, GatewayState};
use poem::EndpointExt;
use std::path::Path;
use std::sync::Arc;
// Re-export public types that callers reference as `crate::gateway::*`.
pub use crate::service::gateway::{
GatewayConfig, GatewayState as GatewayStateType, GatewayStatusEvent, ProjectEntry,
broadcast_status_event, fetch_all_project_pipeline_statuses, format_aggregate_status_compact,
spawn_gateway_broadcaster_forwarder, spawn_gateway_notification_poller,
subscribe_status_events,
};
/// Build the complete gateway route tree.
///
/// Extracted from `run` so that tests can construct the full route tree and
/// catch duplicate-route panics before they reach production.
pub fn build_gateway_route(state_arc: Arc<GatewayState>) -> impl poem::Endpoint {
poem::Route::new()
.at("/bot-config", poem::get(gateway_bot_config_page_handler))
.at("/api/gateway", poem::get(gateway_api_handler))
.at(
"/api/gateway/projects",
poem::post(gateway_add_project_handler),
)
.at(
"/api/gateway/projects/:name",
poem::delete(gateway_remove_project_handler),
)
.at(
"/api/gateway/bot-config",
poem::get(gateway_bot_config_get_handler).post(gateway_bot_config_save_handler),
)
.at(
"/mcp",
poem::post(gateway_mcp_post_handler).get(gateway_mcp_get_handler),
)
// Agent join endpoints.
.at("/gateway/mode", poem::get(gateway_mode_handler))
.at(
"/gateway/tokens",
poem::post(gateway_generate_token_handler),
)
.at(
"/gateway/events/push",
poem::get(gateway_event_push_handler),
)
// Agent registration via CRDT-sync WebSocket.
.at("/crdt-sync", poem::get(gateway_crdt_sync_handler))
2026-05-12 21:29:04 +00:00
// Sled uplink: permission-forwarding WebSocket from sleds to gateway.
.at("/api/sled-uplink", poem::get(gateway_sled_uplink_handler))
2026-04-29 00:29:54 +00:00
// Agent management REST endpoints.
.at(
"/gateway/agents/:id/assign",
poem::post(gateway_assign_agent_handler),
)
.data(state_arc)
}
/// Start the gateway HTTP server. This is the entry point when `--gateway` is used.
pub async fn run(config_path: &Path, port: u16) -> Result<(), std::io::Error> {
let config_dir = config_path
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf();
let config = gateway::io::load_config(config_path).map_err(std::io::Error::other)?;
// Initialise the CRDT so gateway_config.active_project is persisted across restarts.
let crdt_db = config_dir.join("gateway.db");
if let Err(e) = crate::crdt_state::init(&crdt_db).await {
crate::slog!(
"[gateway] Warning: CRDT init failed ({e}); active-project selection will not persist"
);
}
let state =
GatewayState::new(config, config_dir.clone(), port).map_err(std::io::Error::other)?;
let state_arc = Arc::new(state);
let active = state_arc.active_project.read().await.clone();
crate::slog!("[gateway] Starting gateway on port {port}, active project: {active}");
crate::slog!(
"[gateway] Registered projects: {}",
state_arc
.projects
.read()
.await
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
);
// Write `.mcp.json` so that the gateway's bot connects to this gateway's MCP endpoint.
if let Err(e) = gateway::io::write_gateway_mcp_json(&config_dir, port) {
crate::slog!("[gateway] Warning: could not write .mcp.json: {e}");
}
// Spawn the Matrix bot if `.huskies/bot.toml` exists in the config directory.
2026-05-12 16:30:55 +00:00
let (bot_abort, bot_shutdown_tx) = gateway::io::spawn_gateway_bot(
2026-04-29 00:29:54 +00:00
&config_dir,
Arc::clone(&state_arc.active_project),
Arc::clone(&state_arc.projects),
2026-04-29 00:29:54 +00:00
port,
Some(state_arc.event_tx.clone()),
2026-05-12 21:29:04 +00:00
Arc::clone(&state_arc.perm_rx),
2026-04-29 00:29:54 +00:00
);
*state_arc.bot_handle.lock().await = bot_abort;
2026-05-12 16:30:55 +00:00
*state_arc.bot_shutdown_tx.lock().await = Some(bot_shutdown_tx);
2026-04-29 00:29:54 +00:00
2026-05-12 16:30:55 +00:00
let route = build_gateway_route(state_arc.clone());
2026-04-29 00:29:54 +00:00
let host = std::env::var("HUSKIES_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let addr = format!("{host}:{port}");
crate::slog!("[gateway] Listening on {addr}");
2026-05-12 16:30:55 +00:00
let result = poem::Server::new(poem::listener::TcpListener::bind(&addr))
2026-04-29 00:29:54 +00:00
.run(route)
2026-05-12 16:30:55 +00:00
.await;
// Best-effort shutdown notification: signal the Matrix bot so it can post
// "going offline" before the process exits. Mirror of main.rs:346.
{
let guard = state_arc.bot_shutdown_tx.lock().await;
if let Some(tx) = guard.as_ref() {
let _ = tx.send(Some(ShutdownReason::Manual));
}
}
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
result
2026-04-29 00:29:54 +00:00
}
#[cfg(test)]
mod tests;