Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,29 @@ test("relayAgentIsSharedWithUser: accepts shared anyone agents and rejects unsha
);
});

test("relayAgentIsSharedWithUser: active channel must match the agent channel", () => {
const sharedChannelIds = new Set(["general", "ops"]);

assert.equal(
relayAgentIsSharedWithUser(
{ respondTo: "anyone", respondToAllowlist: [], channelIds: ["general"] },
sharedChannelIds,
CURRENT_PUBKEY,
"general",
),
true,
);
assert.equal(
relayAgentIsSharedWithUser(
{ respondTo: "anyone", respondToAllowlist: [], channelIds: ["ops"] },
sharedChannelIds,
CURRENT_PUBKEY,
"general",
),
false,
);
});

test("relayAgentIsSharedWithUser: accepts allowlist agents for the current user", () => {
const sharedChannelIds = new Set(["general"]);

Expand All @@ -85,7 +108,7 @@ test("relayAgentIsSharedWithUser: accepts allowlist agents for the current user"
{
respondTo: "allowlist",
respondToAllowlist: [OTHER_OWNER_PUBKEY, CURRENT_PUBKEY.toUpperCase()],
channelIds: ["other"],
channelIds: ["general"],
},
sharedChannelIds,
CURRENT_PUBKEY,
Expand All @@ -106,6 +129,23 @@ test("relayAgentIsSharedWithUser: accepts allowlist agents for the current user"
);
});

test("relayAgentIsSharedWithUser: allowlist agents still require shared channel placement", () => {
const sharedChannelIds = new Set(["general"]);

assert.equal(
relayAgentIsSharedWithUser(
{
respondTo: "allowlist",
respondToAllowlist: [CURRENT_PUBKEY],
channelIds: ["other"],
},
sharedChannelIds,
CURRENT_PUBKEY,
),
false,
);
});

test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents", () => {
const result = getMentionableAgentPubkeys({
managedAgentPubkeys: [PUB_A],
Expand All @@ -121,7 +161,7 @@ test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents",
pubkey: PUB_C,
respondTo: "allowlist",
respondToAllowlist: [CURRENT_PUBKEY],
channelIds: ["other"],
channelIds: ["general"],
},
{
pubkey: PUB_D,
Expand All @@ -136,6 +176,31 @@ test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents",
assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C]));
});

test("getMentionableAgentPubkeys: scopes relay agents to the active channel", () => {
const result = getMentionableAgentPubkeys({
managedAgentPubkeys: [PUB_A],
currentPubkey: CURRENT_PUBKEY,
relayAgents: [
{
pubkey: PUB_B,
respondTo: "anyone",
respondToAllowlist: [],
channelIds: ["general"],
},
{
pubkey: PUB_C,
respondTo: "anyone",
respondToAllowlist: [],
channelIds: ["ops"],
},
],
sharedChannelIds: new Set(["general", "ops"]),
activeChannelId: "general",
});

assert.deepEqual(result, new Set([PUB_A, PUB_B]));
});

test("isAgentIdentityInManagedList: keeps people and only current managed agent identities", () => {
const managedAgentPubkeys = new Set([PUB_A]);

Expand All @@ -162,14 +227,24 @@ test("isAgentIdentityInManagedList: keeps people and only current managed agent
);
});

test("isAgentIdentityInManagedList: keeps remote agents explicitly allowed by the caller", () => {
assert.equal(
isAgentIdentityInManagedList(
{ isAgent: true, pubkey: PUB_B.toUpperCase() },
new Set([PUB_A]),
new Set([PUB_B]),
),
true,
);
});

test("shouldHideAgentFromMentions: never hides non-agents", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: false,
isMember: false,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set([PUB_A]),
}),
false,
);
Expand All @@ -182,7 +257,6 @@ test("shouldHideAgentFromMentions: shows invocable agents even when non-member",
isMember: false,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set([PUB_A]),
directoryAgentPubkeys: new Set([PUB_A]),
}),
false,
);
Expand All @@ -195,22 +269,20 @@ test("shouldHideAgentFromMentions: hides non-member non-invocable agents", () =>
isMember: false,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set(),
}),
true,
);
});

test("shouldHideAgentFromMentions: hides member agents with an explicit not-invocable directory entry (Fizz)", () => {
test("shouldHideAgentFromMentions: shows member agents even with a non-invocable directory entry", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: true,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set([PUB_A]),
}),
true,
false,
);
});

Expand All @@ -221,28 +293,11 @@ test("shouldHideAgentFromMentions: shows member agents with unknown invocability
isMember: true,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set(),
}),
false,
);
});

