mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-11 03:12:55 +00:00
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:
1 parent
3fc55dfad8
commit
88e715405e
13 files changed
+584
-204
No files matched your search
@@ -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('');
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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": "変更を保存できませんでした。もう一度お試しください。"
|
||||
|
||||
@@ -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": "Не удалось сохранить изменение. Попробуйте ещё раз."
|
||||
|
||||
@@ -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": "無法儲存變更。請再試一次。"
|
||||
|
||||
@@ -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": "无法保存更改。请重试。"
|
||||
|
||||
Reference in new issue
Block a user