diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index c2171e9d7d6..b7f59422a48 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -130,6 +130,94 @@ test("relayAgentIsSharedWithUser: accepts allowlist agents for the current user" ); }); +test("relayAgentIsSharedWithUser: admits the owner of an allowlist agent", () => { + const sharedChannelIds = new Set(["general"]); + + // `author_allowed` ORs the explicit list with the owner check, so an owner + // absent from the array is still answered by the harness. + assert.equal( + relayAgentIsSharedWithUser( + { + respondTo: "allowlist", + respondToAllowlist: [OTHER_OWNER_PUBKEY], + ownerPubkey: CURRENT_PUBKEY, + channelIds: ["general"], + }, + sharedChannelIds, + CURRENT_PUBKEY, + ), + true, + ); + + // An empty allowlist is the first configuration an owner reaches for, and it + // used to hide the agent from everyone, the owner included, without an error. + assert.equal( + relayAgentIsSharedWithUser( + { + respondTo: "allowlist", + respondToAllowlist: [], + ownerPubkey: CURRENT_PUBKEY.toUpperCase(), + channelIds: ["general"], + }, + sharedChannelIds, + CURRENT_PUBKEY, + ), + true, + ); + + // Someone else's agent is unchanged: the array alone decides. + assert.equal( + relayAgentIsSharedWithUser( + { + respondTo: "allowlist", + respondToAllowlist: [OTHER_OWNER_PUBKEY], + ownerPubkey: OWNER_PUBKEY, + channelIds: ["general"], + }, + sharedChannelIds, + CURRENT_PUBKEY, + ), + false, + ); + + // An owner that does not resolve must keep failing closed. On closed relays + // the NIP-OA attestation often never materializes, and admitting on absence + // would expose the agent to every viewer. + for (const ownerPubkey of [undefined, null, ""]) { + assert.equal( + relayAgentIsSharedWithUser( + { + respondTo: "allowlist", + respondToAllowlist: [OTHER_OWNER_PUBKEY], + ownerPubkey, + channelIds: ["general"], + }, + sharedChannelIds, + CURRENT_PUBKEY, + ), + false, + ); + } +}); + +test("relayAgentCanRespondInChannel: an allowlist owner still needs the agent in the channel", () => { + const agent = { + respondTo: "allowlist", + respondToAllowlist: [], + ownerPubkey: CURRENT_PUBKEY, + channelIds: ["general"], + }; + + assert.equal( + relayAgentCanRespondInChannel(agent, "general", CURRENT_PUBKEY), + true, + ); + assert.equal( + relayAgentCanRespondInChannel(agent, "other", CURRENT_PUBKEY), + false, + ); +}); + test("relayAgentCanRespondInChannel: requires exact channel membership and viewer access", () => { const agent = { respondTo: "allowlist", diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts index 516520e2ca3..a39ff8440cd 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -30,6 +30,19 @@ export function relayAgentIsSharedWithUser( } if (agent.respondTo === "allowlist" && normalizedCurrentPubkey) { + // The harness accepts the owner under `allowlist` as well as under + // `owner-only`: `author_allowed` ORs the explicit list with + // `is_owner_or_sibling` (`crates/buzz-acp/src/lib.rs`). Reading the array + // literally here made a *wider* policy hide the agent from its own owner. + // An unresolved owner still falls through to the list, so a missing NIP-OA + // attestation fails closed rather than admitting everyone. + if ( + agent.ownerPubkey && + normalizePubkey(agent.ownerPubkey) === normalizedCurrentPubkey + ) { + return true; + } + return agent.respondToAllowlist .map((pubkey) => normalizePubkey(pubkey)) .includes(normalizedCurrentPubkey);