huskies: merge 1207 story fleet_resources MCP tool: on-demand host + per-container disk, load, and CPU/mem

This commit is contained in:
Huskies Agent
2026-07-18 01:43:24 +00:00
parent 5243854129
commit 25bc952dff
4 changed files with 932 additions and 0 deletions
+87
View File
@@ -29,6 +29,8 @@ const GATEWAY_TOOLS: &[&str] = &[
"prompt_permission",
// One-shot container rebuild: build fresh image, swap container, preserve state.
"project_rebuild",
// On-demand host + per-container disk/load/CPU/mem snapshot (story 1207).
"fleet_resources",
// Read sled identity pins vs. live signed identity, and TOFU re-pin.
"fleet_identity",
];
@@ -156,6 +158,31 @@ pub(crate) fn gateway_tool_definitions() -> Vec<Value> {
"required": ["name"]
}
}),
json!({
"name": "fleet_resources",
"description": "On-demand host and per-container resource snapshot: host disk free/total, load averages, core count, and memory; per-container CPU%/mem sourced via `docker stats`; and per-project `target/`+`.huskies/worktrees/` directory sizes sourced via `docker exec ... find` (bounded, skips node_modules/.git, cached briefly per container+path). Flags disk/load conditions past thresholds first so problems lead the response.",
"inputSchema": {
"type": "object",
"properties": {
"disk_warn_gb": {
"type": "integer",
"description": "Host free-disk warn threshold in GB (default: 50)."
},
"disk_critical_gb": {
"type": "integer",
"description": "Host free-disk critical threshold in GB (default: 20)."
},
"load_warn_per_core": {
"type": "number",
"description": "1-minute load average per core above which a warn flag fires (default: 1.0)."
},
"load_critical_per_core": {
"type": "number",
"description": "1-minute load average per core above which a critical flag fires (default: 2.0)."
}
}
}
}),
json!({
"name": "fleet_identity",
"description": "Read mode (default): for every registered sled, report project, url, connected, the recorded pin (expected_node_id), the live signature-verified node_id from a signed challenge-response (never the unsigned /identity display field), and whether they match. Repin mode: capture a sled's live verified identity via TOFU and persist it as the new pin, refusing when the signature is missing or does not verify.",
@@ -440,6 +467,7 @@ async fn handle_gateway_tool(
"agents.list" => handle_agents_list_tool(id),
"prompt_permission" => handle_prompt_permission_tool(params, state, id).await,
"project_rebuild" => handle_project_rebuild_tool(params, state, id).await,
"fleet_resources" => handle_fleet_resources_tool(params, state, id).await,
"fleet_identity" => handle_fleet_identity_tool(params, state, id).await,
_ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")),
}
@@ -943,6 +971,65 @@ async fn handle_project_rebuild_tool(
)
}
/// Handle the `fleet_resources` gateway tool (story 1207).
///
/// Collects host disk/load/cpu/mem, per-container CPU%/mem (via `docker
/// stats`), and per-project `target/`/`worktrees/` sizes (via `docker exec
/// ... find`) — all sourced gateway-side rather than requiring each project's
/// sled to self-report. Thresholds for the leading `flags` list are supplied
/// as optional arguments (see `gateway_tool_definitions`), defaulting to
/// `ResourceThresholds::default()`.
async fn handle_fleet_resources_tool(
params: &Value,
state: &GatewayState,
id: Option<Value>,
) -> JsonRpcResponse {
use crate::service::gateway::resources::ResourceThresholds;
let args = params.get("arguments").unwrap_or(params);
let defaults = ResourceThresholds::default();
let thresholds = ResourceThresholds {
disk_warn_gb: args
.get("disk_warn_gb")
.and_then(|v| v.as_u64())
.unwrap_or(defaults.disk_warn_gb),
disk_critical_gb: args
.get("disk_critical_gb")
.and_then(|v| v.as_u64())
.unwrap_or(defaults.disk_critical_gb),
load_warn_per_core: args
.get("load_warn_per_core")
.and_then(|v| v.as_f64())
.unwrap_or(defaults.load_warn_per_core),
load_critical_per_core: args
.get("load_critical_per_core")
.and_then(|v| v.as_f64())
.unwrap_or(defaults.load_critical_per_core),
};
let project_names: Vec<String> = state.projects.read().await.keys().cloned().collect();
match crate::service::gateway::resources::io::collect_fleet_resources(
&state.config_dir,
&project_names,
&thresholds,
)
.await
{
Ok(resources) => JsonRpcResponse::success(
id,
json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&resources).unwrap_or_default()
}],
"resources": serde_json::to_value(&resources).unwrap_or(json!(null)),
}),
),
Err(e) => JsonRpcResponse::error(id, -32603, format!("fleet_resources failed: {e}")),
}
}
/// Handle the `fleet_identity` gateway tool.
///
/// Dispatches on the `action` argument: `"read"` (default) reports every