Skip to content

fix(relay): normalize loopback relay URL to localhost for managed agents - #4345

Open
yougeqiu wants to merge 1 commit into
block:mainfrom
yougeqiu:fix/loopback-localhost
Open

fix(relay): normalize loopback relay URL to localhost for managed agents#4345
yougeqiu wants to merge 1 commit into
block:mainfrom
yougeqiu:fix/loopback-localhost

Conversation

@yougeqiu

@yougeqiu yougeqiu commented Aug 2, 2026

Copy link
Copy Markdown

Problem

On self-hosted dev, every Desktop-managed agent connects and authenticates, then discovers 0 channels and sits idle — while the Desktop UI itself works in the same instance. Reproduces #3505, #3283, #3033, #2444.

Root cause

buzz_core::relay::normalize_relay_url canonicalizes loopback hosts to 127.0.0.1. The desktop's managed-agent launcher (ManagedAgentRuntimeKey::new in desktop/src-tauri/src/managed_agents/runtime_types.rs:22) feeds the agent spawn URL through this function, so an agent created against ws://localhost:PORT is spawned against ws://127.0.0.1:PORT.

The relay resolves community by the connection Host header, and buzz_core::tenant::normalize_host does not collapse loopback spellings — localhost:PORT and 127.0.0.1:PORT resolve to two distinct communities. The dev seed (scripts/seed-local-community.sh) registers both as separate communities rows, so:

  • App's main UI connection (localhost:PORT) → community A (has channels/members)
  • Managed agent (127.0.0.1:PORT) → community B (empty) → discovered 0 channel(s)

Agent log, every start:
```
INFO buzz_acp: connected to relay at ws://127.0.0.1:PORT
INFO buzz_acp: discovered 0 channel(s)
WARN buzz_acp: no channel subscriptions resolved — agent will sit idle
```

Fix

Canonicalize loopback to localhost instead of 127.0.0.1. The dev seed registers localhost as a community host, so the canonical relay URL now matches the host the relay's community lookup resolves — managed agents land in the same community as the app UI.

This is the sole non-test call site of normalize_relay_url (verified: `grep -rn normalize_relay_url` — only `runtime_types.rs:22`). It is not the NIP-42 AUTH comparison helper in `buzz-auth/src/nip42.rs` (a separate function pinning to `127.0.0.1` for the AUTH security boundary); that function is untouched.

Why not fix on the relay side

The relay's normalize_host cannot collapse loopback spellings without breaking NIP-98 HTTP auth: nip98_expected_url (bridge.rs:205) builds the expected URL from tenant.host(), and the agent signs its NIP-98 event with the host it dials. If the relay rewrote 127.0.0.1 → localhost at lookup time, tenant.host() would diverge from the agent's signed u tag → 401. Client-side canonicalization is the safe place to fix this. (This is the same conclusion #2944 reached before pivoting to a caller-side fix.)

Test

  • Updated loopback_spellings_have_one_identity to assert the canonical form is localhost.
  • All 237 buzz-core unit tests pass; cargo fmt --check and cargo clippy clean.
  • End-to-end verified: built a Windows canary via the fork's windows-canary workflow, installed it, and confirmed 4 managed agents now dial ws://localhost:3100 and discover channels (previously ws://127.0.0.1:3100 → 0 channels).

Closes #3505 #3283 #3033 #2444

Managed agents spawned by the desktop launcher dialed ws://127.0.0.1:PORT
even when the operator configured ws://localhost:PORT, because
normalize_relay_url collapsed loopback to 127.0.0.1. The relay resolves
community by the connection Host header, and the dev seed registers
localhost (not 127.0.0.1) as a community host, so agents landed in a
separate empty community and discovered 0 channels — while the desktop UI,
which dials the URL the operator entered, worked in the same instance.

Canonicalize loopback to localhost so the agent's dial host matches the
host the relay's community lookup resolves. This is the only non-test
call site of normalize_relay_url (runtime_types.rs:22); the NIP-42 AUTH
comparison helper in buzz-auth/src/nip42.rs is a separate function and is
not touched. The relay-side normalize_host cannot collapse loopback
without breaking NIP-98 (nip98_expected_url builds the expected URL from
tenant.host(), which would diverge from the agent's signed u tag).

Refs block#3505 block#3283 block#3033 block#2444

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Managed agents sit idle in an empty community: launcher pins 127.0.0.1 while the app UI uses localhost (self-hosted dev)

1 participant