fix(observability): gate composer preview on declared owner

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-06-30 17:01:28 -07:00
co-authored by Taylor Ho
parent 4d22e55b59
commit 7802acc1f6
3 changed files with 86 additions and 22 deletions
@@ -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(() => {
@@ -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<ManagedAgent, "pubkey" | "status">;
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<RelayAgent, "pubkey">[];
}): ObserverBridgeAgent[] {
return React.useMemo(() => {
const byPubkey = new Map<string, ObserverBridgeAgent>(
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,
]);
}
@@ -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;