diff --git a/desktop/src-tauri/src/commands/personas/snapshot/import.rs b/desktop/src-tauri/src/commands/personas/snapshot/import.rs index d0e0f658c..cb0a6bd3a 100644 --- a/desktop/src-tauri/src/commands/personas/snapshot/import.rs +++ b/desktop/src-tauri/src/commands/personas/snapshot/import.rs @@ -288,18 +288,21 @@ pub async fn preview_agent_snapshot_import( .map_err(|e| format!("spawn_blocking failed: {e}"))? } -pub(super) fn validate_snapshot_import_relay_with( - workspace_relay_url: &str, +pub(super) fn validated_snapshot_import_relay_with( + read_workspace_relay: R, validate: F, -) -> Result<(), String> +) -> Result where + R: FnOnce() -> String, F: FnOnce(&crate::managed_agents::BackendKind, &str, &str) -> Result<(), String>, { + let workspace_relay_url = read_workspace_relay(); validate( &crate::managed_agents::BackendKind::Local, "", - workspace_relay_url, - ) + &workspace_relay_url, + )?; + Ok(workspace_relay_url) } // ── `confirm_agent_snapshot_import` ────────────────────────────────────────── @@ -337,10 +340,12 @@ pub async fn confirm_agent_snapshot_import( // Snapshot imports always mint a local agent with an empty relay pin. Reject // a disallowed effective workspace relay before key generation or store I/O. - let workspace_relay_url = relay_ws_url_with_override(&state); - validate_snapshot_import_relay_with(&workspace_relay_url, |backend, pin, workspace| { - crate::managed_agents::validate_effective_local_agent_relay(backend, pin, workspace) - })?; + let workspace_relay_url = validated_snapshot_import_relay_with( + || relay_ws_url_with_override(&state), + |backend, pin, workspace| { + crate::managed_agents::validate_effective_local_agent_relay(backend, pin, workspace) + }, + )?; // ── Resolve behavioral defaults ────────────────────────────────────────── let minted = resolve_snapshot_import_behavior( @@ -527,8 +532,7 @@ pub async fn confirm_agent_snapshot_import( }; // ── Phase 3b: publish kind:0 profile (async, outside lock) ─────────────── - let relay_url = - effective_agent_relay_url(&record.relay_url, &relay_ws_url_with_override(&state)); + let relay_url = effective_agent_relay_url(&record.relay_url, &workspace_relay_url); let profile_sync_error = sync_managed_agent_profile( &state, &relay_url, diff --git a/desktop/src-tauri/src/commands/personas/snapshot/tests.rs b/desktop/src-tauri/src/commands/personas/snapshot/tests.rs index f3d7bd828..5fe06c899 100644 --- a/desktop/src-tauri/src/commands/personas/snapshot/tests.rs +++ b/desktop/src-tauri/src/commands/personas/snapshot/tests.rs @@ -1,6 +1,6 @@ use super::import::{ decode_snapshot_from_bytes, reject_legacy_persona_filename, resolve_snapshot_import_behavior, - validate_snapshot_import_relay_with, AgentSnapshotImportResult, MAX_SNAPSHOT_JSON_BYTES, + validated_snapshot_import_relay_with, AgentSnapshotImportResult, MAX_SNAPSHOT_JSON_BYTES, MAX_SNAPSHOT_PNG_BYTES, }; use super::*; @@ -972,16 +972,27 @@ fn validate_encode_size_png_over_boundary_is_rejected() { } #[test] -fn individual_snapshot_import_preflights_empty_pin_before_mint_or_store() { - let calls = std::cell::Cell::new(0); - let result = - validate_snapshot_import_relay_with("wss://public.example", |backend, pin, relay| { - calls.set(calls.get() + 1); +fn individual_snapshot_import_carries_the_validated_relay_snapshot_to_io() { + let reads = std::cell::Cell::new(0); + let relay = validated_snapshot_import_relay_with( + || { + reads.set(reads.get() + 1); + "wss://allowed.example".into() + }, + |backend, pin, relay| { assert_eq!(backend, &BackendKind::Local); assert!(pin.is_empty()); - assert_eq!(relay, "wss://public.example"); - Err("blocked before mutation".into()) - }); - assert_eq!(result.unwrap_err(), "blocked before mutation"); - assert_eq!(calls.get(), 1); + assert_eq!(relay, "wss://allowed.example"); + Ok(()) + }, + ) + .unwrap(); + assert_eq!(relay, "wss://allowed.example"); + assert_eq!(reads.get(), 1); + + let blocked = validated_snapshot_import_relay_with( + || "wss://public.example".into(), + |_, _, _| Err("blocked before mutation".into()), + ); + assert_eq!(blocked.unwrap_err(), "blocked before mutation"); } diff --git a/desktop/src-tauri/src/commands/team_snapshot.rs b/desktop/src-tauri/src/commands/team_snapshot.rs index 2b3ab4fc6..668247d4c 100644 --- a/desktop/src-tauri/src/commands/team_snapshot.rs +++ b/desktop/src-tauri/src/commands/team_snapshot.rs @@ -496,18 +496,21 @@ pub async fn preview_team_snapshot_import( /// /// Importing the same file twice yields two distinct teams with different /// agent keypairs (same as individual agent import). -fn validate_team_snapshot_import_relay_with( - workspace_relay_url: &str, +fn validated_team_snapshot_import_relay_with( + read_workspace_relay: R, validate: F, -) -> Result<(), String> +) -> Result where + R: FnOnce() -> String, F: FnOnce(&crate::managed_agents::BackendKind, &str, &str) -> Result<(), String>, { + let workspace_relay_url = read_workspace_relay(); validate( &crate::managed_agents::BackendKind::Local, "", - workspace_relay_url, - ) + &workspace_relay_url, + )?; + Ok(workspace_relay_url) } #[tauri::command] @@ -522,10 +525,12 @@ pub async fn confirm_team_snapshot_import( // Team snapshots mint only empty-pin local agents. Preflight the workspace // relay once before generating any member key or mutating any store. - let workspace_relay_url = relay_ws_url_with_override(&state); - validate_team_snapshot_import_relay_with(&workspace_relay_url, |backend, pin, workspace| { - crate::managed_agents::validate_effective_local_agent_relay(backend, pin, workspace) - })?; + let workspace_relay_url = validated_team_snapshot_import_relay_with( + || relay_ws_url_with_override(&state), + |backend, pin, workspace| { + crate::managed_agents::validate_effective_local_agent_relay(backend, pin, workspace) + }, + )?; // Resolve behavioral defaults for every member before any key generation. let definitions = build_import_definitions(&snapshot, input.keep_allowlist, &now)?; @@ -775,7 +780,8 @@ pub async fn confirm_team_snapshot_import( }; // ── Phase 4 & 5: profile sync + memory restore (async, outside lock) ──── - let relay_ws = relay_ws_url_with_override(&state); + // Use the relay snapshot validated before mint/store for all agent-keyed I/O. + let relay_ws = workspace_relay_url; let mut member_results: Vec = Vec::with_capacity(minted.len()); for (m, snap_member) in minted.iter().zip(snapshot.members.iter()) { diff --git a/desktop/src-tauri/src/commands/team_snapshot/tests.rs b/desktop/src-tauri/src/commands/team_snapshot/tests.rs index dd085f048..115647abb 100644 --- a/desktop/src-tauri/src/commands/team_snapshot/tests.rs +++ b/desktop/src-tauri/src/commands/team_snapshot/tests.rs @@ -725,17 +725,40 @@ fn full_rollback_at_teams_boundary_absent_agents_store() { assert_eq!(errors.len(), 1, "only the teams-write error"); } +#[test] +fn team_snapshot_import_carries_the_validated_relay_snapshot_to_io() { + let workspace_reads = std::cell::Cell::new(0); + let validated_relay = validated_team_snapshot_import_relay_with( + || { + workspace_reads.set(workspace_reads.get() + 1); + "wss://allowed.example".to_string() + }, + |backend, pin, relay| { + assert_eq!(backend, &crate::managed_agents::BackendKind::Local); + assert!(pin.is_empty()); + assert_eq!(relay, "wss://allowed.example"); + Ok(()) + }, + ) + .unwrap(); + + assert_eq!(validated_relay, "wss://allowed.example"); + assert_eq!(workspace_reads.get(), 1); +} + #[test] fn team_snapshot_import_preflights_empty_pin_once_before_mint_or_store() { let calls = std::cell::Cell::new(0); - let result = - validate_team_snapshot_import_relay_with("wss://public.example", |backend, pin, relay| { + let result = validated_team_snapshot_import_relay_with( + || "wss://public.example".to_string(), + |backend, pin, relay| { calls.set(calls.get() + 1); assert_eq!(backend, &crate::managed_agents::BackendKind::Local); assert!(pin.is_empty()); assert_eq!(relay, "wss://public.example"); Err("blocked before mutation".into()) - }); + }, + ); assert_eq!(result.unwrap_err(), "blocked before mutation"); assert_eq!(calls.get(), 1); }