Label who and whose credentials each agent item runs with, and let owners allow every outside write

The share dialog's list now follows who an item runs as now (runs_as),
not a sponsor on record, and names whose saved credentials a tool
without a connection uses. A tool each person connects says API and
widget users get the owner's account, and a tool whose owner the reader
shares no team with says it's someone else's. Badges say whether all or
some of a tool's writes are off for API use, or that an admin turned
changes off. The note names only API and widget users for tools each
person connects, since public-link users use their own account there.

The API write allowlist now builds its choices from the agent's
resource_states, so it offers tools an editor sponsored and a workflow
agent's node tools as well as the owner's own; the owner's tool list only
adds action descriptions. Both read the agent through one shared hook.
This commit is contained in:
arc53-machine committed 2026-09-29 17:49:54 +01:00
1 parent 3fc55dfad8
commit 88e715405e
13 files changed
+584 -204

No files matched your search

+96 -1
View File
@@ -11,10 +11,14 @@ vi.mock('react-i18next', () => ({
}));
const getUserTools = vi.fn();
const getAgent = vi.fn();
const getWorkflow = vi.fn();
const updateAgent = vi.fn();
vi.mock('../api/services/userService', () => ({
default: {
getUserTools: (...args: unknown[]) => getUserTools(...args),
getAgent: (...args: unknown[]) => getAgent(...args),
getWorkflow: (...args: unknown[]) => getWorkflow(...args),
updateAgent: (...args: unknown[]) => updateAgent(...args),
},
}));
@@ -24,7 +28,7 @@ import actionToastReducer, {
} from '../notifications/actionToastSlice';
import { prefSlice } from '../preferences/preferenceSlice';
import ApiWriteAllowlist from './ApiWriteAllowlist';
import type { Agent } from './types';
import type { Agent, ResourceState } from './types';
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
@@ -57,6 +61,58 @@ const TOOLS = {
],
};
// What the agent read says each tool may write on stored credentials: the
// owner's own tools, one an editor sponsored that the owner can't see, and
// one that stopped.
const state = (over: Partial<ResourceState>): ResourceState => ({
key: `tool:${over.id}`,
type: 'tool',
id: 'x',
name: 'Tool',
state: 'active',
reason: null,
owner_credential_writes: [],
...over,
});
const STATES: ResourceState[] = [
state({
id: 'tg',
name: 'Telegram',
owner_credential_writes: ['telegram_send_message'],
}),
state({ id: 'memory', name: 'Memory' }),
state({
id: 'crm',
name: 'CRM API',
owner_credential_writes: ['create_lead'],
}),
state({
id: 'bob-jira',
name: 'Bob Jira',
runs_as: { user_id: 'bob', label: 'bob@example.com' },
owner_credential_writes: ['create_issue'],
}),
state({
id: 'gone',
name: 'Gone',
state: 'stopped',
reason: 'deleted',
owner_credential_writes: ['delete_all'],
}),
];
const readAgent = (tools: string[], extra: Partial<Agent> = {}) => ({
ok: true,
json: async () => ({
id: 'agent-1',
agent_type: 'classic',
tools,
resource_states: STATES.filter((s) => tools.includes(s.id)),
...extra,
}),
});
const agent = (overrides: Partial<Agent> = {}): Agent =>
({
id: 'agent-1',
@@ -79,6 +135,10 @@ describe('ApiWriteAllowlist', () => {
beforeEach(() => {
getUserTools.mockResolvedValue({ json: async () => TOOLS });
getAgent
.mockReset()
.mockImplementation(async () => readAgent(currentTools));
getWorkflow.mockReset();
updateAgent.mockReset();
container = document.createElement('div');
document.body.appendChild(container);
@@ -90,11 +150,13 @@ describe('ApiWriteAllowlist', () => {
container.remove();
});
let currentTools: string[] = [];
const render = async (
a: Agent,
onConfigChange = vi.fn(),
getSavedConfig?: () => Agent['config'],
) => {
currentTools = a.tools ?? [];
store = makeStore();
await act(async () => {
root.render(
@@ -183,6 +245,39 @@ describe('ApiWriteAllowlist', () => {
expect(labels).toEqual(['CRM API: Create lead']);
});
it("lists a sponsor's tool the owner can't see, but not a stopped one", async () => {
await render(agent({ tools: ['bob-jira', 'gone'] }));
const labels = Array.from(container.querySelectorAll('label')).map(
(l) => l.textContent,
);
expect(labels).toEqual(['Bob Jira: Create issue']);
});
it("lists writes of a workflow agent's node tools", async () => {
getAgent.mockResolvedValue(
readAgent([], { agent_type: 'workflow', workflow: 'w1' }),
);
getWorkflow.mockResolvedValue({
ok: true,
json: async () => ({
data: {
resource_states: [
state({
id: 'node-tool',
name: 'Node Slack',
owner_credential_writes: ['post_message'],
}),
],
},
}),
});
await render(agent({ tools: [] }));
const labels = Array.from(container.querySelectorAll('label')).map(
(l) => l.textContent,
);
expect(labels).toEqual(['Node Slack: Post message']);
});
it('renders nothing for an agent without connected tools', async () => {
await render(agent({ tools: ['memory'] }));
expect(container.textContent).toBe('');
+27 -33
View File
@@ -10,6 +10,7 @@ import { SectionHeader } from '../components/ui/section-header';
import { showActionToast } from '../notifications/actionToastSlice';
import { selectToken } from '../preferences/preferenceSlice';
import type { Agent, AgentConfig } from './types';
import useAgentResourceStates from './useAgentResourceStates';
import { actionTitle } from '../connectors/i18n';
type WriteAction = {
@@ -21,16 +22,7 @@ type WriteAction = {
type UserTool = {
id: string;
displayName?: string;
customName?: string;
connection_id?: string | null;
owner_credential_writes?: string[];
actions?: {
name: string;
description?: string;
access?: string;
active?: boolean;
}[];
actions?: { name: string; description?: string }[];
};
/**
@@ -38,7 +30,9 @@ type UserTool = {
* anyone reaching this agent through its API key, widget or its public link
* may run. Nobody there can approve an action for the owner, so
* the server refuses every other such write. The server names these writes
* per tool (`owner_credential_writes`).
* per running tool in the agent's `resource_states` (`owner_credential_writes`),
* which covers tools an editor sponsored and a workflow agent's node tools
* too; the owner's own tool list only adds action descriptions.
*
* A toggle saves at once, on top of the agent's last saved config
* (`getSavedConfig`), so edits still pending in the form are not saved with
@@ -56,47 +50,47 @@ export default function ApiWriteAllowlist({
const { t } = useTranslation();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const [actions, setActions] = useState<WriteAction[]>([]);
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
const [allowed, setAllowed] = useState<string[]>(
agent.config?.api_write_allowlist ?? [],
);
const loaded = useAgentResourceStates(agent.id, (agent.tools ?? []).join());
useEffect(() => {
setAllowed(agent.config?.api_write_allowlist ?? []);
}, [agent.config?.api_write_allowlist]);
// Descriptions of the actions of tools the owner can open themselves.
useEffect(() => {
if (!agent.id || !agent.tools?.length) {
setActions([]);
return;
}
let cancelled = false;
userService
.getUserTools(token)
.then((response: Response) => response.json())
.then((data: { tools?: UserTool[] }) => {
if (cancelled) return;
const agentTools = new Set(agent.tools);
setActions(
(data.tools ?? [])
.filter((tool) => agentTools.has(tool.id))
.flatMap((tool) =>
(tool.owner_credential_writes ?? []).map((name) => ({
entry: `${tool.id}:${name}`,
tool: tool.customName || tool.displayName || '',
action: name,
description:
tool.actions?.find((action) => action.name === name)
?.description ?? '',
})),
),
);
const found: Record<string, string> = {};
for (const tool of data.tools ?? [])
for (const action of tool.actions ?? [])
if (action.description)
found[`${tool.id}:${action.name}`] = action.description;
setDescriptions(found);
})
.catch(() => !cancelled && setActions([]));
.catch(() => undefined);
return () => {
cancelled = true;
};
}, [agent.id, agent.tools, token]);
}, [token]);
const actions: WriteAction[] = (loaded?.items ?? [])
.filter((item) => item.type === 'tool' && item.state === 'active')
.flatMap((item) =>
(item.owner_credential_writes ?? []).map((name) => ({
entry: `${item.id}:${name}`,
tool: item.name || t('agents.form.sponsors.unknownItem'),
action: name,
description: descriptions[`${item.id}:${name}`] ?? '',
})),
);
if (actions.length === 0) return null;
@@ -136,7 +136,7 @@ describe('AgentUsesSection', () => {
item({
id: 't2',
name: 'Bobs',
sponsor: { user_id: 'bob', label: 'bob@example.com' },
runs_as: { user_id: 'bob', label: 'bob@example.com' },
}),
item({
id: 't3',
@@ -166,6 +166,7 @@ describe('AgentUsesSection', () => {
expect(rowOf('Bobs')?.textContent).toContain(
`${K}.access.person(person=bob@example.com)`,
);
// API and widget callers run it as the owner: the owner's account.
expect(rowOf('Slack tool')?.textContent).toContain(
`${K}.access.member(service=Slack)`,
);
@@ -173,7 +174,7 @@ describe('AgentUsesSection', () => {
`${K}.access.yourAccount(service=Notion)`,
);
expect(rowOf('Jira tool')?.textContent).toContain(
`${K}.access.personAccount(person=carol@example.com,service=Jira)`,
`${K}.access.personAccount(service=Jira,person=carol@example.com)`,
);
expect(rowOf('Tone')?.textContent).toContain(`${K}.access.you`);
});
@@ -186,7 +187,13 @@ describe('AgentUsesSection', () => {
item({
id: 't2',
name: 'Editors',
sponsor: { user_id: 'me', label: 'me@example.com' },
runs_as: { user_id: 'me', label: 'me@example.com' },
}),
item({
id: 't3',
name: 'Slack tool',
credential_mode: 'member',
connection: { id: null, connector_key: 'slack', name: 'Slack' },
}),
],
{ access: 'editor', allowed_actions: ['edit', 'share', 'use'] },
@@ -195,6 +202,67 @@ describe('AgentUsesSection', () => {
await open();
expect(rowOf('Owners')?.textContent).toContain(`${K}.access.owner`);
expect(rowOf('Editors')?.textContent).toContain(`${K}.access.you`);
expect(rowOf('Slack tool')?.textContent).toContain(
`${K}.access.memberShared(service=Slack)`,
);
});
it('follows who it runs as now, not a sponsor on record', async () => {
await render(
agentWith([
item({
id: 't1',
name: 'Once sponsored',
sponsor: { user_id: 'bob', label: 'bob@example.com' },
runs_as: null,
}),
]),
);
await open();
expect(rowOf('Once sponsored')?.textContent).toContain(`${K}.access.you`);
expect(rowOf('Once sponsored')?.textContent).not.toContain('bob');
});
it('names whose saved credentials a tool without a connection uses', async () => {
await render(
agentWith([
item({
id: 't1',
name: 'My API',
account: { user_id: 'me', label: 'me@example.com' },
}),
item({
id: 't2',
name: 'Carols API',
account: { user_id: 'carol', label: 'carol@example.com' },
}),
item({
id: 't3',
name: 'Hidden API',
account: { user_id: null, label: null },
}),
item({
id: 't4',
name: 'Hidden Jira',
credential_mode: 'owner',
account: { user_id: null, label: null },
connection: { id: null, connector_key: 'jira', name: 'Jira' },
}),
]),
);
await open();
expect(rowOf('My API')?.textContent).toContain(
`${K}.access.yourCredentials`,
);
expect(rowOf('Carols API')?.textContent).toContain(
`${K}.access.personCredentials(person=carol@example.com)`,
);
expect(rowOf('Hidden API')?.textContent).toContain(
`${K}.access.otherCredentials`,
);
expect(rowOf('Hidden Jira')?.textContent).toContain(
`${K}.access.otherAccount(service=Jira)`,
);
});
it('opens by itself and marks a stopped item with why it stopped', async () => {
@@ -234,11 +302,76 @@ describe('AgentUsesSection', () => {
),
);
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`);
// One of two writes still blocked: some, not all.
expect(rowOf('Blocked')?.textContent).toContain(`${K}.writesSomeOff`);
expect(rowOf('Allowed')?.textContent).not.toContain(`${K}.writes`);
expect(rowOf('Reads')?.textContent).not.toContain(`${K}.writes`);
const alert = container.querySelector('[data-slot="alert"]');
expect(alert?.textContent).toContain(`${K}.writesNote`);
expect(alert?.textContent).not.toContain(`${K}.writesNoteMemberTail`);
});
it('marks a tool with every write blocked', async () => {
await render(
agentWith([
item({ id: 't1', name: 'Blocked', owner_credential_writes: ['a'] }),
]),
);
await open();
expect(rowOf('Blocked')?.textContent).toContain(`${K}.writesOff`);
expect(rowOf('Blocked')?.textContent).not.toContain(`${K}.writesSomeOff`);
});
it('names only API and widget users for tools each person connects', async () => {
await render(
agentWith([
item({
id: 't1',
name: 'Slack tool',
credential_mode: 'member',
connection: { id: null, connector_key: 'slack', name: 'Slack' },
owner_credential_writes: ['send'],
}),
]),
);
await open();
const alert = container.querySelector('[data-slot="alert"]');
expect(alert?.textContent).toBe(`${K}.writesNoteApi`);
});
it('says public-link users use their own account on mixed tools', async () => {
await render(
agentWith([
item({ id: 't1', name: 'Mine', owner_credential_writes: ['a'] }),
item({
id: 't2',
name: 'Slack tool',
credential_mode: 'member',
owner_credential_writes: ['send'],
}),
]),
);
await open();
const alert = container.querySelector('[data-slot="alert"]');
expect(alert?.textContent).toBe(
`${K}.writesNote ${K}.writesNoteMemberTail`,
);
});
it('says when an admin turned changes off', async () => {
await render(
agentWith([
item({
id: 't1',
name: 'GitHub tool',
writes_allowed: false,
owner_credential_writes: [],
}),
]),
);
await open();
expect(rowOf('GitHub tool')?.textContent).toContain(`${K}.adminOff`);
expect(container.querySelector('[data-slot="alert"]')).toBeNull();
});
it('tells an editor the owner allows the writes', async () => {
@@ -5,9 +5,8 @@ import {
TriangleAlert,
Wrench,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { 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';
@@ -16,10 +15,9 @@ 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 type { ResourceState } from '../types';
import useAgentResourceStates from '../useAgentResourceStates';
import { reasonKey } from './ResourceStatusNotice';
type AgentUsesSectionProps = {
@@ -38,15 +36,6 @@ const TYPE_ICONS: Record<ResourceState['type'], typeof Wrench> = {
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()));
@@ -58,66 +47,32 @@ function blockedWrites(item: ResourceState, allowlist: string[]): string[] {
/**
* "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.
* whose access, account or saved credentials 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.
* and while it loads or when it fails. A stopped item says why. A tool with
* writes on stored credentials that aren't in the API write allowlist is
* marked (all or some of them), since API and widget users, and public-link
* users on the owner's accounts, can't make those changes; so is a tool an
* admin allows no changes through.
*/
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]);
const loaded = useAgentResourceStates(agentId);
// Like Access settings: open once by itself when something stopped, then
// follow the reader's clicks.
const [open, setOpen] = useState<boolean | null>(null);
if (!loaded || !can(loaded.agent, 'edit') || loaded.items.length === 0)
return null;
const { agent, items } = loaded;
const expanded = open ?? items.some((item) => item.state === 'stopped');
const ownerReads = isOwner(agent);
// Without a user id (authentication off) the one local user is everyone.
const isYou = (userId: string) =>
@@ -126,32 +81,43 @@ export default function AgentUsesSection({
const nameOf = (item: ResourceState) =>
item.name || t('agents.form.sponsors.unknownItem');
const accessLabel = (item: ResourceState): string => {
// Whose account or saved credentials a tool acts with.
const credentialsLabel = (item: ResourceState): string | null => {
const service = item.connection?.name;
const suffix = service ? '' : 'NoService';
const named = service ? { ...plain, service } : plain;
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)
// API and widget callers run the agent as its owner, so they use the
// owner's own account.
return t(
`${K}.access.member${ownerReads ? '' : 'Shared'}${suffix}`,
named,
);
const account = item.account;
if (!account) return null;
// A connection names its service; saved credentials (an API key, an
// MCP sign-in) have none.
const connected = item.credential_mode === 'owner';
const key = connected ? `Account${suffix}` : 'Credentials';
const opts = connected ? named : plain;
if (!account.user_id) return t(`${K}.access.other${key}`, opts);
if (isYou(account.user_id)) return t(`${K}.access.your${key}`, opts);
return t(`${K}.access.person${key}`, {
...opts,
person: account.label || account.user_id,
});
};
const accessLabel = (item: ResourceState): string => {
const credentials = credentialsLabel(item);
if (credentials) return credentials;
if (item.runs_as)
return isYou(item.runs_as.user_id)
? t(`${K}.access.you`)
: t(`${K}.access.person`, { ...plain, person: item.sponsor.label });
: t(`${K}.access.person`, {
...plain,
person: item.runs_as.label || item.runs_as.user_id,
});
return ownerReads ? t(`${K}.access.you`) : t(`${K}.access.owner`);
};
@@ -165,10 +131,46 @@ export default function AgentUsesSection({
person: item.sponsor?.label || item.sponsor?.user_id,
});
const anyBlocked = items.some(
const badgeFor = (item: ResourceState) => {
if (item.state === 'stopped')
return <Badge variant="warning">{t(`${K}.stopped`)}</Badge>;
if (item.writes_allowed === false)
return <Badge variant="neutral">{t(`${K}.adminOff`)}</Badge>;
const blocked = blockedWrites(item, allowlist).length;
if (blocked === 0) return null;
const all = blocked === (item.owner_credential_writes ?? []).length;
return (
<Badge variant="warning">
{t(all ? `${K}.writesOff` : `${K}.writesSomeOff`)}
</Badge>
);
};
// Public-link users act with their own account on a tool each person
// connects, so only API and widget users are held back there.
const blocked = items.filter(
(item) =>
item.state === 'active' && blockedWrites(item, allowlist).length > 0,
item.state === 'active' &&
item.writes_allowed !== false &&
blockedWrites(item, allowlist).length > 0,
);
const blockedMember = blocked.some(
(item) => item.credential_mode === 'member',
);
const blockedOwned = blocked.some(
(item) => item.credential_mode !== 'member',
);
const editor = ownerReads ? '' : 'Editor';
const writesNote = blockedOwned
? [
t(`${K}.writesNote${editor}`),
blockedMember ? t(`${K}.writesNoteMemberTail`) : '',
]
.filter(Boolean)
.join(' ')
: blockedMember
? t(`${K}.writesNoteApi${editor}`)
: null;
return (
<section className="flex flex-col gap-3">
@@ -177,32 +179,30 @@ export default function AgentUsesSection({
type="button"
variant="link"
size="sm"
aria-expanded={open}
aria-expanded={expanded}
className="-ml-3 w-fit justify-start"
onClick={() => setOpen((value) => !value)}
onClick={() => setOpen(!expanded)}
>
<ChevronRight
aria-hidden="true"
className={cn(
'transition-transform duration-200',
open && 'rotate-90',
expanded && 'rotate-90',
)}
/>
{t(`${K}.title`)}
</Button>
{open && (
{expanded && (
<>
<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;
const description =
item.state === 'stopped'
? stoppedText(item)
: accessLabel(item);
return (
<ListRow
key={item.key}
@@ -213,24 +213,16 @@ export default function AgentUsesSection({
}
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
}
trailing={badgeFor(item)}
/>
);
})}
</ListRows>
</Card>
{anyBlocked && (
{writesNote && (
<Alert variant="warning">
<TriangleAlert />
<AlertDescription>
{ownerReads ? t(`${K}.writesNote`) : t(`${K}.writesNoteEditor`)}
</AlertDescription>
<AlertDescription>{writesNote}</AlertDescription>
</Alert>
)}
</>
+9 -2
View File
@@ -67,12 +67,19 @@ export type ResourceState = {
connector_key: string | null;
name: string | null;
} | null;
/** The live sponsor a running item runs as; null for the owner. */
runs_as?: ResourcePerson | 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;
/**
* Whose saved credentials or owner-mode connection a running tool uses
* (the tool's owner); both null when the reader may not see who.
*/
account?: { user_id: string | null; label: string | null } | null;
/** Its write actions on credentials its owner stored (the API write allowlist's). */
owner_credential_writes?: string[];
/** False when an admin turned off changes through its connector. */
writes_allowed?: boolean;
/** 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. */
@@ -0,0 +1,75 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import { selectToken } from '../preferences/preferenceSlice';
import type { Agent, ResourceState } from './types';
/** The agent as read, and every tool, source and prompt it runs. */
export type AgentResources = { agent: Agent; items: ResourceState[] };
/** The agent's own items, then its workflow nodes' ones it doesn't have. */
export 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()))];
}
/**
* Reads an agent and the run state of what it uses: `resource_states` from
* the agent read, plus the workflow read's for a workflow agent's node
* tools and sources. Only owners and editors get the states; everyone else
* gets an empty list.
*
* Args:
* agentId: The agent to read; nothing is read without one.
* reloadKey: Read again when this changes (the agent's saved tools).
*
* Returns:
* The agent and its items, or null while loading or when the agent
* can't be read. A failed workflow read leaves the agent's own items.
*/
export default function useAgentResourceStates(
agentId: string | undefined,
reloadKey = '',
): AgentResources | null {
const token = useSelector(selectToken);
const [loaded, setLoaded] = useState<AgentResources | null>(null);
useEffect(() => {
let cancelled = false;
setLoaded(null);
if (!agentId) return;
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) setLoaded({ agent, items });
};
void load();
return () => {
cancelled = true;
};
}, [agentId, reloadKey, token]);
return loaded;
}
+20 -8
View File
@@ -767,20 +767,32 @@
"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.",
"writesOff": "API-Änderungen aus",
"writesNote": "API-, Widget- und Link-Nutzer können die hier markierten Änderungen erst vornehmen, wenn du sie unter Zugangsdaten erlaubst.",
"writesNoteEditor": "API-, Widget- und Link-Nutzer können die hier markierten Änderungen erst vornehmen, wenn der Eigentümer sie 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",
"member": "Das eigene {{service}}-Konto jeder Person (API und Widget: deins)",
"memberNoService": "Das eigene Konto jeder Person (API und Widget: deins)",
"yourAccount": "Dein {{service}}-Konto",
"yourAccountNoService": "Dein Konto",
"personAccount": "{{service}}-Konto von {{person}}",
"personAccountNoService": "Konto von {{person}}"
}
"personAccountNoService": "Konto von {{person}}",
"memberShared": "Das eigene {{service}}-Konto jeder Person (API und Widget: das des Eigentümers)",
"memberSharedNoService": "Das eigene Konto jeder Person (API und Widget: das des Eigentümers)",
"otherAccount": "{{service}}-Konto einer anderen Person",
"otherAccountNoService": "Konto einer anderen Person",
"yourCredentials": "Deine gespeicherten Zugangsdaten",
"personCredentials": "Gespeicherte Zugangsdaten von {{person}}",
"otherCredentials": "Gespeicherte Zugangsdaten einer anderen Person"
},
"writesSomeOff": "Einige API-Änderungen aus",
"adminOff": "Änderungen von einem Admin deaktiviert",
"writesNoteApi": "API- und Widget-Nutzer können die hier markierten Änderungen erst vornehmen, wenn du sie unter Zugangsdaten erlaubst.",
"writesNoteApiEditor": "API- und Widget-Nutzer können die hier markierten Änderungen erst vornehmen, wenn der Eigentümer sie unter Zugangsdaten erlaubt.",
"writesNoteMemberTail": "Bei Werkzeugen, bei denen jede Person ihr eigenes Konto nutzt, ändern Link-Nutzer mit ihrem eigenen Konto."
}
},
"memberCount_one": "{{formatted}} Mitglied",
@@ -1568,7 +1580,7 @@
"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": "Jede Person nutzt ihr eigenes {{name}}-Konto und verbindet es, wenn sie dieses Werkzeug zum ersten Mal nutzt.",
"memberNote": "Jede Person nutzt ihr eigenes {{name}}-Konto und verbindet es, wenn sie dieses Werkzeug zum ersten Mal nutzt. Über den API-Schlüssel oder das Widget eines Agenten wird das Konto des Agent-Eigentümers genutzt.",
"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."
+20 -8
View File
@@ -773,20 +773,32 @@
"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.",
"writesOff": "API changes off",
"writesNote": "API, widget and public-link users can't make the changes marked here until you allow them in Access Details.",
"writesNoteEditor": "API, widget and public-link users can't make the changes marked here 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",
"member": "Each person's own {{service}} account (API and widget: yours)",
"memberNoService": "Each person's own account (API and widget: yours)",
"yourAccount": "Your {{service}} account",
"yourAccountNoService": "Your account",
"personAccount": "{{person}}'s {{service}} account",
"personAccountNoService": "{{person}}'s account"
}
"personAccountNoService": "{{person}}'s account",
"memberShared": "Each person's own {{service}} account (API and widget: the owner's)",
"memberSharedNoService": "Each person's own account (API and widget: the owner's)",
"otherAccount": "Someone else's {{service}} account",
"otherAccountNoService": "Someone else's account",
"yourCredentials": "Your saved credentials",
"personCredentials": "{{person}}'s saved credentials",
"otherCredentials": "Someone else's saved credentials"
},
"writesSomeOff": "Some API changes off",
"adminOff": "Changes off by an admin",
"writesNoteApi": "API and widget users can't make the changes marked here until you allow them in Access Details.",
"writesNoteApiEditor": "API and widget users can't make the changes marked here until the owner allows them in Access Details.",
"writesNoteMemberTail": "On tools where each person uses their own account, public-link users make changes with their own account."
}
},
"memberCount_one": "{{formatted}} member",
@@ -1574,7 +1586,7 @@
"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 person uses their own {{name}} account, and connects it the first time they use this tool.",
"memberNote": "Each person uses their own {{name}} account, and connects it the first time they use this tool. Through an agent's API key or widget, it uses the agent owner's account.",
"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."
+20 -8
View File
@@ -767,20 +767,32 @@
"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.",
"writesOff": "Cambios por API desactivados",
"writesNote": "Los usuarios de la API, el widget y el enlace público no pueden hacer los cambios marcados aquí hasta que los permitas en Access Details.",
"writesNoteEditor": "Los usuarios de la API, el widget y el enlace público no pueden hacer los cambios marcados aquí 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",
"member": "La propia cuenta de {{service}} de cada persona (API y widget: la tuya)",
"memberNoService": "La propia cuenta de cada persona (API y widget: la tuya)",
"yourAccount": "Tu cuenta de {{service}}",
"yourAccountNoService": "Tu cuenta",
"personAccount": "Cuenta de {{service}} de {{person}}",
"personAccountNoService": "Cuenta de {{person}}"
}
"personAccountNoService": "Cuenta de {{person}}",
"memberShared": "La propia cuenta de {{service}} de cada persona (API y widget: la del propietario)",
"memberSharedNoService": "La propia cuenta de cada persona (API y widget: la del propietario)",
"otherAccount": "Cuenta de {{service}} de otra persona",
"otherAccountNoService": "Cuenta de otra persona",
"yourCredentials": "Tus credenciales guardadas",
"personCredentials": "Credenciales guardadas de {{person}}",
"otherCredentials": "Credenciales guardadas de otra persona"
},
"writesSomeOff": "Algunos cambios por API desactivados",
"adminOff": "Cambios desactivados por un administrador",
"writesNoteApi": "Los usuarios de la API y el widget no pueden hacer los cambios marcados aquí hasta que los permitas en Access Details.",
"writesNoteApiEditor": "Los usuarios de la API y el widget no pueden hacer los cambios marcados aquí hasta que el propietario los permita en Access Details.",
"writesNoteMemberTail": "En las herramientas donde cada persona usa su propia cuenta, los usuarios del enlace público hacen cambios con su propia cuenta."
}
},
"memberCount_one": "{{formatted}} miembro",
@@ -1568,7 +1580,7 @@
"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 persona usa su propia cuenta de {{name}} y la conecta la primera vez que usa esta herramienta.",
"memberNote": "Cada persona usa su propia cuenta de {{name}} y la conecta la primera vez que usa esta herramienta. A través de la clave de API o el widget de un agente, se usa la cuenta del propietario del agente.",
"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."
+20 -8
View File
@@ -766,20 +766,32 @@
"title": "このエージェントが使うもの",
"intro": "このエージェントを使うすべての人が、表示されたアクセスでこれらを使います。",
"stopped": "停止中",
"writesOff": "API から変更不可",
"writesNote": "API、ウィジェット、公開リンクの利用者は、「API から変更不可」の項目で変更を行えません。Access Details で許可すると行えます。",
"writesNoteEditor": "API、ウィジェット、公開リンクの利用者は、「API から変更不可」の項目で変更を行えません。オーナーが Access Details で許可すると行えます。",
"writesOff": "API からの変更はオフ",
"writesNote": "API、ウィジェット、公開リンクの利用者は、ここで示された変更を行えません。Access Details で許可すると行えます。",
"writesNoteEditor": "API、ウィジェット、公開リンクの利用者は、ここで示された変更を行えません。オーナーが Access Details で許可すると行えます。",
"access": {
"you": "あなたのアクセス",
"person": "{{person}} のアクセス",
"owner": "オーナーのアクセス",
"member": "各自の {{service}} アカウント",
"memberNoService": "各自のアカウント",
"member": "各自の {{service}} アカウント(API とウィジェット:あなたのもの)",
"memberNoService": "各自のアカウント(API とウィジェット:あなたのもの)",
"yourAccount": "あなたの {{service}} アカウント",
"yourAccountNoService": "あなたのアカウント",
"personAccount": "{{person}} の {{service}} アカウント",
"personAccountNoService": "{{person}} のアカウント"
}
"personAccountNoService": "{{person}} のアカウント",
"memberShared": "各自の {{service}} アカウント(API とウィジェット:オーナーのもの)",
"memberSharedNoService": "各自のアカウント(API とウィジェット:オーナーのもの)",
"otherAccount": "他の人の {{service}} アカウント",
"otherAccountNoService": "他の人のアカウント",
"yourCredentials": "あなたの保存済み認証情報",
"personCredentials": "{{person}} の保存済み認証情報",
"otherCredentials": "他の人の保存済み認証情報"
},
"writesSomeOff": "一部の API からの変更はオフ",
"adminOff": "管理者が変更をオフにしています",
"writesNoteApi": "API とウィジェットの利用者は、ここで示された変更を行えません。Access Details で許可すると行えます。",
"writesNoteApiEditor": "API とウィジェットの利用者は、ここで示された変更を行えません。オーナーが Access Details で許可すると行えます。",
"writesNoteMemberTail": "各自のアカウントを使うツールでは、公開リンクの利用者は自分のアカウントで変更します。"
}
},
"memberCount_one": "{{formatted}}人のメンバー",
@@ -1558,7 +1570,7 @@
"ownerWarningShared": "チームメンバーは所有者の {{name}} アカウントで操作します。",
"confirmWrite": "チームメンバーが自分のアカウントで操作を行えることを理解しました。",
"confirmWriteShared": "チームメンバーが所有者の {{name}} アカウントで操作を行えることを理解しました。",
"memberNote": "各自が自分の {{name}} アカウントを使い、このツールを初めて使うときに接続します。",
"memberNote": "各自が自分の {{name}} アカウントを使い、このツールを初めて使うときに接続します。エージェントの API キーやウィジェット経由では、エージェントのオーナーのアカウントが使われます。",
"forced": "このコネクタのすべての共有に対して管理者が設定しています。",
"ownerChooses": "これを変更できるのは所有者だけです。",
"saveFailed": "変更を保存できませんでした。もう一度お試しください。"
+20 -8
View File
@@ -811,20 +811,32 @@
"title": "Что использует этот агент",
"intro": "Все, кто пользуется этим агентом, получают это, каждое — с указанным доступом.",
"stopped": "Остановлено",
"writesOff": "Без изменений через API",
"writesNote": "Пользователи API, виджета и публичной ссылки не могут вносить изменения через элементы с пометкой «Без изменений через API», пока вы не разрешите это в Access Details.",
"writesNoteEditor": "Пользователи API, виджета и публичной ссылки не могут вносить изменения через элементы с пометкой «Без изменений через API», пока владелец не разрешит это в Access Details.",
"writesOff": "Изменения через API выключены",
"writesNote": "Пользователи API, виджета и публичной ссылки не могут вносить отмеченные здесь изменения, пока вы не разрешите их в Access Details.",
"writesNoteEditor": "Пользователи API, виджета и публичной ссылки не могут вносить отмеченные здесь изменения, пока владелец не разрешит их в Access Details.",
"access": {
"you": "Ваш доступ",
"person": "Доступ {{person}}",
"owner": "Доступ владельца",
"member": "Собственный аккаунт {{service}} у каждого",
"memberNoService": "Собственный аккаунт у каждого",
"member": "Собственный аккаунт {{service}} у каждого (API и виджет: ваш)",
"memberNoService": "Собственный аккаунт у каждого (API и виджет: ваш)",
"yourAccount": "Ваш аккаунт {{service}}",
"yourAccountNoService": "Ваш аккаунт",
"personAccount": "Аккаунт {{service}} пользователя {{person}}",
"personAccountNoService": "Аккаунт пользователя {{person}}"
}
"personAccountNoService": "Аккаунт пользователя {{person}}",
"memberShared": "Собственный аккаунт {{service}} у каждого (API и виджет: владельца)",
"memberSharedNoService": "Собственный аккаунт у каждого (API и виджет: владельца)",
"otherAccount": "Аккаунт {{service}} другого пользователя",
"otherAccountNoService": "Аккаунт другого пользователя",
"yourCredentials": "Ваши сохранённые учётные данные",
"personCredentials": "Сохранённые учётные данные {{person}}",
"otherCredentials": "Сохранённые учётные данные другого пользователя"
},
"writesSomeOff": "Часть изменений через API выключена",
"adminOff": "Изменения выключены администратором",
"writesNoteApi": "Пользователи API и виджета не могут вносить отмеченные здесь изменения, пока вы не разрешите их в Access Details.",
"writesNoteApiEditor": "Пользователи API и виджета не могут вносить отмеченные здесь изменения, пока владелец не разрешит их в Access Details.",
"writesNoteMemberTail": "В инструментах, где у каждого свой аккаунт, пользователи публичной ссылки вносят изменения через свой аккаунт."
}
},
"memberCount_one": "{{formatted}} участник",
@@ -1650,7 +1662,7 @@
"ownerWarningShared": "Участники команды будут действовать от имени аккаунта {{name}} владельца.",
"confirmWrite": "Я понимаю, что участники команды могут выполнять действия от имени моего аккаунта.",
"confirmWriteShared": "Я понимаю, что участники команды могут выполнять действия от имени аккаунта {{name}} владельца.",
"memberNote": "Каждый использует свой аккаунт {{name}} и подключает его при первом использовании этого инструмента.",
"memberNote": "Каждый использует свой аккаунт {{name}} и подключает его при первом использовании этого инструмента. Через API-ключ или виджет агента используется аккаунт владельца агента.",
"forced": "Администратор задал это для всех общих доступов к этому коннектору.",
"ownerChooses": "Изменить это может только владелец.",
"saveFailed": "Не удалось сохранить изменение. Попробуйте ещё раз."
+20 -8
View File
@@ -766,20 +766,32 @@
"title": "此代理使用的內容",
"intro": "使用此代理的每個人都會用到這些內容,各自依所示的存取權限執行。",
"stopped": "已停止",
"writesOff": "API 無法變更",
"writesNote": "在你於 Access Details 中允許之前,API、小工具和公開連結的使用者無法透過標示為「API 無法變更」的項目進行變更。",
"writesNoteEditor": "在擁有者於 Access Details 中允許之前,API、小工具和公開連結的使用者無法透過標示為「API 無法變更」的項目進行變更。",
"writesOff": "API 變更已關閉",
"writesNote": "在你於 Access Details 中允許之前,API、小工具和公開連結的使用者無法進行此處標示的變更。",
"writesNoteEditor": "在擁有者於 Access Details 中允許之前,API、小工具和公開連結的使用者無法進行此處標示的變更。",
"access": {
"you": "你的存取權限",
"person": "{{person}} 的存取權限",
"owner": "擁有者的存取權限",
"member": "每個人自己的 {{service}} 帳號",
"memberNoService": "每個人自己的帳號",
"member": "每個人自己的 {{service}} 帳號(API 和小工具:你的)",
"memberNoService": "每個人自己的帳號(API 和小工具:你的)",
"yourAccount": "你的 {{service}} 帳號",
"yourAccountNoService": "你的帳號",
"personAccount": "{{person}} 的 {{service}} 帳號",
"personAccountNoService": "{{person}} 的帳號"
}
"personAccountNoService": "{{person}} 的帳號",
"memberShared": "每個人自己的 {{service}} 帳號(API 和小工具:擁有者的)",
"memberSharedNoService": "每個人自己的帳號(API 和小工具:擁有者的)",
"otherAccount": "其他人的 {{service}} 帳號",
"otherAccountNoService": "其他人的帳號",
"yourCredentials": "你儲存的憑證",
"personCredentials": "{{person}} 儲存的憑證",
"otherCredentials": "其他人儲存的憑證"
},
"writesSomeOff": "部分 API 變更已關閉",
"adminOff": "管理員已關閉變更",
"writesNoteApi": "在你於 Access Details 中允許之前,API 和小工具的使用者無法進行此處標示的變更。",
"writesNoteApiEditor": "在擁有者於 Access Details 中允許之前,API 和小工具的使用者無法進行此處標示的變更。",
"writesNoteMemberTail": "在每個人使用自己帳號的工具上,公開連結的使用者以自己的帳號進行變更。"
}
},
"memberCount_one": "{{formatted}} 位成員",
@@ -1558,7 +1570,7 @@
"ownerWarningShared": "團隊成員將以擁有者的 {{name}} 帳號操作。",
"confirmWrite": "我了解團隊成員可以使用我的帳號執行操作。",
"confirmWriteShared": "我了解團隊成員可以使用擁有者的 {{name}} 帳號執行操作。",
"memberNote": "每個人使用自己的 {{name}} 帳號,並在首次使用此工具時連線。",
"memberNote": "每個人使用自己的 {{name}} 帳號,並在首次使用此工具時連線。透過代理的 API 金鑰或小工具使用時,會使用代理擁有者的帳號。",
"forced": "管理員已為此連接器的所有共用設定此項。",
"ownerChooses": "只有擁有者可以變更此項。",
"saveFailed": "無法儲存變更。請再試一次。"
+20 -8
View File
@@ -766,20 +766,32 @@
"title": "此智能体使用的内容",
"intro": "使用此智能体的每个人都会用到这些内容,各自按所示的访问权限运行。",
"stopped": "已停止",
"writesOff": "API 不可更改",
"writesNote": "在你于 Access Details 中允许之前,API、小组件和公开链接的用户无法通过标记为“API 不可更改”的项目进行更改。",
"writesNoteEditor": "在所有者于 Access Details 中允许之前,API、小组件和公开链接的用户无法通过标记为“API 不可更改”的项目进行更改。",
"writesOff": "API 更改已关闭",
"writesNote": "在你于 Access Details 中允许之前,API、小组件和公开链接的用户无法进行此处标记的更改。",
"writesNoteEditor": "在所有者于 Access Details 中允许之前,API、小组件和公开链接的用户无法进行此处标记的更改。",
"access": {
"you": "你的访问权限",
"person": "{{person}} 的访问权限",
"owner": "所有者的访问权限",
"member": "每个人自己的 {{service}} 账号",
"memberNoService": "每个人自己的账号",
"member": "每个人自己的 {{service}} 账号(API 和小组件:你的)",
"memberNoService": "每个人自己的账号(API 和小组件:你的)",
"yourAccount": "你的 {{service}} 账号",
"yourAccountNoService": "你的账号",
"personAccount": "{{person}} 的 {{service}} 账号",
"personAccountNoService": "{{person}} 的账号"
}
"personAccountNoService": "{{person}} 的账号",
"memberShared": "每个人自己的 {{service}} 账号(API 和小组件:所有者的)",
"memberSharedNoService": "每个人自己的账号(API 和小组件:所有者的)",
"otherAccount": "其他人的 {{service}} 账号",
"otherAccountNoService": "其他人的账号",
"yourCredentials": "你保存的凭据",
"personCredentials": "{{person}} 保存的凭据",
"otherCredentials": "其他人保存的凭据"
},
"writesSomeOff": "部分 API 更改已关闭",
"adminOff": "管理员已关闭更改",
"writesNoteApi": "在你于 Access Details 中允许之前,API 和小组件的用户无法进行此处标记的更改。",
"writesNoteApiEditor": "在所有者于 Access Details 中允许之前,API 和小组件的用户无法进行此处标记的更改。",
"writesNoteMemberTail": "在每个人使用自己账号的工具上,公开链接的用户用自己的账号进行更改。"
}
},
"memberCount_one": "{{formatted}} 名成员",
@@ -1558,7 +1570,7 @@
"ownerWarningShared": "团队成员将以所有者的 {{name}} 账号操作。",
"confirmWrite": "我了解团队成员可以使用我的账号执行操作。",
"confirmWriteShared": "我了解团队成员可以使用所有者的 {{name}} 账号执行操作。",
"memberNote": "每个人使用自己的 {{name}} 账号,并在首次使用此工具时连接。",
"memberNote": "每个人使用自己的 {{name}} 账号,并在首次使用此工具时连接。通过智能体的 API 密钥或小组件使用时,使用智能体所有者的账号。",
"forced": "管理员已为此连接器的所有共享设定了此项。",
"ownerChooses": "只有所有者可以更改此项。",
"saveFailed": "无法保存更改。请重试。"