List what an agent uses, and whose access each runs with, in its share dialog

The agent share dialog gains a collapsed "What this agent uses" section:
each attached tool, source and prompt, and a workflow agent's node tools
and sources, with whose access it runs with (your access, a sponsor's, the
owner's, your or someone's service account, or each person's own).
Stopped items are marked with why they stopped, and the section opens by
itself when one did. Tools with writes on stored credentials that aren't
in the API write allowlist are marked, with a note that API, widget and
public-link users can't make those changes until they're allowed in
Access Details. Only owners and editors get the data it reads.

A tool's share dialog uses the same words: "Your account" or "Each
person's own", and "The owner's account" for an editor who may share it.
This commit is contained in:
arc53-machine committed 2026-09-29 17:33:41 +01:00
1 parent 1945c312dc
commit 05bac6dc7f
13 files changed
+797 -55

No files matched your search

@@ -0,0 +1,314 @@
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
vi.mock('react-redux', () => ({
useSelector: (selector: (s: unknown) => unknown) =>
selector({ preference: { token: 'tok' } }),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, opts?: Record<string, unknown>) => {
if (!opts) return key;
const params = Object.entries(opts)
.filter(([k]) => k !== 'defaultValue' && k !== 'interpolation')
.map(([k, v]) => `${k}=${v}`)
.join(',');
return params ? `${key}(${params})` : key;
},
i18n: { language: 'en' },
}),
}));
const getAgent = vi.fn();
const getWorkflow = vi.fn();
vi.mock('../../api/services/userService', () => ({
default: {
getAgent: (...a: unknown[]) => getAgent(...a),
getWorkflow: (...a: unknown[]) => getWorkflow(...a),
},
}));
import type { Agent, ResourceState } from '../types';
import AgentUsesSection from './AgentUsesSection';
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
const K = 'settings.teams.share.uses';
const ok = (body: unknown) => ({ ok: true, json: async () => body });
const item = (over: Partial<ResourceState>): ResourceState => ({
key: `${over.type ?? 'tool'}:${over.id ?? 't1'}`,
type: 'tool',
id: 't1',
name: 'Item',
state: 'active',
reason: null,
...over,
});
const agentWith = (
states: ResourceState[],
over: Partial<Agent> = {},
): Agent => ({
id: 'a1',
name: 'A',
description: '',
image: '',
source: '',
chunks: '6',
retriever: '',
prompt_id: '',
tools: [],
agent_type: 'classic',
status: 'published',
access: 'owner',
allowed_actions: ['edit', 'share', 'use'],
resource_states: states,
...over,
});
const flush = async () => {
for (let i = 0; i < 6; i += 1) {
await act(async () => {
await Promise.resolve();
});
}
};
describe('AgentUsesSection', () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
getAgent.mockReset();
getWorkflow.mockReset();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
const render = async (agent: Agent | null, readerId = 'me') => {
getAgent.mockResolvedValue(agent ? ok(agent) : { ok: false });
act(() => {
root.render(<AgentUsesSection agentId="a1" readerId={readerId} />);
});
await flush();
};
const toggle = () =>
Array.from(container.querySelectorAll('button')).find((b) =>
b.textContent?.includes(`${K}.title`),
);
const open = async () => {
const button = toggle();
if (button?.getAttribute('aria-expanded') === 'false') {
act(() => button.click());
await flush();
}
};
const rowOf = (name: string) =>
Array.from(container.querySelectorAll('li')).find((li) =>
li.textContent?.includes(name),
);
it('starts collapsed and lists what the agent uses when opened', async () => {
await render(agentWith([item({ name: 'Docs', type: 'source', id: 's1' })]));
expect(toggle()?.getAttribute('aria-expanded')).toBe('false');
expect(rowOf('Docs')).toBeUndefined();
await open();
expect(toggle()?.getAttribute('aria-expanded')).toBe('true');
expect(rowOf('Docs')?.textContent).toContain(`${K}.access.you`);
});
it('names whose access each item runs with, for the owner', async () => {
await render(
agentWith([
item({ id: 't1', name: 'Mine' }),
item({
id: 't2',
name: 'Bobs',
sponsor: { user_id: 'bob', label: 'bob@example.com' },
}),
item({
id: 't3',
name: 'Slack tool',
credential_mode: 'member',
connection: { id: 'c1', connector_key: 'slack', name: 'Slack' },
}),
item({
id: 't4',
name: 'Notion tool',
credential_mode: 'owner',
account: { user_id: 'me', label: 'me@example.com' },
connection: { id: 'c2', connector_key: 'notion', name: 'Notion' },
}),
item({
id: 't5',
name: 'Jira tool',
credential_mode: 'owner',
account: { user_id: 'carol', label: 'carol@example.com' },
connection: { id: 'c3', connector_key: 'jira', name: 'Jira' },
}),
item({ type: 'prompt', id: 'p1', name: 'Tone' }),
]),
);
await open();
expect(rowOf('Mine')?.textContent).toContain(`${K}.access.you`);
expect(rowOf('Bobs')?.textContent).toContain(
`${K}.access.person(person=bob@example.com)`,
);
expect(rowOf('Slack tool')?.textContent).toContain(
`${K}.access.member(service=Slack)`,
);
expect(rowOf('Notion tool')?.textContent).toContain(
`${K}.access.yourAccount(service=Notion)`,
);
expect(rowOf('Jira tool')?.textContent).toContain(
`${K}.access.personAccount(person=carol@example.com,service=Jira)`,
);
expect(rowOf('Tone')?.textContent).toContain(`${K}.access.you`);
});
it("says the owner's access to an editor, and theirs where they sponsor", async () => {
await render(
agentWith(
[
item({ id: 't1', name: 'Owners' }),
item({
id: 't2',
name: 'Editors',
sponsor: { user_id: 'me', label: 'me@example.com' },
}),
],
{ access: 'editor', allowed_actions: ['edit', 'share', 'use'] },
),
);
await open();
expect(rowOf('Owners')?.textContent).toContain(`${K}.access.owner`);
expect(rowOf('Editors')?.textContent).toContain(`${K}.access.you`);
});
it('opens by itself and marks a stopped item with why it stopped', async () => {
await render(
agentWith([
item({ id: 't1', name: 'Gone', state: 'stopped', reason: 'deleted' }),
item({ id: 't2', name: 'Fine' }),
]),
);
expect(toggle()?.getAttribute('aria-expanded')).toBe('true');
const gone = rowOf('Gone');
expect(gone?.textContent).toContain(`${K}.stopped`);
expect(gone?.textContent).toContain(
'agents.form.resourceStates.reason.deleted(name=Gone',
);
expect(gone?.textContent).not.toContain(`${K}.access.you`);
expect(rowOf('Fine')?.textContent).not.toContain(`${K}.stopped`);
});
it('flags writes outside callers cannot make until they are allowed', async () => {
await render(
agentWith(
[
item({
id: 't1',
name: 'Blocked',
owner_credential_writes: ['send', 'delete'],
}),
item({
id: 't2',
name: 'Allowed',
owner_credential_writes: ['send'],
}),
item({ id: 't3', name: 'Reads', owner_credential_writes: [] }),
],
{ config: { api_write_allowlist: ['t1:send', 't2:send'] } },
),
);
await open();
expect(rowOf('Blocked')?.textContent).toContain(`${K}.writesOff`);
expect(rowOf('Allowed')?.textContent).not.toContain(`${K}.writesOff`);
expect(rowOf('Reads')?.textContent).not.toContain(`${K}.writesOff`);
const alert = container.querySelector('[data-slot="alert"]');
expect(alert?.textContent).toContain(`${K}.writesNote`);
});
it('tells an editor the owner allows the writes', async () => {
await render(
agentWith(
[item({ id: 't1', name: 'Blocked', owner_credential_writes: ['x'] })],
{ access: 'editor', allowed_actions: ['edit', 'share'] },
),
);
await open();
expect(
container.querySelector('[data-slot="alert"]')?.textContent,
).toContain(`${K}.writesNoteEditor`);
});
it('shows no writes note when every write is allowed', async () => {
await render(
agentWith([
item({ id: 't1', name: 'Plain', owner_credential_writes: [] }),
]),
);
await open();
expect(container.querySelector('[data-slot="alert"]')).toBeNull();
});
it('includes the resources of a workflow agent’s nodes', async () => {
getWorkflow.mockResolvedValue(
ok({
success: true,
data: {
resource_states: [
item({ id: 'n1', name: 'Node tool' }),
item({ id: 't1', name: 'Shared tool' }),
],
},
}),
);
await render(
agentWith([item({ id: 't1', name: 'Shared tool' })], {
agent_type: 'workflow',
workflow: 'w1',
}),
);
await open();
expect(getWorkflow).toHaveBeenCalledWith('w1', 'tok');
expect(rowOf('Node tool')).toBeDefined();
expect(
Array.from(container.querySelectorAll('li')).filter((li) =>
li.textContent?.includes('Shared tool'),
),
).toHaveLength(1);
});
it('renders nothing for someone who may not edit the agent', async () => {
await render(
agentWith([item({ name: 'Hidden' })], {
access: 'viewer',
allowed_actions: ['use'],
}),
);
expect(container.innerHTML).toBe('');
});
it('renders nothing when the agent uses nothing listed', async () => {
await render(agentWith([]));
expect(container.innerHTML).toBe('');
});
it('renders nothing when the agent fails to load', async () => {
await render(null);
expect(container.innerHTML).toBe('');
});
});
@@ -0,0 +1,240 @@
import {
ChevronRight,
Database,
ScrollText,
TriangleAlert,
Wrench,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ListRow, ListRows } from '@/components/ui/list-row';
import { cn } from '@/lib/utils';
import userService from '../../api/services/userService';
import { selectToken } from '../../preferences/preferenceSlice';
import { can, isOwner } from '../../utils/accessUtils';
import type { Agent, ResourceState } from '../types';
import { reasonKey } from './ResourceStatusNotice';
type AgentUsesSectionProps = {
/** The agent the share dialog is for. */
agentId: string;
/** The reader's user id, to say "your" for what runs as them. */
readerId?: string;
};
const TYPE_ICONS: Record<ResourceState['type'], typeof Wrench> = {
tool: Wrench,
source: Database,
prompt: ScrollText,
};
const K = 'settings.teams.share.uses';
const plain = { interpolation: { escapeValue: false } };
/** The agent's own items, then its workflow nodes' ones it doesn't have. */
function mergeStates(
own: ResourceState[],
nodes: ResourceState[],
): ResourceState[] {
const seen = new Set(own.map((item) => item.key.toLowerCase()));
return [...own, ...nodes.filter((item) => !seen.has(item.key.toLowerCase()))];
}
/** Write actions outside callers can't take: not in the API write allowlist. */
function blockedWrites(item: ResourceState, allowlist: string[]): string[] {
const allowed = new Set(allowlist.map((entry) => entry.toLowerCase()));
return (item.owner_credential_writes ?? []).filter(
(action) => !allowed.has(`${item.id}:${action}`.toLowerCase()),
);
}
/**
* "What this agent uses" in the agent's share dialog: each attached tool,
* source and prompt (and a workflow agent's node tools and sources), with
* whose access it runs with for the people the agent is shared with.
*
* Built from `resource_states` on the agent read (and the workflow read),
* which only owners and editors get; the section is hidden from anyone else,
* and while it loads or when it fails. A stopped item says why. A tool whose
* writes act on stored credentials and aren't in the API write allowlist is
* marked, since API, widget and public-link users can't make those changes.
*/
export default function AgentUsesSection({
agentId,
readerId,
}: AgentUsesSectionProps) {
const { t } = useTranslation();
const token = useSelector(selectToken);
const [loaded, setLoaded] = useState<{
agent: Agent;
items: ResourceState[];
} | null>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
let cancelled = false;
setLoaded(null);
setOpen(false);
const load = async () => {
let agent: Agent;
try {
const response = await userService.getAgent(agentId, token);
if (!response.ok) return;
agent = await response.json();
} catch {
return;
}
let items = agent.resource_states ?? [];
if (agent.agent_type === 'workflow' && agent.workflow) {
try {
const response = await userService.getWorkflow(agent.workflow, token);
if (response.ok) {
const body = await response.json();
items = mergeStates(items, body?.data?.resource_states ?? []);
}
} catch {
// The agent's own items still show.
}
}
if (cancelled) return;
setLoaded({ agent, items });
// Like Access settings: open when there is something to look at.
if (items.some((item) => item.state === 'stopped')) setOpen(true);
};
void load();
return () => {
cancelled = true;
};
}, [agentId, token]);
if (!loaded || !can(loaded.agent, 'edit') || loaded.items.length === 0)
return null;
const { agent, items } = loaded;
const ownerReads = isOwner(agent);
// Without a user id (authentication off) the one local user is everyone.
const isYou = (userId: string) =>
readerId ? userId === readerId : ownerReads;
const allowlist = agent.config?.api_write_allowlist ?? [];
const nameOf = (item: ResourceState) =>
item.name || t('agents.form.sponsors.unknownItem');
const accessLabel = (item: ResourceState): string => {
const service = item.connection?.name;
if (item.credential_mode === 'member')
return service
? t(`${K}.access.member`, { ...plain, service })
: t(`${K}.access.memberNoService`);
if (item.credential_mode === 'owner' && item.account) {
if (isYou(item.account.user_id))
return service
? t(`${K}.access.yourAccount`, { ...plain, service })
: t(`${K}.access.yourAccountNoService`);
return service
? t(`${K}.access.personAccount`, {
...plain,
person: item.account.label,
service,
})
: t(`${K}.access.personAccountNoService`, {
...plain,
person: item.account.label,
});
}
if (item.sponsor)
return isYou(item.sponsor.user_id)
? t(`${K}.access.you`)
: t(`${K}.access.person`, { ...plain, person: item.sponsor.label });
return ownerReads ? t(`${K}.access.you`) : t(`${K}.access.owner`);
};
const stoppedText = (item: ResourceState) =>
t(reasonKey(item.reason, ownerReads), {
...plain,
name: nameOf(item),
service:
item.connection?.name ||
t('agents.form.resourceStates.serviceFallback'),
person: item.sponsor?.label || item.sponsor?.user_id,
});
const anyBlocked = items.some(
(item) =>
item.state === 'active' && blockedWrites(item, allowlist).length > 0,
);
return (
<section className="flex flex-col gap-3">
{/* The same inline disclosure as Access settings. */}
<Button
type="button"
variant="link"
size="sm"
aria-expanded={open}
className="-ml-3 w-fit justify-start"
onClick={() => setOpen((value) => !value)}
>
<ChevronRight
aria-hidden="true"
className={cn(
'transition-transform duration-200',
open && 'rotate-90',
)}
/>
{t(`${K}.title`)}
</Button>
{open && (
<>
<p className="text-muted-foreground text-xs">{t(`${K}.intro`)}</p>
<Card variant="outline" padding="none" className="overflow-hidden">
<ListRows>
{items.map((item) => {
const Icon = TYPE_ICONS[item.type] ?? Wrench;
const stopped = item.state === 'stopped';
const description = stopped
? stoppedText(item)
: accessLabel(item);
const writesOff =
!stopped && blockedWrites(item, allowlist).length > 0;
return (
<ListRow
key={item.key}
leading={
<span className="bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md">
<Icon className="size-4" aria-hidden="true" />
</span>
}
title={<span title={nameOf(item)}>{nameOf(item)}</span>}
description={<span title={description}>{description}</span>}
trailing={
stopped ? (
<Badge variant="warning">{t(`${K}.stopped`)}</Badge>
) : writesOff ? (
<Badge variant="warning">{t(`${K}.writesOff`)}</Badge>
) : null
}
/>
);
})}
</ListRows>
</Card>
{anyBlocked && (
<Alert variant="warning">
<TriangleAlert />
<AlertDescription>
{ownerReads ? t(`${K}.writesNote`) : t(`${K}.writesNoteEditor`)}
</AlertDescription>
</Alert>
)}
</>
)}
</section>
);
}
@@ -38,7 +38,10 @@ type ResourceStatusNoticeProps = {
};
/** The message key that says why an item stopped. */
function reasonKey(reason: ResourceStateReason | null, ownerReads: boolean) {
export function reasonKey(
reason: ResourceStateReason | null,
ownerReads: boolean,
) {
switch (reason) {
case 'deleted':
return 'agents.form.resourceStates.reason.deleted';
+7 -1
View File
@@ -61,12 +61,18 @@ export type ResourceState = {
sponsor?: ResourcePerson | null;
/** Someone other than the reader who can fix it. */
contact?: ResourcePerson | null;
/** The service, for a connection reason. */
/** The service of a connected tool, or of one a connection reason stopped. */
connection?: {
id: string | null;
connector_key: string | null;
name: string | null;
} | null;
/** A running connected tool's mode: the owner's account or each person's own. */
credential_mode?: 'owner' | 'member' | null;
/** Whose account an owner-mode connection acts as. */
account?: ResourcePerson | null;
/** Its write actions on credentials its owner stored (the API write allowlist's). */
owner_credential_writes?: string[];
/** The reader may run it with their access by confirming on a save. */
can_confirm?: boolean;
/** The reader owns the connection that needs signing in again. */
+27 -7
View File
@@ -762,6 +762,25 @@
"teams": "Teams",
"people": "Personen",
"editors": "Bearbeiter"
},
"uses": {
"title": "Was dieser Agent nutzt",
"intro": "Alle, die diesen Agenten nutzen, erhalten diese Elemente, jeweils mit dem angezeigten Zugriff.",
"stopped": "Gestoppt",
"writesOff": "Keine API-Änderungen",
"writesNote": "API-, Widget- und Link-Nutzer können mit Elementen, die mit „Keine API-Änderungen“ markiert sind, nichts ändern, bis du es unter Zugangsdaten erlaubst.",
"writesNoteEditor": "API-, Widget- und Link-Nutzer können mit Elementen, die mit „Keine API-Änderungen“ markiert sind, nichts ändern, bis der Eigentümer es unter Zugangsdaten erlaubt.",
"access": {
"you": "Dein Zugriff",
"person": "Zugriff von {{person}}",
"owner": "Zugriff des Eigentümers",
"member": "Das eigene {{service}}-Konto jeder Person",
"memberNoService": "Das eigene Konto jeder Person",
"yourAccount": "Dein {{service}}-Konto",
"yourAccountNoService": "Dein Konto",
"personAccount": "{{service}}-Konto von {{person}}",
"personAccountNoService": "Konto von {{person}}"
}
}
},
"memberCount_one": "{{formatted}} Mitglied",
@@ -816,7 +835,7 @@
"viewers_can_use_in_agents": {
"label": "Betrachter dürfen es in eigenen Agenten nutzen",
"description": "Es läuft mit deinen Anmeldedaten.",
"descriptionMember": "Jedes Mitglied nutzt sein eigenes Konto."
"descriptionMember": "Jede Person nutzt ihr eigenes Konto."
}
},
"prompt": {
@@ -1537,18 +1556,19 @@
"defaultKey": "Ein Admin muss ENCRYPTION_SECRET_KEY setzen, bevor Dienste verbunden werden können."
},
"sharing": {
"owner": "Teammitglieder nutzen mein Konto",
"member": "Jedes Mitglied verbindet sein eigenes Konto",
"ownerShort": "Mein Konto",
"memberShort": "Jeweils eigenes"
"owner": "Alle nutzen dein Konto",
"member": "Jede Person verbindet ihr eigenes Konto",
"ownerShort": "Dein Konto",
"memberShort": "Jeweils eigenes",
"ownerShortShared": "Konto des Eigentümers"
},
"share": {
"heading": "Welches Konto Teammitglieder nutzen",
"heading": "Mit wessen Konto es läuft",
"ownerWarning": "Teammitglieder handeln in {{name}} als {{account}}.",
"ownerWarningShared": "Teammitglieder handeln mit dem {{name}}-Konto des Eigentümers.",
"confirmWrite": "Ich verstehe, dass Teammitglieder mit meinem Konto Aktionen ausführen können.",
"confirmWriteShared": "Ich verstehe, dass Teammitglieder mit dem {{name}}-Konto des Eigentümers Aktionen ausführen können.",
"memberNote": "Jedes Mitglied verbindet sein eigenes {{name}}-Konto, bevor es dieses Werkzeug nutzen kann.",
"memberNote": "Jede Person nutzt ihr eigenes {{name}}-Konto und verbindet es, wenn sie dieses Werkzeug zum ersten Mal nutzt.",
"forced": "Ein Admin hat das für alle Freigaben dieses Konnektors festgelegt.",
"ownerChooses": "Nur der Eigentümer kann das ändern.",
"saveFailed": "Die Änderung konnte nicht gespeichert werden. Versuche es erneut."
+27 -7
View File
@@ -768,6 +768,25 @@
"teams": "Teams",
"people": "People",
"editors": "Editors"
},
"uses": {
"title": "What this agent uses",
"intro": "Everyone who uses this agent gets these, each with the access shown.",
"stopped": "Stopped",
"writesOff": "No API changes",
"writesNote": "API, widget and public-link users can't make changes with items marked “No API changes” until you allow them in Access Details.",
"writesNoteEditor": "API, widget and public-link users can't make changes with items marked “No API changes” until the owner allows them in Access Details.",
"access": {
"you": "Your access",
"person": "{{person}}'s access",
"owner": "The owner's access",
"member": "Each person's own {{service}} account",
"memberNoService": "Each person's own account",
"yourAccount": "Your {{service}} account",
"yourAccountNoService": "Your account",
"personAccount": "{{person}}'s {{service}} account",
"personAccountNoService": "{{person}}'s account"
}
}
},
"memberCount_one": "{{formatted}} member",
@@ -822,7 +841,7 @@
"viewers_can_use_in_agents": {
"label": "Viewers can use it in their own agents",
"description": "It runs with your credentials.",
"descriptionMember": "Each member uses their own account."
"descriptionMember": "Each person uses their own account."
}
},
"prompt": {
@@ -1543,18 +1562,19 @@
"defaultKey": "An admin must set ENCRYPTION_SECRET_KEY before services can be connected."
},
"sharing": {
"owner": "Team members use my account",
"member": "Each member connects their own account",
"ownerShort": "My account",
"memberShort": "Each member's own"
"owner": "Everyone uses your account",
"member": "Each person connects their own account",
"ownerShort": "Your account",
"memberShort": "Each person's own",
"ownerShortShared": "The owner's account"
},
"share": {
"heading": "Whose account team members use",
"heading": "Whose account it runs with",
"ownerWarning": "Team members will act as {{account}} on {{name}}.",
"ownerWarningShared": "Team members will act as the owner's {{name}} account.",
"confirmWrite": "I understand teammates can take actions with my account.",
"confirmWriteShared": "I understand teammates can take actions with the owner's {{name}} account.",
"memberNote": "Each member connects their own {{name}} account before they can use this tool.",
"memberNote": "Each person uses their own {{name}} account, and connects it the first time they use this tool.",
"forced": "An admin chose this for every share of this connector.",
"ownerChooses": "Only the owner can change this.",
"saveFailed": "Could not save the change. Try again."
+27 -7
View File
@@ -762,6 +762,25 @@
"teams": "Equipos",
"people": "Personas",
"editors": "Editores"
},
"uses": {
"title": "Qué usa este agente",
"intro": "Todos los que usan este agente reciben estos elementos, cada uno con el acceso indicado.",
"stopped": "Detenido",
"writesOff": "Sin cambios por API",
"writesNote": "Los usuarios de la API, el widget y el enlace público no pueden hacer cambios con los elementos marcados «Sin cambios por API» hasta que los permitas en Access Details.",
"writesNoteEditor": "Los usuarios de la API, el widget y el enlace público no pueden hacer cambios con los elementos marcados «Sin cambios por API» hasta que el propietario los permita en Access Details.",
"access": {
"you": "Tu acceso",
"person": "Acceso de {{person}}",
"owner": "Acceso del propietario",
"member": "La propia cuenta de {{service}} de cada persona",
"memberNoService": "La propia cuenta de cada persona",
"yourAccount": "Tu cuenta de {{service}}",
"yourAccountNoService": "Tu cuenta",
"personAccount": "Cuenta de {{service}} de {{person}}",
"personAccountNoService": "Cuenta de {{person}}"
}
}
},
"memberCount_one": "{{formatted}} miembro",
@@ -816,7 +835,7 @@
"viewers_can_use_in_agents": {
"label": "Los lectores pueden usarla en sus propios agentes",
"description": "Se ejecuta con tus credenciales.",
"descriptionMember": "Cada miembro usa su propia cuenta."
"descriptionMember": "Cada persona usa su propia cuenta."
}
},
"prompt": {
@@ -1537,18 +1556,19 @@
"defaultKey": "Un administrador debe definir ENCRYPTION_SECRET_KEY antes de poder conectar servicios."
},
"sharing": {
"owner": "Los miembros usan mi cuenta",
"member": "Cada miembro conecta su propia cuenta",
"ownerShort": "Mi cuenta",
"memberShort": "La de cada miembro"
"owner": "Todos usan tu cuenta",
"member": "Cada persona conecta su propia cuenta",
"ownerShort": "Tu cuenta",
"memberShort": "La de cada persona",
"ownerShortShared": "La cuenta del propietario"
},
"share": {
"heading": "Qué cuenta usan los miembros del equipo",
"heading": "Con qué cuenta se ejecuta",
"ownerWarning": "Los miembros del equipo actuarán como {{account}} en {{name}}.",
"ownerWarningShared": "Los miembros del equipo actuarán con la cuenta de {{name}} del propietario.",
"confirmWrite": "Entiendo que los miembros del equipo pueden realizar acciones con mi cuenta.",
"confirmWriteShared": "Entiendo que los miembros del equipo pueden realizar acciones con la cuenta de {{name}} del propietario.",
"memberNote": "Cada miembro conecta su propia cuenta de {{name}} antes de poder usar esta herramienta.",
"memberNote": "Cada persona usa su propia cuenta de {{name}} y la conecta la primera vez que usa esta herramienta.",
"forced": "Un administrador eligió esto para todos los recursos compartidos de este conector.",
"ownerChooses": "Solo el propietario puede cambiar esto.",
"saveFailed": "No se pudo guardar el cambio. Inténtalo de nuevo."
+27 -7
View File
@@ -761,6 +761,25 @@
"teams": "チーム",
"people": "ユーザー",
"editors": "編集者"
},
"uses": {
"title": "このエージェントが使うもの",
"intro": "このエージェントを使うすべての人が、表示されたアクセスでこれらを使います。",
"stopped": "停止中",
"writesOff": "API から変更不可",
"writesNote": "API、ウィジェット、公開リンクの利用者は、「API から変更不可」の項目で変更を行えません。Access Details で許可すると行えます。",
"writesNoteEditor": "API、ウィジェット、公開リンクの利用者は、「API から変更不可」の項目で変更を行えません。オーナーが Access Details で許可すると行えます。",
"access": {
"you": "あなたのアクセス",
"person": "{{person}} のアクセス",
"owner": "オーナーのアクセス",
"member": "各自の {{service}} アカウント",
"memberNoService": "各自のアカウント",
"yourAccount": "あなたの {{service}} アカウント",
"yourAccountNoService": "あなたのアカウント",
"personAccount": "{{person}} の {{service}} アカウント",
"personAccountNoService": "{{person}} のアカウント"
}
}
},
"memberCount_one": "{{formatted}}人のメンバー",
@@ -815,7 +834,7 @@
"viewers_can_use_in_agents": {
"label": "閲覧者が自分のエージェントで使える",
"description": "あなたの認証情報で実行されます。",
"descriptionMember": "各メンバーが自分のアカウントを使います。"
"descriptionMember": "各自が自分のアカウントを使います。"
}
},
"prompt": {
@@ -1527,18 +1546,19 @@
"defaultKey": "サービスを接続する前に、管理者が ENCRYPTION_SECRET_KEY を設定する必要があります。"
},
"sharing": {
"owner": "メンバーは自分のアカウントを使う",
"member": "各メンバーが自分のアカウントを接続する",
"ownerShort": "自分のアカウント",
"memberShort": "各メンバーのもの"
"owner": "全員があなたのアカウントを使う",
"member": "各自が自分のアカウントを接続する",
"ownerShort": "あなたのアカウント",
"memberShort": "各自のもの",
"ownerShortShared": "オーナーのアカウント"
},
"share": {
"heading": "チームメンバーが使うアカウント",
"heading": "実行に使うアカウント",
"ownerWarning": "チームメンバーは {{name}} で {{account}} として操作します。",
"ownerWarningShared": "チームメンバーは所有者の {{name}} アカウントで操作します。",
"confirmWrite": "チームメンバーが自分のアカウントで操作を行えることを理解しました。",
"confirmWriteShared": "チームメンバーが所有者の {{name}} アカウントで操作を行えることを理解しました。",
"memberNote": "各メンバーは、このツールを使う前に自分の {{name}} アカウントを接続します。",
"memberNote": "各自が自分の {{name}} アカウントを使い、このツールを初めて使うときに接続します。",
"forced": "このコネクタのすべての共有に対して管理者が設定しています。",
"ownerChooses": "これを変更できるのは所有者だけです。",
"saveFailed": "変更を保存できませんでした。もう一度お試しください。"
+27 -7
View File
@@ -806,6 +806,25 @@
"teams": "Команды",
"people": "Люди",
"editors": "Редакторы"
},
"uses": {
"title": "Что использует этот агент",
"intro": "Все, кто пользуется этим агентом, получают это, каждое — с указанным доступом.",
"stopped": "Остановлено",
"writesOff": "Без изменений через API",
"writesNote": "Пользователи API, виджета и публичной ссылки не могут вносить изменения через элементы с пометкой «Без изменений через API», пока вы не разрешите это в Access Details.",
"writesNoteEditor": "Пользователи API, виджета и публичной ссылки не могут вносить изменения через элементы с пометкой «Без изменений через API», пока владелец не разрешит это в Access Details.",
"access": {
"you": "Ваш доступ",
"person": "Доступ {{person}}",
"owner": "Доступ владельца",
"member": "Собственный аккаунт {{service}} у каждого",
"memberNoService": "Собственный аккаунт у каждого",
"yourAccount": "Ваш аккаунт {{service}}",
"yourAccountNoService": "Ваш аккаунт",
"personAccount": "Аккаунт {{service}} пользователя {{person}}",
"personAccountNoService": "Аккаунт пользователя {{person}}"
}
}
},
"memberCount_one": "{{formatted}} участник",
@@ -864,7 +883,7 @@
"viewers_can_use_in_agents": {
"label": "Читатели могут использовать его в своих агентах",
"description": "Он работает с вашими учётными данными.",
"descriptionMember": "Каждый участник использует свой аккаунт."
"descriptionMember": "Каждый использует свой аккаунт."
}
},
"prompt": {
@@ -1619,18 +1638,19 @@
"defaultKey": "Администратор должен задать ENCRYPTION_SECRET_KEY, прежде чем можно будет подключать сервисы."
},
"sharing": {
"owner": "Участники используют мой аккаунт",
"member": "Каждый участник подключает свой аккаунт",
"ownerShort": "Мой аккаунт",
"memberShort": "У каждого свой"
"owner": "Все используют ваш аккаунт",
"member": "Каждый подключает свой аккаунт",
"ownerShort": "Ваш аккаунт",
"memberShort": "У каждого свой",
"ownerShortShared": "Аккаунт владельца"
},
"share": {
"heading": "Чей аккаунт используют участники команды",
"heading": "С чьим аккаунтом он работает",
"ownerWarning": "Участники команды будут действовать в {{name}} как {{account}}.",
"ownerWarningShared": "Участники команды будут действовать от имени аккаунта {{name}} владельца.",
"confirmWrite": "Я понимаю, что участники команды могут выполнять действия от имени моего аккаунта.",
"confirmWriteShared": "Я понимаю, что участники команды могут выполнять действия от имени аккаунта {{name}} владельца.",
"memberNote": "Каждый участник подключает свой аккаунт {{name}}, прежде чем пользоваться этим инструментом.",
"memberNote": "Каждый использует свой аккаунт {{name}} и подключает его при первом использовании этого инструмента.",
"forced": "Администратор задал это для всех общих доступов к этому коннектору.",
"ownerChooses": "Изменить это может только владелец.",
"saveFailed": "Не удалось сохранить изменение. Попробуйте ещё раз."
+27 -7
View File
@@ -761,6 +761,25 @@
"teams": "團隊",
"people": "人員",
"editors": "編輯者"
},
"uses": {
"title": "此代理使用的內容",
"intro": "使用此代理的每個人都會用到這些內容,各自依所示的存取權限執行。",
"stopped": "已停止",
"writesOff": "API 無法變更",
"writesNote": "在你於 Access Details 中允許之前,API、小工具和公開連結的使用者無法透過標示為「API 無法變更」的項目進行變更。",
"writesNoteEditor": "在擁有者於 Access Details 中允許之前,API、小工具和公開連結的使用者無法透過標示為「API 無法變更」的項目進行變更。",
"access": {
"you": "你的存取權限",
"person": "{{person}} 的存取權限",
"owner": "擁有者的存取權限",
"member": "每個人自己的 {{service}} 帳號",
"memberNoService": "每個人自己的帳號",
"yourAccount": "你的 {{service}} 帳號",
"yourAccountNoService": "你的帳號",
"personAccount": "{{person}} 的 {{service}} 帳號",
"personAccountNoService": "{{person}} 的帳號"
}
}
},
"memberCount_one": "{{formatted}} 位成員",
@@ -815,7 +834,7 @@
"viewers_can_use_in_agents": {
"label": "檢視者可以在自己的代理中使用",
"description": "它會以你的憑證執行。",
"descriptionMember": "每位成員使用自己的帳號。"
"descriptionMember": "每個人使用自己的帳號。"
}
},
"prompt": {
@@ -1527,18 +1546,19 @@
"defaultKey": "管理員必須先設定 ENCRYPTION_SECRET_KEY,才能連線服務。"
},
"sharing": {
"owner": "團隊成員使用我的帳號",
"member": "每位成員連線自己的帳號",
"ownerShort": "我的帳號",
"memberShort": "各自的帳號"
"owner": "所有人使用你的帳號",
"member": "每個人連線自己的帳號",
"ownerShort": "你的帳號",
"memberShort": "各自的帳號",
"ownerShortShared": "擁有者的帳號"
},
"share": {
"heading": "團隊成員使用誰的帳號",
"heading": "使用誰的帳號執行",
"ownerWarning": "團隊成員將在 {{name}} 中以 {{account}} 的身分操作。",
"ownerWarningShared": "團隊成員將以擁有者的 {{name}} 帳號操作。",
"confirmWrite": "我了解團隊成員可以使用我的帳號執行操作。",
"confirmWriteShared": "我了解團隊成員可以使用擁有者的 {{name}} 帳號執行操作。",
"memberNote": "每位成員須先連線自己的 {{name}} 帳號,才能使用此工具。",
"memberNote": "每個人使用自己的 {{name}} 帳號,並在首次使用此工具時連線。",
"forced": "管理員已為此連接器的所有共用設定此項。",
"ownerChooses": "只有擁有者可以變更此項。",
"saveFailed": "無法儲存變更。請再試一次。"
+27 -7
View File
@@ -761,6 +761,25 @@
"teams": "团队",
"people": "人员",
"editors": "编辑者"
},
"uses": {
"title": "此智能体使用的内容",
"intro": "使用此智能体的每个人都会用到这些内容,各自按所示的访问权限运行。",
"stopped": "已停止",
"writesOff": "API 不可更改",
"writesNote": "在你于 Access Details 中允许之前,API、小组件和公开链接的用户无法通过标记为“API 不可更改”的项目进行更改。",
"writesNoteEditor": "在所有者于 Access Details 中允许之前,API、小组件和公开链接的用户无法通过标记为“API 不可更改”的项目进行更改。",
"access": {
"you": "你的访问权限",
"person": "{{person}} 的访问权限",
"owner": "所有者的访问权限",
"member": "每个人自己的 {{service}} 账号",
"memberNoService": "每个人自己的账号",
"yourAccount": "你的 {{service}} 账号",
"yourAccountNoService": "你的账号",
"personAccount": "{{person}} 的 {{service}} 账号",
"personAccountNoService": "{{person}} 的账号"
}
}
},
"memberCount_one": "{{formatted}} 名成员",
@@ -815,7 +834,7 @@
"viewers_can_use_in_agents": {
"label": "查看者可以在自己的代理中使用",
"description": "它使用你的凭据运行。",
"descriptionMember": "每位成员使用自己的账号。"
"descriptionMember": "每个人使用自己的账号。"
}
},
"prompt": {
@@ -1527,18 +1546,19 @@
"defaultKey": "管理员必须先设置 ENCRYPTION_SECRET_KEY,才能连接服务。"
},
"sharing": {
"owner": "团队成员使用我的账号",
"member": "每位成员连接自己的账号",
"ownerShort": "我的账号",
"memberShort": "各自的账号"
"owner": "所有人使用你的账号",
"member": "每个人连接自己的账号",
"ownerShort": "你的账号",
"memberShort": "各自的账号",
"ownerShortShared": "所有者的账号"
},
"share": {
"heading": "团队成员使用谁的账号",
"heading": "使用谁的账号运行",
"ownerWarning": "团队成员将在 {{name}} 中以 {{account}} 的身份操作。",
"ownerWarningShared": "团队成员将以所有者的 {{name}} 账号操作。",
"confirmWrite": "我了解团队成员可以使用我的账号执行操作。",
"confirmWriteShared": "我了解团队成员可以使用所有者的 {{name}} 账号执行操作。",
"memberNote": "每位成员需先连接自己的 {{name}} 账号才能使用此工具。",
"memberNote": "每个人使用自己的 {{name}} 账号,并在首次使用此工具时连接。",
"forced": "管理员已为此连接器的所有共享设定了此项。",
"ownerChooses": "只有所有者可以更改此项。",
"saveFailed": "无法保存更改。请重试。"
+28 -3
View File
@@ -54,6 +54,13 @@ vi.mock('../api/services/connectorsService', () => ({
},
}));
// The agent section loads the agent itself; its own tests cover it.
vi.mock('../agents/components/AgentUsesSection', () => ({
default: ({ agentId }: { agentId: string }) => (
<div data-testid="agent-uses">{agentId}</div>
),
}));
// Mark formatted counts so a raw number in the UI shows up in a test.
vi.mock('../utils/dateTimeUtils', async (importOriginal) => ({
...(await importOriginal<typeof import('../utils/dateTimeUtils')>()),
@@ -138,6 +145,13 @@ describe('ShareToTeamModal', () => {
await flush();
};
it('lists what the agent uses', async () => {
await render();
expect(
body().querySelector('[data-testid="agent-uses"]')?.textContent,
).toBe('a1');
});
describe('access settings', () => {
it('shows a collapsed Access settings toggle to the owner', async () => {
await render();
@@ -421,9 +435,8 @@ describe('ShareToTeamModal credentials', () => {
body().querySelectorAll<HTMLButtonElement>(
'[data-slot="toggle-group-item"]',
),
).find(
(item) =>
item.textContent === `settings.connectors.sharing.${value}Short`,
).find((item) =>
item.textContent?.startsWith(`settings.connectors.sharing.${value}Short`),
)!;
const picker = () =>
body().querySelector<HTMLButtonElement>('[role="combobox"]')!;
@@ -433,8 +446,16 @@ describe('ShareToTeamModal credentials', () => {
expect(text()).not.toContain('settings.connectors.share.heading');
});
it('has no agent resource list for a tool', async () => {
await render(credentials());
expect(body().querySelector('[data-testid="agent-uses"]')).toBeNull();
});
it('says whose account members use on a tool that only reads', async () => {
await render(credentials());
expect(toggle('owner').textContent).toBe(
'settings.connectors.sharing.ownerShort',
);
expect(body().querySelector('[role="note"]')).toBeNull();
expect(body().querySelector('[data-slot="option-card"]')).toBeNull();
expect(toggle('owner').getAttribute('aria-checked')).toBe('true');
@@ -561,6 +582,10 @@ describe('ShareToTeamModal credentials', () => {
it("shows whose account shares use but doesn't let them change it", async () => {
await render(shared());
// The owner's account, not "Your account".
expect(toggle('owner').textContent).toBe(
'settings.connectors.sharing.ownerShortShared',
);
expect(toggle('owner').getAttribute('aria-checked')).toBe('true');
expect(toggle('owner').disabled).toBe(true);
expect(toggle('member').disabled).toBe(true);
+15 -1
View File
@@ -20,6 +20,7 @@ import teamsService, {
TeamMember,
} from '../api/services/teamsService';
import connectorsService from '../api/services/connectorsService';
import AgentUsesSection from '../agents/components/AgentUsesSection';
import SearchInput from '../components/SearchInput';
import { Alert, AlertDescription } from '../components/ui/alert';
import { Checkbox } from '../components/ui/checkbox';
@@ -898,7 +899,13 @@ export default function ShareToTeamModal({
}
>
{mode === 'owner' ? <UserRound /> : <UsersRound />}
{t(`settings.connectors.sharing.${mode}Short`)}
{/* "Your account" to the owner; an editor sees the
owner's, like the agent's "What this agent uses". */}
{t(
mode === 'owner' && credentials.readOnly
? 'settings.connectors.sharing.ownerShortShared'
: `settings.connectors.sharing.${mode}Short`,
)}
</ToggleGroupItem>
))}
</ToggleGroup>
@@ -1129,6 +1136,13 @@ export default function ShareToTeamModal({
</>
)}
{/* Whose access each of the agent's tools, sources and prompt runs
with, for the people it is shared with (owners and editors only:
viewers never open this dialog). */}
{resourceType === 'agent' && (
<AgentUsesSection agentId={resourceId} readerId={currentUserId} />
)}
{accessSettings}
</div>
);