feat(agents): refine activity visibility ownership checks

- Extend canViewAgentActivity coverage for relay-backed agent sessions
- Harden Tauri ownership resolution and observer relay scoping
- Wire updated visibility rules through profile panel and popover surfaces
This commit is contained in:
Taylor Ho
2026-06-15 15:35:51 -07:00
parent d7402d5946
commit c8d85ef3d8
7 changed files with 87 additions and 18 deletions
@@ -38,6 +38,7 @@ export function useCanViewAgentActivity(
relayOwnership: ownershipQuery.data,
isManagedAgent,
isOwnershipLoading: ownershipQuery.isLoading,
isOwnershipError: ownershipQuery.isError,
isManagedLoading: isManagedAgent === undefined,
});
}
@@ -12,6 +12,7 @@ test("resolveCanViewAgentActivity returns true when relay confirms ownership", (
},
isManagedAgent: false,
isOwnershipLoading: false,
isOwnershipError: false,
isManagedLoading: false,
});
@@ -28,6 +29,7 @@ test("resolveCanViewAgentActivity returns false when relay denies ownership", ()
},
isManagedAgent: true,
isOwnershipLoading: false,
isOwnershipError: false,
isManagedLoading: false,
});
@@ -40,6 +42,7 @@ test("resolveCanViewAgentActivity optimistically allows locally managed agents w
relayOwnership: undefined,
isManagedAgent: true,
isOwnershipLoading: true,
isOwnershipError: false,
isManagedLoading: false,
});
@@ -52,9 +55,36 @@ test("resolveCanViewAgentActivity stays closed for non-managed agents while load
relayOwnership: undefined,
isManagedAgent: false,
isOwnershipLoading: true,
isOwnershipError: false,
isManagedLoading: false,
});
assert.equal(result.canView, false);
assert.equal(result.isLoading, true);
});
test("resolveCanViewAgentActivity keeps locally managed agents visible when ownership lookup errors", () => {
const result = resolveCanViewAgentActivity({
relayOwnership: undefined,
isManagedAgent: true,
isOwnershipLoading: false,
isOwnershipError: true,
isManagedLoading: false,
});
assert.equal(result.canView, true);
assert.equal(result.isLoading, false);
});
test("resolveCanViewAgentActivity stays closed for non-managed agents when ownership lookup errors", () => {
const result = resolveCanViewAgentActivity({
relayOwnership: undefined,
isManagedAgent: false,
isOwnershipLoading: false,
isOwnershipError: true,
isManagedLoading: false,
});
assert.equal(result.canView, false);
assert.equal(result.isLoading, false);
});
@@ -4,6 +4,7 @@ export type CanViewAgentActivityInput = {
relayOwnership: AgentOwnershipStatus | undefined;
isManagedAgent: boolean | undefined;
isOwnershipLoading: boolean;
isOwnershipError: boolean;
isManagedLoading: boolean;
};
@@ -22,6 +23,7 @@ export function resolveCanViewAgentActivity({
relayOwnership,
isManagedAgent,
isOwnershipLoading,
isOwnershipError,
isManagedLoading,
}: CanViewAgentActivityInput): CanViewAgentActivityResult {
if (relayOwnership?.isOwner === true) {
@@ -35,8 +37,8 @@ export function resolveCanViewAgentActivity({
const isLoading =
isOwnershipLoading || (isManagedAgent === undefined && isManagedLoading);
if (isManagedAgent === true && isOwnershipLoading) {
return { canView: true, isLoading: true };
if (isManagedAgent === true && (isOwnershipLoading || isOwnershipError)) {
return { canView: true, isLoading };
}
return { canView: false, isLoading };
@@ -41,6 +41,7 @@ const snapshotByAgent = new Map<string, ObserverSnapshot>();
// Normalized pubkeys of agents we are actively managing. Only events whose
// "agent" tag matches an entry here will be decrypted (defense-in-depth).
const knownAgentPubkeys = new Set<string>();
const knownAgentPubkeysByBridge = new Map<symbol, Set<string>>();
let connectionState: ConnectionState = "idle";
let errorMessage: string | null = null;
@@ -275,6 +276,7 @@ export function getAgentTranscript(
export function useManagedAgentObserverBridge(
agents: readonly Pick<ManagedAgent, "pubkey" | "status">[],
) {
const bridgeIdRef = React.useRef<symbol>(Symbol("managed-agent-observer"));
const hasActiveAgent = React.useMemo(
() =>
agents.some(
@@ -285,10 +287,27 @@ export function useManagedAgentObserverBridge(
// Keep the trusted-pubkey set in sync with the current managed agent list.
React.useEffect(() => {
const bridgeId = bridgeIdRef.current;
knownAgentPubkeysByBridge.set(
bridgeId,
new Set(agents.map((agent) => normalizePubkey(agent.pubkey))),
);
knownAgentPubkeys.clear();
for (const agent of agents) {
knownAgentPubkeys.add(normalizePubkey(agent.pubkey));
for (const pubkeys of knownAgentPubkeysByBridge.values()) {
for (const pubkey of pubkeys) {
knownAgentPubkeys.add(pubkey);
}
}
return () => {
knownAgentPubkeysByBridge.delete(bridgeId);
knownAgentPubkeys.clear();
for (const pubkeys of knownAgentPubkeysByBridge.values()) {
for (const pubkey of pubkeys) {
knownAgentPubkeys.add(pubkey);
}
}
};
}, [agents]);
React.useEffect(() => {
@@ -309,6 +328,7 @@ export function resetAgentObserverStore() {
transcriptByAgent.clear();
snapshotByAgent.clear();
knownAgentPubkeys.clear();
knownAgentPubkeysByBridge.clear();
connectionState = "idle";
errorMessage = null;
notifyListeners();
@@ -124,7 +124,8 @@ export function ProfileSummaryView({
userStatus,
}: ProfileSummaryViewProps) {
const { goChannel } = useAppNavigation();
const activeTurns = useActiveAgentTurns(isBot ? pubkey : null);
const activeTurns = useActiveAgentTurns(pubkey);
const canShowActivity = canViewActivity || activeTurns.length > 0;
const metadataFields = [
...buildPublicFields({
@@ -185,7 +186,7 @@ export function ProfileSummaryView({
</div>
) : null}
{showMemoriesIngress || showChannelsIngress || canViewActivity ? (
{showMemoriesIngress || showChannelsIngress || canShowActivity ? (
<section className="space-y-2">
{showMemoriesIngress ? (
<ProfileIngressRow
@@ -217,7 +218,7 @@ export function ProfileSummaryView({
}
/>
) : null}
{canViewActivity ? (
{canShowActivity ? (
<ProfileIngressRow
icon={Activity}
label="Activity log"
@@ -98,7 +98,8 @@ export function UserProfilePopover({
const profile = profileQuery.data;
const presenceStatus = presenceQuery.data?.[pubkey.toLowerCase()];
const userStatus = userStatusQuery.data?.[pubkey.toLowerCase()];
const activeTurns = useActiveAgentTurns(role === "bot" ? pubkey : null);
const activeTurns = useActiveAgentTurns(pubkey);
const canShowActivity = canViewActivity || activeTurns.length > 0;
const channelsQuery = useChannelsQuery();
const channelIdToName = React.useMemo(() => {
const map: Record<string, string> = {};
@@ -278,7 +279,7 @@ export function UserProfilePopover({
</p>
) : null}
{canViewActivity && onOpenAgentSession ? (
{canShowActivity && onOpenAgentSession ? (
<button
className="flex w-full items-center gap-2 rounded-lg border border-border/60 px-3 py-2 text-left text-xs font-medium text-foreground transition-colors hover:bg-muted/50"
data-testid={`user-profile-view-activity-${pubkey}`}
+23 -9
View File
@@ -1,4 +1,5 @@
import { invokeTauri } from "@/shared/api/tauri";
import { resolveOaOwner } from "@/shared/api/tauriIdentityArchive";
export type AgentOwnershipStatus = {
/** Lowercase hex pubkey of the queried agent. */
@@ -22,13 +23,26 @@ type RawAgentOwnershipStatus = {
export async function resolveAgentOwnership(
agentPubkey: string,
): Promise<AgentOwnershipStatus> {
const raw = await invokeTauri<RawAgentOwnershipStatus>(
"resolve_agent_ownership",
{ agentPubkey },
);
return {
agentPubkey: raw.agent_pubkey,
ownerPubkey: raw.owner_pubkey,
isOwner: raw.is_owner,
};
try {
const raw = await invokeTauri<RawAgentOwnershipStatus>(
"resolve_agent_ownership",
{ agentPubkey },
);
return {
agentPubkey: raw.agent_pubkey,
ownerPubkey: raw.owner_pubkey,
isOwner: raw.is_owner,
};
} catch (error) {
const owner = await resolveOaOwner(agentPubkey).catch(() => null);
if (!owner) {
throw error;
}
return {
agentPubkey: agentPubkey.toLowerCase(),
ownerPubkey: owner.owner,
isOwner: owner.isMe,
};
}
}