Skip to content

fix(desktop): keep the agent relay authority unnormalized at spawn - #3941

Open
D12Labs wants to merge 1 commit into
block:mainfrom
D12Labs:ruben/agent-relay-authority
Open

fix(desktop): keep the agent relay authority unnormalized at spawn#3941
D12Labs wants to merge 1 commit into
block:mainfrom
D12Labs:ruben/agent-relay-authority

Conversation

@D12Labs

@D12Labs D12Labs commented Jul 31, 2026

Copy link
Copy Markdown

Summary

Desktop-managed agents on a self-hosted relay authenticate into a different tenant than their owner, discover zero channels, and sit idle while reporting healthy:

INFO buzz_acp: buzz-acp starting: relay=ws://127.0.0.1:3000 ...
INFO buzz_acp: discovered 0 channel(s)
WARN buzz_acp: no channel subscriptions resolved — agent will sit idle

spawn_agent_child derived the child's BUZZ_RELAY_URL from runtime_key.relay_url. ManagedAgentRuntimeKey::new canonicalizes that field via buzz_core::relay::normalize_relay_url, which folds every loopback host to 127.0.0.1 so a pair spelled localhost and one spelled 127.0.0.1 collapse to a single runtime identity. That is correct for identity and wrong for connecting: the relay resolves each connection's tenant from the request authority, and under row-zero host binding localhost:3000 and 127.0.0.1:3000 are distinct communities.

So a desktop on ws://localhost:3000 writes its channels, owner row and events into the localhost:3000 community, while every agent it spawns connects to ws://127.0.0.1:3000 and finds an empty one. Fail-closed tenancy then correctly gives those agents nothing, and buzz-acp's kind:39002 discovery legitimately returns zero — so nothing errors and it reads as a broken agent rather than a tenant split.

relay::relay_http_base_url already documents and tests this exact invariant:

Tenant host-binding keys off the HTTP Host/authority. The desktop must not rewrite localhost to 127.0.0.1, or local dev HTTP calls target a different unmapped community than the WebSocket URL.

That guard was defeated upstream — it received a value this code had already rewritten. The git-credential-nostr HTTP base is derived from the same variable, so it was mis-scoped too.

The change

Route the child's relay URL through a named, authority-preserving helper instead of the canonical pair URL. Only whitespace and a trailing root slash are trimmed; the host is never rewritten.

Identity stays canonical — dedup, runtime_id, log paths are all unchanged. Only the connection keeps the caller's spelling. Remote hosts are unaffected, since normalization only ever rewrote loopback.

Related issues

This implements suggested fix 3 from #3283 ("Don't canonicalize the relay URL handed to spawned harnesses — pass through the community URL the app itself uses"), and addresses the launcher half of #3505, #2444 and #2641. Four independent reports of the same trap.

I went with 3 rather than the alternatives deliberately:

  • 1 — normalize loopback in tenant resolution. Changes tenancy semantics for every deployment to fix a dev-only symptom. Too broad for this.
  • 2 — seed only the canonical loopback host. Fixes the default just dev path but not a user who types localhost into the community dialog, which is the actual reproduction here.
  • 4 — warn on zero-discovery mismatch. Complementary and worth doing, but a diagnostic, not a fix.

The seeding contradiction #3283 describes still stands — seed-local-community.sh continues to register four loopback spellings as four isolated tenants, so anything else that canonicalizes a host before a tenant lookup can hit the same class of bug. I would not close #3283 on this PR; the agent symptom is fixed, the underlying "one host = one community" vs "localhost == 127.0.0.1" contradiction is not.

Testing

  • 5 new regression tests in managed_agents::runtime::tests, including one pinning the invariant directly: ManagedAgentRuntimeKey::new(.., "ws://localhost:3000") canonicalizes to 127.0.0.1 and must differ from the connect URL. Whitespace/trailing-slash trimming and remote-host passthrough are covered.
  • cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib2008 passed, 0 failed, 14 ignored.
  • just desktop-tauri-clippy (-D warnings) and just desktop-tauri-fmt clean.

Reproduced the original failure on macOS with Colima against a self-hosted relay: four managed agents (3× claude-agent-acp, 1× codex-acp) all logged discovered 0 channel(s) while the desktop had 4 channels and 89 events in the localhost:3000 tenant, confirmed by SELECT on communities / channels / events.

Limits of the verification, stated plainly: the new tests pin the helper's contract, not that spawn_agent_child calls it — that path needs an AppHandle and isn't unit-testable, so a future edit to the call site would not fail the suite. Closing that properly needs a seam around spawn-env construction, which felt out of scope for a one-line behavioral fix; happy to add it if maintainers prefer. I also verified the fix only by unit test and inspection rather than end-to-end, because unblocking my own instance required aligning both sides onto 127.0.0.1, and demonstrating the localhost path again would mean recreating the split.

Managed agents on a local relay authenticated into a different tenant
than their owner, discovered zero channels, and idled while reporting
healthy.

spawn_agent_child derived the child's BUZZ_RELAY_URL from
runtime_key.relay_url. ManagedAgentRuntimeKey::new canonicalizes that
field with buzz_core::relay::normalize_relay_url, which folds every
loopback host to 127.0.0.1 so a pair spelled "localhost" and one spelled
"127.0.0.1" collapse to one runtime identity. That is right for identity
and wrong for connecting: the relay resolves each connection's tenant
from the request authority, and under row-zero host binding
localhost:3000 and 127.0.0.1:3000 are distinct communities.

So a desktop on ws://localhost:3000 published its channels, owner row,
and events into the localhost:3000 community while every agent it
spawned connected to ws://127.0.0.1:3000 and saw an empty one. The
harness then logged "discovered 0 channel(s)" and "no channel
subscriptions resolved — agent will sit idle", which reads as a
misconfigured agent rather than a tenant split.

relay::relay_http_base_url already documents and tests this invariant
("The desktop must not rewrite localhost to 127.0.0.1, or local dev HTTP
calls target a different unmapped community than the WebSocket URL"), but
the guard was defeated upstream — it received a value this code had
already rewritten, so the git-credential HTTP base was mis-scoped too.

Pass the caller's workspace relay URL through a named,
authority-preserving helper instead. Identity stays canonical (dedup,
runtime_id, log paths); only the connection keeps the caller's spelling.
Remote hosts are unaffected, since normalization only rewrote loopback.

Signed-off-by: D12Labs <Ruben.Carrera@District12Labs.com>
@D12Labs
D12Labs requested a review from a team as a code owner July 31, 2026 15:43
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.

Local dev: loopback hosts are seeded as separate communities, so desktop-managed agents silently discover 0 channels

1 participant