test("shouldHideAgentFromMentions: normalizes the pubkey before lookup", () => {
const mixedCase = "Ab".repeat(32);
const normalized = mixedCase.toLowerCase();

assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: true,
pubkey: mixedCase,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set([normalized]),
}),
true,
);
});

test("coalesceAgentAutocompleteCandidates: merges agents with the same persona id", () => {
const first = makeAgent({ pubkey: PUB_A, personaId: "pinky" });
const second = makeAgent({
Expand Down
53 changes: 32 additions & 21 deletions desktop/src/features/agents/lib/agentAutocompleteEligibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,59 @@ export function relayAgentIsSharedWithUser(
agent: Pick<RelayAgent, "channelIds" | "respondTo" | "respondToAllowlist">,
sharedChannelIds: ReadonlySet<string>,
currentPubkey?: string | null,
activeChannelId?: string | null,
) {
const normalizedCurrentPubkey = currentPubkey
? normalizePubkey(currentPubkey)
: null;
const sharesActiveChannel =
activeChannelId == null || agent.channelIds.includes(activeChannelId);
if (!sharesActiveChannel) {
return false;
}

const sharesAnyJoinedChannel = agent.channelIds.some((channelId) =>
sharedChannelIds.has(channelId),
);
if (!sharesAnyJoinedChannel) {
return false;
}

if (agent.respondTo === "allowlist" && normalizedCurrentPubkey) {
return agent.respondToAllowlist
.map((pubkey) => normalizePubkey(pubkey))
.includes(normalizedCurrentPubkey);
}

return (
agent.respondTo === "anyone" &&
agent.channelIds.some((channelId) => sharedChannelIds.has(channelId))
);
return agent.respondTo === "anyone";
}

export function getMentionableAgentPubkeys({
currentPubkey,
managedAgentPubkeys,
relayAgents,
sharedChannelIds,
activeChannelId,
}: {
currentPubkey?: string | null;
managedAgentPubkeys: Iterable<string>;
relayAgents: readonly RelayAgent[] | undefined;
sharedChannelIds: ReadonlySet<string>;
activeChannelId?: string | null;
}) {
const pubkeys = new Set(
[...managedAgentPubkeys].map((pubkey) => normalizePubkey(pubkey)),
);

for (const agent of relayAgents ?? []) {
if (relayAgentIsSharedWithUser(agent, sharedChannelIds, currentPubkey)) {
if (
relayAgentIsSharedWithUser(
agent,
sharedChannelIds,
currentPubkey,
activeChannelId,
)
) {
pubkeys.add(normalizePubkey(agent.pubkey));
}
}
Expand All @@ -57,10 +76,13 @@ export function getMentionableAgentPubkeys({
export function isAgentIdentityInManagedList(
candidate: { isAgent?: boolean; pubkey: string },
managedAgentPubkeys: ReadonlySet<string>,
allowedAgentPubkeys: ReadonlySet<string> = new Set(),
) {
const normalizedPubkey = normalizePubkey(candidate.pubkey);
return (
candidate.isAgent !== true ||
managedAgentPubkeys.has(normalizePubkey(candidate.pubkey))
managedAgentPubkeys.has(normalizedPubkey) ||
allowedAgentPubkeys.has(normalizedPubkey)
);
}

Expand All @@ -69,32 +91,21 @@ export function shouldHideAgentFromMentions({
isMember,
pubkey,
mentionableAgentPubkeys,
directoryAgentPubkeys,
}: {
isAgent: boolean;
isMember: boolean;
pubkey: string;
mentionableAgentPubkeys: ReadonlySet<string>;
directoryAgentPubkeys: ReadonlySet<string>;
}) {
if (!isAgent) return false;
const normalized = normalizePubkey(pubkey);
// Invocable => always show.
if (mentionableAgentPubkeys.has(normalized)) return false;
// Channel membership is enough to expose the identity as mentionable. The
// receiving agent still enforces whether it will respond.
if (isMember) return false;
// Non-member, non-invocable => hide (preserves prior behavior).
if (!isMember) return true;
// Member (Option B): hide only when we have an explicit not-invocable
// signal — a relay directory (kind:10100) entry that excludes us.
// Unknown invocability (not in directory) => show.
//
// NOTE: this assumes `directoryAgentPubkeys` and `mentionableAgentPubkeys`
// share the same source query (`relayAgentsQuery.data`), so directory
// presence without membership in `mentionableAgentPubkeys` is a real
// explicit-exclusion signal. If a future change sources the directory set
// from a different query, an agent that's directory-present but whose
// mentionability is still loading could be hidden prematurely — keep the
// two sets derived from the same query.
return directoryAgentPubkeys.has(normalized);
return true;
}

type AgentAutocompleteCandidate = {
Expand Down
Loading