Skip to content

fix(desktop): make cross-owner channel-member agents mentionable - #4716

Open
oleg-ai-dev wants to merge 4 commits into
block:mainfrom
oleg-ai-dev:fix-cross-owner-agent-mentions
Open

fix(desktop): make cross-owner channel-member agents mentionable#4716
oleg-ai-dev wants to merge 4 commits into
block:mainfrom
oleg-ai-dev:fix-cross-owner-agent-mentions

Conversation

@oleg-ai-dev

Copy link
Copy Markdown

Problem

A bot agent that is a member of a channel shared between two different Buzz identities can never be @-mentioned (or manually addressed via a typed mention) by anyone except the human who owns/hosts it — even when the agent's respond_to is anyone. Reported and reproduced independently across desktop v0.4.20–v0.5.3, macOS/Windows, hosted/self-hosted relays: #3776 (canonical, most precise), #3277 (original report + a documented client-side stopgap), #2349 (long thread with the business case for team use), #2950.

Heads up to reviewers: I'm aware this issue already has a large number of open PRs attempting the same fix. I looked through the ones I could find before opening this — none of the ones I checked source respond_to/respond_to_allowlist from the correct (instance-tier) fields (see Cause 2 below), which as far as I can tell would leave the exact same bug at a different codepath if merged as-is. Flagging that here explicitly in case it's useful for triage, independent of whether this particular PR is the one that ends up merged.

Root cause (two independent, stacked causes — both are required to fix this)

Cause 1 — the mention picker drops any agent you don't personally manage. isAgentIdentityInManagedList (in agentAutocompleteEligibility.ts) only admits a candidate if it isn't flagged as an agent, or if its pubkey is in the viewer's own managedAgentPubkeys — built solely from the local list_managed_agents Tauri command. An agent owned by the other identity never enters that set (by design — managed-agent records hold device-local secrets and are never minted from a relay event). So the other person's agent is dropped in useMentions.ts before shouldHideAgentFromMentions / relayAgentIsSharedWithUser ever run — including for a manually typed mention, since extractMentionPubkeys only resolves p tags from the already-filtered candidate list.

Cause 2 — the eligibility check reads an event kind nothing publishes. Even with Cause 1 fixed, relayAgentIsSharedWithUser reads channelIds/respondTo/respondToAllowlist from a RelayAgent sourced from list_relay_agents (agent_discovery.rs), which queries the relay for kind:10100. Nothing in the codebase publishes kind:10100 as an agent profile (the only writer is buzz channels set-add-policy, which writes unrelated channel_add_policy content). Agent identity/config is actually published as kind:0 + kind:30177 (managed_agents/agent_events.rs), which the eligibility code never reads — so the RelayAgent it needs is always empty, and relayAgentIsSharedWithUser always returns false.

Note: the agent-side authorization logic is already correct and needed no change — author_allowed in buzz-acp already permits any author for RespondTo::Anyone outside DMs. The agent would answer if it could be addressed; it just could never produce a mention.

Fix

  • Cause 1: thread the already-computed mentionableAgentPubkeys set (which already accounts for relay-directory sharing) into the isAgentIdentityInManagedList gate, so a real channel member is admitted when it's shared, while relay-directory-only (non-member) agents stay hidden exactly as before. Same relaxation applied to extractMentionPubkeys so a p-tag is actually emitted for a manually typed mention of such an agent.
  • Cause 2: repoint list_relay_agents (agent_discovery.rs) from kind:10100 to kind:30177, and resolve channel membership via a kind:39002 #p query — the same pattern buzz-acp's own channel-discovery code already uses. The agent's pubkey is read from kind:30177's d tag (not the event's own pubkey, since kind:30177 is a parameterized-replaceable event published by the owner). respond_to/respond_to_allowlist are sourced from the record's instance-tier runtime fields, not the definition-tier fields — the snapshot this event is built from carries both, and they can diverge in practice (the running harness is spawned from instance-tier fields). Sourcing from definition-tier would ship a policy the running agent doesn't actually enforce (agent visible but silent, or answering but hidden).

