Fix cross-owner relay agent mentions in owner-only builds (#6338)

## Summary

- keep `BUZZ_BUILD_AGENT_ACCESS_OWNER_ONLY` scoped to Desktop-managed
local start and provider deployment boundaries
- stop filtering independently operated relay agents by viewer/owner
equality in native discovery, autocomplete, and send-time revalidation
- preserve NIP-OA ownership verification, owner-authored `respond_to`
policy, shared-channel membership, and fail-closed send-time checks
- replace the packaged-build regression expectation with coverage for
cross-owner allowlisted and `respond_to=anyone` relay agents, including
the emitted agent `p` tag

Fixes #6329.

## Why

The packaged 0.5.17 build reused its managed-runtime owner-only
capability in relay-agent mention admission. That silently hid correctly
configured shared agents owned by another operator, even when their
verified policy explicitly authorized the viewer. The capability is
intended to constrain runtimes Desktop starts or deploys, not external
relay agents.

## Validation

- `pnpm --dir desktop test` (5093 passed)
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
relay_directory --lib` (4 passed, 2 ignored real-relay tests)
- focused agent mention unit tests (35 passed)
- Desktop TypeScript/E2E build
- focused Playwright mention tests (3 passed)
- pre-commit formatting hooks
- pre-push branch-skew, file-size, Desktop check/typecheck/unit tests,
and Tauri checks
- `git diff --check`

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-20 12:35:07 -06:00
committed by GitHub
parent 84c095f8be
commit ee0c7076af
8 changed files with 146 additions and 223 deletions

View File

@@ -112,15 +112,10 @@ async fn query_all_relay_pages(
}
}
fn owner_only_relay_directory() -> bool {
crate::managed_agents::owner_only_access_build()
}
fn retain_verified_owner(
verified_owners: &mut std::collections::HashMap<String, String>,
required_owner: &str,
) {
verified_owners.retain(|_, owner| owner.eq_ignore_ascii_case(required_owner));
fn retain_agents_allowed_by_build(agents: &mut Vec<RelayAgentInfo>, require_verified_owner: bool) {
if require_verified_owner {
agents.retain(|agent| agent.owner_pubkey.is_some());
}
}
pub(crate) async fn list_relay_agents_for_state(
@@ -135,7 +130,6 @@ async fn list_relay_agents_for_selection(
channel_id: Option<&str>,
) -> Result<Vec<RelayAgentInfo>, String> {
let viewer_pubkey = current_user_pubkey(state)?;
let owner_only = owner_only_relay_directory();
let relay_pubkey = identity_archive::fetch_relay_self(state)
.await?
.ok_or_else(|| "relay agent membership authority is unavailable".to_string())?;
@@ -189,14 +183,7 @@ async fn list_relay_agents_for_selection(
// query. Each exact `(owner, d=agent)` filter returns at most one current
// replaceable event, so forged 30177 coordinates cannot amplify or crowd
// the authentic policy out of a bounded result page.
let mut verified_owners = nostr_convert::verified_agent_owners_from_profiles(&profile_events);
// The internal capability narrows the remote directory to cryptographically
// verified agents owned by the active user. Same-owner siblings remain
// mentionable because they are inside the harness's owner-only boundary;
// all cross-owner coordinates are discarded before policy lookup.
if owner_only {
retain_verified_owner(&mut verified_owners, &viewer_pubkey);
}
let verified_owners = nostr_convert::verified_agent_owners_from_profiles(&profile_events);
let managed_filters = managed_policy_filters(&candidate_pubkeys, &verified_owners);
let managed_agent_events = query_filter_batches(
state,
@@ -211,14 +198,14 @@ async fn list_relay_agents_for_selection(
&managed_agent_events,
&profile_events,
);
if owner_only {
agents.retain(|agent| {
agent
.owner_pubkey
.as_deref()
.is_some_and(|owner| owner.eq_ignore_ascii_case(&viewer_pubkey))
});
}
// Marked builds reject legacy directory records that lack a verified
// NIP-OA owner, but do not require that owner to equal the viewer. The
// verified owner's signed respond_to policy remains the authorization
// boundary for independently operated relay agents.
retain_agents_allowed_by_build(
&mut agents,
crate::managed_agents::owner_only_access_build(),
);
agents.retain(|agent| member_agent_channel_ids.contains_key(&agent.pubkey));
for agent in &mut agents {
agent.channel_ids = member_agent_channel_ids
@@ -260,24 +247,66 @@ mod tests {
use super::*;
#[test]
fn owner_only_directory_keeps_only_verified_same_owner_coordinates() {
let viewer = "a".repeat(64);
let other_owner = "b".repeat(64);
let same_owner_agent = "c".repeat(64);
let other_owner_agent = "d".repeat(64);
let mut owners = std::collections::HashMap::from([
(same_owner_agent.clone(), viewer.to_uppercase()),
(other_owner_agent, other_owner),
]);
fn marked_build_requires_verified_owner_without_requiring_viewer_ownership() {
let cross_owner = "b".repeat(64);
let mut agents = vec![
RelayAgentInfo {
pubkey: "a".repeat(64),
owner_pubkey: Some(cross_owner.clone()),
name: "Verified cross-owner".to_string(),
agent_type: "agent".to_string(),
channels: Vec::new(),
channel_ids: Vec::new(),
capabilities: Vec::new(),
status: "offline".to_string(),
respond_to: None,
respond_to_allowlist: Vec::new(),
},
RelayAgentInfo {
pubkey: "c".repeat(64),
owner_pubkey: None,
name: "Ownerless legacy".to_string(),
agent_type: "agent".to_string(),
channels: Vec::new(),
channel_ids: Vec::new(),
capabilities: Vec::new(),
status: "online".to_string(),
respond_to: None,
respond_to_allowlist: Vec::new(),
},
];
retain_verified_owner(&mut owners, &viewer);
retain_agents_allowed_by_build(&mut agents, true);
assert_eq!(agents.len(), 1);
assert_eq!(agents[0].name, "Verified cross-owner");
assert_eq!(
owners,
std::collections::HashMap::from([(same_owner_agent, viewer.to_uppercase())])
agents[0].owner_pubkey.as_deref(),
Some(cross_owner.as_str())
);
}
#[test]
fn oss_build_preserves_ownerless_legacy_agents() {
let mut agents = vec![RelayAgentInfo {
pubkey: "a".repeat(64),
owner_pubkey: None,
name: "Ownerless legacy".to_string(),
agent_type: "agent".to_string(),
channels: Vec::new(),
channel_ids: Vec::new(),
capabilities: Vec::new(),
status: "online".to_string(),
respond_to: None,
respond_to_allowlist: Vec::new(),
}];
retain_agents_allowed_by_build(&mut agents, false);
assert_eq!(agents.len(), 1);
assert!(agents[0].owner_pubkey.is_none());
}
#[test]
fn exact_author_queries_prevent_noisy_agent_crowd_out() {
let pubkeys = vec!["a".repeat(64), "b".repeat(64)];

View File

@@ -236,17 +236,19 @@ with a TypeScript lookup table or an id comparison in a component.
mid-conversation effort control without a plan ruling. The archived live-effort
machinery lives on `archive/claude-config-gaps-live-effort` for reference only.
12. **Owner-only builds discover only verified same-owner remote agents.**
The native `list_relay_agents` boundary authenticates ownership through the
agent's NIP-OA profile, then retains only agents owned by the active user
when the compiled owner-only capability is present. Keep this as the
authoritative backstop: internal builds must never admit cross-owner remote
agents, while same-owner agents on another machine remain inside the
documented owner-only trust boundary. OSS builds retain the complete
policy-filtered relay directory and send-time fail-closed mention
revalidation. Local `agents-data-changed` events refresh only local
persona/team/managed-agent caches; they must never invalidate the remote
relay directory.
12. **Owner-only builds constrain managed runtimes, not relay-agent mentions.**
The compiled owner-only capability applies when Desktop starts or deploys a
managed agent. Independently operated relay agents with NIP-OA ownership
remain eligible in every build when their verified owner's signed
`respond_to` policy admits the viewer and relay membership includes the
target channel. Marked builds require that verified owner coordinate but do
not require it to equal the viewer; OSS builds retain compatibility with
self-authored legacy directory records. Keep native discovery and send-time
revalidation fail closed on invalid ownership or managed policy evidence,
and on missing membership or directory evidence; do not add a cross-owner
clamp to either mention path. Local `agents-data-changed` events
refresh only local persona/team/managed-agent caches; they must never
invalidate the remote relay directory.
## The tests that enforce this

View File

@@ -278,7 +278,6 @@ test("isAgentIdentityInAllowedList: keeps people and only explicitly allowed age
test("shouldHideAgentFromMentions: never hides non-agents", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: false,
isMember: false,
pubkey: PUB_A,
@@ -292,7 +291,6 @@ test("shouldHideAgentFromMentions: never hides non-agents", () => {
test("shouldHideAgentFromMentions: shows invocable agents even when non-member", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: false,
pubkey: PUB_A,
@@ -306,7 +304,6 @@ test("shouldHideAgentFromMentions: shows invocable agents even when non-member",
test("shouldHideAgentFromMentions: hides non-member non-invocable agents", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: false,
pubkey: PUB_A,
@@ -320,7 +317,6 @@ test("shouldHideAgentFromMentions: hides non-member non-invocable agents", () =>
test("shouldHideAgentFromMentions: hides member agents with an explicit not-invocable directory entry (Fizz)", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: true,
pubkey: PUB_A,
@@ -334,7 +330,6 @@ test("shouldHideAgentFromMentions: hides member agents with an explicit not-invo
test("shouldHideAgentFromMentions: hides member agents without an affirmative directory grant", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: true,
pubkey: PUB_A,
@@ -348,7 +343,6 @@ test("shouldHideAgentFromMentions: hides member agents without an affirmative di
test("shouldHideAgentFromMentions: hides unknown member agents while directories load", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: true,
pubkey: PUB_A,
@@ -363,7 +357,6 @@ test("shouldHideAgentFromMentions: hides unknown member agents while directories
test("shouldHideAgentFromMentions: hides mentionable member agents while directories load", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: true,
pubkey: PUB_A,
@@ -378,7 +371,6 @@ test("shouldHideAgentFromMentions: hides mentionable member agents while directo
test("shouldHideAgentFromMentions: shows non-agent members while directories load", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: false,
isMember: true,
pubkey: PUB_A,
@@ -393,7 +385,6 @@ test("shouldHideAgentFromMentions: shows non-agent members while directories loa
test("shouldHideAgentFromMentions: hides unknown member agents after empty directories settle", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: true,
pubkey: PUB_A,
@@ -405,16 +396,15 @@ test("shouldHideAgentFromMentions: hides unknown member agents after empty direc
);
});
test("shouldHideAgentFromMentions: hides agents while owner policy loads", () => {
test("shouldHideAgentFromMentions: shows authorized agents without managed-owner policy", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set([PUB_A]),
directoryReady: true,
ownerOnly: undefined,
}),
true,
false,
);
});
@@ -424,7 +414,6 @@ test("shouldHideAgentFromMentions: normalizes the pubkey before lookup", () => {
assert.equal(
shouldHideAgentFromMentions({
ownerOnly: false,
isAgent: true,
isMember: true,
pubkey: mixedCase,
@@ -435,42 +424,31 @@ test("shouldHideAgentFromMentions: normalizes the pubkey before lookup", () => {
);
});
test("getAgentMentionAdmission: owner-only requires current verified ownership", () => {
test("getAgentMentionAdmission: authorized relay agents are independent of owner", () => {
const common = {
isAgent: true,
isManagedAgent: false,
pubkey: PUB_A,
currentPubkey: CURRENT_PUBKEY,
mentionableAgentPubkeys: new Set([PUB_A]),
directoryReady: true,
ownerOnly: true,
};
assert.equal(getAgentMentionAdmission(common), "allow");
assert.equal(
getAgentMentionAdmission({ ...common, ownerPubkey: CURRENT_PUBKEY }),
"allow",
);
assert.equal(
getAgentMentionAdmission({ ...common, ownerPubkey: OTHER_OWNER_PUBKEY }),
getAgentMentionAdmission({
...common,
mentionableAgentPubkeys: new Set(),
}),
"deny",
);
assert.equal(
getAgentMentionAdmission({ ...common, ownerPubkey: null }),
"unknown",
);
});
test("getAgentMentionAdmission: unresolved directory state stays unknown", () => {
assert.equal(
getAgentMentionAdmission({
isAgent: true,
isManagedAgent: false,
pubkey: PUB_A,
currentPubkey: CURRENT_PUBKEY,
ownerPubkey: CURRENT_PUBKEY,
mentionableAgentPubkeys: new Set([PUB_A]),
directoryReady: false,
ownerOnly: false,
}),
"unknown",
);

View File

@@ -110,65 +110,40 @@ export type AgentMentionAdmission = "allow" | "deny" | "unknown";
export function getAgentMentionAdmission({
isAgent,
isManagedAgent,
pubkey,
ownerPubkey,
currentPubkey,
mentionableAgentPubkeys,
directoryReady,
ownerOnly,
}: {
isAgent: boolean;
isManagedAgent: boolean;
pubkey: string;
ownerPubkey?: string | null;
currentPubkey?: string | null;
mentionableAgentPubkeys: ReadonlySet<string>;
directoryReady: boolean;
ownerOnly: boolean | undefined;
}): AgentMentionAdmission {
if (!isAgent) return "allow";
if (!directoryReady || ownerOnly === undefined) return "unknown";
if (!directoryReady) return "unknown";
const normalized = normalizePubkey(pubkey);
if (!mentionableAgentPubkeys.has(normalized)) return "deny";
if (!ownerOnly || isManagedAgent) return "allow";
if (!ownerPubkey || !currentPubkey) return "unknown";
return normalizePubkey(ownerPubkey) === normalizePubkey(currentPubkey)
return mentionableAgentPubkeys.has(normalizePubkey(pubkey))
? "allow"
: "deny";
}
export function shouldHideAgentFromMentions({
isAgent,
isManagedAgent = false,
pubkey,
ownerPubkey,
currentPubkey,
mentionableAgentPubkeys,
directoryReady = true,
ownerOnly,
}: {
isAgent: boolean;
isManagedAgent?: boolean;
pubkey: string;
ownerPubkey?: string | null;
currentPubkey?: string | null;
mentionableAgentPubkeys: ReadonlySet<string>;
directoryReady?: boolean;
ownerOnly: boolean | undefined;
}) {
return (
getAgentMentionAdmission({
isAgent,
isManagedAgent,
pubkey,
ownerPubkey,
currentPubkey,
mentionableAgentPubkeys,
directoryReady,
ownerOnly,
}) !== "allow"
);
}

View File

@@ -6,18 +6,15 @@ import { revalidateAgentMentionPubkeys } from "./agentMentionRevalidation.ts";
const CURRENT = "a".repeat(64);
const AGENT = "b".repeat(64);
const HUMAN = "c".repeat(64);
const OTHER_OWNER = "d".repeat(64);
const LOCAL_AGENT = "e".repeat(64);
function options(refetchOwnerProfiles) {
function options() {
return {
pubkeys: [HUMAN, AGENT],
agentPubkeys: new Set([AGENT]),
currentPubkey: CURRENT,
eligibilityScope: { type: "channel", channelId: "general" },
sharedChannelIds: new Set(["general"]),
ownerOnly: true,
ownerPolicyError: null,
refetchManagedAgents: async () => ({ data: [], error: null }),
fetchRelayAgents: async () => [
{
@@ -27,31 +24,19 @@ function options(refetchOwnerProfiles) {
channelIds: ["general"],
},
],
refetchOwnerProfiles,
};
}
test("owner-only revalidation admits an agent only from a fresh same-owner proof", async () => {
const requested = [];
const result = await revalidateAgentMentionPubkeys(
options(async (pubkeys) => {
requested.push(...pubkeys);
return {
profiles: { [AGENT]: { ownerPubkey: CURRENT } },
missing: [],
};
}),
);
assert.deepEqual(requested, [AGENT]);
assert.deepEqual(result, [HUMAN, AGENT]);
test("relay policy revalidation admits an authorized external agent", async () => {
assert.deepEqual(await revalidateAgentMentionPubkeys(options()), [
HUMAN,
AGENT,
]);
});
test("fresh managed evidence survives unrelated relay authorization errors", async () => {
const result = await revalidateAgentMentionPubkeys({
...options(async () => {
throw new Error("owner profiles unavailable");
}),
...options(),
pubkeys: [HUMAN, LOCAL_AGENT],
agentPubkeys: new Set([LOCAL_AGENT]),
refetchManagedAgents: async () => ({
@@ -68,10 +53,7 @@ test("fresh managed evidence survives unrelated relay authorization errors", asy
test("relay-only agents still fail closed when relay discovery fails", async () => {
const result = await revalidateAgentMentionPubkeys({
...options(async () => ({
profiles: { [AGENT]: { ownerPubkey: CURRENT } },
missing: [],
})),
...options(),
fetchRelayAgents: async () => {
throw new Error("relay directory unavailable");
},
@@ -99,27 +81,3 @@ test("mixed evidence preserves only fresh managed agents and humans", async () =
assert.deepEqual(result, [HUMAN, LOCAL_AGENT]);
});
for (const [name, refetchOwnerProfiles] of [
["revoked owner proof", async () => ({ profiles: {}, missing: [AGENT] })],
[
"changed owner proof",
async () => ({
profiles: { [AGENT]: { ownerPubkey: OTHER_OWNER } },
missing: [],
}),
],
[
"owner profile query error",
async () => {
throw new Error("relay unavailable");
},
],
]) {
test(`owner-only revalidation fails closed on ${name}`, async () => {
assert.deepEqual(
await revalidateAgentMentionPubkeys(options(refetchOwnerProfiles)),
[HUMAN],
);
});
}

View File

@@ -4,16 +4,9 @@ import {
getMentionableAgentPubkeys,
type AgentEligibilityScope,
} from "@/features/agents/lib/agentAutocompleteEligibility";
import { evictUsersBatchEntries } from "@/features/profile/hooks";
import { getUsersBatch } from "@/shared/api/tauriProfiles";
import { revalidateRelayAgents } from "@/shared/api/tauriRelayAgents";
import type {
ManagedAgent,
RelayAgent,
UsersBatchResponse,
} from "@/shared/api/types";
import type { ManagedAgent, RelayAgent } from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { useQueryClient } from "@tanstack/react-query";
import * as React from "react";
type DirectoryResult<T> = {
@@ -27,22 +20,16 @@ export async function revalidateAgentMentionPubkeys({
currentPubkey,
eligibilityScope,
sharedChannelIds,
ownerOnly,
ownerPolicyError,
refetchManagedAgents,
fetchRelayAgents,
refetchOwnerProfiles,
}: {
pubkeys: readonly string[];
agentPubkeys: ReadonlySet<string>;
currentPubkey: string | null;
eligibilityScope: AgentEligibilityScope;
sharedChannelIds: ReadonlySet<string>;
ownerOnly: boolean | undefined;
ownerPolicyError: Error | null;
refetchManagedAgents: () => Promise<DirectoryResult<ManagedAgent[]>>;
fetchRelayAgents: (pubkeys: string[]) => Promise<RelayAgent[]>;
refetchOwnerProfiles: (pubkeys: string[]) => Promise<UsersBatchResponse>;
}) {
const requestedAgentPubkeys = new Set(
pubkeys.map(normalizePubkey).filter((pubkey) => agentPubkeys.has(pubkey)),
@@ -51,20 +38,12 @@ export async function revalidateAgentMentionPubkeys({
return [...pubkeys];
}
const [managedResult, relayAgents, ownerProfiles] = await Promise.all([
const [managedResult, relayAgents] = await Promise.all([
refetchManagedAgents(),
fetchRelayAgents([...requestedAgentPubkeys]).catch(() => null),
ownerOnly
? refetchOwnerProfiles([...requestedAgentPubkeys]).catch(() => null)
: Promise.resolve(null),
]);
const relayDirectoryReady = relayAgents !== null;
if (
ownerOnly === undefined ||
ownerPolicyError !== null ||
managedResult.error !== null ||
managedResult.data === undefined
) {
if (managedResult.error !== null || managedResult.data === undefined) {
return filterAdmittedMentionPubkeys(pubkeys, agentPubkeys, new Set());
}
@@ -81,19 +60,13 @@ export async function revalidateAgentMentionPubkeys({
const admittedPubkeys = new Set(
[...agentPubkeys].filter((pubkey) => {
const isManagedAgent = managedPubkeys.has(normalizePubkey(pubkey));
const directoryReady =
isManagedAgent ||
(relayDirectoryReady && (!ownerOnly || ownerProfiles !== null));
const directoryReady = isManagedAgent || relayDirectoryReady;
return (
getAgentMentionAdmission({
isAgent: true,
isManagedAgent,
pubkey,
ownerPubkey: ownerProfiles?.profiles[pubkey]?.ownerPubkey,
currentPubkey,
mentionableAgentPubkeys: mentionablePubkeys,
directoryReady,
ownerOnly,
}) === "allow"
);
}),
@@ -107,8 +80,6 @@ export function useAgentMentionRevalidation({
currentPubkey,
eligibilityScope,
sharedChannelIds,
ownerOnly,
ownerPolicyError,
refetchManagedAgents,
}: {
agentPubkeys: ReadonlySet<string>;
@@ -116,18 +87,8 @@ export function useAgentMentionRevalidation({
currentPubkey: string | null;
eligibilityScope: AgentEligibilityScope;
sharedChannelIds: ReadonlySet<string>;
ownerOnly: boolean | undefined;
ownerPolicyError: Error | null;
refetchManagedAgents: () => Promise<DirectoryResult<ManagedAgent[]>>;
}) {
const queryClient = useQueryClient();
const refetchOwnerProfiles = React.useCallback(
async (pubkeys: string[]) => {
evictUsersBatchEntries(queryClient, pubkeys);
return getUsersBatch(pubkeys);
},
[queryClient],
);
return React.useCallback(
(pubkeys: readonly string[]) =>
revalidateAgentMentionPubkeys({
@@ -136,8 +97,6 @@ export function useAgentMentionRevalidation({
currentPubkey,
eligibilityScope,
sharedChannelIds,
ownerOnly,
ownerPolicyError,
refetchManagedAgents,
fetchRelayAgents: (requestedPubkeys) =>
revalidateRelayAgents(
@@ -146,17 +105,13 @@ export function useAgentMentionRevalidation({
? eligibilityScope.channelId
: undefined,
),
refetchOwnerProfiles,
}),
[
agentPubkeys,
currentPubkey,
eligibilityScope,
getSelectedAgentPubkeys,
ownerOnly,
ownerPolicyError,
refetchManagedAgents,
refetchOwnerProfiles,
sharedChannelIds,
],
);

View File

@@ -5,7 +5,6 @@ import {
useRelayAgentsQuery,
useTeamsQuery,
} from "@/features/agents/hooks";
import { useAgentAccessOwnerOnlyQuery } from "@/features/agents/useAgentAccessOwnerOnly";
import {
useChannelMembersQuery,
useChannelsQuery,
@@ -101,7 +100,6 @@ export function useMentions(
const channelsQuery = useChannelsQuery();
const personasQuery = usePersonasQuery();
const teamsQuery = useTeamsQuery();
const agentAccessOwnerOnlyQuery = useAgentAccessOwnerOnlyQuery();
const managedAgentDirectoryReady =
managedAgentsQuery.data !== undefined &&
managedAgentsQuery.error === null &&
@@ -110,12 +108,8 @@ export function useMentions(
relayAgentsQuery.data !== undefined &&
relayAgentsQuery.error === null &&
!relayAgentsQuery.isFetching;
const ownerPolicyReady =
agentAccessOwnerOnlyQuery.data !== undefined &&
agentAccessOwnerOnlyQuery.error === null &&
!agentAccessOwnerOnlyQuery.isFetching;
const agentDirectoriesReady =
managedAgentDirectoryReady && relayAgentDirectoryReady && ownerPolicyReady;
managedAgentDirectoryReady && relayAgentDirectoryReady;
const canSearchGlobalUsers = canSearchGlobalPeople && agentDirectoriesReady;
const userSearchQuery = useInfiniteUserSearchQuery(mentionQuery ?? "", {
allowEmpty: true,
@@ -256,16 +250,12 @@ export function useMentions(
if (
shouldHideAgentFromMentions({
isAgent: candidate.isAgent === true,
isManagedAgent: candidate.isManagedAgent === true,
pubkey,
ownerPubkey: candidate.ownerPubkey,
currentPubkey,
mentionableAgentPubkeys,
directoryReady:
candidate.isManagedAgent === true
? managedAgentDirectoryReady
: relayAgentDirectoryReady,
ownerOnly: agentAccessOwnerOnlyQuery.data,
})
) {
return;
@@ -416,7 +406,6 @@ export function useMentions(
}, [
activePersonaById,
activePersonas,
agentAccessOwnerOnlyQuery.data,
userSearchResults,
canSearchGlobalUsers,
currentPubkey,
@@ -825,8 +814,6 @@ export function useMentions(
? { type: "channel", channelId: mentionChannelId }
: { type: "managed-only" },
sharedChannelIds,
ownerOnly: agentAccessOwnerOnlyQuery.data,
ownerPolicyError: agentAccessOwnerOnlyQuery.error,
refetchManagedAgents: managedAgentsQuery.refetch,
});

View File

@@ -1566,7 +1566,9 @@ test("selected relay agents revoked during send emit no p tag", async ({
.not.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
});
test("owner-only builds hide other-owned relay agents", async ({ page }) => {
test("owner-only builds admit cross-owner relay agents authorized by allowlist", async ({
page,
}) => {
await installMockBridge(page, {
ownerOnlyAccessBuild: true,
searchProfiles: [
@@ -1580,6 +1582,7 @@ test("owner-only builds hide other-owned relay agents", async ({ page }) => {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
@@ -1589,9 +1592,35 @@ test("owner-only builds hide other-owned relay agents", async ({ page }) => {
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.getByTestId("message-input").fill("@quinn");
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
},
{
channelId: GENERAL_CHANNEL_ID,
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
},
);
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await page.keyboard.type("hello");
await page.getByTestId("send-message").click();
await expect(autocomplete(page)).toHaveCount(0);
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
});
test("owner-only builds show verified same-owner relay agents", async ({
@@ -1647,13 +1676,23 @@ test("relay-only allowlisted agents stay hidden outside their channel", async ({
await expect(autocomplete(page)).toHaveCount(0);
});
test("relay-only anyone agents are visible when a channel is shared", async ({
test("owner-only builds admit cross-owner relay agents authorized for anyone", async ({
page,
}) => {
await installMockBridge(page, {
ownerOnlyAccessBuild: true,
searchProfiles: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
displayName: "quinn",
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
isAgent: true,
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
name: "quinn",
respondTo: "anyone",
channelNames: ["general"],