From 7802acc1f6fecb4521dae2eabc6f4dbd61612ccf Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Tue, 30 Jun 2026 17:01:28 -0700 Subject: [PATCH] fix(observability): gate composer preview on declared owner Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../features/channels/ui/ChannelScreen.tsx | 31 +++------- .../channels/ui/useObserverBridgeAgents.ts | 60 +++++++++++++++++++ desktop/src/features/profile/lib/identity.ts | 17 ++++++ 3 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 desktop/src/features/channels/ui/useObserverBridgeAgents.ts diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index ef4748d4a..e8207d5f5 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -73,6 +73,7 @@ import { } from "./useChannelActivityTyping"; import { useChannelAgentSessions } from "./useChannelAgentSessions"; import { useChannelPanelHistoryState } from "./useChannelPanelHistoryState"; +import { useObserverBridgeAgents } from "./useObserverBridgeAgents"; import { useChannelProfilePanel } from "./useChannelProfilePanel"; import { useChannelRouteTarget } from "./useChannelRouteTarget"; import { useChannelUnreadState } from "./useChannelUnreadState"; @@ -373,28 +374,14 @@ export function ChannelScreen({ ), [activeAgentChannelTurns, activeChannelId], ); - const observerBridgeAgents = React.useMemo(() => { - if ( - !profilePanelPubkey || - !openAgentSessionPubkey || - normalizePubkey(profilePanelPubkey) !== - normalizePubkey(openAgentSessionPubkey) || - managedAgents.some( - (agent) => - normalizePubkey(agent.pubkey) === normalizePubkey(profilePanelPubkey), - ) - ) { - return managedAgents; - } - - return [ - ...managedAgents, - { - pubkey: profilePanelPubkey, - status: "deployed" as const, - }, - ]; - }, [managedAgents, openAgentSessionPubkey, profilePanelPubkey]); + const observerBridgeAgents = useObserverBridgeAgents({ + currentPubkey, + managedAgents, + openAgentSessionPubkey, + profilePanelPubkey, + profiles: messageProfilesQuery.data?.profiles, + relayAgents, + }); useManagedAgentObserverBridge(observerBridgeAgents); useActiveAgentTurnsBridge(observerBridgeAgents); const messageProfiles = React.useMemo(() => { diff --git a/desktop/src/features/channels/ui/useObserverBridgeAgents.ts b/desktop/src/features/channels/ui/useObserverBridgeAgents.ts new file mode 100644 index 000000000..2e940dfbb --- /dev/null +++ b/desktop/src/features/channels/ui/useObserverBridgeAgents.ts @@ -0,0 +1,60 @@ +import * as React from "react"; + +import { ownsAuthorAgent } from "@/features/profile/lib/identity"; +import type { UserProfileLookup } from "@/features/profile/lib/identity"; +import type { ManagedAgent, RelayAgent } from "@/shared/api/types"; +import { normalizePubkey } from "@/shared/lib/pubkey"; + +type ObserverBridgeAgent = Pick; + +export function useObserverBridgeAgents({ + currentPubkey, + managedAgents, + openAgentSessionPubkey, + profilePanelPubkey, + profiles, + relayAgents, +}: { + currentPubkey: string | undefined; + managedAgents: readonly ObserverBridgeAgent[]; + openAgentSessionPubkey: string | null; + profilePanelPubkey: string | null; + profiles: UserProfileLookup | undefined; + relayAgents: readonly Pick[]; +}): ObserverBridgeAgent[] { + return React.useMemo(() => { + const byPubkey = new Map( + managedAgents.map((agent) => [normalizePubkey(agent.pubkey), agent]), + ); + + for (const agent of relayAgents) { + const key = normalizePubkey(agent.pubkey); + if (byPubkey.has(key)) continue; + if (ownsAuthorAgent(profiles?.[key], currentPubkey)) { + byPubkey.set(key, { pubkey: agent.pubkey, status: "deployed" }); + } + } + + if ( + profilePanelPubkey && + openAgentSessionPubkey && + normalizePubkey(profilePanelPubkey) === + normalizePubkey(openAgentSessionPubkey) && + !byPubkey.has(normalizePubkey(profilePanelPubkey)) + ) { + byPubkey.set(normalizePubkey(profilePanelPubkey), { + pubkey: profilePanelPubkey, + status: "deployed", + }); + } + + return [...byPubkey.values()]; + }, [ + currentPubkey, + managedAgents, + openAgentSessionPubkey, + profilePanelPubkey, + profiles, + relayAgents, + ]); +} diff --git a/desktop/src/features/profile/lib/identity.ts b/desktop/src/features/profile/lib/identity.ts index 491883a0a..3093cebf6 100644 --- a/desktop/src/features/profile/lib/identity.ts +++ b/desktop/src/features/profile/lib/identity.ts @@ -85,6 +85,23 @@ export function resolveUserLabel(input: { return truncatePubkey(pubkey); } +/** + * Returns true when the current user owns the agent that authored a message. + * Mirrors the relay's `is_agent_owner` gate: ownership is determined by the + * NIP-OA `ownerPubkey` field on the author's profile, NOT by the local + * managed-agents list (which can diverge from server-side ownership). + */ +export function ownsAuthorAgent( + profile: { ownerPubkey: string | null } | undefined, + currentPubkey: string | undefined, +): boolean { + return ( + !!currentPubkey && + !!profile?.ownerPubkey && + normalizePubkey(profile.ownerPubkey) === normalizePubkey(currentPubkey) + ); +} + export function resolveUserSecondaryLabel(input: { pubkey: string; profiles?: UserProfileLookup;