Fold the API write allowlist into one choice per tool

Access details now shows the actions API, widget and public-link users
may take as the owner as a collapsed section with a summary line (which
tools can make changes, how many of all are allowed). Open, each tool has
one Off / All choice with its actions one by one under Customize, like a
connector's permissions. Entries and saving are unchanged.
This commit is contained in:
arc53-machine committed 2026-09-29 18:31:36 +01:00
1 parent 4b08e94be7
commit 05bdb60956
9 files changed
+505 -124

No files matched your search

+183 -57
View File
@@ -5,8 +5,15 @@ import { Provider } from 'react-redux';
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, opts?: Record<string, unknown>) =>
opts?.action ? `${opts.tool}: ${opts.action}` : key,
t: (key: string, opts?: Record<string, unknown>) => {
if (!opts) return key;
const params = Object.entries(opts)
.filter(([k]) => k !== 'interpolation' && k !== 'count')
.map(([k, v]) => `${k}=${v}`)
.join(',');
return params ? `${key}(${params})` : key;
},
i18n: { language: 'en' },
}),
}));
@@ -28,7 +35,9 @@ import actionToastReducer, {
} from '../notifications/actionToastSlice';
import { prefSlice } from '../preferences/preferenceSlice';
import ApiWriteAllowlist from './ApiWriteAllowlist';
import type { Agent, ResourceState } from './types';
import type { Mock } from 'vitest';
import type { Agent, AgentConfig, ResourceState } from './types';
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
@@ -40,7 +49,12 @@ const TOOLS = {
connection_id: 'c1',
owner_credential_writes: ['telegram_send_message'],
actions: [
{ name: 'telegram_send_message', access: 'write', active: true },
{
name: 'telegram_send_message',
description: 'Sends a message.',
access: 'write',
active: true,
},
{ name: 'telegram_read', access: 'read', active: true },
],
},
@@ -79,7 +93,7 @@ const STATES: ResourceState[] = [
state({
id: 'tg',
name: 'Telegram',
owner_credential_writes: ['telegram_send_message'],
owner_credential_writes: ['telegram_send_message', 'telegram_pin_message'],
}),
state({ id: 'memory', name: 'Memory' }),
state({
@@ -153,8 +167,15 @@ describe('ApiWriteAllowlist', () => {
let currentTools: string[] = [];
const render = async (
a: Agent,
onConfigChange = vi.fn(),
getSavedConfig?: () => Agent['config'],
{
onConfigChange = vi.fn<(config: AgentConfig) => void>(),
getSavedConfig,
defaultOpen,
}: {
onConfigChange?: Mock<(config: AgentConfig) => void>;
getSavedConfig?: () => Agent['config'];
defaultOpen?: boolean;
} = {},
) => {
currentTools = a.tools ?? [];
store = makeStore();
@@ -165,6 +186,7 @@ describe('ApiWriteAllowlist', () => {
agent={a}
onConfigChange={onConfigChange}
getSavedConfig={getSavedConfig}
defaultOpen={defaultOpen}
/>
</Provider>,
);
@@ -172,36 +194,141 @@ describe('ApiWriteAllowlist', () => {
return onConfigChange;
};
it("lists only writes on the owner's credentials, unchecked by default", async () => {
await render(agent());
const labels = Array.from(container.querySelectorAll('label')).map(
(l) => l.textContent,
const K = 'modals.agentDetails.apiWrites';
const disclosure = () =>
Array.from(container.querySelectorAll('button')).find((b) =>
b.textContent?.includes(`${K}.title`),
)!;
const expand = async () => {
if (disclosure().getAttribute('aria-expanded') === 'false')
await act(async () => disclosure().click());
};
const groups = () =>
Array.from(container.querySelectorAll<HTMLElement>('[data-tool]'));
const groupTitles = () =>
groups().map((g) => g.querySelector('h4')?.textContent);
const group = (id: string) =>
container.querySelector<HTMLElement>(`[data-tool="${id}"]`)!;
const choice = (id: string, value: 'off' | 'all') =>
group(id).querySelector<HTMLButtonElement>(`[data-choice="${value}"]`)!;
const customize = async (id: string) => {
const button = Array.from(group(id).querySelectorAll('button')).find(
(b) => b.getAttribute('aria-expanded') === 'false',
);
expect(labels).toEqual(['Telegram: Telegram send message']);
if (button) await act(async () => button.click());
};
const actionSwitch = (id: string, label: string) => {
const row = Array.from(
group(id).querySelectorAll<HTMLElement>('[data-slot="setting-row"]'),
).find((r) => r.querySelector('label')?.textContent === label)!;
return row.querySelector<HTMLButtonElement>('[role="switch"]')!;
};
const summary = () =>
container.querySelector('[data-testid="api-writes-summary"]')?.textContent;
const savedConfig = () =>
JSON.parse(
(updateAgent.mock.calls.at(-1)![1] as FormData).get('config') as string,
);
it('starts folded to a summary line, and nothing is allowed by default', async () => {
await render(agent());
expect(disclosure().getAttribute('aria-expanded')).toBe('false');
expect(summary()).toBe(`${K}.summaryNone`);
expect(groups()).toHaveLength(0);
await expand();
expect(disclosure().getAttribute('aria-expanded')).toBe('true');
expect(choice('tg', 'off').getAttribute('data-state')).toBe('on');
});
it('opens straight away when asked to', async () => {
await render(agent(), { defaultOpen: true });
expect(disclosure().getAttribute('aria-expanded')).toBe('true');
expect(groupTitles()).toEqual(['Telegram']);
});
it('names the tools that can make changes and counts what is allowed', async () => {
await render(
agent({
tools: ['tg', 'crm', 'memory'],
config: {
api_write_allowlist: ['tg:telegram_send_message', 'crm:create_lead'],
},
} as Partial<Agent>),
);
expect(summary()).toBe(
`${K}.summaryTools(tools=Telegram and CRM API) · ${K}.summaryCount(allowed=2,formatted=3)`,
);
});
it("groups only writes on the owner's credentials by tool, each action under Customize", async () => {
await render(agent());
await expand();
expect(groupTitles()).toEqual(['Telegram']);
expect(container.textContent).not.toContain('Telegram send message');
await customize('tg');
const labels = Array.from(
group('tg').querySelectorAll('[data-slot="setting-row"] label'),
).map((l) => l.textContent);
expect(labels).toEqual(['Telegram send message', 'Telegram pin message']);
expect(group('tg').textContent).toContain('Sends a message.');
expect(
container
.querySelector('button[role="checkbox"]')!
.getAttribute('aria-checked'),
actionSwitch('tg', 'Telegram send message').getAttribute('aria-checked'),
).toBe('false');
});
it('saves the allowlist without dropping the rest of the config', async () => {
it("allows all of a tool's changes at once, without dropping the rest of the config", async () => {
updateAgent.mockResolvedValue({ ok: true });
const onConfigChange = await render(agent());
await act(async () =>
container
.querySelector<HTMLButtonElement>('button[role="checkbox"]')!
.click(),
await expand();
await act(async () => choice('tg', 'all').click());
const expected = {
guardrails: { controls: [] },
api_write_allowlist: [
'tg:telegram_send_message',
'tg:telegram_pin_message',
],
};
expect(updateAgent).toHaveBeenCalledTimes(1);
expect(savedConfig()).toEqual(expected);
expect(onConfigChange).toHaveBeenCalledWith(expected);
expect(choice('tg', 'all').getAttribute('data-state')).toBe('on');
expect(summary()).toBe(
`${K}.summaryTools(tools=Telegram) · ${K}.summaryCount(allowed=2,formatted=2)`,
);
const form = updateAgent.mock.calls[0][1] as FormData;
expect(JSON.parse(form.get('config') as string)).toEqual({
guardrails: { controls: [] },
api_write_allowlist: ['tg:telegram_send_message'],
});
expect(onConfigChange).toHaveBeenCalledWith({
guardrails: { controls: [] },
api_write_allowlist: ['tg:telegram_send_message'],
});
});
it('turns a tool off again, keeping entries of tools no longer listed', async () => {
updateAgent.mockResolvedValue({ ok: true });
await render(
agent({
config: {
api_write_allowlist: [
'tg:telegram_send_message',
'tg:telegram_pin_message',
'old:gone_action',
],
},
} as Partial<Agent>),
);
await expand();
await act(async () => choice('tg', 'off').click());
expect(savedConfig().api_write_allowlist).toEqual(['old:gone_action']);
});
it('allows one action under Customize, leaving the tool choice mixed', async () => {
updateAgent.mockResolvedValue({ ok: true });
await render(agent());
await expand();
await customize('tg');
await act(async () => actionSwitch('tg', 'Telegram pin message').click());
expect(savedConfig().api_write_allowlist).toEqual([
'tg:telegram_pin_message',
]);
expect(
actionSwitch('tg', 'Telegram pin message').getAttribute('aria-checked'),
).toBe('true');
expect(choice('tg', 'off').getAttribute('data-state')).toBe('off');
expect(choice('tg', 'all').getAttribute('data-state')).toBe('off');
});
it('saves on top of the last saved config, not unsaved form edits', async () => {
@@ -212,45 +339,47 @@ describe('ApiWriteAllowlist', () => {
const draft = agent({
config: { guardrails: { controls: [{ id: 'unsaved' }] } },
} as unknown as Partial<Agent>);
const onConfigChange = await render(draft, vi.fn(), () => saved);
await act(async () =>
container
.querySelector<HTMLButtonElement>('button[role="checkbox"]')!
.click(),
);
const onConfigChange = await render(draft, {
getSavedConfig: () => saved,
});
await expand();
await act(async () => choice('tg', 'all').click());
const expected = {
guardrails: { controls: [] },
api_write_allowlist: ['tg:telegram_send_message'],
api_write_allowlist: [
'tg:telegram_send_message',
'tg:telegram_pin_message',
],
};
const form = updateAgent.mock.calls[0][1] as FormData;
expect(JSON.parse(form.get('config') as string)).toEqual(expected);
expect(savedConfig()).toEqual(expected);
expect(onConfigChange).toHaveBeenCalledWith(expected);
});
it('puts the choice back and says so when saving fails', async () => {
updateAgent.mockResolvedValue({ ok: false });
await render(agent());
const box = () =>
container.querySelector<HTMLButtonElement>('button[role="checkbox"]')!;
await act(async () => box().click());
expect(box().getAttribute('aria-checked')).toBe('false');
await expand();
await act(async () => choice('tg', 'all').click());
expect(choice('tg', 'off').getAttribute('data-state')).toBe('on');
expect(summary()).toBe(`${K}.summaryNone`);
expect(selectActionToast(store.getState())?.variant).toBe('destructive');
});
it('lists writes on stored credentials of tools without a connection', async () => {
await render(agent({ tools: ['crm'] }));
const labels = Array.from(container.querySelectorAll('label')).map(
(l) => l.textContent,
);
expect(labels).toEqual(['CRM API: Create lead']);
await render(agent({ tools: ['crm'] }), { defaultOpen: true });
expect(groupTitles()).toEqual(['CRM API']);
await customize('crm');
expect(
group('crm').querySelector('[data-slot="setting-row"] label')
?.textContent,
).toBe('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']);
await render(agent({ tools: ['bob-jira', 'gone'] }), {
defaultOpen: true,
});
expect(groupTitles()).toEqual(['Bob Jira']);
});
it("lists writes of a workflow agent's node tools", async () => {
@@ -271,11 +400,8 @@ describe('ApiWriteAllowlist', () => {
},
}),
});
await render(agent({ tools: [] }));
const labels = Array.from(container.querySelectorAll('label')).map(
(l) => l.textContent,
);
expect(labels).toEqual(['Node Slack: Post message']);
await render(agent({ tools: [] }), { defaultOpen: true });
expect(groupTitles()).toEqual(['Node Slack']);
});
it('renders nothing for an agent without connected tools', async () => {
+222 -53
View File
@@ -1,30 +1,149 @@
import { ChevronRight } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import userService from '../api/services/userService';
import { Button } from '../components/ui/button';
import { Card } from '../components/ui/card';
import { Checkbox } from '../components/ui/checkbox';
import { Label } from '../components/ui/label';
import { SectionHeader } from '../components/ui/section-header';
import { SettingRow, SettingRows } from '../components/ui/setting-row';
import { Switch } from '../components/ui/switch';
import { ToggleGroup, ToggleGroupItem } from '../components/ui/toggle-group';
import { actionTitle } from '../connectors/i18n';
import { cn } from '../lib/utils';
import { showActionToast } from '../notifications/actionToastSlice';
import { selectToken } from '../preferences/preferenceSlice';
import { formatCount, intlLocale } from '../utils/dateTimeUtils';
import type { Agent, AgentConfig } from './types';
import useAgentResourceStates from './useAgentResourceStates';
import { actionTitle } from '../connectors/i18n';
type WriteAction = {
entry: string;
tool: string;
action: string;
description: string;
};
/** One running tool and its writes on the owner's credentials. */
type ToolWrites = {
id: string;
name: string;
actions: WriteAction[];
};
type UserTool = {
id: string;
actions?: { name: string; description?: string }[];
};
/** The tool-wide choice: every write off, every write on, or mixed (''). */
type ToolChoice = 'off' | 'all' | '';
const K = 'modals.agentDetails.apiWrites';
/**
* One tool's writes: an Off / All choice for all of them at once, and each
* write on its own switch under Customize, like a connector's permissions.
*/
function ToolAllowlist({
tool,
allowed,
onChange,
}: {
tool: ToolWrites;
allowed: Set<string>;
onChange: (entries: string[], allow: boolean) => void;
}) {
const { t } = useTranslation();
const [unfolded, setUnfolded] = useState(false);
const allowedCount = tool.actions.filter((item) =>
allowed.has(item.entry),
).length;
const choice: ToolChoice =
allowedCount === 0
? 'off'
: allowedCount === tool.actions.length
? 'all'
: '';
const plain = { interpolation: { escapeValue: false } };
return (
<div data-tool={tool.id} className="flex flex-col gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<SectionHeader
as="h4"
size="xs"
className="min-w-0"
title={tool.name}
description={t(`${K}.toolCount`, {
allowed: formatCount(allowedCount),
formatted: formatCount(tool.actions.length),
})}
/>
<div className="bg-muted rounded-full p-1">
<ToggleGroup
type="single"
size="xs"
value={choice}
aria-label={t(`${K}.toolLabel`, { ...plain, tool: tool.name })}
onValueChange={(value) =>
value &&
onChange(
tool.actions.map((item) => item.entry),
value === 'all',
)
}
>
{(['off', 'all'] as const).map((value) => (
<ToggleGroupItem key={value} value={value} data-choice={value}>
{t(`${K}.choice.${value}`)}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
</div>
<Button
type="button"
variant="link"
size="inline"
className="self-start"
aria-expanded={unfolded}
onClick={() => setUnfolded(!unfolded)}
>
{unfolded
? t(`${K}.fold`)
: t(`${K}.customize`, {
count: tool.actions.length,
formatted: formatCount(tool.actions.length),
})}
</Button>
{unfolded && (
<SettingRows>
{tool.actions.map((item) => {
const id = `api-write-${item.entry}`;
return (
<SettingRow
key={item.entry}
htmlFor={id}
alignStart
label={
<span title={item.action}>{actionTitle(item.action)}</span>
}
description={item.description || undefined}
>
<Switch
id={id}
checked={allowed.has(item.entry)}
onCheckedChange={(checked) => onChange([item.entry], checked)}
/>
</SettingRow>
);
})}
</SettingRows>
)}
</div>
);
}
/**
* The write actions on the owner's accounts and stored credentials that
* anyone reaching this agent through its API key, widget or its public link
@@ -34,7 +153,12 @@ type UserTool = {
* 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
* The section starts folded to a one-line summary (which tools can make
* changes, and how many of all the writes are allowed). Open, each tool has
* one Off / All choice with its writes one by one under Customize. Entries
* are `tool_id:action`.
*
* A change saves at once, on top of the agent's last saved config
* (`getSavedConfig`), so edits still pending in the form are not saved with
* it. `onConfigChange` receives the config as saved.
*/
@@ -42,14 +166,18 @@ export default function ApiWriteAllowlist({
agent,
onConfigChange,
getSavedConfig,
defaultOpen = false,
}: {
agent: Agent;
onConfigChange?: (config: AgentConfig) => void;
getSavedConfig?: () => AgentConfig | undefined;
/** Start open (Access details opened to allow changes). */
defaultOpen?: boolean;
}) {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const dispatch = useDispatch();
const token = useSelector(selectToken);
const [open, setOpen] = useState(defaultOpen);
const [descriptions, setDescriptions] = useState<Record<string, string>>({});
const [allowed, setAllowed] = useState<string[]>(
agent.config?.api_write_allowlist ?? [],
@@ -81,24 +209,58 @@ export default function ApiWriteAllowlist({
};
}, [token]);
const actions: WriteAction[] = (loaded?.items ?? [])
.filter((item) => item.type === 'tool' && item.state === 'active')
.flatMap((item) =>
(item.owner_credential_writes ?? []).map((name) => ({
const tools: ToolWrites[] = (loaded?.items ?? [])
.filter(
(item) =>
item.type === 'tool' &&
item.state === 'active' &&
(item.owner_credential_writes ?? []).length > 0,
)
.map((item) => ({
id: item.id,
name: item.name || t('agents.form.sponsors.unknownItem'),
actions: (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;
if (tools.length === 0) return null;
const toggle = async (entry: string, checked: boolean) => {
const allowedSet = new Set(allowed);
const total = tools.reduce((sum, tool) => sum + tool.actions.length, 0);
const allowedCount = tools.reduce(
(sum, tool) =>
sum + tool.actions.filter((item) => allowedSet.has(item.entry)).length,
0,
);
const changing = tools
.filter((tool) => tool.actions.some((item) => allowedSet.has(item.entry)))
.map((tool) => tool.name);
const summary =
changing.length === 0
? t(`${K}.summaryNone`)
: [
t(`${K}.summaryTools`, {
count: changing.length,
tools: new Intl.ListFormat(intlLocale(i18n.language), {
type: 'conjunction',
}).format(changing),
interpolation: { escapeValue: false },
}),
t(`${K}.summaryCount`, {
allowed: formatCount(allowedCount),
formatted: formatCount(total),
}),
].join(' · ');
/** Allow or refuse these entries, and save the list. */
const change = async (entries: string[], allow: boolean) => {
const previous = allowed;
const next = checked
? [...allowed, entry]
: allowed.filter((item) => item !== entry);
const next = allow
? [...allowed, ...entries.filter((entry) => !allowed.includes(entry))]
: allowed.filter((entry) => !entries.includes(entry));
setAllowed(next);
const config: AgentConfig = {
...((getSavedConfig ? getSavedConfig() : agent.config) ?? {}),
@@ -119,7 +281,7 @@ export default function ApiWriteAllowlist({
dispatch(
showActionToast({
variant: 'destructive',
message: t('modals.agentDetails.apiWrites.saveFailed'),
message: t(`${K}.saveFailed`),
}),
);
}
@@ -127,42 +289,49 @@ export default function ApiWriteAllowlist({
return (
<section className="flex flex-col gap-3">
<SectionHeader
as="h4"
size="xs"
title={t('modals.agentDetails.apiWrites.title')}
description={t('modals.agentDetails.apiWrites.description')}
/>
<Card padding="sm" className="gap-3">
{actions.map((item) => {
const id = `api-write-${item.entry}`;
return (
<div key={item.entry} className="flex items-start gap-2">
<Checkbox
id={id}
checked={allowed.includes(item.entry)}
onCheckedChange={(checked) =>
toggle(item.entry, checked === true)
}
{/* A collapsible group in a modal: the inline disclosure toggle. */}
<div className="flex flex-col gap-0.5">
<Button
type="button"
variant="link"
size="sm"
aria-expanded={open}
className="-ml-3 w-fit justify-start"
onClick={() => setOpen(!open)}
>
<ChevronRight
aria-hidden="true"
className={cn(
'transition-transform duration-200',
open && 'rotate-90',
)}
/>
{t(`${K}.title`)}
</Button>
<p
data-testid="api-writes-summary"
className="text-muted-foreground text-xs"
>
{summary}
</p>
</div>
{open && (
<>
<p className="text-muted-foreground text-xs">
{t(`${K}.description`)}
</p>
<Card variant="outline" padding="sm" className="gap-4">
{tools.map((tool) => (
<ToolAllowlist
key={tool.id}
tool={tool}
allowed={allowedSet}
onChange={change}
/>
<div className="flex min-w-0 flex-col gap-0.5">
<Label htmlFor={id} className="font-normal">
{t('modals.agentDetails.apiWrites.item', {
tool: item.tool,
action: actionTitle(item.action),
interpolation: { escapeValue: false },
})}
</Label>
{item.description && (
<span className="text-muted-foreground text-xs">
{item.description}
</span>
)}
</div>
</div>
);
})}
</Card>
))}
</Card>
</>
)}
</section>
);
}
+14 -2
View File
@@ -1934,8 +1934,20 @@
"apiWrites": {
"title": "Aktionen, die API-, Widget- und Public-Link-Nutzer in deinem Namen ausführen können",
"description": "Über den API-Schlüssel, das Widget oder den öffentlichen Link des Agenten kann niemand eine Aktion für dich bestätigen. Änderungen mit deinen verbundenen Konten und gespeicherten Zugangsdaten sind daher blockiert, außer du erlaubst sie hier.",
"item": "{{tool}}: {{action}}",
"saveFailed": "Speichern fehlgeschlagen. Versuche es erneut."
"saveFailed": "Speichern fehlgeschlagen. Versuche es erneut.",
"summaryNone": "Noch nichts erlaubt",
"summaryTools_one": "{{tools}} kann Änderungen vornehmen",
"summaryTools_other": "{{tools}} können Änderungen vornehmen",
"summaryCount": "{{allowed}} von {{formatted}} erlaubt",
"toolCount": "Änderungen über API, Widget und öffentliche Links · {{allowed}} von {{formatted}} erlaubt",
"toolLabel": "Änderungen über API, Widget und öffentliche Links für {{tool}} erlauben",
"choice": {
"off": "Aus",
"all": "Alle"
},
"customize_one": "Änderung einzeln festlegen",
"customize_other": "Alle {{formatted}} einzeln festlegen",
"fold": "Änderungen ausblenden"
},
"actionFailed": "Das hat nicht funktioniert. Bitte versuche es erneut.",
"apiKeyAfterPublish": "Veröffentliche den Agenten, um seinen API-Schlüssel zu erstellen."
+14 -2
View File
@@ -1940,8 +1940,20 @@
"apiWrites": {
"title": "Actions API, widget and public-link users can take as you",
"description": "Nobody can approve an action for you through the API key, the widget or the agent's public link, so changes made with your connected accounts and saved credentials are blocked unless you allow them here.",
"item": "{{tool}}: {{action}}",
"saveFailed": "Could not save. Try again."
"saveFailed": "Could not save. Try again.",
"summaryNone": "None allowed yet",
"summaryTools_one": "{{tools}} can make changes",
"summaryTools_other": "{{tools}} can make changes",
"summaryCount": "{{allowed}} of {{formatted}} allowed",
"toolCount": "Changes from API, widget and public links · {{allowed}} of {{formatted}} allowed",
"toolLabel": "Allow changes from API, widget and public links on {{tool}}",
"choice": {
"off": "Off",
"all": "All"
},
"customize_one": "Customize the change",
"customize_other": "Customize each of {{formatted}}",
"fold": "Hide changes"
},
"actionFailed": "That didn't work. Please try again.",
"apiKeyAfterPublish": "Publish the agent to create its API key."
+14 -2
View File
@@ -1934,8 +1934,20 @@
"apiWrites": {
"title": "Acciones que los usuarios de la API, del widget y del enlace público pueden realizar en tu nombre",
"description": "Nadie puede aprobar una acción por ti a través de la clave de API, el widget o el enlace público del agente, así que los cambios con tus cuentas conectadas y credenciales guardadas se bloquean salvo que los permitas aquí.",
"item": "{{tool}}: {{action}}",
"saveFailed": "No se pudo guardar. Inténtalo de nuevo."
"saveFailed": "No se pudo guardar. Inténtalo de nuevo.",
"summaryNone": "Aún no hay nada permitido",
"summaryTools_one": "{{tools}} puede hacer cambios",
"summaryTools_other": "{{tools}} pueden hacer cambios",
"summaryCount": "{{allowed}} de {{formatted}} permitidos",
"toolCount": "Cambios por API, widget y enlaces públicos · {{allowed}} de {{formatted}} permitidos",
"toolLabel": "Permitir cambios por API, widget y enlaces públicos en {{tool}}",
"choice": {
"off": "Desactivado",
"all": "Todos"
},
"customize_one": "Ajustar el cambio",
"customize_other": "Ajustar cada uno de {{formatted}}",
"fold": "Ocultar cambios"
},
"actionFailed": "No funcionó. Inténtalo de nuevo.",
"apiKeyAfterPublish": "Publica el agente para crear su clave de API."
+14 -2
View File
@@ -1923,8 +1923,20 @@
"apiWrites": {
"title": "API、ウィジェット、公開リンクの利用者があなたとして実行できる操作",
"description": "API キー、ウィジェット、エージェントの公開リンクでは誰もあなたの代わりに操作を承認できないため、接続済みアカウントや保存済みの認証情報を使う変更は、ここで許可しない限りブロックされます。",
"item": "{{tool}}: {{action}}",
"saveFailed": "保存できませんでした。もう一度お試しください。"
"saveFailed": "保存できませんでした。もう一度お試しください。",
"summaryNone": "まだ何も許可されていません",
"summaryTools_one": "{{tools}} は変更を実行できます",
"summaryTools_other": "{{tools}} は変更を実行できます",
"summaryCount": "{{formatted}} 件中 {{allowed}} 件を許可",
"toolCount": "API、ウィジェット、公開リンクからの変更 · {{formatted}} 件中 {{allowed}} 件を許可",
"toolLabel": "{{tool}} で API、ウィジェット、公開リンクからの変更を許可",
"choice": {
"off": "オフ",
"all": "すべて"
},
"customize_one": "変更を個別に設定",
"customize_other": "{{formatted}} 件を個別に設定",
"fold": "変更を隠す"
},
"actionFailed": "うまくいきませんでした。もう一度お試しください。",
"apiKeyAfterPublish": "APIキーを作成するには、エージェントを公開してください。"
+18 -2
View File
@@ -2018,8 +2018,24 @@
"apiWrites": {
"title": "Действия, которые пользователи API, виджета и публичной ссылки могут выполнить от вашего имени",
"description": "Через API-ключ, виджет или публичную ссылку агента никто не может подтвердить действие за вас, поэтому изменения с вашими подключёнными аккаунтами и сохранёнными учётными данными заблокированы, если вы не разрешите их здесь.",
"item": "{{tool}}: {{action}}",
"saveFailed": "Не удалось сохранить. Попробуйте ещё раз."
"saveFailed": "Не удалось сохранить. Попробуйте ещё раз.",
"summaryNone": "Пока ничего не разрешено",
"summaryTools_one": "{{tools}} может вносить изменения",
"summaryTools_few": "{{tools}} могут вносить изменения",
"summaryTools_many": "{{tools}} могут вносить изменения",
"summaryTools_other": "{{tools}} могут вносить изменения",
"summaryCount": "разрешено {{allowed}} из {{formatted}}",
"toolCount": "Изменения через API, виджет и публичные ссылки · разрешено {{allowed}} из {{formatted}}",
"toolLabel": "Разрешить изменения через API, виджет и публичные ссылки для {{tool}}",
"choice": {
"off": "Выкл.",
"all": "Все"
},
"customize_one": "Настроить изменение",
"customize_few": "Настроить каждое из {{formatted}}",
"customize_many": "Настроить каждое из {{formatted}}",
"customize_other": "Настроить каждое из {{formatted}}",
"fold": "Скрыть изменения"
},
"actionFailed": "Не получилось. Попробуйте ещё раз.",
"apiKeyAfterPublish": "Опубликуйте агента, чтобы создать его API-ключ."
+14 -2
View File
@@ -1923,8 +1923,20 @@
"apiWrites": {
"title": "API、小工具和公開連結使用者能以你的身分執行的操作",
"description": "透過 API 金鑰、小工具或代理的公開連結,無人能替你核准操作,因此除非你在此允許,否則使用你的已連線帳號和已儲存憑證的變更都會被封鎖。",
"item": "{{tool}}:{{action}}",
"saveFailed": "無法儲存。請再試一次。"
"saveFailed": "無法儲存。請再試一次。",
"summaryNone": "尚未允許任何變更",
"summaryTools_one": "{{tools}} 可以進行變更",
"summaryTools_other": "{{tools}} 可以進行變更",
"summaryCount": "已允許 {{formatted}} 項中的 {{allowed}} 項",
"toolCount": "透過 API、小工具和公開連結的變更 · 已允許 {{formatted}} 項中的 {{allowed}} 項",
"toolLabel": "允許 {{tool}} 透過 API、小工具和公開連結進行變更",
"choice": {
"off": "關閉",
"all": "全部"
},
"customize_one": "分別設定 {{formatted}} 項",
"customize_other": "分別設定 {{formatted}} 項",
"fold": "隱藏變更"
},
"actionFailed": "操作未成功,請再試一次。",
"apiKeyAfterPublish": "發布此代理後即可建立其 API 金鑰。"
+12 -2
View File
@@ -1923,8 +1923,18 @@
"apiWrites": {
"title": "API、小组件和公开链接用户能以你的身份执行的操作",
"description": "通过 API 密钥、小组件或智能体的公开链接,无人能替你批准操作,因此除非你在此允许,否则使用你的已连接账号和已保存凭据的更改都会被阻止。",
"item": "{{tool}}:{{action}}",
"saveFailed": "无法保存。请重试。"
"saveFailed": "无法保存。请重试。",
"summaryNone": "尚未允许任何更改",
"summaryTools_other": "{{tools}} 可以进行更改",
"summaryCount": "已允许 {{formatted}} 项中的 {{allowed}} 项",
"toolCount": "通过 API、小组件和公开链接的更改 · 已允许 {{formatted}} 项中的 {{allowed}} 项",
"toolLabel": "允许 {{tool}} 通过 API、小组件和公开链接进行更改",
"choice": {
"off": "关闭",
"all": "全部"
},
"customize_other": "分别设置 {{formatted}} 项",
"fold": "隐藏更改"
},
"actionFailed": "操作未成功,请重试。",
"apiKeyAfterPublish": "发布该代理后即可创建其 API 密钥。"