Test plan

  • Added/updated unit coverage in agentAutocompleteEligibility.test.mjs for the new gate logic, including the two cases that must stay hidden: a non-member agent that's only relay-directory-listed, and a channel member with an explicit not-invocable directory entry.
  • Added an e2e test in mentions.spec.ts mirroring the exact reported repro: identity A creates an agent with respond_to: anyone, adds it as a bot member of a channel shared with identity B; identity B can now see and use the mention, and a non-member/non-invocable agent stays hidden.
  • Verified manually end-to-end against a local dev relay with two separate identities, and separately against a real hosted community with two independent machines/humans.
  • cargo fmt --all -- --check and cargo fmt (Tauri crate) clean.
  • cargo clippy --all-features -- -D warnings reports zero issues in either file this PR touches (agent_discovery.rs, nostr_convert.rs). Note: the same command surfaces ~30 pre-existing warnings/errors in unrelated files on a clean origin/main checkout under Windows + --all-features (confirmed by checking out main and re-running) — those are not introduced by this change and are left untouched per the "no drive-by fixes" guidance in CONTRIBUTING.md.
  • Full desktop unit suite: 4131/4131 passing. tsc --noEmit clean. biome check clean on all changed files.

Closes #3776. Related: #3277, #2349, #2950.

…plete

isAgentIdentityInManagedList dropped any agent candidate not in the
viewer's own locally-managed set, before shouldHideAgentFromMentions's
existing relay-sharing logic ever ran. A bot member of a shared channel
owned by a different identity could never be @-mentioned or respond.

Thread the already-computed mentionableAgentPubkeys set into the gate so
a real channel member is admitted when relayAgentIsSharedWithUser says
the agent is shared, while relay-directory-only (non-member) agents stay
hidden exactly as before.

Signed-off-by: oleg-ai-dev <oleg.ai.development@gmail.com>
…ests

Decouple the default relay-agent-directory fixture (alice/charlie) from
the real human TEST_IDENTITIES pubkeys they coincidentally reused, which
started colliding with the mention-eligibility fix (a human member whose
pubkey happens to also carry relay-agent-directory data should not be
mistaken for a mentionable agent).

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>
…lished kind:10100

list_relay_agents queried kind:10100 agent profile events, but nothing
in the codebase publishes that kind — managed agents are published as
kind:30177 (managed_agents/agent_events.rs). The relay-agent directory
the mention-eligibility check depends on was always empty, so
relayAgentIsSharedWithUser could never return true for anyone.

Repoint the query at kind:30177 and resolve channel membership via a
kind:39002 #p query — the same pattern buzz-acp's own
RelayClient::discover_channels and get_channels already use. The
agent's pubkey is read from kind:30177's d tag, not the event's own
pubkey (which is the owner's identity, since kind:30177 is a
parameterized-replaceable event published by the owner).

Signed-off-by: oleg-ai-dev <oleg.ai.development@gmail.com>
biome missed the wrapped pubkey literals in e2eBridge.ts (Task 3), and
cargo fmt was never run against the Tauri crate's separate format domain
for nostr_convert.rs (Task 4) — just fmt only covers the root workspace,
not desktop/src-tauri. Pure formatting, no logic change; verified via diff.

Signed-off-by: oleg-ai-dev <oleg.ai.development@gmail.com>

Copy link
Copy Markdown

@oleg-ai-dev @dexsynccom @anindyar, after comparing all three patches, I think each contributes part of the best fix. #4714 keeps the mention change narrow. #4716 correctly identifies the instance-tier policy and adds the cross-identity E2E proof this bug needs. #4713 recognizes that runtime discovery should be signed by the agent itself. I would preserve all three contributions.

I still don’t think #4716’s kind:30177 path is safe as written. The event is owner-signed, while the agent pubkey comes from an unverified d tag. It also represents managed configuration, not necessarily what the running harness currently enforces.

My preferred combination is #4714’s mention fix, #4716’s policy handling and E2E coverage, and an agent-signed kind:10100 runtime profile. Channel membership remains authoritative in kind:39002.

kind:10100 then needs one publisher to own the complete document; the harness and set-add-policy cannot safely publish partial replacement bodies independently.

If the kind:30177 approach can prove the owner-to-agent binding and effective-runtime semantics, I’m happy to reconsider it.

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.

Cross-owner agent mentions are impossible: candidates filtered by local ownership, and eligibility reads a kind (10100) nothing publishes

2 participants