From 54ba86b647bed4d6caa59121ae5f6b1e2de0c719 Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Fri, 28 Aug 2026 15:44:27 +0800 Subject: [PATCH] fix(src-tauri): register missing Tauri command bridges for k8s onboarding tools (studio#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real bug, caught by Brett actually running the app: the console called invoke("list_namespaces", ...) etc., and oab-mcp had the underlying MCP tools, but nothing bridged them — Tauri requires every command explicitly registered via #[tauri::command] + generate_handler![], and this layer was never built for list_aws_profiles/list_k8s_contexts/list_namespaces/ list_service_accounts/k8s_fleet_config_write. Symptom: "Command list_namespaces not found" in the running app, exactly what showed up. Adds the five missing bridges, mirroring the existing fleet_config/ fleet_config_write pattern exactly (lock the core client, call_tool, log + propagate errors). Also extends deploy_provision's bridge with provider/context/expected_principal so the k8s dispatch path (already wired server-side in oab-mcp) is actually reachable once console's "k8s not available yet" block is lifted — not reachable yet today since that block is still in place, but there's no point fixing the tool-listing bridges without also completing this one, since it's the same class of gap. Verification note: this crate (src-tauri, package studio-desktop) is NOT a member of the root Cargo workspace — it's Tauri's own project, building the macOS desktop shell specifically (CI's bundle-macos job, not build-test). Confirmed it can't be locally compiled in this environment at all (missing system glib-2.0/GTK dev packages this sandbox doesn't have, unrelated to the aws-sdk-ec2 OOM constraint hit everywhere else this session) — every new function here mechanically mirrors an already-working sibling (fleet_config_write's exact shape), hand-verified against it line by line. CI's bundle-macos job, which already builds this exact crate on a real macOS runner, is the actual gate. Ref: studio#104. --- src-tauri/src/lib.rs | 149 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cbcfafa..bd7ba19 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -118,9 +118,11 @@ async fn compose_preview( /// Provision an agent from the compose library over MCP (`deploy_provision`): /// compose `template ⊕ overlay`, push the bundle to the agent's S3 artifacts -/// prefix, and redeploy the ECS service at the chosen image tag (agent-deployment -/// ADR slice 2). The heavy lifting (compose + S3 + apply) runs in the sidecar -/// with its hermetic AWS env; this is a thin bridge like `deploy_scale`. +/// prefix, and either redeploy the ECS service (AWS, default) or apply a k8s +/// Deployment (studio#104: `provider = "k8s"`, applied through the given +/// `context` — see `t_provision`'s dispatch in `oab-mcp`) at the chosen image +/// tag. The heavy lifting (compose + S3 + apply) runs in the sidecar with its +/// hermetic AWS/kube env; this is a thin bridge like `deploy_scale`. #[tauri::command] async fn deploy_provision( core: tauri::State<'_, Core>, @@ -131,6 +133,9 @@ async fn deploy_provision( namespace: Option, image: Option, cluster: Option, + provider: Option, + context: Option, + expected_principal: Option, ) -> Result { let cluster = cluster.unwrap_or_else(default_cluster); let client = { @@ -155,6 +160,15 @@ async fn deploy_provision( if let Some(img) = image.filter(|s| !s.is_empty()) { params["image_tag"] = json!(img); } + if let Some(p) = provider.filter(|s| !s.is_empty()) { + params["provider"] = json!(p); + } + if let Some(c) = context.filter(|s| !s.is_empty()) { + params["context"] = json!(c); + } + if let Some(ep) = expected_principal.filter(|s| !s.is_empty()) { + params["expected_principal"] = json!(ep); + } match client.call_tool("deploy_provision", params).await { Ok(v) => Ok(v), Err(e) => { @@ -292,6 +306,130 @@ async fn fleet_config_write(core: tauri::State<'_, Core>, text: String) -> Resul } } +/// Bridge command: AWS profiles discovered on this machine (studio#104), via +/// the sidecar's `list_aws_profiles` tool — backs the New Fleet wizard's AWS +/// profile picker. +#[tauri::command] +async fn list_aws_profiles(core: tauri::State<'_, Core>) -> Result { + let client = { + let guard = core.0.lock().await; + guard + .as_ref() + .cloned() + .ok_or_else(|| "core not started yet".to_string())? + }; + match client.call_tool("list_aws_profiles", json!({})).await { + Ok(v) => Ok(v), + Err(e) => { + client.log("error", &format!("list_aws_profiles: {e}")); + Err(e) + } + } +} + +/// Bridge command: kubeconfig contexts discovered on this machine (studio#104), +/// via the sidecar's `list_k8s_contexts` tool — backs the New Fleet wizard's +/// k8s context picker. +#[tauri::command] +async fn list_k8s_contexts(core: tauri::State<'_, Core>) -> Result { + let client = { + let guard = core.0.lock().await; + guard + .as_ref() + .cloned() + .ok_or_else(|| "core not started yet".to_string())? + }; + match client.call_tool("list_k8s_contexts", json!({})).await { + Ok(v) => Ok(v), + Err(e) => { + client.log("error", &format!("list_k8s_contexts: {e}")); + Err(e) + } + } +} + +/// Bridge command: namespaces in a kubeconfig context (studio#104), via the +/// sidecar's `list_namespaces` tool — backs the New Fleet wizard's namespace +/// field's autocomplete. +#[tauri::command] +async fn list_namespaces(core: tauri::State<'_, Core>, context: Option) -> Result { + let client = { + let guard = core.0.lock().await; + guard + .as_ref() + .cloned() + .ok_or_else(|| "core not started yet".to_string())? + }; + let mut params = json!({}); + if let Some(c) = context.filter(|s| !s.is_empty()) { + params["context"] = json!(c); + } + match client.call_tool("list_namespaces", params).await { + Ok(v) => Ok(v), + Err(e) => { + client.log("error", &format!("list_namespaces: {e}")); + Err(e) + } + } +} + +/// Bridge command: service accounts in one namespace of a kubeconfig context +/// (studio#104), via the sidecar's `list_service_accounts` tool — backs the +/// New Fleet wizard's optional service-account picker. Per the tool's own +/// contract, a failure here should read to the caller as "leave it unset," +/// not an error — this bridge doesn't editorialize that, it just forwards +/// whatever the sidecar returns (including an `Err`) and the console decides +/// how to treat it (deploy.ts's `loadK8sServiceAccounts` swallows failures). +#[tauri::command] +async fn list_service_accounts( + core: tauri::State<'_, Core>, + context: Option, + namespace: String, +) -> Result { + let client = { + let guard = core.0.lock().await; + guard + .as_ref() + .cloned() + .ok_or_else(|| "core not started yet".to_string())? + }; + let mut params = json!({ "namespace": namespace }); + if let Some(c) = context.filter(|s| !s.is_empty()) { + params["context"] = json!(c); + } + match client.call_tool("list_service_accounts", params).await { + Ok(v) => Ok(v), + Err(e) => { + client.log("error", &format!("list_service_accounts: {e}")); + Err(e) + } + } +} + +/// Bridge command: persist the edited `fleets-k8s.toml` text (studio#104, +/// k8s counterpart to `fleet_config_write`) via the sidecar's +/// `k8s_fleet_config_write` tool. +#[tauri::command] +async fn k8s_fleet_config_write(core: tauri::State<'_, Core>, text: String) -> Result { + let client = { + let guard = core.0.lock().await; + guard + .as_ref() + .cloned() + .ok_or_else(|| "core not started yet".to_string())? + }; + match client + .call_tool("k8s_fleet_config_write", json!({ "text": text })) + .await + { + Ok(v) => Ok(v), + Err(e) => { + client.log("error", &format!("k8s_fleet_config_write: {e}")); + Err(e) + } + } +} + /// Bridge command: start (size 1) / stop (size 0) a deployment via the sidecar's /// `deploy_scale` tool (ADR-2 write model — stop = scale→0, start = scale→1; the /// Spec is kept by ECS, so it's reversible). An OAB service runs a single bot @@ -571,6 +709,11 @@ pub fn run() { runtime_context, fleet_config, fleet_config_write, + list_aws_profiles, + list_k8s_contexts, + list_namespaces, + list_service_accounts, + k8s_fleet_config_write, deploy_scale, remote_config, remote_agents,