From 93ea37c2364f5f648b0b312ed7375ec36d28b402 Mon Sep 17 00:00:00 2001 From: "7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz" <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> Date: Thu, 23 Jul 2026 13:14:46 -0700 Subject: [PATCH] Close remaining local-agent relay gaps Co-authored-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> Signed-off-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> --- desktop/scripts/check-file-sizes.mjs | 3 +- .../src-tauri/src/commands/agent_models.rs | 16 +++- desktop/src-tauri/src/commands/agents.rs | 9 ++- desktop/src-tauri/src/commands/channels.rs | 7 ++ desktop/src-tauri/src/managed_agents/mod.rs | 4 +- .../src/managed_agents/relay_policy.rs | 75 +++++++++++++++++++ 6 files changed, 105 insertions(+), 9 deletions(-) diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index e7cd6a2a1..64c80991b 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -468,7 +468,8 @@ const overrides = new Map([ // (if let Some(provider_update) = input.provider { record.provider = provider_update; }). // +8: harness_override thread-through in update_managed_agent so a deliberate // Custom pin routes to update_time_agent_command_override (comment + call). - ["src-tauri/src/commands/agent_models.rs", 1079], + // +4: internal relay lockdown validates effective relay before update/profile sync. + ["src-tauri/src/commands/agent_models.rs", 1083], // global-agent-config: get_agent_config_surface / write_agent_config_field / // put_agent_session_config commands + GlobalAgentConfig serde types. New file // in this PR; queued to split with the command module refactor. diff --git a/desktop/src-tauri/src/commands/agent_models.rs b/desktop/src-tauri/src/commands/agent_models.rs index a90170e93..355e2b636 100644 --- a/desktop/src-tauri/src/commands/agent_models.rs +++ b/desktop/src-tauri/src/commands/agent_models.rs @@ -801,6 +801,7 @@ pub async fn update_managed_agent( state: State<'_, AppState>, ) -> Result { // Phase 1: local save (synchronous, under lock) + let workspace_relay_url = relay_ws_url_with_override(&state); let (summary, sync_params, rollback) = { let _store_guard = state .managed_agents_store_lock @@ -848,9 +849,11 @@ pub async fn update_managed_agent( // read-time. A name-only edit (relay_url == None) leaves the pin intact. if let Some(relay_url) = input.relay_url { let relay_url = relay_url.trim().to_string(); - if !relay_url.is_empty() { - crate::managed_agents::validate_local_agent_relay(&record.backend, &relay_url)?; - } + crate::managed_agents::validate_effective_local_agent_relay( + &record.backend, + &relay_url, + &workspace_relay_url, + )?; record.relay_url = relay_url; } if let Some(acp_command) = input.acp_command { @@ -908,6 +911,13 @@ pub async fn update_managed_agent( record.updated_at = now_iso(); + if name_changed { + crate::managed_agents::validate_effective_local_agent_relay( + &record.backend, + &record.relay_url, + &workspace_relay_url, + )?; + } save_managed_agents(&app, &records)?; let record = records diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index 328cbc335..2cbf6a4b3 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -588,6 +588,7 @@ pub async fn create_managed_agent( // Snapshot the workspace owner pubkey for the legacy-record auth_tag // fallback. Computed outside the records lock to keep lock ordering simple. let owner_hex = workspace_owner_hex(&state)?; + let workspace_relay_url = relay_ws_url_with_override(&state); // ── Phase 1: generate keys (sync lock) ──────────────────────────────────── let (agent_keys, private_key_nsec, pubkey, resolved_relay_url, input) = { @@ -632,9 +633,11 @@ pub async fn create_managed_agent( .map(str::trim) .unwrap_or("") .to_string(); - if !resolved_relay_url.is_empty() { - crate::managed_agents::validate_local_agent_relay(&input.backend, &resolved_relay_url)?; - } + crate::managed_agents::validate_effective_local_agent_relay( + &input.backend, + &resolved_relay_url, + &workspace_relay_url, + )?; (keys, private_key_nsec, pubkey, resolved_relay_url, input) }; diff --git a/desktop/src-tauri/src/commands/channels.rs b/desktop/src-tauri/src/commands/channels.rs index 1a5c1008f..bf58660fc 100644 --- a/desktop/src-tauri/src/commands/channels.rs +++ b/desktop/src-tauri/src/commands/channels.rs @@ -838,6 +838,7 @@ pub async fn change_channel_member_role( channel_id: String, pubkey: String, role: String, + app: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { let uuid = parse_channel_uuid(&channel_id)?; @@ -848,6 +849,12 @@ pub async fn change_channel_member_role( "owner" => return Err("cannot assign owner role — use transfer ownership".into()), other => return Err(format!("invalid role: {other}")), }; + let relay_url = crate::relay::relay_ws_url_with_override(&state); + crate::managed_agents::validate_local_agent_members_from_store( + std::slice::from_ref(&pubkey), + &relay_url, + || crate::managed_agents::load_managed_agents(&app), + )?; let builder = events::build_add_member(uuid, &pubkey, Some(role_str))?; submit_event(builder, &state).await?; Ok(()) diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index ace593481..0bf598036 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -12,8 +12,8 @@ pub(crate) use agent_env::{ baked_build_env, build_buzz_agent_provider_defaults, discovery_env_with_baked_floor, }; pub(crate) use relay_policy::{ - validate_local_agent_members_from_store, validate_local_agent_relay, - validate_managed_agent_relay_pin, + validate_effective_local_agent_relay, validate_local_agent_members_from_store, + validate_local_agent_relay, validate_managed_agent_relay_pin, }; mod backend; pub(crate) mod config_bridge; diff --git a/desktop/src-tauri/src/managed_agents/relay_policy.rs b/desktop/src-tauri/src/managed_agents/relay_policy.rs index a82308473..3abad09af 100644 --- a/desktop/src-tauri/src/managed_agents/relay_policy.rs +++ b/desktop/src-tauri/src/managed_agents/relay_policy.rs @@ -80,6 +80,34 @@ where )) } +pub(crate) fn validate_effective_local_agent_relay( + backend: &BackendKind, + relay_pin: &str, + workspace_relay_url: &str, +) -> Result<(), String> { + validate_effective_local_agent_relay_with_policy( + backend, + relay_pin, + workspace_relay_url, + super::internal_build(), + baked_allowlist, + ) +} + +fn validate_effective_local_agent_relay_with_policy( + backend: &BackendKind, + relay_pin: &str, + workspace_relay_url: &str, + internal: bool, + allowlist: F, +) -> Result<(), String> +where + F: FnOnce() -> Result, String>, +{ + let effective = crate::relay::effective_agent_relay_url(relay_pin, workspace_relay_url); + validate_local_agent_relay_with_policy(backend, &effective, internal, allowlist) +} + /// Validate a legacy explicit record pin at persistence boundaries. Empty pins /// are workspace-relative and are checked when their effective relay is known. pub(crate) fn validate_managed_agent_relay_pin(record: &ManagedAgentRecord) -> Result<(), String> { @@ -219,6 +247,44 @@ mod tests { .is_ok()); } + #[test] + fn empty_pin_validates_the_effective_workspace_relay() { + assert!(validate_effective_local_agent_relay_with_policy( + &BackendKind::Local, + "", + "wss://public.example", + true, + allowlist, + ) + .is_err()); + assert!(validate_effective_local_agent_relay_with_policy( + &BackendKind::Local, + "", + "wss://buzz.block.builderlab.xyz", + true, + allowlist, + ) + .is_ok()); + } + + #[test] + fn effective_relay_validation_skips_oss_and_provider_backends() { + let provider = BackendKind::Provider { + id: "provider".into(), + config: serde_json::json!({}), + }; + for (backend, internal) in [(&BackendKind::Local, false), (&provider, true)] { + assert!(validate_effective_local_agent_relay_with_policy( + backend, + "", + "not-even-a-relay", + internal, + || Err("allowlist must not load".into()), + ) + .is_ok()); + } + } + #[test] fn member_enrollment_validates_only_matching_local_agents() { let provider = BackendKind::Provider { @@ -251,6 +317,15 @@ mod tests { assert_eq!(calls.get(), 1); } + #[test] + fn role_change_rejects_matching_local_agent_before_membership_emit() { + let records = vec![record("local", BackendKind::Local)]; + let result = validate_local_agent_members_with(&records, &["LOCAL".into()], |backend| { + validate_local_agent_relay_with_policy(backend, "wss://public.example", true, allowlist) + }); + assert!(result.is_err()); + } + #[test] fn oss_member_enrollment_does_not_load_the_agent_store() { let loads = std::cell::Cell::new(0);