From 89e0350bbf7af9ee107f0bf952c8c53f2799fe09 Mon Sep 17 00:00:00 2001 From: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Date: Mon, 3 Aug 2026 08:32:15 -0400 Subject: [PATCH] Fix local relay auth and community persistence Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> --- crates/buzz-db/src/lib.rs | 21 +++++++++++ crates/buzz-relay/src/main.rs | 9 ++++- desktop/src-tauri/src/commands/agents.rs | 40 +++++++++++++++++++++ desktop/src-tauri/src/commands/workspace.rs | 9 ++++- desktop/src-tauri/src/local_relay.rs | 2 ++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/crates/buzz-db/src/lib.rs b/crates/buzz-db/src/lib.rs index 9b9a2d32a..cf4aaf94e 100644 --- a/crates/buzz-db/src/lib.rs +++ b/crates/buzz-db/src/lib.rs @@ -1439,6 +1439,27 @@ impl Db { Ok(()) } + /// Rebind the durable loopback community to this single-node process's + /// current ephemeral authority while preserving its stable UUID and data. + /// SQLite-only: production deployments never rewrite tenant host mappings. + pub async fn rebind_single_node_community_host( + &self, + normalized_host: &str, + owner_pubkey: &str, + ) -> Result> { + if matches!(&self.backend, DbBackend::SQLite(_)) { + return sqlite::rebind_single_node_community_host( + self.sqlite_pool()?, + normalized_host, + owner_pubkey, + ) + .await; + } + Err(DbError::UnsupportedBackend( + "single-node community rebind requires SQLite", + )) + } + /// Ensure a configured community host exists and return its row. /// /// This is the startup/config seeding path for N=1 deployments. Migrations diff --git a/crates/buzz-relay/src/main.rs b/crates/buzz-relay/src/main.rs index fb5aacc15..f972caaff 100644 --- a/crates/buzz-relay/src/main.rs +++ b/crates/buzz-relay/src/main.rs @@ -1231,7 +1231,14 @@ async fn run_single_node(config: Config, tracer_init: telemetry::TracerInit) -> "Cannot derive community host from BUZZ_RELAY_URL" )); } - let community = db.ensure_configured_community(&host).await?.id; + let community = if let Some(owner) = config.relay_owner_pubkey.as_deref() { + match db.rebind_single_node_community_host(&host, owner).await? { + Some(record) => record.id, + None => db.ensure_configured_community(&host).await?.id, + } + } else { + db.ensure_configured_community(&host).await?.id + }; if let Some(owner) = config.relay_owner_pubkey.as_deref() { db.bootstrap_owner(community, owner).await?; } diff --git a/desktop/src-tauri/src/commands/agents.rs b/desktop/src-tauri/src/commands/agents.rs index dd61fc939..db9562e8d 100644 --- a/desktop/src-tauri/src/commands/agents.rs +++ b/desktop/src-tauri/src/commands/agents.rs @@ -18,6 +18,46 @@ use crate::{ util::now_iso, }; +pub(super) fn backfill_missing_agent_auth_tags( + app: &AppHandle, + state: &AppState, +) -> Result { + let owner_keys = state.signing_keys()?; + let compat_owner = nostr::Keys::parse(&owner_keys.secret_key().to_secret_hex()) + .map_err(|e| format!("failed to bridge owner keys: {e}"))?; + let _store_guard = state + .managed_agents_store_lock + .lock() + .map_err(|e| e.to_string())?; + let mut records = load_managed_agents(app)?; + let mut changed = 0; + for record in &mut records { + if record.auth_tag.is_some() + || record + .pubkey + .eq_ignore_ascii_case(&owner_keys.public_key().to_hex()) + { + continue; + } + let agent = nostr::PublicKey::from_hex(&record.pubkey) + .map_err(|e| format!("invalid managed agent pubkey {}: {e}", record.pubkey))?; + record.auth_tag = Some( + buzz_sdk_pkg::nip_oa::compute_auth_tag(&compat_owner, &agent, "").map_err(|e| { + format!( + "failed to compute NIP-OA auth tag for {}: {e}", + record.pubkey + ) + })?, + ); + record.updated_at = now_iso(); + changed += 1; + } + if changed > 0 { + save_managed_agents(app, &records)?; + } + Ok(changed) +} + /// Read the workspace owner pubkey without holding the lock. Used to populate `BUZZ_ACP_AGENT_OWNER` /// as a fallback for legacy agent records that have no NIP-OA `auth_tag`. pub(super) fn workspace_owner_hex(state: &AppState) -> Result { diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs index f7b022d04..32f041cda 100644 --- a/desktop/src-tauri/src/commands/workspace.rs +++ b/desktop/src-tauri/src/commands/workspace.rs @@ -145,7 +145,8 @@ pub async fn apply_workspace( None => None, }; - if crate::local_relay::is_local_relay_url(&relay_url) { + let is_local_workspace = crate::local_relay::is_local_relay_url(&relay_url); + if is_local_workspace { // Only the UI-created local sentinel can launch the bundled relay. // An arbitrary loopback URL remains a normal remote community. let keys = parsed_keys.clone().unwrap_or(state.signing_keys()?); @@ -228,6 +229,12 @@ pub async fn apply_workspace( } try_regenerate_nest(&app); + if is_local_workspace { + let changed = crate::commands::agents::backfill_missing_agent_auth_tags(&app, &state)?; + if changed > 0 { + eprintln!("buzz-desktop: backfilled NIP-OA auth tags for {changed} local agent(s)"); + } + } Ok::<(), String>(()) }) diff --git a/desktop/src-tauri/src/local_relay.rs b/desktop/src-tauri/src/local_relay.rs index dc33243f9..7a70d8155 100644 --- a/desktop/src-tauri/src/local_relay.rs +++ b/desktop/src-tauri/src/local_relay.rs @@ -53,6 +53,7 @@ impl LocalRelayConfig { ("BUZZ_LOCAL_DB", sqlite_url(&db_path)), ("BUZZ_LOCAL_MEDIA_DIR", media_dir.display().to_string()), ("BUZZ_REQUIRE_RELAY_MEMBERSHIP", "true".to_string()), + ("BUZZ_ALLOW_NIP_OA_AUTH", "true".to_string()), ("RELAY_OWNER_PUBKEY", self.owner_pubkey.clone()), ("BUZZ_RELAY_PRIVATE_KEY", self.relay_private_key.clone()), ] @@ -369,6 +370,7 @@ mod tests { env.get("BUZZ_REQUIRE_RELAY_MEMBERSHIP"), Some(&"true".to_string()) ); + assert_eq!(env.get("BUZZ_ALLOW_NIP_OA_AUTH"), Some(&"true".to_string())); assert_eq!(env.get("RELAY_OWNER_PUBKEY"), Some(&"owner".to_string())); assert_eq!( env.get("BUZZ_RELAY_PRIVATE_KEY"),