feat(ui): the microVM path is reachable from the mission wizard

Everything built today — Firecracker missions, the four backends, the local GPU
one — was unreachable from the dashboard. The wizard offered `zeroclaw` and
`local_herdr` and nothing else, so a mission created in the UI could not be a
microVM mission at all, and `local-ornith`/`glm`/`kimi` were API-only. Testing
"our workflows in the UI" would have exercised none of it.

Adds the runtime option and a backend picker, fed by a new
`GET /api/fleet/backends` that returns `mission_roster::available_backends`
verbatim — the SAME list the roster planner is handed, not a second one. Its two
rules are both load-bearing and neither is visible from a node's capabilities
alone: the image must be built on an online node, and the backend must have a
credential contract. `agent-terminal` passes the first and fails the second —
bootable, with nothing for the agent inside to authenticate with — so offering
it would produce a mission that validates, launches, and dies at the agent turn.

Ids are deployment vocabulary, so the picker labels them: a user choosing
between `local-ornith` and `canary-claude` should not have to know which company
each one bills. An empty list says why (no rootfs built) instead of showing an
empty dropdown, and no node is chosen for a microVM mission because
`vm_placement` picks it per phase.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-09 15:09:07 -07:00
co-authored by Claude Opus 5
parent 1f6108f769
commit c66c3c6377
4 changed files with 123 additions and 2 deletions
+46
View File
@@ -455,3 +455,49 @@ pub async fn capacity(
pub struct CapacityQuery {
pub backend: Option<String>,
}
/// `GET /api/fleet/backends` — the microVM backends a mission may actually use.
///
/// The SAME `available_backends` the roster planner is handed, not a second
/// list. The two rules it applies are both load-bearing and neither is obvious
/// from a node's capabilities alone: a backend must be built on an online node,
/// and it must have a credential contract. `agent-terminal` satisfies the first
/// and not the second — bootable, with nothing for the agent inside to
/// authenticate with — so offering it would produce a mission that validates,
/// launches, and fails at the agent turn, which is the expensive kind of late.
///
/// Exists because the UI had no backend selector at all: every mission created
/// from the dashboard ran on `claude`, so `local-ornith`, `glm` and `kimi` were
/// reachable only by calling the API directly.
pub async fn backends(
State(state): State<AppState>,
Authed(user): Authed,
) -> Result<Json<Value>, ApiError> {
let ws = user.workspace_id.as_uuid().to_owned();
let list = crate::mission_roster::available_backends(&state.pool, ws)
.await
.map_err(|e| {
eprintln!("fleet backends: {e}");
ApiError::Internal
})?;
Ok(Json(json!({
"backends": list.iter().map(|b| json!({
"id": b,
"label": backend_label(b),
})).collect::<Vec<_>>(),
})))
}
/// A name a person can choose between. The ids are deployment vocabulary
/// (`local-ornith`, `canary-claude`); a picker showing those alone asks the user
/// to know which company each one bills.
fn backend_label(id: &str) -> String {
match id {
"claude" | "default" => "Claude (Anthropic subscription)".into(),
"canary-claude" => "Claude — candidate CLI (canary)".into(),
"glm" => "GLM 4.7 (z.ai)".into(),
"kimi" => "Kimi (Moonshot)".into(),
"local-ornith" => "Ornith 9B — this fleet's own GPU".into(),
other => other.to_string(),
}
}