Skip to content
Open
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 @@ -342,6 +342,7 @@ mod tests {
RelayAgentInfo {
pubkey: "a".repeat(64),
owner_pubkey: Some(cross_owner.clone()),
persona_id: None,
name: "Verified cross-owner".to_string(),
agent_type: "agent".to_string(),
channels: Vec::new(),
Expand All @@ -354,6 +355,7 @@ mod tests {
RelayAgentInfo {
pubkey: "c".repeat(64),
owner_pubkey: None,
persona_id: None,
name: "Ownerless legacy".to_string(),
agent_type: "agent".to_string(),
channels: Vec::new(),
Expand All @@ -380,6 +382,7 @@ mod tests {
let mut agents = vec![RelayAgentInfo {
pubkey: "a".repeat(64),
owner_pubkey: None,
persona_id: None,
name: "Ownerless legacy".to_string(),
agent_type: "agent".to_string(),
channels: Vec::new(),
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/managed_agents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ pub struct RelayAgentInfo {
pub pubkey: String,
#[serde(default)]
pub owner_pubkey: Option<String>,
/// Persona declared by the owner-authenticated managed-agent policy.
/// Legacy and persona-less agents leave this unset.
#[serde(default)]
pub persona_id: Option<String>,
Comment thread
ignacioarsuaga marked this conversation as resolved.
pub name: String,
pub agent_type: String,
pub channels: Vec<String>,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/nostr_convert/agent_directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ fn relay_agent_from_managed_policy(agent_pubkey: &str, event: &Event) -> Option<
Some(RelayAgentInfo {
pubkey: agent_pubkey.to_string(),
owner_pubkey: Some(event.pubkey.to_hex()),
persona_id: content.persona_id,
name: content.name,
agent_type: "agent".to_string(),
channels: Vec::new(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ fn assert_merge(directory: &[Event], profile: &Event, policy: &Event, status: &s
assert_eq!(serde_json::to_value(agent).unwrap()["status"], status);
assert_eq!(agent.pubkey, profile.pubkey.to_hex());
assert_eq!(agent.owner_pubkey, Some(policy.pubkey.to_hex()));
assert_eq!(agent.persona_id.as_deref(), Some("persona-1"));
assert_eq!(agent.name, "Policy name");
assert_eq!(
agent.respond_to,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/nostr_convert/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fn managed_agent_event(
) -> Event {
let content = serde_json::json!({
"name": name,
"persona_id": "persona-1",
"parallelism": 1,
"respond_to": respond_to,
"respond_to_allowlist": respond_to_allowlist,
Expand Down
114 changes: 114 additions & 0 deletions desktop/src/features/messages/lib/buildMentionCandidates.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,120 @@ test("active personas join unless a managed agent already carries them", () => {
);
});

test("an owned relay agent suppresses its mintable persona on a second desktop", () => {
const persona = {
id: "planner",
displayName: "Claude Hall",
avatarUrl: null,
isActive: true,
};
const candidates = buildMentionCandidates(
input({
activePersonas: [persona],
currentPubkey: MEMBER_PUBKEY,
memberPubkeys: new Set([AGENT_PUBKEY]),
members: [
{
pubkey: AGENT_PUBKEY,
displayName: "Claude Hall",
isAgent: true,
role: "bot",
},
],
mentionableAgentPubkeys: new Set([AGENT_PUBKEY]),
relayAgents: [
{
pubkey: AGENT_PUBKEY,
ownerPubkey: MEMBER_PUBKEY,
personaId: persona.id,
name: "Claude Hall",
status: "online",
channelIds: [],
},
],
}),
);

assert.equal(candidates.length, 1);
assert.equal(candidates[0].kind, "identity");
assert.equal(candidates[0].pubkey, AGENT_PUBKEY);
assert.equal(candidates[0].personaId, persona.id);
});

test("a foreign relay agent cannot suppress a colliding local persona", () => {
const persona = {
id: "planner",
displayName: "Planner",
avatarUrl: null,
isActive: true,
};
const candidates = buildMentionCandidates(
input({
activePersonas: [persona],
currentPubkey: MEMBER_PUBKEY,
mentionableAgentPubkeys: new Set([AGENT_PUBKEY]),
relayAgents: [
{
pubkey: AGENT_PUBKEY,
ownerPubkey: SEARCHED_PUBKEY,
personaId: persona.id,
name: "Remote Planner",
status: "online",
channelIds: [],
},
],
}),
);

assert.equal(candidates.length, 2);
assert.equal(
candidates.filter((candidate) => candidate.kind === "persona").length,
1,
);
assert.equal(
candidates.find((candidate) => candidate.pubkey === AGENT_PUBKEY)
?.personaId,
undefined,
);
});

for (const hiddenBy of ["archive", "eligibility"]) {
test(`a relay agent hidden by ${hiddenBy} does not suppress its persona`, () => {
const persona = {
id: "planner",
displayName: "Planner",
avatarUrl: null,
isActive: true,
};
const candidates = buildMentionCandidates(
input({
activePersonas: [persona],
currentPubkey: MEMBER_PUBKEY,
isArchived:
hiddenBy === "archive"
? (pubkey) => pubkey === AGENT_PUBKEY
: () => false,
mentionableAgentPubkeys:
hiddenBy === "eligibility" ? new Set() : new Set([AGENT_PUBKEY]),
relayAgents: [
{
pubkey: AGENT_PUBKEY,
ownerPubkey: MEMBER_PUBKEY,
personaId: persona.id,
name: "Remote Planner",
status: "online",
channelIds: [],
},
],
}),
);

assert.equal(candidates.length, 1);
assert.equal(candidates[0].kind, "persona");
assert.equal(candidates[0].personaId, persona.id);
});
}

test("global search results join only while global search is enabled", () => {
const userSearchResults = [
{
Expand Down
25 changes: 24 additions & 1 deletion desktop/src/features/messages/lib/buildMentionCandidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ export function buildMentionCandidates({
relayAgents,
userSearchResults,
}: BuildMentionCandidatesInput): MentionCandidate[] {
const normalizedCurrentPubkey = currentPubkey
? normalizePubkey(currentPubkey)
: null;
const candidatesByPubkey = new Map<string, MentionCandidate>();
const ownedRelayPersonaIds = new Set<string>();
const addCandidate = (candidate: MentionCandidate & { pubkey: string }) => {
const pubkey = normalizePubkey(candidate.pubkey);
if (isArchived(pubkey)) {
Expand Down Expand Up @@ -166,6 +170,12 @@ export function buildMentionCandidates({
}
for (const agent of relayAgents ?? []) {
const pubkey = normalizePubkey(agent.pubkey);
const ownedRelayPersonaId =
normalizedCurrentPubkey &&
agent.ownerPubkey &&
normalizePubkey(agent.ownerPubkey) === normalizedCurrentPubkey
? (agent.personaId ?? undefined)
: undefined;
addCandidate({
kind: "identity",
pubkey,
Expand All @@ -179,11 +189,20 @@ export function buildMentionCandidates({
agent.channelIds.includes(mentionChannelId)),
personaId:
managedAgentPersonaIdsByPubkey.get(pubkey) ??
ownedRelayPersonaId ??
(activePersonaById.has(pubkey) ? pubkey : undefined),
ownerPubkey: agent.ownerPubkey,
isAgent: true,
isActiveAgent: agent.status === "online" || agent.status === "away",
});
const relayCandidate = candidatesByPubkey.get(pubkey);
if (
ownedRelayPersonaId &&
relayCandidate?.isAgent === true &&
relayCandidate.personaId === ownedRelayPersonaId
) {
ownedRelayPersonaIds.add(ownedRelayPersonaId);
}
}
for (const agent of managedAgents ?? []) {
const pubkey = normalizePubkey(agent.pubkey);
Expand Down Expand Up @@ -226,7 +245,11 @@ export function buildMentionCandidates({
}
}
const personaCandidates: MentionCandidate[] = activePersonas
.filter((persona) => !managedAgentPersonaIds.has(persona.id))
.filter(
(persona) =>
!managedAgentPersonaIds.has(persona.id) &&
!ownedRelayPersonaIds.has(persona.id),
)
.map((persona) => ({
kind: "persona" as const,
personaId: persona.id,
Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/pulse/ui/PulseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function PulseView({ currentPubkey }: PulseViewProps) {
agentsByPubkey.set(agent.pubkey, {
pubkey: agent.pubkey,
ownerPubkey: null,
personaId: null,
name: agent.name,
agentType: agent.agentCommand,
channels: [],
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/shared/api/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type RawSearchResponse = {
type RawRelayAgent = {
pubkey: string;
owner_pubkey?: string | null;
persona_id?: string | null;
name: string;
agent_type: string;
channels: string[];
Expand Down Expand Up @@ -614,6 +615,7 @@ function fromRawRelayAgent(agent: RawRelayAgent): RelayAgent {
return {
pubkey: agent.pubkey,
ownerPubkey: agent.owner_pubkey ?? null,
personaId: agent.persona_id ?? null,
name: agent.name,
agentType: agent.agent_type,
channels: agent.channels,
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/shared/api/tauriRelayAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RelayAgent } from "@/shared/api/types";
type RawRelayAgent = {
pubkey: string;
owner_pubkey?: string | null;
persona_id?: string | null;
name: string;
agent_type: string;
channels: string[];
Expand All @@ -25,6 +26,7 @@ export async function revalidateRelayAgents(
return agents.map((agent) => ({
pubkey: agent.pubkey,
ownerPubkey: agent.owner_pubkey ?? null,
personaId: agent.persona_id ?? null,
name: agent.name,
agentType: agent.agent_type,
channels: agent.channels,
Expand Down
1 change: 1 addition & 0 deletions desktop/src/shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export type RelayMember = {
export type RelayAgent = {
pubkey: string;
ownerPubkey: string | null;
personaId: string | null;
name: string;
agentType: string;
channels: string[];
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/testing/e2eBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ type RawSendChannelMessageResponse = {
type RawRelayAgent = {
pubkey: string;
owner_pubkey?: string | null;
persona_id?: string | null;
name: string;
agent_type: string;
channels: string[];
Expand Down Expand Up @@ -4239,6 +4240,8 @@ function syncMockRelayAgentsFromManagedAgents() {

return {
pubkey: agent.pubkey,
owner_pubkey: MOCK_IDENTITY_PUBKEY,
persona_id: agent.persona_id,
name: agent.name,
agent_type: agent.agent_command,
channels: memberships.channels,
Expand Down