From fd168e7952f5527f7eb8b7734616872261ad1978 Mon Sep 17 00:00:00 2001
From: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh
<146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
Date: Sat, 1 Aug 2026 12:58:32 -0400
Subject: [PATCH] Add desktop local relay sidecar flow
Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
---
Justfile | 7 +-
desktop/src-tauri/src/commands/identity.rs | 7 +-
desktop/src-tauri/src/commands/workspace.rs | 27 +-
desktop/src-tauri/src/lib.rs | 1 +
desktop/src-tauri/src/local_relay.rs | 323 ++++++++++++++++++
desktop/src-tauri/tauri.conf.json | 3 +-
.../communities/communityStorage.test.mjs | 3 +
.../features/communities/communityStorage.ts | 12 +-
desktop/src/features/communities/types.ts | 2 +
.../features/communities/ui/WelcomeSetup.tsx | 26 ++
.../features/communities/useCommunityInit.ts | 11 +-
.../onboarding/communityOnboarding.tsx | 2 +
scripts/bundle-sidecars.sh | 6 +-
13 files changed, 414 insertions(+), 16 deletions(-)
create mode 100644 desktop/src-tauri/src/local_relay.rs
diff --git a/Justfile b/Justfile
index 0a43249d5..bff1c48fc 100644
--- a/Justfile
+++ b/Justfile
@@ -155,7 +155,7 @@ _ensure-sidecar-stubs:
set -euo pipefail
TARGET=$(rustc -vV | sed -n 's|host: ||p')
mkdir -p desktop/src-tauri/binaries
- SIDECARS=(buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz)
+ SIDECARS=(buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz buzz-relay)
if [[ "$TARGET" != *windows* ]]; then
SIDECARS+=(buzz-backend-kubernetes)
fi
@@ -258,6 +258,7 @@ desktop-release-build target="aarch64-apple-darwin":
touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET"
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
touch "desktop/src-tauri/binaries/buzz-$TARGET"
+ touch "desktop/src-tauri/binaries/buzz-relay-$TARGET"
pnpm install
cd {{desktop_dir}} && pnpm tauri build --features mesh-llm --target {{target}}
@@ -503,10 +504,10 @@ desktop-standalone *ARGS: _ensure-sidecar-stubs
#!/usr/bin/env bash
set -euo pipefail
export PATH="{{justfile_directory()}}/bin:$PATH"
- cargo build -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
+ cargo build -p buzz-acp -p buzz-agent -p buzz-backend-kubernetes -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr -p buzz-relay
TARGET=$(rustc -vV | sed -n 's|host: ||p')
TARGET_DIR=$(cargo metadata --format-version 1 --no-deps | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).target_directory")
- for bin in buzz-acp buzz-agent buzz-backend-kubernetes buzz-dev-mcp git-credential-nostr buzz; do
+ for bin in buzz-acp buzz-agent buzz-backend-kubernetes buzz-dev-mcp git-credential-nostr buzz buzz-relay; do
cp "${TARGET_DIR}/debug/${bin}" "desktop/src-tauri/binaries/${bin}-${TARGET}"
chmod +x "desktop/src-tauri/binaries/${bin}-${TARGET}"
done
diff --git a/desktop/src-tauri/src/commands/identity.rs b/desktop/src-tauri/src/commands/identity.rs
index ec2357b85..d6350a7ee 100644
--- a/desktop/src-tauri/src/commands/identity.rs
+++ b/desktop/src-tauri/src/commands/identity.rs
@@ -51,8 +51,11 @@ pub fn get_identity(state: State<'_, AppState>) -> Result
}
#[tauri::command]
-pub fn get_default_relay_url() -> String {
- relay::relay_ws_url()
+pub fn get_default_relay_url(state: State<'_, AppState>) -> Result {
+ // A hosted default remains just that: resolving it never spawns an unused
+ // local sidecar. The explicit local workspace sentinel starts its sidecar
+ // during `apply_workspace`.
+ Ok(relay::relay_ws_url_with_override(&state))
}
#[tauri::command]
diff --git a/desktop/src-tauri/src/commands/workspace.rs b/desktop/src-tauri/src/commands/workspace.rs
index aa88bfe39..f7b022d04 100644
--- a/desktop/src-tauri/src/commands/workspace.rs
+++ b/desktop/src-tauri/src/commands/workspace.rs
@@ -135,7 +135,9 @@ pub async fn apply_workspace(
tokio::task::spawn_blocking(move || {
let state = app.state::();
- // ── Validate before mutating ──────────────────────────────────────────
+ // Validate credentials before starting a local sidecar: an onboarding
+ // import may replace the in-memory identity, and the relay's durable
+ // nest/owner must match the identity that this workspace will use.
let parsed_keys = match nsec.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
Some(nsec_trimmed) => {
Some(Keys::parse(nsec_trimmed).map_err(|e| format!("invalid nsec: {e}"))?)
@@ -143,6 +145,18 @@ pub async fn apply_workspace(
None => None,
};
+ if crate::local_relay::is_local_relay_url(&relay_url) {
+ // 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()?);
+ let runtime = app.state::();
+ crate::local_relay::ensure_started(&app, &runtime, &keys, None)?;
+ } else {
+ // A desktop-owned relay is private to the active local workspace;
+ // do not leave it serving after a switch to any remote workspace.
+ crate::local_relay::stop(&app.state::());
+ }
+
// Decide the effective repos_dir from the candidate. A bad path does NOT
// reject — it is treated as if no override were set: relay/keys still
// apply, the bad value is not persisted, and a `repos-dir-error` surfaces
@@ -165,8 +179,17 @@ pub async fn apply_workspace(
// ── Apply all state changes (nothing below can fail) ──────────────────
{
+ // The local sentinel persists across restarts while the sidecar
+ // endpoint is deliberately ephemeral. Resolve it only after
+ // `ensure_started` above, before any agent can be restored.
+ let effective_relay_url = if crate::local_relay::is_local_relay_url(&relay_url) {
+ crate::local_relay::relay_url(&app.state::())
+ .ok_or("local relay did not start")?
+ } else {
+ relay_url
+ };
let mut override_guard = state.relay_url_override.lock().map_err(|e| e.to_string())?;
- *override_guard = Some(relay_url);
+ *override_guard = Some(effective_relay_url);
}
// Reset the Rust-side admission gate when switching workspace/community,
// matching `resetRateLimitGate()` on the TS side (useCommunityInit.ts:38).
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 7aa954ce8..555fee19b 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -14,6 +14,7 @@ mod initial_window;
mod key_backup;
mod link_preview_tags;
mod linux_media;
+mod local_relay;
#[cfg(target_os = "macos")]
mod macos_notifications;
mod managed_agents;
diff --git a/desktop/src-tauri/src/local_relay.rs b/desktop/src-tauri/src/local_relay.rs
new file mode 100644
index 000000000..c7d69e44c
--- /dev/null
+++ b/desktop/src-tauri/src/local_relay.rs
@@ -0,0 +1,323 @@
+//! Lifecycle management for the embedded single-node relay.
+//!
+//! The relay remains the source of truth for local-mode behavior. Desktop only
+//! chooses private loopback ports, supplies its identity and durable paths, and
+//! supervises the bundled `buzz-relay` binary.
+
+use std::{
+ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
+ path::{Path, PathBuf},
+ process::{Child, Command, Stdio},
+ sync::Mutex,
+ time::{Duration, Instant},
+};
+
+use nostr::Keys;
+use tauri::{AppHandle, Manager};
+
+const STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
+const POLL_INTERVAL: Duration = Duration::from_millis(100);
+
+pub(crate) struct LocalRelayRuntime {
+ child: Child,
+ url: String,
+ owner_pubkey: String,
+}
+
+pub(crate) type RuntimeState = Mutex
+
+
+