huskies: merge 832
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
//! 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::*;
|
||||
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))
|
||||
// Agent management REST endpoints.
|
||||
.at(
|
||||
"/gateway/agents/:id/assign",
|
||||
poem::post(gateway_assign_agent_handler),
|
||||
)
|
||||
// Serve the embedded React frontend so the gateway has a UI.
|
||||
.at(
|
||||
"/assets/*path",
|
||||
poem::get(crate::http::assets::embedded_asset),
|
||||
)
|
||||
.at("/*path", poem::get(crate::http::assets::embedded_file))
|
||||
.at("/", poem::get(crate::http::assets::embedded_index))
|
||||
.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.
|
||||
let gateway_projects: Vec<String> = state_arc.projects.read().await.keys().cloned().collect();
|
||||
let gateway_project_urls: std::collections::BTreeMap<String, String> = state_arc
|
||||
.projects
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(name, entry)| (name.clone(), entry.url.clone()))
|
||||
.collect();
|
||||
let bot_abort = gateway::io::spawn_gateway_bot(
|
||||
&config_dir,
|
||||
Arc::clone(&state_arc.active_project),
|
||||
gateway_projects,
|
||||
gateway_project_urls,
|
||||
port,
|
||||
Some(state_arc.event_tx.clone()),
|
||||
);
|
||||
*state_arc.bot_handle.lock().await = bot_abort;
|
||||
|
||||
let route = build_gateway_route(state_arc);
|
||||
|
||||
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}");
|
||||
|
||||
poem::Server::new(poem::listener::TcpListener::bind(&addr))
|
||||
.run(route)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user