Fix cross-owner relay agent mentions in owner-only builds - #6338
Conversation
Keep the packaged owner-only capability scoped to Desktop-managed runtime boundaries. Relay agents remain governed by verified ownership, signed respond_to policy, shared channel membership, and send-time revalidation. Fixes #6329. Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
wesbillman
left a comment
There was a problem hiding this comment.
BLOCK — the marked build now admits legacy relay records with no verified NIP-OA owner.
The equality clamp should go, but this patch removes the final authenticity gate with it. relay_agents_from_directory_events deliberately merges self-authored legacy kind:10100 entries and sets owner_pubkey = None for them (desktop/src-tauri/src/nostr_convert/agent_directory.rs:28-52,59-76). At head 8ee70ba1d, list_relay_agents_for_selection returns those entries after only relay-signed bot membership (desktop/src-tauri/src/commands/agent_discovery/relay_directory.rs:131-150,190-202). The removed marked-build final filter was also the only thing excluding owner_pubkey=None records. Consequently, a bot member can publish a legacy respond_to=anyone kind:10100 without any NIP-OA owner proof and become mentionable/sendable in the packaged owner-only client. That contradicts the PR's promise to preserve verified ownership and the issue's explicit requirement not to weaken fail-closed behavior for missing/invalid ownership proof.
The Playwright additions do not catch this because their mock relayAgents inputs bypass native profile/policy verification; notably the new allowlist and anyone fixtures omit ownerPubkey on the relay DTO while searchProfiles is a separate frontend mock.
Smallest safe remedy: in the marked build, retain relay entries whose owner_pubkey is present, without comparing that owner to the viewer. Keep ordinary OSS legacy compatibility unchanged. Add a native regression that the marked policy admits a verified cross-owner entry but rejects an ownerless legacy entry, plus a packaged E2E whose relay DTO carries the verified cross-owner owner.
The managed-runtime boundaries themselves remain intact: local spawn still projects through owner_only() and provider reconciliation still reads owner_only_access_build().
Verdict: BLOCK until marked builds preserve NIP-OA verification while dropping only viewer/owner equality. Reviewed exact head 8ee70ba1dfd7ecfaf7ad0c4703d6fc500db87fdf; git diff --check origin/main...HEAD passed and the review worktree was clean.
Preserve ownerless legacy relay directory compatibility for OSS builds while keeping packaged builds fail closed on missing NIP-OA ownership. Verified cross-owner agents remain governed by their owner's signed respond_to policy. Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
wesbillman
left a comment
There was a problem hiding this comment.
CLEAR at corrected head f43411f6a957aa84e73a0d76085e1d30f138f2c7.
The blocker is resolved at the correct boundary:
- marked builds now retain only converted relay entries with
owner_pubkey.is_some(), preserving fail-closed NIP-OA ownership without restoring viewer/owner equality (desktop/src-tauri/src/commands/agent_discovery/relay_directory.rs:115-119,198-208); - that same filtering helper is reached by both full autocomplete discovery and selected-agent send-time revalidation through
list_relay_agents_for_selection(relay_directory.rs:121-125,215-242); - OSS passes
falseand therefore retains ownerless legacy kind:10100 compatibility; - native regressions pin verified cross-owner retention, ownerless marked-build rejection, and OSS legacy retention (
relay_directory.rs:249-308); - packaged allowlist and
anyoneE2E fixtures now carry the external verified owner (desktop/tests/e2e/mentions.spec.ts:1476-1483,1585-1592).
The original patch's other security boundaries remain intact: owner-authored policy lookup still derives coordinates from agent-signed NIP-OA profiles, membership remains relay-signed and channel-scoped, frontend authorization still evaluates signed respond_to, and send-time discovery remains fail closed. Local spawn and provider deployment owner-only projection are unchanged.
I found no material correctness or authorization defect in the corrected diff. git diff origin/main...HEAD --check passed; review worktree was clean and exact head matched the GitHub PR. I did not duplicate Carl's reported full gates, and CI was still in progress when reviewed.
Verdict: CLEAR / approve. This supersedes my BLOCK at 8ee70ba1d.
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Reviewed: bbd20fae75ecc3bd7a83cc12a65379fac22a2b79..f43411f6a957aa84e73a0d76085e1d30f138f2c7 (exact head f43411f6a957aa84e73a0d76085e1d30f138f2c7)
Risk: high — this changes a packaged-build ownership/authz boundary in relay-agent discovery and the user-visible mention/send path.
Behavior/contracts traced: relay-signed shared-channel membership → candidate-authored NIP-OA ownership profile → exact owner-authored 30177 response policy → autocomplete eligibility and owner disclosure → exact-pubkey selection → two send-time native revalidations. Marked builds now require a verified owner without requiring that owner to equal the viewer; OSS retains ownerless legacy compatibility. Discovery stays on the active relay/community, and revocation, membership loss, ownership/policy loss or mutation, malformed managed policy, and query failure all fail closed by removing relay admission rather than trusting cached UI state.
Findings: no unresolved material correctness, authorization, tenancy, race, accessibility, or misleading-state finding. The corrected native boundary and regressions also resolve the earlier ownerless-record concern at the reviewed head.
Validation at the exact clean head:
just desktop-test: 5093/5093 passed.just desktop-tauri-test: full Tauri workspace passed.just desktop-tauri-test-compiled-flags: unmarked and marked matrix passed; marked Tauri lib 2593 passed, 17 ignored.just desktop-check: passed.pnpm build:e2eplus focused Playwright owner-only mention cases: verified cross-owner allowlist, verified same-owner, and cross-owneranyonepassed independently; assertions cover the exact emitted agent pubkey.- Causal mutation removing marked-build verified-owner retention made the new native regression fail (
left: 2, right: 1); restoring this head passed. git diff --checkpassed. Current Desktop core/build, macOS, Windows Rust, smoke/integration E2E, Rust lint, release-candidate, and DCO checks are green.
The UI path also centrally excludes archived identities, presents managed by <owner> in autocomplete, provides screen-reader owner text/fallback, and routes by pubkey rather than ambiguous display name.
Residual risk: focused workflow proof used the E2E bridge, not a freshly launched native GUI against an isolated live relay. One combined three-case Playwright run lost its first row and then its web server exited; all three cases passed independently, so this is recorded as a non-reproduced runner/server anomaly rather than a product finding. Native authenticity and build-state behavior are covered by the full Tauri suites, compiled-flag matrix, source trace, CI, and mutation proof, but fresh live-relay GUI lifecycle behavior was not exercised.
— :bot: Jude’s code review agent
…-scoping * origin/main: perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) Fix cross-owner relay agent mentions in owner-only builds (#6338) feat(cli): accept Buzz message links for thread reads (#6359) feat(workflows): add workflow editor (#6248) fix(desktop): preserve huddle speech boundaries (#6397) test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356) fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360) fix(acp): guard against unrequested public relay skills (#6394) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ounting-and-solo * origin/main: (48 commits) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) Fix cross-owner relay agent mentions in owner-only builds (#6338) feat(cli): accept Buzz message links for thread reads (#6359) feat(workflows): add workflow editor (#6248) fix(desktop): preserve huddle speech boundaries (#6397) test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356) fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360) fix(acp): guard against unrequested public relay skills (#6394) feat(desktop): refine context-aware Projects collaboration (#6396) fix(desktop): distinguish duplicate agent devices (#6337) feat(desktop): close Buzz window with Cmd+W (#6314) refactor(prompt): simplify Buzz agent guidance (#6340) feat(desktop): make Projects workspaces selectable (#6368) Add Buzz-native collaboration benchmarks (#6264) Polish mobile timeline and emoji interactions (#6297) feat(desktop): make the Projects overview follow the selected section (#6335) refactor(desktop): coordinate TTS playback (#6341) ... Signed-off-by: Atish Patel <atishpatel2012@gmail.com>
…ifications-pr * origin/main: (33 commits) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) Fix cross-owner relay agent mentions in owner-only builds (#6338) feat(cli): accept Buzz message links for thread reads (#6359) feat(workflows): add workflow editor (#6248) fix(desktop): preserve huddle speech boundaries (#6397) test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356) fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360) fix(acp): guard against unrequested public relay skills (#6394) feat(desktop): refine context-aware Projects collaboration (#6396) fix(desktop): distinguish duplicate agent devices (#6337) feat(desktop): close Buzz window with Cmd+W (#6314) refactor(prompt): simplify Buzz agent guidance (#6340) feat(desktop): make Projects workspaces selectable (#6368) Add Buzz-native collaboration benchmarks (#6264) Polish mobile timeline and emoji interactions (#6297) feat(desktop): make the Projects overview follow the selected section (#6335) refactor(desktop): coordinate TTS playback (#6341) ... Signed-off-by: Tom Brow <tomb@block.xyz>
* origin/main: (64 commits) chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392) polish(desktop): finish Projects navigation and context chrome (#6429) fix(desktop): clarify add agents channel action (#6374) Repair stale large channel roster snapshots (#6251) feat(desktop-messages): show compact Buzz link metadata (#6252) feat(workflows): reply in-thread from send_message action (#6178) perf(desktop): split discover_acp_providers into cheap and forced paths (#6330) fix(desktop): restore recent channel sorting (#6402) fix(desktop): isolate main timeline stacking context from focus drawer (#6398) fix(desktop): make reconnect repair lossless (#6415) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) Fix cross-owner relay agent mentions in owner-only builds (#6338) feat(cli): accept Buzz message links for thread reads (#6359) feat(workflows): add workflow editor (#6248) fix(desktop): preserve huddle speech boundaries (#6397) test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356) fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360) ... Signed-off-by: Luke Tornquist <tornquist@squareup.com>
…kflow-management * origin/main: (27 commits) chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392) polish(desktop): finish Projects navigation and context chrome (#6429) fix(desktop): clarify add agents channel action (#6374) Repair stale large channel roster snapshots (#6251) feat(desktop-messages): show compact Buzz link metadata (#6252) feat(workflows): reply in-thread from send_message action (#6178) perf(desktop): split discover_acp_providers into cheap and forced paths (#6330) fix(desktop): restore recent channel sorting (#6402) fix(desktop): isolate main timeline stacking context from focus drawer (#6398) fix(desktop): make reconnect repair lossless (#6415) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) Fix cross-owner relay agent mentions in owner-only builds (#6338) feat(cli): accept Buzz message links for thread reads (#6359) feat(workflows): add workflow editor (#6248) fix(desktop): preserve huddle speech boundaries (#6397) test(desktop): use a wordlist-safe separator in passphrase word-count test (#6356) fix(models): curate Databricks alias-aware labels for 5 missing endpoints (#6360) ... Signed-off-by: Luke Tornquist <tornquist@squareup.com> # Conflicts: # desktop/src/features/workflows/ui/WorkflowDeleteDialog.tsx # desktop/src/features/workflows/ui/WorkflowsView.tsx
…ntion-phase1 * origin/main: (37 commits) fix(desktop): sender names in notifications + macOS click-through routing (#6427) docs: clarify two-layer moderation ownership (#6481) Fix mobile thread tail and iOS channel header (#6399) chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392) polish(desktop): finish Projects navigation and context chrome (#6429) fix(desktop): clarify add agents channel action (#6374) Repair stale large channel roster snapshots (#6251) feat(desktop-messages): show compact Buzz link metadata (#6252) feat(workflows): reply in-thread from send_message action (#6178) perf(desktop): split discover_acp_providers into cheap and forced paths (#6330) fix(desktop): restore recent channel sorting (#6402) fix(desktop): isolate main timeline stacking context from focus drawer (#6398) fix(desktop): make reconnect repair lossless (#6415) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) Fix cross-owner relay agent mentions in owner-only builds (#6338) feat(cli): accept Buzz message links for thread reads (#6359) feat(workflows): add workflow editor (#6248) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Resolves the desktop agent-mention conflicts from upstream block#6338 ("Fix cross-owner relay agent mentions in owner-only builds"), which removed the owner-only gate from mention admission that this fork had also been carrying. - agentAutocompleteEligibility: take upstream's removal of the ownerOnly/isManagedAgent/ownerPubkey gate; keep the fork's lenient channel-member branch (#5) and its directoryAgentPubkeys input, which upstream's change does not cover. - agentMentionRevalidation: same — drop the owner-profile proof fetch, keep the roster fetch that the member branch depends on. - useMentions: keep the fork's per-community relay URL (#4), drop the owner-only query. - MembersSidebar: keep the fork's MembersSidebarAddMemberRows split and delete upstream's duplicate AddMemberSearchResultRow.tsx. - relayReconnectReplay.test.mjs: upstream's new coupling guard reads the drift literal from ingest.rs; this fork hoists it into buzz-core for buzz-waker, so point the guard at the real definition. Validated: desktop tsc, 5438 desktop tests, biome, px/file-size guards, cargo fmt, cargo check (workspace + Tauri, all targets), 2781 Tauri lib tests. Signed-off-by: Junchao Yan <yjc801@gmail.com>
…tream block#6224, block#6338) Ported from block/buzz 3fdf289 and the block#6338 follow-up. Crew keeps its inline list_relay_agents directory command and invoke.rs handler macro; the bounded send-time check lands as a new commands::mention_authorization module instead of upstream's relay_directory.rs rewrite. Cross-owner relay agents stay mentionable in owner-only builds when relay policy plus bot-role membership authorize them; Crew's RelayAgent has no ownerPubkey field, so admission is derived from respondTo/allowlist and channel membership rather than owner identity. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
… RUSTSEC-2026-0258 (#306) * port(desktop): enforce agent mention authorization at send boundaries (upstream block#5681) Ported from block/buzz bcf353c. Crew keeps its extracted useMentionSendComplete hook, explicit-empty imeta edit save, removed-mention diff and Project workspace resolution; upstream revalidation is layered on top. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com> * port(desktop): bound send-time relay agent mention authorization (upstream block#6224, block#6338) Ported from block/buzz 3fdf289 and the block#6338 follow-up. Crew keeps its inline list_relay_agents directory command and invoke.rs handler macro; the bounded send-time check lands as a new commands::mention_authorization module instead of upstream's relay_directory.rs rewrite. Cross-owner relay agents stay mentionable in owner-only builds when relay policy plus bot-role membership authorize them; Crew's RelayAgent has no ownerPubkey field, so admission is derived from respondTo/allowlist and channel membership rather than owner identity. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com> * port(acp): guard against unrequested public relay skills (upstream block#6394) Ported from block/buzz d274a6e. Adds the base-prompt restriction plus a regression test asserting the guard stays in the shared prompt. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com> * port(deps): bump h2 to 0.4.16 for RUSTSEC-2026-0258 (upstream block#6222) Ported from block/buzz cc8a8b0. Only the h2 entry is bumped; upstream's incidental windows-sys re-resolution churn is left out to keep the lockfile diff scoped. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com> * test(e2e): accept the invite prompt in the cross-owner relay mention test The owner-only cross-owner case still surfaces the not-in-channel invite prompt before publication, so the test must accept it to observe the outgoing p tag. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com> * fix(mentions): drop reference mention tags for denied edit mentions Edit-save revalidation stripped denied agents from mentionPubkeys but the non-notifying reference tags were built before revalidation, so a revoked agent's pubkey was still published and rendered as an agent chip. Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com> * fix(mentions): map RelayAgent.ownerPubkey in revalidate wrapper Main now requires ownerPubkey on RelayAgent. Restore the mapping that lived in the previous merge resolution so send-time revalidation stays type-correct after the rebase. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: oscarlehuu <oscarlehuu@users.noreply.github.com> --------- Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Oscar Le <oscar.lehuu@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: oscarlehuu <oscarlehuu@users.noreply.github.com>
This skill said two lists — the harness gate and the agent's kind 10100 — had to agree. There are three, and the one it omitted is the one Buzz Desktop actually enforces. Since block/buzz#6338 (Desktop v0.5.18) the owner-signed kind 30177 does not merely outrank the agent's own 10100, it replaces it: relay_agents_from_directory_events removes the 10100 entry for any pubkey with a verified managed policy and inserts the owner's record instead. So an agent can publish respond_to "anyone", the relay can serve it correctly, and Desktop shows it to nobody but its owner. owner-setup.py had no way to express that policy: it hardcoded allowlist:[owner] into every 30177 it wrote. Harmless while v0.5.17's build gate hid cross-owner agents regardless — and load-bearing the moment #6338 made the owner's record authoritative. Adds --respond-to and --allowlist, validated as 64-char hex before signing, owner always included. The trap worth repeating, and now documented in three places: kind 30177 is replaceable and the script rewrites it wholesale, so any later run without the policy flags silently reverts the agent to owner-only — including the run that seats it in a channel. Same for --model/--provider on a definition-less agent. Also replaces the v0.5.17 ceiling section. That limitation was real, was reported as block/buzz#6329, and was fixed in #6338 and shipped in v0.5.18 on 2026-08-21 — so the advice to pin v0.5.14 is now wrong and is removed everywhere, along with the superseded #6333. v0.5.18 is the floor instead, with a note that it also requires a verified NIP-OA owner, which means the attestation stopped being optional. Verified end to end on a live relay: three agents built from scratch with the fixed script published three genuinely different policies, and a non-owner saw exactly the two intended to admit them. A production agent whose harness admitted four people while its 30177 named one was corrected the same way; all three of its lists now agree.
## Summary - keep `BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY` scoped to Desktop-managed local start and provider deployment boundaries - stop filtering independently operated relay agents by viewer/owner equality in native discovery, autocomplete, and send-time revalidation - preserve NIP-OA ownership verification, owner-authored `respond_to` policy, shared-channel membership, and fail-closed send-time checks - replace the packaged-build regression expectation with coverage for cross-owner allowlisted and `respond_to=anyone` relay agents, including the emitted agent `p` tag Fixes block#6329. ## Why The packaged 0.5.17 build reused its managed-runtime owner-only capability in relay-agent mention admission. That silently hid correctly configured shared agents owned by another operator, even when their verified policy explicitly authorized the viewer. The capability is intended to constrain runtimes Desktop starts or deploys, not external relay agents. ## Validation - `pnpm --dir desktop test` (5093 passed) - `cargo test --manifest-path desktop/src-tauri/Cargo.toml relay_directory --lib` (4 passed, 2 ignored real-relay tests) - focused agent mention unit tests (35 passed) - Desktop TypeScript/E2E build - focused Playwright mention tests (3 passed) - pre-commit formatting hooks - pre-push branch-skew, file-size, Desktop check/typecheck/unit tests, and Tauri checks - `git diff --check` --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
…ests main's frontend mention gate (block#4913, block#6338) and backend list_relay_agents sourcing already fix the original bug this branch targeted, so those changes are dropped as superseded during this rebase. block#6338 added coverage for the owner-only-build variant, but the default-build path for a cross-owner relay agent with respond_to=anyone was still untested. Decouple the default relay-agent-directory fixture (alice/charlie) from the real human TEST_IDENTITIES pubkeys they coincidentally reused, extend the relay-agent mock seed to also register real channel bot membership, and add tests mirroring the exact reported repro: a bot member of a shared channel, owned by another identity, with respond_to=anyone, must be mentionable — and must stay hidden if it is only relay-directory-listed without real channel membership. Signed-off-by: oleg-ai-dev <oleg.ai.development@gmail.com>
Summary
BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLYscoped to Desktop-managed local start and provider deployment boundariesrespond_topolicy, shared-channel membership, and fail-closed send-time checksrespond_to=anyonerelay agents, including the emitted agentptagFixes #6329.
Why
The packaged 0.5.17 build reused its managed-runtime owner-only capability in relay-agent mention admission. That silently hid correctly configured shared agents owned by another operator, even when their verified policy explicitly authorized the viewer. The capability is intended to constrain runtimes Desktop starts or deploys, not external relay agents.
Validation
pnpm --dir desktop test(5093 passed)cargo test --manifest-path desktop/src-tauri/Cargo.toml relay_directory --lib(4 passed, 2 ignored real-relay tests)git diff --check