mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(agents): gate activity by relay ownership
- Add a relay ownership endpoint backed by agent_owner_pubkey and is_agent_owner so desktop activity visibility uses the same source of truth as observer telemetry authorization. - Add a Tauri ownership command, frontend API wrapper, and useCanViewAgentActivity hook with local managed-agent optimism only while relay ownership is loading. - Replace profile popover, profile panel, and members sidebar activity gates so owned agents can open activity across different builds and worktrees without relying on local managed-agent lists. - Refactor channel agent session candidate resolution so owned agents can keep an activity panel open when channel metadata is stale, while preserving local-only lifecycle controls. - Add focused desktop unit coverage for the ownership predicate and stale-metadata session resolution path, plus E2E bridge support for ownership mocks.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
//! Agent ownership lookup — GET /api/agents/:pubkey/ownership (NIP-98 auth).
|
||||
//!
|
||||
//! Returns the relay-authoritative `agent_owner_pubkey` mapping and whether
|
||||
//! the authenticated caller is the registered owner. Used by the desktop to
|
||||
//! gate observer activity visibility without relying on channel membership or
|
||||
//! local managed-agent store state.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::bridge::{canonical_url, check_nip98_replay, verify_bridge_auth};
|
||||
use super::{api_error, internal_error};
|
||||
|
||||
/// Response body for the agent-ownership lookup endpoint.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AgentOwnershipResponse {
|
||||
/// Hex-encoded pubkey of the agent whose ownership was queried.
|
||||
pub agent_pubkey: String,
|
||||
/// Hex-encoded pubkey of the registered owner, if one is set.
|
||||
pub owner_pubkey: Option<String>,
|
||||
/// Whether the authenticated caller is the registered owner of the agent.
|
||||
pub is_owner: bool,
|
||||
}
|
||||
|
||||
/// Resolve whether the authenticated user owns `agent_pubkey` per relay DB.
|
||||
pub async fn get_agent_ownership(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
Path(agent_pubkey): Path<String>,
|
||||
) -> Result<Json<AgentOwnershipResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let agent_hex = agent_pubkey.trim().to_ascii_lowercase();
|
||||
if agent_hex.len() != 64 || !agent_hex.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err(api_error(StatusCode::BAD_REQUEST, "invalid agent pubkey"));
|
||||
}
|
||||
|
||||
let agent_bytes = hex::decode(&agent_hex)
|
||||
.map_err(|_| api_error(StatusCode::BAD_REQUEST, "invalid agent pubkey hex"))?;
|
||||
|
||||
let path = format!("/api/agents/{agent_hex}/ownership");
|
||||
let url = canonical_url(&state.config.relay_url, &path);
|
||||
let (actor_pubkey, event_id_bytes) =
|
||||
verify_bridge_auth(&headers, "GET", &url, None, state.config.require_auth_token)?;
|
||||
check_nip98_replay(&state, event_id_bytes)?;
|
||||
|
||||
let actor_bytes = actor_pubkey.to_bytes().to_vec();
|
||||
let auth_tag = headers.get("x-auth-tag").and_then(|v| v.to_str().ok());
|
||||
super::relay_members::enforce_relay_membership(&state, &actor_bytes, auth_tag).await?;
|
||||
|
||||
let owner_pubkey = state
|
||||
.db
|
||||
.get_agent_channel_policy(&agent_bytes)
|
||||
.await
|
||||
.map_err(|e| internal_error(&format!("ownership lookup failed: {e}")))?
|
||||
.and_then(|(_policy, owner)| owner);
|
||||
|
||||
let is_owner = state
|
||||
.db
|
||||
.is_agent_owner(&agent_bytes, &actor_bytes)
|
||||
.await
|
||||
.map_err(|e| internal_error(&format!("ownership check failed: {e}")))?;
|
||||
|
||||
Ok(Json(AgentOwnershipResponse {
|
||||
agent_pubkey: agent_hex,
|
||||
owner_pubkey: owner_pubkey.map(hex::encode),
|
||||
is_owner,
|
||||
}))
|
||||
}
|
||||
@@ -24,7 +24,7 @@ use super::{api_error, internal_error, not_found};
|
||||
///
|
||||
/// Returns the authenticated public key and an event ID for replay detection.
|
||||
/// For X-Pubkey dev mode, the event ID is a zero hash (no replay concern).
|
||||
fn verify_bridge_auth(
|
||||
pub(crate) fn verify_bridge_auth(
|
||||
headers: &HeaderMap,
|
||||
method: &str,
|
||||
url: &str,
|
||||
@@ -73,7 +73,7 @@ fn verify_bridge_auth(
|
||||
///
|
||||
/// Uses moka's `entry` API for atomic insert-if-absent — no race window
|
||||
/// between "check if seen" and "mark as seen".
|
||||
fn check_nip98_replay(
|
||||
pub(crate) fn check_nip98_replay(
|
||||
state: &AppState,
|
||||
event_id_bytes: [u8; 32],
|
||||
) -> Result<(), (StatusCode, Json<Value>)> {
|
||||
@@ -95,7 +95,7 @@ fn check_nip98_replay(
|
||||
}
|
||||
|
||||
/// Reconstruct the canonical URL for NIP-98 verification from the relay config.
|
||||
fn canonical_url(relay_url: &str, path: &str) -> String {
|
||||
pub(crate) fn canonical_url(relay_url: &str, path: &str) -> String {
|
||||
let base = relay_url
|
||||
.trim()
|
||||
.trim_end_matches('/')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! HTTP API — media, git, NIP-05, and the Nostr HTTP bridge.
|
||||
|
||||
pub mod agents;
|
||||
pub mod bridge;
|
||||
pub mod events;
|
||||
pub mod git;
|
||||
|
||||
@@ -64,6 +64,10 @@ pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
.route("/events", post(api::bridge::submit_event))
|
||||
.route("/query", post(api::bridge::query_events))
|
||||
.route("/count", post(api::bridge::count_events))
|
||||
.route(
|
||||
"/api/agents/{pubkey}/ownership",
|
||||
get(api::agents::get_agent_ownership),
|
||||
)
|
||||
// Webhook trigger (secret-authenticated, no NIP-98)
|
||||
.route("/hooks/{id}", post(api::bridge::workflow_webhook))
|
||||
// Huddle audio WebSocket route
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Relay-authoritative agent ownership lookup for activity visibility gates.
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
relay::{get_relay_json, relay_api_base_url_with_override},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, serde::Deserialize)]
|
||||
pub struct AgentOwnershipStatus {
|
||||
/// Lowercase hex pubkey of the queried agent.
|
||||
pub agent_pubkey: String,
|
||||
/// Lowercase hex owner pubkey from relay `agent_owner_pubkey`, if set.
|
||||
pub owner_pubkey: Option<String>,
|
||||
/// True iff the current workspace identity is the relay-recorded owner.
|
||||
pub is_owner: bool,
|
||||
}
|
||||
|
||||
/// Resolve whether the current identity owns `agent_pubkey` per relay DB.
|
||||
#[tauri::command]
|
||||
pub async fn resolve_agent_ownership(
|
||||
agent_pubkey: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<AgentOwnershipStatus, String> {
|
||||
let agent_hex = agent_pubkey.trim().to_ascii_lowercase();
|
||||
if agent_hex.len() != 64 {
|
||||
return Err("agent pubkey must be 64 hex characters".to_string());
|
||||
}
|
||||
|
||||
let api_base = relay_api_base_url_with_override(&state);
|
||||
let path = format!("/api/agents/{agent_hex}/ownership");
|
||||
let url = format!("{api_base}{path}");
|
||||
|
||||
get_relay_json::<AgentOwnershipStatus>(&state, Method::GET, &url, &[]).await
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod agent_discovery;
|
||||
mod agent_models;
|
||||
mod agent_ownership;
|
||||
mod agent_settings;
|
||||
mod agents;
|
||||
mod canvas;
|
||||
@@ -29,6 +30,7 @@ mod workspace;
|
||||
|
||||
pub use agent_discovery::*;
|
||||
pub use agent_models::*;
|
||||
pub use agent_ownership::*;
|
||||
pub use agent_settings::*;
|
||||
pub use agents::*;
|
||||
pub use canvas::*;
|
||||
|
||||
@@ -749,6 +749,7 @@ pub fn run() {
|
||||
unarchive_identity,
|
||||
list_archived_identities,
|
||||
resolve_oa_owner,
|
||||
resolve_agent_ownership,
|
||||
list_relay_agents,
|
||||
list_managed_agents,
|
||||
create_managed_agent,
|
||||
|
||||
@@ -281,6 +281,32 @@ pub async fn query_relay_at(
|
||||
parse_json_response(response).await
|
||||
}
|
||||
|
||||
// ── HTTP bridge: GET (JSON) ─────────────────────────────────────────────────
|
||||
|
||||
/// Execute an authenticated GET against the relay HTTP API and deserialize JSON.
|
||||
pub async fn get_relay_json<T: DeserializeOwned>(
|
||||
state: &AppState,
|
||||
method: Method,
|
||||
url: &str,
|
||||
body: &[u8],
|
||||
) -> Result<T, String> {
|
||||
let auth = build_nip98_auth_header(&method, url, body, state)?;
|
||||
|
||||
let response = state
|
||||
.http_client
|
||||
.request(method, url)
|
||||
.header("Authorization", auth)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| classify_request_error(&e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(relay_error_message(response).await);
|
||||
}
|
||||
|
||||
parse_json_response(response).await
|
||||
}
|
||||
|
||||
// ── Command response parsing ────────────────────────────────────────────────
|
||||
|
||||
/// Parse a command-event OK message of the form `"response:<json>"`.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { useIsManagedAgent } from "@/features/agent-memory/hooks";
|
||||
import { resolveCanViewAgentActivity } from "@/features/agents/lib/canViewAgentActivity";
|
||||
import { resolveAgentOwnership } from "@/shared/api/tauriAgentOwnership";
|
||||
|
||||
export const agentOwnershipQueryKey = (agentPubkey: string) =>
|
||||
["agentOwnership", agentPubkey.toLowerCase()] as const;
|
||||
|
||||
export function useAgentOwnershipQuery(
|
||||
agentPubkey: string | null | undefined,
|
||||
enabled = true,
|
||||
) {
|
||||
return useQuery({
|
||||
enabled: enabled && Boolean(agentPubkey),
|
||||
queryKey: agentOwnershipQueryKey(agentPubkey ?? ""),
|
||||
queryFn: () => resolveAgentOwnership(agentPubkey as string),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Relay-authoritative gate for observer activity visibility.
|
||||
*
|
||||
* Returns `{ canView, isLoading }`. While ownership is loading, locally
|
||||
* managed agents may show activity optimistically; the final answer always
|
||||
* comes from relay `is_agent_owner`.
|
||||
*/
|
||||
export function useCanViewAgentActivity(
|
||||
agentPubkey: string | null | undefined,
|
||||
options?: { enabled?: boolean },
|
||||
) {
|
||||
const enabled = (options?.enabled ?? true) && Boolean(agentPubkey);
|
||||
const ownershipQuery = useAgentOwnershipQuery(agentPubkey, enabled);
|
||||
const isManagedAgent = useIsManagedAgent(enabled ? agentPubkey : null);
|
||||
|
||||
return resolveCanViewAgentActivity({
|
||||
relayOwnership: ownershipQuery.data,
|
||||
isManagedAgent,
|
||||
isOwnershipLoading: ownershipQuery.isLoading,
|
||||
isManagedLoading: isManagedAgent === undefined,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getChannelAgentSessionAgents,
|
||||
resolveOpenAgentSessionAgent,
|
||||
} from "../../channels/lib/agentSessionCandidates.ts";
|
||||
|
||||
const agent = (pubkey, source) => ({
|
||||
pubkey,
|
||||
name: pubkey.slice(0, 8),
|
||||
status: "deployed",
|
||||
agentSource: source,
|
||||
canInterruptTurn: source === "managed",
|
||||
});
|
||||
|
||||
test("resolveOpenAgentSessionAgent prefers channel-scoped candidate", () => {
|
||||
const channelAgent = agent("aa".repeat(32), "managed");
|
||||
const otherAgent = agent("bb".repeat(32), "relay");
|
||||
|
||||
const resolved = resolveOpenAgentSessionAgent({
|
||||
allAgentCandidates: [channelAgent, otherAgent],
|
||||
channelAgentSessionAgents: [channelAgent],
|
||||
openAgentSessionPubkey: channelAgent.pubkey,
|
||||
});
|
||||
|
||||
assert.equal(resolved, channelAgent);
|
||||
});
|
||||
|
||||
test("resolveOpenAgentSessionAgent falls back to owned agent outside channel list", () => {
|
||||
const ownedAgent = agent("cc".repeat(32), "relay");
|
||||
|
||||
const resolved = resolveOpenAgentSessionAgent({
|
||||
allAgentCandidates: [ownedAgent],
|
||||
channelAgentSessionAgents: [],
|
||||
openAgentSessionPubkey: ownedAgent.pubkey,
|
||||
});
|
||||
|
||||
assert.equal(resolved, ownedAgent);
|
||||
});
|
||||
|
||||
test("resolveOpenAgentSessionAgent synthesizes minimal agent when metadata is stale", () => {
|
||||
const pubkey = "dd".repeat(32);
|
||||
|
||||
const resolved = resolveOpenAgentSessionAgent({
|
||||
allAgentCandidates: [],
|
||||
channelAgentSessionAgents: [],
|
||||
openAgentSessionPubkey: pubkey,
|
||||
});
|
||||
|
||||
assert.deepEqual(resolved, {
|
||||
pubkey,
|
||||
name: pubkey.slice(0, 8),
|
||||
status: "deployed",
|
||||
agentSource: "relay",
|
||||
canInterruptTurn: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("getChannelAgentSessionAgents keeps managed agents visible in channel membership", () => {
|
||||
const activeChannel = {
|
||||
id: "channel-1",
|
||||
name: "general",
|
||||
};
|
||||
const candidates = [agent("ee".repeat(32), "managed")];
|
||||
|
||||
const visible = getChannelAgentSessionAgents({
|
||||
activeChannel,
|
||||
activeChannelId: activeChannel.id,
|
||||
agents: candidates,
|
||||
channelMembers: [
|
||||
{
|
||||
pubkey: candidates[0].pubkey,
|
||||
role: "bot",
|
||||
displayName: "Scout",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(visible.length, 1);
|
||||
assert.equal(visible[0]?.pubkey, candidates[0].pubkey);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { resolveCanViewAgentActivity } from "./canViewAgentActivity.ts";
|
||||
|
||||
test("resolveCanViewAgentActivity returns true when relay confirms ownership", () => {
|
||||
const result = resolveCanViewAgentActivity({
|
||||
relayOwnership: {
|
||||
agentPubkey: "aa".repeat(32),
|
||||
ownerPubkey: "bb".repeat(32),
|
||||
isOwner: true,
|
||||
},
|
||||
isManagedAgent: false,
|
||||
isOwnershipLoading: false,
|
||||
isManagedLoading: false,
|
||||
});
|
||||
|
||||
assert.equal(result.canView, true);
|
||||
assert.equal(result.isLoading, false);
|
||||
});
|
||||
|
||||
test("resolveCanViewAgentActivity returns false when relay denies ownership", () => {
|
||||
const result = resolveCanViewAgentActivity({
|
||||
relayOwnership: {
|
||||
agentPubkey: "aa".repeat(32),
|
||||
ownerPubkey: "bb".repeat(32),
|
||||
isOwner: false,
|
||||
},
|
||||
isManagedAgent: true,
|
||||
isOwnershipLoading: false,
|
||||
isManagedLoading: false,
|
||||
});
|
||||
|
||||
assert.equal(result.canView, false);
|
||||
assert.equal(result.isLoading, false);
|
||||
});
|
||||
|
||||
test("resolveCanViewAgentActivity optimistically allows locally managed agents while loading", () => {
|
||||
const result = resolveCanViewAgentActivity({
|
||||
relayOwnership: undefined,
|
||||
isManagedAgent: true,
|
||||
isOwnershipLoading: true,
|
||||
isManagedLoading: false,
|
||||
});
|
||||
|
||||
assert.equal(result.canView, true);
|
||||
assert.equal(result.isLoading, true);
|
||||
});
|
||||
|
||||
test("resolveCanViewAgentActivity stays closed for non-managed agents while loading", () => {
|
||||
const result = resolveCanViewAgentActivity({
|
||||
relayOwnership: undefined,
|
||||
isManagedAgent: false,
|
||||
isOwnershipLoading: true,
|
||||
isManagedLoading: false,
|
||||
});
|
||||
|
||||
assert.equal(result.canView, false);
|
||||
assert.equal(result.isLoading, true);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { AgentOwnershipStatus } from "@/shared/api/tauriAgentOwnership";
|
||||
|
||||
export type CanViewAgentActivityInput = {
|
||||
relayOwnership: AgentOwnershipStatus | undefined;
|
||||
isManagedAgent: boolean | undefined;
|
||||
isOwnershipLoading: boolean;
|
||||
isManagedLoading: boolean;
|
||||
};
|
||||
|
||||
export type CanViewAgentActivityResult = {
|
||||
canView: boolean;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unified predicate for Show Activity / Activity log ingresses.
|
||||
*
|
||||
* Final permission comes from relay `is_agent_owner`. While the relay lookup
|
||||
* is in flight, locally managed agents may show activity optimistically.
|
||||
*/
|
||||
export function resolveCanViewAgentActivity({
|
||||
relayOwnership,
|
||||
isManagedAgent,
|
||||
isOwnershipLoading,
|
||||
isManagedLoading,
|
||||
}: CanViewAgentActivityInput): CanViewAgentActivityResult {
|
||||
if (relayOwnership?.isOwner === true) {
|
||||
return { canView: true, isLoading: false };
|
||||
}
|
||||
|
||||
if (relayOwnership?.isOwner === false) {
|
||||
return { canView: false, isLoading: false };
|
||||
}
|
||||
|
||||
const isLoading =
|
||||
isOwnershipLoading || (isManagedAgent === undefined && isManagedLoading);
|
||||
|
||||
if (isManagedAgent === true && isOwnershipLoading) {
|
||||
return { canView: true, isLoading: true };
|
||||
}
|
||||
|
||||
return { canView: false, isLoading };
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import type {
|
||||
Channel,
|
||||
ChannelMember,
|
||||
ManagedAgent,
|
||||
RelayAgent,
|
||||
} from "@/shared/api/types";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
|
||||
export type ChannelAgentSessionAgent = Pick<
|
||||
ManagedAgent,
|
||||
"pubkey" | "name" | "status"
|
||||
> & {
|
||||
agentSource: "managed" | "member-bot" | "relay";
|
||||
canInterruptTurn: boolean;
|
||||
channelIds?: string[];
|
||||
channels?: string[];
|
||||
};
|
||||
|
||||
function relayStatusToManagedStatus(
|
||||
status: RelayAgent["status"],
|
||||
): ManagedAgent["status"] {
|
||||
return status === "offline" ? "stopped" : "deployed";
|
||||
}
|
||||
|
||||
export function buildChannelAgentSessionCandidates({
|
||||
channelMembers,
|
||||
managedAgents,
|
||||
relayAgents,
|
||||
}: {
|
||||
channelMembers?: ChannelMember[];
|
||||
managedAgents: ManagedAgent[];
|
||||
relayAgents: RelayAgent[];
|
||||
}): ChannelAgentSessionAgent[] {
|
||||
const byPubkey = new Map<string, ChannelAgentSessionAgent>();
|
||||
|
||||
for (const agent of relayAgents) {
|
||||
byPubkey.set(normalizePubkey(agent.pubkey), {
|
||||
pubkey: agent.pubkey,
|
||||
name: agent.name,
|
||||
status: relayStatusToManagedStatus(agent.status),
|
||||
agentSource: "relay",
|
||||
canInterruptTurn: false,
|
||||
channelIds: agent.channelIds,
|
||||
channels: agent.channels,
|
||||
});
|
||||
}
|
||||
|
||||
for (const agent of managedAgents) {
|
||||
const key = normalizePubkey(agent.pubkey);
|
||||
const existing = byPubkey.get(key);
|
||||
byPubkey.set(key, {
|
||||
pubkey: agent.pubkey,
|
||||
name: agent.name,
|
||||
status: agent.status,
|
||||
agentSource: "managed",
|
||||
canInterruptTurn: true,
|
||||
channelIds: existing?.channelIds,
|
||||
channels: existing?.channels,
|
||||
});
|
||||
}
|
||||
|
||||
for (const member of channelMembers ?? []) {
|
||||
const key = normalizePubkey(member.pubkey);
|
||||
if (member.role !== "bot" || byPubkey.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byPubkey.set(key, {
|
||||
pubkey: member.pubkey,
|
||||
name: member.displayName ?? member.pubkey.slice(0, 8),
|
||||
status: "deployed",
|
||||
agentSource: "member-bot",
|
||||
canInterruptTurn: false,
|
||||
});
|
||||
}
|
||||
|
||||
return [...byPubkey.values()];
|
||||
}
|
||||
|
||||
export function getChannelAgentSessionAgents({
|
||||
activeChannel,
|
||||
activeChannelId,
|
||||
agents,
|
||||
channelMembers,
|
||||
}: {
|
||||
activeChannel: Channel | null;
|
||||
activeChannelId: string | null;
|
||||
agents: ChannelAgentSessionAgent[];
|
||||
channelMembers?: ChannelMember[];
|
||||
}): ChannelAgentSessionAgent[] {
|
||||
if (!activeChannelId || !activeChannel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const memberPubkeys = channelMembers
|
||||
? new Set(channelMembers.map((member) => normalizePubkey(member.pubkey)))
|
||||
: null;
|
||||
const botMemberPubkeys = channelMembers
|
||||
? new Set(
|
||||
channelMembers
|
||||
.filter((member) => member.role === "bot")
|
||||
.map((member) => normalizePubkey(member.pubkey)),
|
||||
)
|
||||
: null;
|
||||
|
||||
return agents.filter((agent) => {
|
||||
const normalizedPubkey = normalizePubkey(agent.pubkey);
|
||||
const channelIds = agent.channelIds ?? [];
|
||||
const channels = agent.channels ?? [];
|
||||
const hasDeclaredChannelScope =
|
||||
channelIds.length > 0 || channels.length > 0;
|
||||
const matchesDeclaredChannel =
|
||||
channelIds.includes(activeChannelId) ||
|
||||
channels.includes(activeChannel.name);
|
||||
|
||||
if (agent.agentSource === "member-bot") {
|
||||
return botMemberPubkeys?.has(normalizedPubkey) ?? matchesDeclaredChannel;
|
||||
}
|
||||
|
||||
if (agent.agentSource === "managed") {
|
||||
return memberPubkeys?.has(normalizedPubkey) ?? matchesDeclaredChannel;
|
||||
}
|
||||
|
||||
if (matchesDeclaredChannel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!hasDeclaredChannelScope && Boolean(memberPubkeys?.has(normalizedPubkey))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveOpenAgentSessionAgent({
|
||||
allAgentCandidates,
|
||||
channelAgentSessionAgents,
|
||||
openAgentSessionPubkey,
|
||||
}: {
|
||||
allAgentCandidates: ChannelAgentSessionAgent[];
|
||||
channelAgentSessionAgents: ChannelAgentSessionAgent[];
|
||||
openAgentSessionPubkey: string | null;
|
||||
}): ChannelAgentSessionAgent | null {
|
||||
if (!openAgentSessionPubkey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = normalizePubkey(openAgentSessionPubkey);
|
||||
return (
|
||||
channelAgentSessionAgents.find(
|
||||
(agent) => normalizePubkey(agent.pubkey) === normalized,
|
||||
) ??
|
||||
allAgentCandidates.find(
|
||||
(agent) => normalizePubkey(agent.pubkey) === normalized,
|
||||
) ?? {
|
||||
pubkey: openAgentSessionPubkey,
|
||||
name: openAgentSessionPubkey.slice(0, 8),
|
||||
status: "deployed",
|
||||
agentSource: "relay",
|
||||
canInterruptTurn: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -124,6 +124,7 @@ type ChannelPaneProps = {
|
||||
personaLookup?: Map<string, string>;
|
||||
profiles?: UserProfileLookup;
|
||||
openThreadHeadId: string | null;
|
||||
openAgentSessionAgent: ChannelAgentSessionAgent | null;
|
||||
openAgentSessionPubkey: string | null;
|
||||
profilePanelPubkey?: string | null;
|
||||
threadHeadMessage: TimelineMessage | null;
|
||||
@@ -246,6 +247,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
personaLookup,
|
||||
profiles,
|
||||
openThreadHeadId,
|
||||
openAgentSessionAgent,
|
||||
openAgentSessionPubkey,
|
||||
profilePanelPubkey,
|
||||
targetMessageId,
|
||||
@@ -606,16 +608,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
|
||||
const isOverlay = useIsThreadPanelOverlay();
|
||||
const useSplitAuxiliaryPane = !isSinglePanelView && !isOverlay;
|
||||
|
||||
const selectedAgent = React.useMemo(
|
||||
() =>
|
||||
openAgentSessionPubkey
|
||||
? (agentSessionAgents.find(
|
||||
(agent) => agent.pubkey === openAgentSessionPubkey,
|
||||
) ?? null)
|
||||
: null,
|
||||
[agentSessionAgents, openAgentSessionPubkey],
|
||||
);
|
||||
const selectedAgent = openAgentSessionAgent;
|
||||
return (
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-row overflow-hidden">
|
||||
{!isSinglePanelView ? (
|
||||
|
||||
@@ -61,7 +61,10 @@ import {
|
||||
mergeAgentNamesIntoProfiles,
|
||||
useChannelActivityTyping,
|
||||
} from "./useChannelActivityTyping";
|
||||
import { useChannelAgentSessions } from "./useChannelAgentSessions";
|
||||
import {
|
||||
buildChannelAgentSessionCandidates,
|
||||
useChannelAgentSessions,
|
||||
} from "./useChannelAgentSessions";
|
||||
import { useChannelProfilePanel } from "./useChannelProfilePanel";
|
||||
import { useChannelRouteTarget } from "./useChannelRouteTarget";
|
||||
import type { ChannelScreenProps } from "./ChannelScreen.types";
|
||||
@@ -222,6 +225,15 @@ export function ChannelScreen({
|
||||
}
|
||||
return pubkeys;
|
||||
}, [channelMembers, managedAgents, relayAgents]);
|
||||
const allAgentSessionCandidates = React.useMemo(
|
||||
() =>
|
||||
buildChannelAgentSessionCandidates({
|
||||
channelMembers,
|
||||
managedAgents,
|
||||
relayAgents,
|
||||
}),
|
||||
[channelMembers, managedAgents, relayAgents],
|
||||
);
|
||||
const {
|
||||
botTypingEntries,
|
||||
channelAgentSessionAgents: activeChannelAgentSessionAgents,
|
||||
@@ -414,14 +426,15 @@ export function ChannelScreen({
|
||||
channelAgentSessionAgents,
|
||||
closeAgentSession: handleCloseAgentSession,
|
||||
openAgentSession: handleOpenAgentSession,
|
||||
openAgentSessionAgent,
|
||||
openAgentSessionPubkey,
|
||||
openThreadAndCloseAgentSession: handleOpenThreadAndCloseAgentSession,
|
||||
} = useChannelAgentSessions({
|
||||
activeChannel,
|
||||
activeChannelId,
|
||||
agentCandidates: allAgentSessionCandidates,
|
||||
channelMembers,
|
||||
handleOpenThread,
|
||||
managedAgents: activeChannelAgentSessionAgents,
|
||||
setExpandedThreadReplyIds,
|
||||
setOpenThreadHeadId,
|
||||
setProfilePanelPubkey,
|
||||
@@ -653,6 +666,7 @@ export function ChannelScreen({
|
||||
}
|
||||
onThreadPanelResizeStart={handleThreadPanelResizeStart}
|
||||
onToggleReaction={effectiveToggleReaction}
|
||||
openAgentSessionAgent={openAgentSessionAgent}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
openThreadHeadId={openThreadHeadId}
|
||||
profilePanelPubkey={profilePanelPubkey}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useCanViewAgentActivity } from "@/features/agents/hooks/useCanViewAgentActivity";
|
||||
import {
|
||||
getManagedAgentPrimaryActionLabel,
|
||||
isManagedAgentActive,
|
||||
@@ -110,13 +111,13 @@ export function MembersSidebarMemberCard({
|
||||
}: MembersSidebarMemberCardProps) {
|
||||
const roleLabel = formatRoleLabel(member, memberIsBot);
|
||||
const disabled = isActionPending || isArchived;
|
||||
const canViewActivity =
|
||||
memberIsBot &&
|
||||
managedAgent?.backend.type === "local" &&
|
||||
Boolean(onViewActivity);
|
||||
const { canView: canViewActivity } = useCanViewAgentActivity(member.pubkey, {
|
||||
enabled: Boolean(onViewActivity),
|
||||
});
|
||||
const canShowActivity = canViewActivity && Boolean(onViewActivity);
|
||||
const hasActions = memberIsBot
|
||||
? Boolean(managedAgent) || canRemoveMember || canViewActivity
|
||||
: canRemoveMember || canChangeRole;
|
||||
? Boolean(managedAgent) || canRemoveMember || canShowActivity
|
||||
: canRemoveMember || canChangeRole || canShowActivity;
|
||||
|
||||
const memberIdentity = (
|
||||
<div className="pointer-events-none relative z-10 flex min-w-0 flex-1 items-center gap-3">
|
||||
@@ -207,7 +208,7 @@ export function MembersSidebarMemberCard({
|
||||
<MemberActionsMenu
|
||||
canChangeRole={canChangeRole}
|
||||
canRemoveMember={canRemoveMember}
|
||||
canViewActivity={canViewActivity}
|
||||
canViewActivity={canShowActivity}
|
||||
disabled={disabled}
|
||||
managedAgent={managedAgent}
|
||||
member={member}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import {
|
||||
buildChannelAgentSessionCandidates,
|
||||
getChannelAgentSessionAgents,
|
||||
} from "./useChannelAgentSessions";
|
||||
} from "../lib/agentSessionCandidates";
|
||||
|
||||
export function useChannelActivityTyping({
|
||||
activeChannel,
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { useAgentOwnershipQuery } from "@/features/agents/hooks/useCanViewAgentActivity";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type {
|
||||
Channel,
|
||||
ChannelMember,
|
||||
ManagedAgent,
|
||||
RelayAgent,
|
||||
} from "@/shared/api/types";
|
||||
import type { Channel, ChannelMember } from "@/shared/api/types";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
|
||||
export type ChannelAgentSessionAgent = Pick<
|
||||
ManagedAgent,
|
||||
"pubkey" | "name" | "status"
|
||||
> & {
|
||||
agentSource: "managed" | "member-bot" | "relay";
|
||||
canInterruptTurn: boolean;
|
||||
channelIds?: string[];
|
||||
channels?: string[];
|
||||
};
|
||||
import {
|
||||
type ChannelAgentSessionAgent,
|
||||
getChannelAgentSessionAgents,
|
||||
resolveOpenAgentSessionAgent,
|
||||
} from "../lib/agentSessionCandidates";
|
||||
|
||||
export type { ChannelAgentSessionAgent } from "../lib/agentSessionCandidates";
|
||||
export {
|
||||
buildChannelAgentSessionCandidates,
|
||||
getChannelAgentSessionAgents,
|
||||
resolveOpenAgentSessionAgent,
|
||||
} from "../lib/agentSessionCandidates";
|
||||
|
||||
type UseChannelAgentSessionsOptions = {
|
||||
activeChannel: Channel | null;
|
||||
activeChannelId: string | null;
|
||||
agentCandidates: ChannelAgentSessionAgent[];
|
||||
channelMembers?: ChannelMember[];
|
||||
handleOpenThread: (message: TimelineMessage) => void;
|
||||
managedAgents: ChannelAgentSessionAgent[];
|
||||
setExpandedThreadReplyIds: (value: Set<string>) => void;
|
||||
setOpenThreadHeadId: (value: string | null) => void;
|
||||
setProfilePanelPubkey: (value: string | null) => void;
|
||||
@@ -32,127 +31,12 @@ type UseChannelAgentSessionsOptions = {
|
||||
setThreadScrollTargetId: (value: string | null) => void;
|
||||
};
|
||||
|
||||
function relayStatusToManagedStatus(
|
||||
status: RelayAgent["status"],
|
||||
): ManagedAgent["status"] {
|
||||
return status === "offline" ? "stopped" : "deployed";
|
||||
}
|
||||
|
||||
export function buildChannelAgentSessionCandidates({
|
||||
channelMembers,
|
||||
managedAgents,
|
||||
relayAgents,
|
||||
}: {
|
||||
channelMembers?: ChannelMember[];
|
||||
managedAgents: ManagedAgent[];
|
||||
relayAgents: RelayAgent[];
|
||||
}): ChannelAgentSessionAgent[] {
|
||||
const byPubkey = new Map<string, ChannelAgentSessionAgent>();
|
||||
|
||||
for (const agent of relayAgents) {
|
||||
byPubkey.set(normalizePubkey(agent.pubkey), {
|
||||
pubkey: agent.pubkey,
|
||||
name: agent.name,
|
||||
status: relayStatusToManagedStatus(agent.status),
|
||||
agentSource: "relay",
|
||||
canInterruptTurn: false,
|
||||
channelIds: agent.channelIds,
|
||||
channels: agent.channels,
|
||||
});
|
||||
}
|
||||
|
||||
for (const agent of managedAgents) {
|
||||
const key = normalizePubkey(agent.pubkey);
|
||||
const existing = byPubkey.get(key);
|
||||
byPubkey.set(key, {
|
||||
pubkey: agent.pubkey,
|
||||
name: agent.name,
|
||||
status: agent.status,
|
||||
agentSource: "managed",
|
||||
canInterruptTurn: true,
|
||||
channelIds: existing?.channelIds,
|
||||
channels: existing?.channels,
|
||||
});
|
||||
}
|
||||
|
||||
for (const member of channelMembers ?? []) {
|
||||
const key = normalizePubkey(member.pubkey);
|
||||
if (member.role !== "bot" || byPubkey.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byPubkey.set(key, {
|
||||
pubkey: member.pubkey,
|
||||
name: member.displayName ?? member.pubkey.slice(0, 8),
|
||||
status: "deployed",
|
||||
agentSource: "member-bot",
|
||||
canInterruptTurn: false,
|
||||
});
|
||||
}
|
||||
|
||||
return [...byPubkey.values()];
|
||||
}
|
||||
|
||||
export function getChannelAgentSessionAgents({
|
||||
activeChannel,
|
||||
activeChannelId,
|
||||
agents,
|
||||
channelMembers,
|
||||
}: {
|
||||
activeChannel: Channel | null;
|
||||
activeChannelId: string | null;
|
||||
agents: ChannelAgentSessionAgent[];
|
||||
channelMembers?: ChannelMember[];
|
||||
}): ChannelAgentSessionAgent[] {
|
||||
if (!activeChannelId || !activeChannel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const memberPubkeys = channelMembers
|
||||
? new Set(channelMembers.map((member) => normalizePubkey(member.pubkey)))
|
||||
: null;
|
||||
const botMemberPubkeys = channelMembers
|
||||
? new Set(
|
||||
channelMembers
|
||||
.filter((member) => member.role === "bot")
|
||||
.map((member) => normalizePubkey(member.pubkey)),
|
||||
)
|
||||
: null;
|
||||
|
||||
return agents.filter((agent) => {
|
||||
const normalizedPubkey = normalizePubkey(agent.pubkey);
|
||||
const channelIds = agent.channelIds ?? [];
|
||||
const channels = agent.channels ?? [];
|
||||
const hasDeclaredChannelScope =
|
||||
channelIds.length > 0 || channels.length > 0;
|
||||
const matchesDeclaredChannel =
|
||||
channelIds.includes(activeChannelId) ||
|
||||
channels.includes(activeChannel.name);
|
||||
|
||||
if (agent.agentSource === "member-bot") {
|
||||
return botMemberPubkeys?.has(normalizedPubkey) ?? matchesDeclaredChannel;
|
||||
}
|
||||
|
||||
if (agent.agentSource === "managed") {
|
||||
return memberPubkeys?.has(normalizedPubkey) ?? matchesDeclaredChannel;
|
||||
}
|
||||
|
||||
if (matchesDeclaredChannel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!hasDeclaredChannelScope && Boolean(memberPubkeys?.has(normalizedPubkey))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useChannelAgentSessions({
|
||||
activeChannel,
|
||||
activeChannelId,
|
||||
agentCandidates,
|
||||
channelMembers,
|
||||
handleOpenThread,
|
||||
managedAgents,
|
||||
setExpandedThreadReplyIds,
|
||||
setOpenThreadHeadId,
|
||||
setProfilePanelPubkey,
|
||||
@@ -168,10 +52,22 @@ export function useChannelAgentSessions({
|
||||
getChannelAgentSessionAgents({
|
||||
activeChannel,
|
||||
activeChannelId,
|
||||
agents: managedAgents,
|
||||
agents: agentCandidates,
|
||||
channelMembers,
|
||||
}),
|
||||
[activeChannel, activeChannelId, channelMembers, managedAgents],
|
||||
[activeChannel, activeChannelId, agentCandidates, channelMembers],
|
||||
);
|
||||
|
||||
const ownershipQuery = useAgentOwnershipQuery(openAgentSessionPubkey);
|
||||
|
||||
const openAgentSessionAgent = React.useMemo(
|
||||
() =>
|
||||
resolveOpenAgentSessionAgent({
|
||||
allAgentCandidates: agentCandidates,
|
||||
channelAgentSessionAgents,
|
||||
openAgentSessionPubkey,
|
||||
}),
|
||||
[agentCandidates, channelAgentSessionAgents, openAgentSessionPubkey],
|
||||
);
|
||||
|
||||
const closeAgentSession = React.useCallback(() => {
|
||||
@@ -210,22 +106,38 @@ export function useChannelAgentSessions({
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
openAgentSessionPubkey &&
|
||||
!channelAgentSessionAgents.some(
|
||||
(agent) =>
|
||||
normalizePubkey(agent.pubkey) ===
|
||||
normalizePubkey(openAgentSessionPubkey),
|
||||
)
|
||||
) {
|
||||
if (!openAgentSessionPubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inChannelList = channelAgentSessionAgents.some(
|
||||
(agent) =>
|
||||
normalizePubkey(agent.pubkey) ===
|
||||
normalizePubkey(openAgentSessionPubkey),
|
||||
);
|
||||
if (inChannelList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ownershipQuery.isLoading || ownershipQuery.data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ownershipQuery.data.isOwner) {
|
||||
setOpenAgentSessionPubkey(null);
|
||||
}
|
||||
}, [channelAgentSessionAgents, openAgentSessionPubkey]);
|
||||
}, [
|
||||
channelAgentSessionAgents,
|
||||
openAgentSessionPubkey,
|
||||
ownershipQuery.data,
|
||||
ownershipQuery.isLoading,
|
||||
]);
|
||||
|
||||
return {
|
||||
channelAgentSessionAgents,
|
||||
closeAgentSession,
|
||||
openAgentSession,
|
||||
openAgentSessionAgent,
|
||||
openAgentSessionPubkey,
|
||||
openThreadAndCloseAgentSession,
|
||||
selectAgentSession,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useAgentMemoryQuery,
|
||||
useIsManagedAgent,
|
||||
} from "@/features/agent-memory/hooks";
|
||||
import { useCanViewAgentActivity } from "@/features/agents/hooks/useCanViewAgentActivity";
|
||||
import { MemoryRefreshButton } from "@/features/agent-memory/ui/MemorySection";
|
||||
import {
|
||||
useRelayAgentsQuery,
|
||||
@@ -178,6 +179,9 @@ export function UserProfilePanel({
|
||||
);
|
||||
const isBot = Boolean(relayAgent || managedAgent);
|
||||
const isOwner = useIsManagedAgent(isBot ? pubkey : null);
|
||||
const { canView: canViewActivity } = useCanViewAgentActivity(pubkey, {
|
||||
enabled: Boolean(onOpenAgentSession),
|
||||
});
|
||||
|
||||
// Populate the active-turns store for this agent so useActiveAgentTurns works
|
||||
// even if the Agents page hasn't been visited yet.
|
||||
@@ -196,7 +200,6 @@ export function UserProfilePanel({
|
||||
});
|
||||
const isSelf =
|
||||
currentPubkey !== undefined && pubkeyLower === currentPubkey.toLowerCase();
|
||||
const canViewActivity = isOwner === true && Boolean(onOpenAgentSession);
|
||||
const isFollowing =
|
||||
!isSelf &&
|
||||
(contactListQuery.data?.contacts.some(
|
||||
@@ -317,7 +320,7 @@ export function UserProfilePanel({
|
||||
{view === "summary" ? (
|
||||
<ProfileSummaryView
|
||||
canEditAgent={canEditAgent}
|
||||
canViewActivity={canViewActivity}
|
||||
canViewActivity={canViewActivity && Boolean(onOpenAgentSession)}
|
||||
channelCount={profileChannels.length}
|
||||
channelIdToName={channelIdToName}
|
||||
channelsLoading={channelsQuery.isLoading}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Activity } from "lucide-react";
|
||||
|
||||
import { useCanViewAgentActivity } from "@/features/agents/hooks/useCanViewAgentActivity";
|
||||
import { useUserProfileQuery } from "@/features/profile/hooks";
|
||||
import {
|
||||
useRelayAgentsQuery,
|
||||
@@ -86,11 +87,14 @@ export function UserProfilePopover({
|
||||
|
||||
const { onOpenAgentSession } = useAgentSession();
|
||||
const { openProfilePanel } = useProfilePanel();
|
||||
const { canView: canViewActivity } = useCanViewAgentActivity(
|
||||
open ? pubkey : null,
|
||||
{ enabled: open && Boolean(onOpenAgentSession) },
|
||||
);
|
||||
const relayAgent = relayAgentsQuery.data?.find((a) => a.pubkey === pubkey);
|
||||
const managedAgent = managedAgentsQuery.data?.find(
|
||||
(a) => a.pubkey === pubkey,
|
||||
);
|
||||
const canViewActivity = role === "bot" && Boolean(onOpenAgentSession);
|
||||
const profile = profileQuery.data;
|
||||
const presenceStatus = presenceQuery.data?.[pubkey.toLowerCase()];
|
||||
const userStatus = userStatusQuery.data?.[pubkey.toLowerCase()];
|
||||
@@ -274,7 +278,7 @@ export function UserProfilePopover({
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{canViewActivity ? (
|
||||
{canViewActivity && 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}`}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { invokeTauri } from "@/shared/api/tauri";
|
||||
|
||||
export type AgentOwnershipStatus = {
|
||||
/** Lowercase hex pubkey of the queried agent. */
|
||||
agentPubkey: string;
|
||||
/** Lowercase hex owner pubkey from relay `agent_owner_pubkey`, if set. */
|
||||
ownerPubkey: string | null;
|
||||
/** True iff the current workspace identity is the relay-recorded owner. */
|
||||
isOwner: boolean;
|
||||
};
|
||||
|
||||
type RawAgentOwnershipStatus = {
|
||||
agent_pubkey: string;
|
||||
owner_pubkey: string | null;
|
||||
is_owner: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve whether the current identity owns `agentPubkey` per relay DB.
|
||||
* Authoritative gate for observer activity visibility.
|
||||
*/
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -85,6 +85,8 @@ type E2eConfig = {
|
||||
// - `resetMockRelayMembers` (relayRole)
|
||||
archivedIdentities?: string[];
|
||||
oaOwnerIsMe?: boolean;
|
||||
/** Drives `resolve_agent_ownership` for activity visibility gates. */
|
||||
agentOwnerIsMe?: boolean;
|
||||
relayRole?: "owner" | "admin" | "member" | null;
|
||||
// Descriptors returned by the mocked `pick_and_upload_media` /
|
||||
// `upload_media_bytes` commands. Lets a spec drive the attachment flow
|
||||
@@ -6562,6 +6564,20 @@ export function maybeInstallE2eTauriMocks() {
|
||||
: "ff".repeat(32);
|
||||
return { owner, is_me: isMe };
|
||||
}
|
||||
case "resolve_agent_ownership": {
|
||||
const agentPubkey =
|
||||
(payload as { agentPubkey?: string }).agentPubkey?.toLowerCase() ??
|
||||
"aa".repeat(32);
|
||||
const isOwner = activeConfig?.mock?.agentOwnerIsMe ?? false;
|
||||
const owner = isOwner
|
||||
? (identity?.pubkey ?? DEFAULT_MOCK_IDENTITY.pubkey)
|
||||
: "ff".repeat(32);
|
||||
return {
|
||||
agent_pubkey: agentPubkey,
|
||||
owner_pubkey: owner,
|
||||
is_owner: isOwner,
|
||||
};
|
||||
}
|
||||
case "list_archived_identities": {
|
||||
const archived = activeConfig?.mock?.archivedIdentities ?? [];
|
||||
return { archived };
|
||||
|
||||
Reference in New Issue
Block a user