diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index df890931f..6df9a58c0 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -886,9 +886,14 @@ pub async fn create_managed_agent( // ── Phase 4: sync agent profile on relay (async, outside lock) ─────────── // Use the avatar persisted on the record so the published profile and any // later reconciliation agree on the same value. + // Blank per-agent relay falls back to the workspace relay (matches reconcile/rename). + let sync_relay_url = crate::relay::effective_agent_relay_url( + &resolved_relay_url, + &relay_ws_url_with_override(&state), + ); let profile_sync_error = (sync_managed_agent_profile( &state, - &resolved_relay_url, + &sync_relay_url, &agent_keys, &name, resolved_avatar_url.as_deref(), diff --git a/desktop/src-tauri/src/relay.rs b/desktop/src-tauri/src/relay.rs index a0fb1c4bd..5d3f90923 100644 --- a/desktop/src-tauri/src/relay.rs +++ b/desktop/src-tauri/src/relay.rs @@ -382,12 +382,21 @@ pub async fn sync_managed_agent_profile( avatar_url: Option<&str>, auth_tag: Option<&str>, // NIP-OA auth tag JSON ) -> Result<(), String> { + // Fail loudly on a blank relay URL — callers must resolve via `effective_agent_relay_url` first. + let base = relay_http_base_url(relay_url); + if base.is_empty() { + return Err( + "no relay configured for profile sync (resolve the agent's relay URL first)" + .to_string(), + ); + } + // Build a signed kind:0 profile event (with optional NIP-OA auth tag). let event = build_profile_event(agent_keys, display_name, avatar_url, auth_tag)?; let event_json = event.as_json(); let body_bytes = event_json.into_bytes(); - let url = format!("{}/events", relay_http_base_url(relay_url)); + let url = format!("{}/events", base); let auth = build_nip98_auth_header_for_keys(agent_keys, &Method::POST, &url, &body_bytes)?; let mut request = state @@ -708,6 +717,30 @@ mod tests { ); } + #[test] + fn blank_relay_http_base_is_empty() { + // A blank relay URL has no scheme to rewrite, so the HTTP base is empty. + assert_eq!(relay_http_base_url(""), ""); + assert_eq!(relay_http_base_url(" "), ""); + } + + #[test] + fn ui_created_agent_blank_relay_resolves_to_reachable_host() { + // A blank per-agent relay (the UI create case) resolves through + // `effective_agent_relay_url` to a real workspace host, not a hostless base. + let resolved_relay_url = ""; // UI create flow: no per-agent relay pinned + let workspace_relay = "wss://staging.example.com"; + + let sync_relay_url = effective_agent_relay_url(resolved_relay_url, workspace_relay); + let base = relay_http_base_url(&sync_relay_url); + + assert!( + !base.is_empty(), + "blank per-agent relay must resolve to a real workspace host, not a hostless base" + ); + assert_eq!(base, "https://staging.example.com"); + } + // ── classify_intercepted_response ──────────────────────────────────────── #[test]