huskies: merge 898
This commit is contained in:
@@ -22,4 +22,6 @@ pub use rest::{
|
||||
gateway_bot_config_save_handler, gateway_generate_token_handler, gateway_mode_handler,
|
||||
gateway_remove_project_handler,
|
||||
};
|
||||
pub use websocket::{gateway_crdt_sync_handler, gateway_event_push_handler};
|
||||
pub use websocket::{
|
||||
gateway_crdt_sync_handler, gateway_event_push_handler, gateway_sled_uplink_handler,
|
||||
};
|
||||
|
||||
@@ -146,6 +146,134 @@ pub async fn gateway_crdt_sync_handler(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// ── Sled uplink WebSocket handler ────────────────────────────────────────────
|
||||
|
||||
/// Query parameters accepted on the `/api/sled-uplink` WebSocket upgrade.
|
||||
#[derive(Deserialize)]
|
||||
struct SledUplinkParams {
|
||||
/// Shared-secret token identifying the connecting sled (from `[sled_tokens]` in `projects.toml`).
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
/// `GET /api/sled-uplink` — gateway-side WebSocket endpoint for sled permission uplinks.
|
||||
///
|
||||
/// # Authentication
|
||||
///
|
||||
/// The connecting sled must supply a valid shared-secret token via the `token`
|
||||
/// query parameter. Tokens are configured in `[sled_tokens]` in `projects.toml`
|
||||
/// as `sled_id = "secret"` entries.
|
||||
///
|
||||
/// # Protocol
|
||||
///
|
||||
/// See `sled_uplink.rs` for the wire format ([`UplinkEnvelope`]). The gateway
|
||||
/// accepts `perm_request` messages, injects them into the local permission
|
||||
/// pipeline (via `state.perm_tx`), and sends `perm_response` frames back to the
|
||||
/// sled once the Matrix bot resolves them. Multiple sleds are demuxed by
|
||||
/// connection: each handler owns exactly one sled's request/response flow.
|
||||
#[handler]
|
||||
pub async fn gateway_sled_uplink_handler(
|
||||
ws: WebSocket,
|
||||
state: Data<&Arc<GatewayState>>,
|
||||
Query(params): Query<SledUplinkParams>,
|
||||
) -> poem::Response {
|
||||
let token = match params.token {
|
||||
Some(t) if !t.is_empty() => t,
|
||||
_ => {
|
||||
return poem::Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body("token query parameter required");
|
||||
}
|
||||
};
|
||||
|
||||
let sled_id = match state.sled_tokens.get(&token) {
|
||||
Some(id) => id.clone(),
|
||||
None => {
|
||||
return poem::Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body("invalid token");
|
||||
}
|
||||
};
|
||||
|
||||
use poem::IntoResponse as _;
|
||||
let perm_tx = state.perm_tx.clone();
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let (mut sink, mut stream) = socket.split();
|
||||
// Aggregator channel: spawned per-request tasks send (req_id, decision) here
|
||||
// so the main loop can write perm_response frames back to the sled.
|
||||
let (agg_tx, mut agg_rx) = tokio::sync::mpsc::unbounded_channel::<(
|
||||
String,
|
||||
crate::http::context::PermissionDecision,
|
||||
)>();
|
||||
|
||||
crate::slog!("[gateway/sled-uplink] Sled '{}' connected", sled_id);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
let text = match msg {
|
||||
Some(Ok(WsMessage::Text(t))) => t,
|
||||
Some(Ok(WsMessage::Close(_))) | None => break,
|
||||
_ => continue,
|
||||
};
|
||||
let Ok(env) = serde_json::from_str::<crate::sled_uplink::UplinkEnvelope>(&text) else {
|
||||
continue;
|
||||
};
|
||||
if env.msg_type == "perm_request" {
|
||||
let req_id = env.req_id.clone();
|
||||
let tool_name = env.payload.get("tool_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let tool_input = env.payload.get("tool_input")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
|
||||
let fwd = crate::http::context::PermissionForward {
|
||||
request_id: format!("{sled_id}:{req_id}"),
|
||||
tool_name,
|
||||
tool_input,
|
||||
response_tx,
|
||||
};
|
||||
if perm_tx.send(fwd).is_err() {
|
||||
break;
|
||||
}
|
||||
let agg_tx2 = agg_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let decision = response_rx
|
||||
.await
|
||||
.unwrap_or(crate::http::context::PermissionDecision::Deny);
|
||||
let _ = agg_tx2.send((req_id, decision));
|
||||
});
|
||||
}
|
||||
}
|
||||
Some((req_id, decision)) = agg_rx.recv() => {
|
||||
use crate::http::context::PermissionDecision;
|
||||
let (approved, always_allow) = match decision {
|
||||
PermissionDecision::AlwaysAllow => (true, true),
|
||||
PermissionDecision::Approve => (true, false),
|
||||
PermissionDecision::Deny => (false, false),
|
||||
};
|
||||
let resp = crate::sled_uplink::UplinkEnvelope {
|
||||
msg_type: "perm_response".to_string(),
|
||||
req_id,
|
||||
payload: serde_json::json!({
|
||||
"approved": approved,
|
||||
"always_allow": always_allow,
|
||||
}),
|
||||
};
|
||||
let Ok(text) = serde_json::to_string(&resp) else { continue };
|
||||
if sink.send(WsMessage::Text(text)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::slog!("[gateway/sled-uplink] Sled '{}' disconnected", sled_id);
|
||||
})
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// ── Event-push WebSocket handler ─────────────────────────────────────────────
|
||||
|
||||
/// Query parameters accepted on the `/gateway/events/push` WebSocket upgrade.
|
||||
|
||||
Reference in New Issue
Block a user