huskies: merge 1180 story Sled↔gateway goes WS-only: remove the deprecated HTTP fallback paths

This commit is contained in:
Huskies Agent
2026-07-16 18:10:14 +00:00
parent 0e684fb06f
commit de7d22cb9f
13 changed files with 203 additions and 1053 deletions
+1 -89
View File
@@ -7,7 +7,7 @@
use super::config::{GatewayConfig, ProjectEntry};
pub use reqwest::Client;
use serde_json::{Value, json};
use std::collections::{BTreeMap, HashMap};
use std::collections::BTreeMap;
use std::path::Path;
// ── Config I/O ───────────────────────────────────────────────────────────────
@@ -239,28 +239,6 @@ pub fn read_installed_manifest(
// ── MCP proxy I/O ───────────────────────────────────────────────────────────
/// Proxy a raw MCP request body to the given project URL.
pub async fn proxy_mcp_call(
client: &Client,
base_url: &str,
request_bytes: &[u8],
) -> Result<Vec<u8>, String> {
let mcp_url = format!("{}/mcp", base_url.trim_end_matches('/'));
let resp = client
.post(&mcp_url)
.header("Content-Type", "application/json")
.body(request_bytes.to_vec())
.send()
.await
.map_err(|e| format!("failed to reach {mcp_url}: {e}"))?;
resp.bytes()
.await
.map(|b| b.to_vec())
.map_err(|e| format!("failed to read response from {mcp_url}: {e}"))
}
/// Proxy an MCP `tools/call` request to the sled with `Accept: text/event-stream`
/// and return the raw response for streaming. No per-request timeout is applied
/// so long-running tool calls (e.g. `run_tests`, up to 1200 s) are not cut short.
@@ -563,72 +541,6 @@ pub fn spawn_gateway_broadcaster_forwarder(
});
}
/// Spawn a background task that polls events from all project servers.
pub fn spawn_gateway_notification_poller(
transport: std::sync::Arc<dyn crate::chat::ChatTransport>,
room_ids: Vec<String>,
project_urls: BTreeMap<String, String>,
poll_interval_secs: u64,
) {
tokio::spawn(async move {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.unwrap_or_else(|_| Client::new());
let interval = std::time::Duration::from_secs(poll_interval_secs.max(1));
let mut last_ts: HashMap<String, u64> = project_urls
.keys()
.map(|name| (name.clone(), 0u64))
.collect();
loop {
for (project_name, base_url) in &project_urls {
let since = last_ts.get(project_name).copied().unwrap_or(0);
let url = format!("{base_url}/api/events?since={since}");
let response = match client.get(&url).send().await {
Ok(r) => r,
Err(e) => {
crate::slog!(
"[gateway-poller] {project_name}: unreachable ({e}); skipping"
);
continue;
}
};
let events: Vec<crate::service::events::StoredEvent> = match response.json().await {
Ok(v) => v,
Err(e) => {
crate::slog!(
"[gateway-poller] {project_name}: failed to parse events: {e}"
);
continue;
}
};
for event in &events {
let ts = event.timestamp_ms();
if ts > *last_ts.get(project_name).unwrap_or(&0) {
last_ts.insert(project_name.clone(), ts);
}
let (plain, html) = super::polling::format_gateway_event(project_name, event);
for room_id in &room_ids {
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
crate::slog!(
"[gateway-poller] Failed to send notification to {room_id}: {e}"
);
}
}
}
}
tokio::time::sleep(interval).await;
}
});
}
// ── Gateway bot spawn ───────────────────────────────────────────────────────
/// Re-export type alias for the active project lock.
+15 -12
View File
@@ -24,7 +24,6 @@ pub use config::{GatewayConfig, ProjectEntry};
pub use identity::{IdentityCheck, check_identity};
pub use io::{
fetch_all_project_pipeline_statuses, probe_identity, spawn_gateway_broadcaster_forwarder,
spawn_gateway_notification_poller,
};
use crate::http::context::PermissionForward;
@@ -364,21 +363,25 @@ impl GatewayState {
}
}
/// Proxy an MCP request to the active project, preferring the live
/// sled-uplink WebSocket when available (story 899 AC 2) and falling
/// back to HTTP otherwise.
/// Proxy an MCP request to the active project over its live sled-uplink
/// WebSocket (story 899 AC 2).
///
/// The gateway is WS-only for MCP proxying (story 1180): when no live
/// uplink connection exists for the active project, this returns an
/// immediate, actionable error naming the sled rather than falling back
/// to HTTP. Callers (e.g. bot chat commands) surface this error straight
/// to the user instead of hanging on an unreachable HTTP endpoint.
///
/// Returns the raw response body bytes ready to be relayed to the caller.
pub async fn proxy_active_mcp(&self, bytes: &[u8]) -> Result<Vec<u8>, String> {
if let Some(conn) = self.active_sled_connection().await {
return proxy_mcp_via_ws(&conn, bytes).await;
let name = self.active_project.read().await.clone();
match self.active_sled_connection().await {
Some(conn) => proxy_mcp_via_ws(&conn, bytes).await,
None => Err(format!(
"sled '{name}' has no live WS uplink connection; \
ensure the sled is running and connected to this gateway"
)),
}
let url = self.active_url().await.map_err(|e| e.to_string())?;
crate::slog!(
"[gateway] MCP proxy: WS uplink unavailable, falling back to HTTP \
(deprecated, will be removed once all sleds are WS-only)"
);
crate::service::gateway::io::proxy_mcp_call(&self.client, &url, bytes).await
}
}