mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -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}`}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user