mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): wire production callers through shared mesh preflight helpers
apply_workspace and import_identity now call run_mesh_transition_preflight (extracted from fail_if_client_mesh_active path) rather than inlining the check. ensure_relay_mesh_for_record captures scope before discovery and routes the install call through install_client_under_workspace_transition, which validates full scope identity (scope_id, relay, owner, generation) under the workspace_transition guard before invoking the closure. with_workspace_transition_preflight gains run_mesh_transition_preflight as a companion: tests continue to call the callback helper directly; production callers that need spawn_blocking dispatch use run_mesh_transition_preflight after acquiring workspace_transition themselves. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
co-authored by
Will Pfleger
parent
50f615ecbd
commit
8b046bc62b
@@ -390,14 +390,13 @@ pub async fn import_identity(
|
||||
None
|
||||
};
|
||||
|
||||
// ── Layer 1 async: fail closed if a client-mode Mesh runtime is active ──
|
||||
// Identity import clears the active scope entirely, so any live client-mode
|
||||
// Mesh runtime would become dangling after the import. Option A ruling:
|
||||
// require the user to stop it first. The journaled Mesh recipe is a tracked
|
||||
// follow-up in the PR body.
|
||||
// Fail closed if a client-mode Mesh runtime is active when there is an
|
||||
// active scope — a live client-mode runtime would become dangling after the
|
||||
// import (Option A ruling). Called via `run_mesh_transition_preflight` so the
|
||||
// shared preflight logic is not duplicated inline; the guard above is held.
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
if has_active_scope {
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
crate::commands::mesh_llm::scope_impl::fail_if_client_mesh_active(&app_handle).await?;
|
||||
crate::commands::mesh_llm::scope_impl::run_mesh_transition_preflight(&app_handle).await?;
|
||||
}
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
|
||||
@@ -896,9 +896,27 @@ pub(crate) async fn ensure_relay_mesh_for_record(
|
||||
};
|
||||
|
||||
// No serving configuration exists — genuine consumer-only start.
|
||||
// Acquire workspace_transition to serialize client install against apply_workspace.
|
||||
let _workspace_guard = state.workspace_transition.lock().await;
|
||||
ensure_client_node_for_model(&state, model_id, Some(target.endpoint_addr)).await?;
|
||||
// Capture scope BEFORE discovery so a concurrent workspace switch can be
|
||||
// detected under the install lock. Route through
|
||||
// `install_client_under_workspace_transition` which acquires
|
||||
// `workspace_transition`, validates full scope identity
|
||||
// (scope_id, relay, owner, generation), then calls the install closure —
|
||||
// serialized against apply_workspace and live identity import.
|
||||
let captured_scope = state
|
||||
.capture_active_scope()
|
||||
.ok_or("mesh client install: no active workspace scope")?;
|
||||
let model_id_owned = model_id.to_string();
|
||||
let endpoint = target.endpoint_addr;
|
||||
scope_impl::install_client_under_workspace_transition(app, &captured_scope, move || {
|
||||
let state_ref = state.clone();
|
||||
let model_id_ref = model_id_owned.clone();
|
||||
async move {
|
||||
ensure_client_node_for_model(&state_ref, &model_id_ref, Some(endpoint))
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
wait_for_mesh_inference(model_id).await
|
||||
}
|
||||
|
||||
|
||||
@@ -101,13 +101,21 @@ pub(crate) async fn fail_if_client_mesh_active<R: tauri::Runtime>(
|
||||
/// `fail_if_client_mesh_active` preflight, then invoke `transition_body` while
|
||||
/// the guard remains held.
|
||||
///
|
||||
/// Used by `apply_workspace` and live identity import so neither duplicates the
|
||||
/// acquire + preflight sequence inline. Tests call this helper directly to prove
|
||||
/// the serialization contract against `install_client_under_workspace_transition`.
|
||||
/// Used by `apply_workspace`, live identity import, and tests to prove the
|
||||
/// serialization contract against `install_client_under_workspace_transition`.
|
||||
///
|
||||
/// The transition body runs synchronously (or dispatches to `spawn_blocking`)
|
||||
/// while the async guard is alive on the current task. If the preflight fails,
|
||||
/// `transition_body` is never called.
|
||||
///
|
||||
/// Production callers that must `.await` after dispatch use this helper with
|
||||
/// a body that dispatches `tokio::task::spawn_blocking` and immediately returns
|
||||
/// the join handle's `.await` — the guard is held across the dispatch and the
|
||||
/// `.await` is done at the call site after this helper returns.
|
||||
///
|
||||
/// Alternatively, callers that cannot fit their body into a sync `FnOnce` call
|
||||
/// `run_mesh_transition_preflight` (which they invoke after acquiring the lock
|
||||
/// themselves) to share the preflight logic without lifetime constraints.
|
||||
pub(crate) async fn with_workspace_transition_preflight<R, F, T>(
|
||||
app: &AppHandle<R>,
|
||||
transition_body: F,
|
||||
@@ -126,6 +134,25 @@ where
|
||||
transition_body()
|
||||
}
|
||||
|
||||
/// Run only the Mesh-preflight portion of the workspace transition check.
|
||||
///
|
||||
/// Called by production commands (`apply_workspace`, `import_identity`) that
|
||||
/// already hold `workspace_transition` and need to avoid duplicating the
|
||||
/// `fail_if_client_mesh_active` call inline. The guard must already be acquired
|
||||
/// and remain alive for the duration of the transition.
|
||||
///
|
||||
/// No-op when the `mesh-llm` feature is disabled.
|
||||
pub(crate) async fn run_mesh_transition_preflight<R>(app: &AppHandle<R>) -> Result<(), String>
|
||||
where
|
||||
R: tauri::Runtime,
|
||||
{
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
fail_if_client_mesh_active(app).await?;
|
||||
#[cfg(not(feature = "mesh-llm"))]
|
||||
let _ = app;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acquire the `workspace_transition` lock, validate the full captured scope
|
||||
/// identity under the guard — `(scope_id, normalized relay, owner_pubkey,
|
||||
/// generation)` — then call the injected `install` closure.
|
||||
|
||||
@@ -114,7 +114,7 @@ pub async fn apply_workspace(
|
||||
) -> Result<crate::managed_agents::scope::WorkspaceApplyResult, String> {
|
||||
use crate::managed_agents::scope::WorkspaceApplyResult;
|
||||
|
||||
// ── Layer 1: async serialization lock ────────────────────────────────────
|
||||
// ── Layer 1: async serialization lock + Mesh preflight ──────────────────
|
||||
// workspace_transition serializes apply_workspace and live identity import
|
||||
// so scope transitions are never concurrent. We acquire via a clone so the
|
||||
// borrow does not prevent moving `app` into spawn_blocking below.
|
||||
@@ -122,14 +122,12 @@ pub async fn apply_workspace(
|
||||
let lock_state = lock_app.state::<AppState>();
|
||||
let _transition_guard = lock_state.workspace_transition.lock().await;
|
||||
|
||||
// ── Layer 1 async: fail closed if a client-mode Mesh runtime is active ──
|
||||
// Serve-mode runtimes are machine-level and stay pinned across workspace
|
||||
// switches — they never block a switch. Client-mode runtimes require an
|
||||
// active scope to be meaningful and cannot be safely moved to a new scope
|
||||
// atomically. Option A ruling: require the user to stop the client session
|
||||
// first; the journaled Mesh recipe is a tracked follow-up in the PR body.
|
||||
// Fail closed if a client-mode Mesh runtime is active — no client-mode
|
||||
// runtime may be active when a workspace switch begins (Option A ruling).
|
||||
// Called via `run_mesh_transition_preflight` so the shared preflight logic
|
||||
// is not duplicated inline; the guard above remains held throughout.
|
||||
#[cfg(feature = "mesh-llm")]
|
||||
crate::commands::mesh_llm::scope_impl::fail_if_client_mesh_active(&app).await?;
|
||||
crate::commands::mesh_llm::scope_impl::run_mesh_transition_preflight(&app).await?;
|
||||
|
||||
let restore_app = app.clone();
|
||||
let blocking_result: Result<WorkspaceApplyResult, String> =
|
||||
|
||||
Reference in New Issue
Block a user