Remove all alternate update paths — fleet redeploy is release + upgrade all

Killed:
- rebuild_and_restart (in-container cargo self-compile): the MCP tool,
  the `rebuild` chat command in all four transports, the web-ui bot
  command, and the underlying function. This was the path that caused
  the exec() deadlocks.
- upgrade_sled gateway MCP tool: second entry point to sled upgrades,
  defaulted to serving the gateway's own macOS binary to Linux sleds.
- GET /api/huskies-binary (both sled and gateway route trees): served
  current_exe(), wrong platform when the gateway is macOS. Superseded
  by /api/artifacts/ which now also serves on the gateway route tree.
- `huskies upgrade` CLI subcommand and --source flag: third way of
  doing the same download-and-replace. Escape hatch for a bricked sled
  is `docker cp` + restart.

Kept, distinct jobs: `project-rebuild` (container/image updates),
`rebuild gateway` + script/local-release (gateway self-update).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fHdm92yjvguPi2LiXfLB9
This commit is contained in:
Timmy
2026-07-15 16:46:11 +01:00
co-authored by Claude Fable 5
parent 83f941b77e
commit f39c4b7c4b
24 changed files with 36 additions and 824 deletions
-107
View File
@@ -27,8 +27,6 @@ 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",
// One-shot container rebuild: build fresh image, swap container, preserve state.
"project_rebuild",
];
@@ -134,23 +132,6 @@ 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."
}
}
}
}),
json!({
"name": "project_rebuild",
"description": "Rebuild a project's Docker image from its Dockerfile.fragment, swap the container, and preserve all CRDT and pipeline state. In-flight coder/merge work is drained before the swap; if not drainable within the timeout the command refuses. On success returns the new image hash and container ID.",
@@ -438,7 +419,6 @@ 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,
"project_rebuild" => handle_project_rebuild_tool(params, state, id).await,
_ => JsonRpcResponse::error(id, -32601, format!("Unknown gateway tool: {tool_name}")),
}
@@ -893,93 +873,6 @@ 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 `project_rebuild` gateway tool.
///
/// Rebuilds a project's Docker image, swaps the container, and preserves all