From f604e84309d71bd7458c2a9dce7f1c830fdb3e5d Mon Sep 17 00:00:00 2001 From: technicallybrantley <77166260+technicallybrantley@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:12:36 -0500 Subject: [PATCH] fix(desktop): make invocable external relay agents @-mentionable The composer's candidate assembly (useMentions addCandidate) gated agent identities on the locally-managed list before shouldHideAgentFromMentions could apply its invocable-always-show rule. Externally-hosted agents - relay members with a kind:10100 announcement whose respond_to allowlist includes the viewer - were therefore unconditionally dropped from the @-mention picker, making the invocable-relay-agent path unreachable. Gate on the mentionable set (managed union invocable relay agents) instead. Managed-agent behavior is unchanged (the mentionable set is a superset); non-invocable external agents are still hidden. Repro: run an agent outside the desktop app (e.g. buzz-acp on a separate host), add it to a members-only relay, publish its kind:10100 with respond_to=allowlist naming a user, join it to a shared channel: that user cannot @-mention it from the composer before this change. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01YFkHsUe1UUBBuvL81Zoe3n Signed-off-by: technicallybrantley <77166260+technicallybrantley@users.noreply.github.com> --- .../lib/agentAutocompleteEligibility.test.mjs | 63 +++++++++++++++++++ .../src/features/messages/lib/useMentions.ts | 9 ++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 4e02b7bd68..1e0a35da8f 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -306,3 +306,66 @@ test("coalesceAgentAutocompleteCandidates: leaves non-agents alone", () => { assert.deepEqual(coalesce([first, second]), [first, second]); }); + +// Regression: an externally-hosted relay agent (kind:10100 announcement with +// respond_to="allowlist" naming the viewer) must survive the composer's +// candidate gates. useMentions gates candidates with +// isAgentIdentityInManagedList using the MENTIONABLE set (managed ∪ invocable +// relay agents); gating on the managed set alone silently dropped external +// agents before shouldHideAgentFromMentions could show them. +test("invocable external relay agent passes the composed mention gates", () => { + const relayAgents = [ + { + pubkey: PUB_A, + respondTo: "allowlist", + respondToAllowlist: [CURRENT_PUBKEY], + channelIds: [], + }, + ]; + const mentionable = getMentionableAgentPubkeys({ + currentPubkey: CURRENT_PUBKEY, + managedAgentPubkeys: new Set(), + relayAgents, + sharedChannelIds: new Set(), + }); + const candidate = { pubkey: PUB_A, isAgent: true, isMember: true }; + + // Gate 1 (managed-list identity gate, fed the mentionable set): + assert.equal(isAgentIdentityInManagedList(candidate, mentionable), true); + // Gate 2 (invocable => always show): + assert.equal( + shouldHideAgentFromMentions({ + isAgent: true, + isMember: true, + pubkey: PUB_A, + mentionableAgentPubkeys: mentionable, + directoryAgentPubkeys: new Set([PUB_A]), + }), + false, + ); +}); + +test("non-invocable external relay agent is still hidden from mentions", () => { + const relayAgents = [ + { + pubkey: PUB_B, + respondTo: "owner-only", + respondToAllowlist: [], + channelIds: [], + }, + ]; + const mentionable = getMentionableAgentPubkeys({ + currentPubkey: CURRENT_PUBKEY, + managedAgentPubkeys: new Set(), + relayAgents, + sharedChannelIds: new Set(), + }); + + assert.equal( + isAgentIdentityInManagedList( + { pubkey: PUB_B, isAgent: true, isMember: true }, + mentionable, + ), + false, + ); +}); diff --git a/desktop/src/features/messages/lib/useMentions.ts b/desktop/src/features/messages/lib/useMentions.ts index 0c73b75339..92bddbac94 100644 --- a/desktop/src/features/messages/lib/useMentions.ts +++ b/desktop/src/features/messages/lib/useMentions.ts @@ -246,7 +246,13 @@ export function useMentions( if (isArchivedDiscovery(pubkey)) { return; } - if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) { + // Gate on the full invocable set (locally managed ∪ relay agents this + // viewer can invoke), not the managed list alone. With managed-only, + // externally-hosted agents were dropped here before + // `shouldHideAgentFromMentions` could apply its invocable-⇒-show rule, + // making that path unreachable — an external agent (kind:10100 with + // respond_to allowlisting the viewer) could never be @-mentioned. + if (!isAgentIdentityInManagedList(candidate, mentionableAgentPubkeys)) { return; } if ( @@ -420,7 +426,6 @@ export function useMentions( managedAgentNamesByPubkey, managedAgentPersonaIds, managedAgentPersonaIdsByPubkey, - managedAgentPubkeys, managedAgentsQuery.data, memberPubkeys, members,