huskies: merge 897
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user