huskies: merge 1138 story In-container huskies self-update — huskies upgrade pulls a fresh binary without docker rebuild
This commit is contained in:
@@ -26,6 +26,8 @@ const GATEWAY_TOOLS: &[&str] = &[
|
||||
// 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",
|
||||
// Binary self-update: gateway serves its own binary and triggers upgrade on sleds.
|
||||
"upgrade_sled",
|
||||
];
|
||||
|
||||
/// Gateway tool definitions.
|
||||
@@ -121,6 +123,23 @@ pub(crate) fn gateway_tool_definitions() -> Vec<Value> {
|
||||
"properties": {}
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "upgrade_sled",
|
||||
"description": "Trigger a binary self-update on a project sled. The sled downloads the new binary from `source_url` (defaults to this gateway's /api/huskies-binary endpoint), atomically replaces its own executable, drains CRDT persistence so no ops are lost, and re-execs. Without `project`, upgrades the active project.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Name of the project sled to upgrade. Defaults to the currently active project."
|
||||
},
|
||||
"source_url": {
|
||||
"type": "string",
|
||||
"description": "HTTP URL of the binary to install (e.g. 'http://gateway:3000/api/huskies-binary'). Defaults to this gateway's own binary endpoint."
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -385,6 +404,7 @@ async fn handle_gateway_tool(
|
||||
"aggregate_pipeline_status" => handle_aggregate_pipeline_status_tool(state, id).await,
|
||||
"agents.list" => handle_agents_list_tool(id),
|
||||
"prompt_permission" => handle_prompt_permission_tool(params, state, id).await,
|
||||
"upgrade_sled" => handle_upgrade_sled_tool(params, state, id).await,
|
||||
_ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")),
|
||||
}
|
||||
}
|
||||
@@ -769,6 +789,93 @@ fn handle_agents_list_tool(id: Option<Value>) -> JsonRpcResponse {
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle the `upgrade_sled` gateway tool.
|
||||
///
|
||||
/// Posts `{"source_url": "<url>"}` to the target sled's `/api/upgrade` endpoint,
|
||||
/// which triggers the sled to download the new binary, drain CRDT persistence,
|
||||
/// and re-exec. Returns 202 text immediately — the sled connection will drop
|
||||
/// shortly after as `exec()` replaces the process.
|
||||
async fn handle_upgrade_sled_tool(
|
||||
params: &Value,
|
||||
state: &GatewayState,
|
||||
id: Option<Value>,
|
||||
) -> JsonRpcResponse {
|
||||
let args = params.get("arguments").unwrap_or(params);
|
||||
|
||||
// Resolve target project URL (explicit project arg or active project).
|
||||
let project_name = args.get("project").and_then(|v| v.as_str());
|
||||
let sled_url = if let Some(name) = project_name {
|
||||
let projects = state.projects.read().await;
|
||||
match projects.get(name).and_then(|e| e.url.clone()) {
|
||||
Some(u) => u,
|
||||
None => {
|
||||
return JsonRpcResponse::error(
|
||||
id,
|
||||
-32602,
|
||||
format!("Project '{name}' not found or has no URL configured"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match state.active_url().await {
|
||||
Ok(u) => u,
|
||||
Err(e) => return JsonRpcResponse::error(id, -32603, e.to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
// Build the binary source URL: caller-supplied or this gateway's own endpoint.
|
||||
let source_url = args
|
||||
.get("source_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| {
|
||||
// Default: the gateway serves its own binary at /api/huskies-binary.
|
||||
// Use the same host/port the gateway is bound to.
|
||||
std::env::var("HUSKIES_GATEWAY_BINARY_URL")
|
||||
.unwrap_or_else(|_| format!("http://gateway:{}/api/huskies-binary", state.port))
|
||||
});
|
||||
|
||||
let upgrade_url = format!("{sled_url}/api/upgrade");
|
||||
let body = serde_json::json!({ "source_url": source_url });
|
||||
|
||||
let active_name = project_name.map(|s| s.to_string()).unwrap_or_else(|| {
|
||||
state
|
||||
.active_project
|
||||
.try_read()
|
||||
.map(|g| g.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
match state.client.post(&upgrade_url).json(&body).send().await {
|
||||
Ok(resp) if resp.status().is_success() || resp.status().as_u16() == 202 => {
|
||||
JsonRpcResponse::success(
|
||||
id,
|
||||
json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!(
|
||||
"Upgrade triggered on '{active_name}'. The sled is downloading the new binary from {source_url} and will re-exec momentarily."
|
||||
)
|
||||
}]
|
||||
}),
|
||||
)
|
||||
}
|
||||
Ok(resp) => JsonRpcResponse::error(
|
||||
id,
|
||||
-32603,
|
||||
format!(
|
||||
"Sled returned HTTP {} for upgrade request to {upgrade_url}",
|
||||
resp.status()
|
||||
),
|
||||
),
|
||||
Err(e) => JsonRpcResponse::error(
|
||||
id,
|
||||
-32603,
|
||||
format!("Failed to send upgrade request to {upgrade_url}: {e}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the `pipeline.get` read-RPC — returns per-project item lists in the
|
||||
/// shape expected by the gateway web UI:
|
||||
/// `{ "active": "...", "projects": { "name": { "active": [...], "backlog_count": N } } }`.
|
||||
|
||||
Reference in New Issue
Block a user