Fleet tools: one-click per-node update (Phase 2)
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 7s
ci / frontend (push) Successful in 23s
ci / e2e (push) Has been skipped

The ↑ badge on each tool card is now a button: confirm → POST
/api/nodes/{id}/tools/{tool}/update → daemon runs the tool's own updater + re-probes.

- daemon: tool_update op (spawned task so the 170s update can't stall the WS loop;
  re-probes + re-sends node_tools after). Fixed command allow-list (no arbitrary
  shell): claude/glm → `claude update`; kimi → `uv tool upgrade kimi-cli`; ollama →
  brew upgrade (mac) / install.sh (linux); else unsupported. 4KB output cap.
- cm-api: call_timeout/request_timeout (long ops); POST .../tools/{tool}/update
  (workspace-scoped, allow-list) → {ok,output}.
- frontend: ↑latest becomes an Update button → confirm → spinner → refresh/err.

Note: claude/kimi/glm are user-space (no sudo); ollama on Linux uses install.sh
(needs sudo — works on passwordless nodes, returns an error otherwise; surfaced in UI).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-27 06:25:30 -07:00
co-authored by Claude Opus 4.8
parent 9263418fcb
commit ed339c2121
5 changed files with 177 additions and 14 deletions
+25
View File
@@ -219,6 +219,31 @@ pub async fn tools(
Ok(Json(json!({ "tools": out })))
}
/// `POST /api/nodes/{id}/tools/{tool}/update` — run the daemon's fixed update for a
/// tool (claude/glm/kimi/ollama), then it re-probes so the version refreshes.
pub async fn tool_update(
State(state): State<AppState>,
Authed(user): Authed,
Path((id, tool)): Path<(Uuid, String)>,
) -> Result<Json<Value>, ApiError> {
let node_id = NodeId::from(id);
nodes::get(&state.pool, node_id, user.workspace_id)
.await?
.ok_or(ApiError::NotFound)?;
if !["claude", "glm", "kimi", "ollama"].contains(&tool.as_str()) {
return Ok(Json(json!({ "ok": false, "output": "tool not updatable" })));
}
// ~180s: tool updates (npm/uv/brew/installer) legitimately run long.
match state
.node_hub
.call_timeout(node_id, "tool_update", json!({ "tool": tool }), 180)
.await
{
Ok(out) => Ok(Json(json!({ "ok": out.ok, "output": out.output }))),
Err(e) => Ok(Json(json!({ "ok": false, "output": e }))),
}
}
/// `POST /api/nodes/{id}/terminal/ticket` — mint a single-use terminal ticket
/// (the browser WS handshake can't carry a bearer header).
pub async fn terminal_ticket(