huskies: merge 897

This commit is contained in:
dave
2026-05-12 22:46:55 +00:00
parent 541433d96e
commit b8ec3e2025
+100
View File
@@ -22,6 +22,9 @@ const GATEWAY_TOOLS: &[&str] = &[
"init_project", "init_project",
"aggregate_pipeline_status", "aggregate_pipeline_status",
"agents.list", "agents.list",
// Handled at the gateway so the Matrix bot's perm_rx listener is used
// rather than the container's (which has no interactive session attached).
"prompt_permission",
]; ];
/// Gateway tool definitions. /// Gateway tool definitions.
@@ -350,6 +353,7 @@ async fn handle_gateway_tool(
"init_project" => handle_init_project_tool(params, state, id).await, "init_project" => handle_init_project_tool(params, state, id).await,
"aggregate_pipeline_status" => handle_aggregate_pipeline_status_tool(state, id).await, "aggregate_pipeline_status" => handle_aggregate_pipeline_status_tool(state, id).await,
"agents.list" => handle_agents_list_tool(id), "agents.list" => handle_agents_list_tool(id),
"prompt_permission" => handle_prompt_permission_tool(params, state, id).await,
_ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")), _ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")),
} }
} }
@@ -531,6 +535,102 @@ async fn handle_aggregate_pipeline_status_tool(
) )
} }
/// Handle the `prompt_permission` tool at the gateway level.
///
/// Mirrors `tool_prompt_permission` in `http/mcp/diagnostics/permission.rs` but
/// uses the gateway's `perm_tx`/`perm_rx` so requests reach the Matrix bot that
/// is listening on the gateway, not the proxied container (which has no
/// interactive session and would auto-deny immediately).
async fn handle_prompt_permission_tool(
params: &Value,
state: &GatewayState,
id: Option<Value>,
) -> JsonRpcResponse {
use crate::http::context::PermissionDecision;
use crate::http::context::PermissionForward;
let args = params.get("arguments").unwrap_or(params);
let tool_name = args
.get("tool_name")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let tool_input = args.get("input").cloned().unwrap_or(json!({}));
// Auto-approve huskies MCP tools — mirrors the standard server's allowlist.
if tool_name.starts_with("mcp__huskies__") {
crate::slog!(
"[gateway/permission] Auto-approved '{tool_name}' (matches mcp__huskies__* allowlist)"
);
let text = json!({"behavior": "allow", "updatedInput": tool_input}).to_string();
return JsonRpcResponse::success(id, json!({"content": [{"type": "text", "text": text}]}));
}
// Auto-deny when no interactive session holds perm_rx (i.e. no Matrix bot
// listener is running — try_lock succeeds when nobody else holds the lock).
if state.perm_rx.try_lock().is_ok() {
crate::slog!("[gateway/permission] Auto-denied '{tool_name}' (no interactive session)");
let text = json!({
"behavior": "deny",
"message": format!("Permission denied for '{tool_name}'. No interactive session active.")
})
.to_string();
return JsonRpcResponse::success(id, json!({"content": [{"type": "text", "text": text}]}));
}
let request_id = uuid::Uuid::new_v4().to_string();
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
if state
.perm_tx
.send(PermissionForward {
request_id,
tool_name: tool_name.clone(),
tool_input: tool_input.clone(),
response_tx,
})
.is_err()
{
crate::slog!("[gateway/permission] Auto-denied '{tool_name}' (perm_tx send failed)");
let text =
json!({"behavior": "deny", "message": format!("Permission denied for '{tool_name}'.")})
.to_string();
return JsonRpcResponse::success(id, json!({"content": [{"type": "text", "text": text}]}));
}
let decision =
match tokio::time::timeout(std::time::Duration::from_secs(300), response_rx).await {
Ok(Ok(d)) => d,
Ok(Err(_)) => {
return JsonRpcResponse::error(
id,
-32603,
"Permission response channel closed unexpectedly".into(),
);
}
Err(_) => {
return JsonRpcResponse::error(
id,
-32603,
format!("Permission request for '{tool_name}' timed out after 5 minutes"),
);
}
};
let text = if matches!(
decision,
PermissionDecision::Approve | PermissionDecision::AlwaysAllow
) {
json!({"behavior": "allow", "updatedInput": tool_input}).to_string()
} else {
crate::slog_warn!("[gateway/permission] User denied permission for '{tool_name}'");
json!({"behavior": "deny", "message": format!("User denied permission for '{tool_name}'")})
.to_string()
};
JsonRpcResponse::success(id, json!({"content": [{"type": "text", "text": text}]}))
}
/// Handle the `agents.list` gateway tool — returns all alive build agents from the CRDT. /// Handle the `agents.list` gateway tool — returns all alive build agents from the CRDT.
fn handle_agents_list_tool(id: Option<Value>) -> JsonRpcResponse { fn handle_agents_list_tool(id: Option<Value>) -> JsonRpcResponse {
let agents = gateway::list_agents(); let agents = gateway::list_agents();