diff --git a/frontend/src/agents/NewAgent.test.tsx b/frontend/src/agents/NewAgent.test.tsx index 5454337d..b022dc96 100644 --- a/frontend/src/agents/NewAgent.test.tsx +++ b/frontend/src/agents/NewAgent.test.tsx @@ -39,6 +39,7 @@ const mocks = vi.hoisted(() => { >(() => jsonResponse({})), guardrailsProps: vi.fn(), detailsProps: vi.fn(), + shareProps: vi.fn(), reconnect: vi.fn(), }; }); @@ -144,7 +145,12 @@ vi.mock('../modals/AgentDetailsModal', () => ({ return null; }, })); -vi.mock('../teams/ShareToTeamModal', () => ({ default: () => null })); +vi.mock('../teams/ShareToTeamModal', () => ({ + default: (props: unknown) => { + mocks.shareProps(props); + return null; + }, +})); vi.mock('../modals/ConfirmationModal', () => ({ default: ({ modalState, @@ -1082,6 +1088,51 @@ describe('NewAgent gating by role', () => { expect(buttonByText('agents.form.buttons.cancel')).toBeUndefined(); }); + // "What this agent uses" in Share sends the owner to the API write + // allowlist, which opens unfolded; opened any other way it starts folded. + it('opens Access details on the allowlist from Share', async () => { + await renderEdit('owner', OWNER); + const details = () => + mocks.detailsProps.mock.calls.at(-1)![0] as { + modalState: string; + openApiWrites?: boolean; + setModalState: (state: string) => void; + }; + await menuLabels(); + await act(async () => + Array.from(document.querySelectorAll('[role="menuitem"]')) + .find((item) => item.textContent === 'agents.shareWithTeam')! + .click(), + ); + const share = mocks.shareProps.mock.calls.at(-1)![0] as { + onOpenAccessDetails?: () => void; + }; + mocks.shareProps.mockClear(); + await act(async () => share.onOpenAccessDetails!()); + expect(mocks.shareProps).not.toHaveBeenCalled(); + expect(details().modalState).toBe('ACTIVE'); + expect(details().openApiWrites).toBe(true); + await act(async () => details().setModalState('INACTIVE')); + expect(details().openApiWrites).toBe(false); + }); + + it('gives Share no way to Access details without manage_access_details', async () => { + await renderEdit( + 'owner', + OWNER.filter((a) => a !== 'manage_access_details'), + ); + await menuLabels(); + await act(async () => + Array.from(document.querySelectorAll('[role="menuitem"]')) + .find((item) => item.textContent === 'agents.shareWithTeam')! + .click(), + ); + const share = mocks.shareProps.mock.calls.at(-1)![0] as { + onOpenAccessDetails?: () => void; + }; + expect(share.onOpenAccessDetails).toBeUndefined(); + }); + it('hides Access details without manage_access_details', async () => { await renderEdit( 'editor', diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index 10878504..c79c91e8 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -221,6 +221,8 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { const [deleteConfirmation, setDeleteConfirmation] = useState('INACTIVE'); const [agentDetails, setAgentDetails] = useState('INACTIVE'); + // Access details opened from Share to allow changes: its allowlist unfolds. + const [detailsOnApiWrites, setDetailsOnApiWrites] = useState(false); const [shareModalOpen, setShareModalOpen] = useState(false); const [addPromptModal, setAddPromptModal] = useState('INACTIVE'); const [hasChanges, setHasChanges] = useState(false); @@ -1137,6 +1139,9 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { // Page-level actions live in the ⋯ beside the title. Until the agent is // published the preview can only say "Publish to preview", so Preview is a // menu item then and a toolbar button after. + const canOpenAccessDetails = + modeConfig[effectiveMode].showAccessDetails && + can(agent, 'manage_access_details'); const menuOptions = [ ...(isPublished ? [] @@ -1147,8 +1152,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { onClick: () => setPreviewOpen(true), }, ]), - ...(modeConfig[effectiveMode].showAccessDetails && - can(agent, 'manage_access_details') + ...(canOpenAccessDetails ? [ { label: t('agents.form.buttons.accessDetails'), @@ -1848,7 +1852,11 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { agent={agent} mode={effectiveMode} modalState={agentDetails} - setModalState={setAgentDetails} + setModalState={(state) => { + setAgentDetails(state); + if (state === 'INACTIVE') setDetailsOnApiWrites(false); + }} + openApiWrites={detailsOnApiWrites} onKeyRegenerated={(key) => setAgent((prev) => ({ ...prev, key }))} onConfigChange={(config) => { // The allowlist is saved already: record it on the saved snapshot @@ -1871,6 +1879,15 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { resourceId={agent.id} resourceName={agent.name} onClose={() => setShareModalOpen(false)} + onOpenAccessDetails={ + canOpenAccessDetails + ? () => { + setShareModalOpen(false); + setDetailsOnApiWrites(true); + setAgentDetails('ACTIVE'); + } + : undefined + } /> )} {uploadModalState === 'ACTIVE' && ( diff --git a/frontend/src/agents/components/AgentUsesSection.test.tsx b/frontend/src/agents/components/AgentUsesSection.test.tsx index 98db5974..afabf5fb 100644 --- a/frontend/src/agents/components/AgentUsesSection.test.tsx +++ b/frontend/src/agents/components/AgentUsesSection.test.tsx @@ -94,10 +94,20 @@ describe('AgentUsesSection', () => { container.remove(); }); - const render = async (agent: Agent | null, readerId = 'me') => { + const render = async ( + agent: Agent | null, + readerId = 'me', + onOpenAccessDetails?: () => void, + ) => { getAgent.mockResolvedValue(agent ? ok(agent) : { ok: false }); act(() => { - root.render(); + root.render( + , + ); }); await flush(); }; @@ -449,6 +459,71 @@ describe('AgentUsesSection', () => { expect(container.querySelector('[data-slot="alert"]')).toBeNull(); }); + // "Allow" on a connector is the in-chat permission; the badge is about + // the agent's own API write allowlist, so only that list clears it. + it('drops the badge and the note once the write is on the allowlist', async () => { + const linear = item({ + id: 'lin', + name: 'Linear', + connection: { id: 'c1', connector_key: 'mcp:linear', name: 'Linear' }, + credential_mode: 'owner', + owner_credential_writes: ['create_issue'], + }); + await render(agentWith([linear])); + await open(); + expect(rowOf('Linear')?.textContent).toContain(`${K}.writesOff`); + + act(() => root.unmount()); + root = createRoot(container); + await render( + agentWith([linear], { + config: { api_write_allowlist: ['LIN:create_issue'] }, + }), + ); + await open(); + expect(rowOf('Linear')?.textContent).not.toContain(`${K}.writes`); + expect(container.querySelector('[data-slot="alert"]')).toBeNull(); + }); + + it('opens Access details from the note for the owner', async () => { + const openDetails = vi.fn(); + await render( + agentWith([ + item({ id: 't1', name: 'Blocked', owner_credential_writes: ['x'] }), + ]), + 'me', + openDetails, + ); + await open(); + const button = Array.from( + container.querySelectorAll( + '[data-slot="alert"] button', + ), + ).find((b) => b.textContent === `${K}.openAccessDetails`); + expect(button).toBeDefined(); + act(() => button!.click()); + expect(openDetails).toHaveBeenCalledTimes(1); + }); + + it('offers no Access details button to an editor or without a way there', async () => { + const blocked = [ + item({ id: 't1', name: 'Blocked', owner_credential_writes: ['x'] }), + ]; + await render( + agentWith(blocked, { access: 'editor', allowed_actions: ['edit'] }), + 'me', + vi.fn(), + ); + await open(); + expect(container.querySelector('[data-slot="alert"] button')).toBeNull(); + + act(() => root.unmount()); + root = createRoot(container); + await render(agentWith(blocked)); + await open(); + expect(container.querySelector('[data-slot="alert"] button')).toBeNull(); + }); + it('includes the resources of a workflow agent’s nodes', async () => { getWorkflow.mockResolvedValue( ok({ diff --git a/frontend/src/agents/components/AgentUsesSection.tsx b/frontend/src/agents/components/AgentUsesSection.tsx index 7b6e8cfa..605e525c 100644 --- a/frontend/src/agents/components/AgentUsesSection.tsx +++ b/frontend/src/agents/components/AgentUsesSection.tsx @@ -25,6 +25,12 @@ type AgentUsesSectionProps = { agentId: string; /** The reader's user id, to say "your" for what runs as them. */ readerId?: string; + /** + * Opens the agent's Access details, where its owner allows changes + * through the API, widget and public links. Without it the note only + * says where to go. + */ + onOpenAccessDetails?: () => void; }; const TYPE_ICONS: Record = { @@ -56,11 +62,14 @@ function blockedWrites(item: ResourceState, allowlist: string[]): string[] { * 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. + * admin allows no changes through. That allowlist is not the connector's + * Allow (the in-chat permission), so the note says so and, for the owner, + * opens Access details where the list is set. */ export default function AgentUsesSection({ agentId, readerId, + onOpenAccessDetails, }: AgentUsesSectionProps) { const { t } = useTranslation(); const loaded = useAgentResourceStates(agentId); @@ -222,6 +231,20 @@ export default function AgentUsesSection({ {writesNote} + {/* Only the owner sets the allowlist. */} + {ownerReads && onOpenAccessDetails && ( +
+ +
+ )}
)} diff --git a/frontend/src/agents/workflow/WorkflowBuilder.tsx b/frontend/src/agents/workflow/WorkflowBuilder.tsx index bb47c51a..ac1ad84d 100644 --- a/frontend/src/agents/workflow/WorkflowBuilder.tsx +++ b/frontend/src/agents/workflow/WorkflowBuilder.tsx @@ -270,6 +270,8 @@ function WorkflowBuilderInner() { const [deleteConfirmation, setDeleteConfirmation] = useState('INACTIVE'); const [agentDetails, setAgentDetails] = useState('INACTIVE'); + // Access details opened from Share to allow changes: its allowlist unfolds. + const [detailsOnApiWrites, setDetailsOnApiWrites] = useState(false); const [shareModalOpen, setShareModalOpen] = useState(false); const [isDeletingAgent, setIsDeletingAgent] = useState(false); const [currentAgent, setCurrentAgent] = useState( @@ -2197,6 +2199,15 @@ function WorkflowBuilderInner() { resourceId={effectiveAgentId} resourceName={workflowName} onClose={() => setShareModalOpen(false)} + onOpenAccessDetails={ + canManageAgent && can(currentAgent, 'manage_access_details') + ? () => { + setShareModalOpen(false); + setDetailsOnApiWrites(true); + setAgentDetails('ACTIVE'); + } + : undefined + } /> )} {canManageAgent && ( @@ -2204,7 +2215,11 @@ function WorkflowBuilderInner() { agent={agentForDetails} mode="edit" modalState={agentDetails} - setModalState={setAgentDetails} + setModalState={(state) => { + setAgentDetails(state); + if (state === 'INACTIVE') setDetailsOnApiWrites(false); + }} + openApiWrites={detailsOnApiWrites} onKeyRegenerated={(key) => setCurrentAgent((prev) => ({ ...prev, key })) } diff --git a/frontend/src/locale/de.json b/frontend/src/locale/de.json index 0f28da29..5718f930 100644 --- a/frontend/src/locale/de.json +++ b/frontend/src/locale/de.json @@ -767,9 +767,9 @@ "title": "Was dieser Agent nutzt", "intro": "Alle, die diesen Agenten nutzen, erhalten diese Elemente, jeweils mit dem angezeigten Zugriff.", "stopped": "Gestoppt", - "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.", + "writesOff": "Nicht über API", + "writesNote": "„Erlauben“ bei einem Connector gilt für Chats. Mit „Nicht über API“ markierte Änderungen bleiben für API-, Widget- und Link-Nutzer aus, bis du sie unter Zugangsdaten erlaubst.", + "writesNoteEditor": "„Erlauben“ bei einem Connector gilt für Chats. Mit „Nicht über API“ markierte Änderungen bleiben für API-, Widget- und Link-Nutzer aus, bis der Eigentümer sie unter Zugangsdaten erlaubt.", "access": { "you": "Dein Zugriff", "person": "Zugriff von {{person}}", @@ -789,11 +789,12 @@ "personCredentials": "Gespeicherte Zugangsdaten von {{person}}", "otherCredentials": "Gespeicherte Zugangsdaten einer anderen Person" }, - "writesSomeOff": "Einige API-Änderungen aus", + "writesSomeOff": "Teils nicht über API", "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." + "writesNoteApi": "„Erlauben“ bei einem Connector gilt für Chats. Mit „Nicht über API“ markierte Änderungen bleiben für API- und Widget-Nutzer aus, bis du sie unter Zugangsdaten erlaubst.", + "writesNoteApiEditor": "„Erlauben“ bei einem Connector gilt für Chats. Mit „Nicht über API“ markierte Änderungen bleiben für API- und Widget-Nutzer aus, bis 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.", + "openAccessDetails": "Zugangsdaten öffnen" } }, "memberCount_one": "{{formatted}} Mitglied", @@ -1462,7 +1463,7 @@ "saveFailed": "Die Berechtigung konnte nicht gespeichert werden. Versuche es erneut.", "groupLabel": "Berechtigung für alles unter {{group}}", "fold": "Aktionen ausblenden", - "hint": "Vorher fragen: Im Chat fragt DocsGPT dich, bevor die Aktion ausgeführt wird. Über die API oder das Widget eines Agenten kann niemand antworten, daher werden diese Aktionen abgelehnt, außer die API-Schreibfreigabe des Agenten enthält sie.", + "hint": "„Erlauben“ und „Vorher fragen“ gelten im Chat; bei „Vorher fragen“ fragt DocsGPT dich, bevor die Aktion ausgeführt wird. Änderungen über die API, das Widget oder den öffentlichen Link eines Agenten erlaubst du pro Agent unter seinen Zugangsdaten.", "actionCount_one": "{{formatted}} Aktion", "actionCount_other": "{{formatted}} Aktionen", "customize_one": "Aktion einzeln festlegen", diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index b31a9ef6..7f2a1d9c 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -773,9 +773,9 @@ "title": "What this agent uses", "intro": "Everyone who uses this agent gets these, each with the access shown.", "stopped": "Stopped", - "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.", + "writesOff": "Not via API", + "writesNote": "“Allow” on a connector applies to chats. Changes marked “Not via API” stay off for API, widget and public-link users until you allow them in Access Details.", + "writesNoteEditor": "“Allow” on a connector applies to chats. Changes marked “Not via API” stay off for API, widget and public-link users until the owner allows them in Access Details.", "access": { "you": "Your access", "person": "{{person}}'s access", @@ -795,11 +795,12 @@ "personCredentials": "{{person}}'s saved credentials", "otherCredentials": "Someone else's saved credentials" }, - "writesSomeOff": "Some API changes off", + "writesSomeOff": "Some not via API", "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." + "writesNoteApi": "“Allow” on a connector applies to chats. Changes marked “Not via API” stay off for API and widget users until you allow them in Access Details.", + "writesNoteApiEditor": "“Allow” on a connector applies to chats. Changes marked “Not via API” stay off for API and widget users 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.", + "openAccessDetails": "Open Access Details" } }, "memberCount_one": "{{formatted}} member", @@ -1468,7 +1469,7 @@ "saveFailed": "Could not save the permission. Try again.", "groupLabel": "Permission for all of {{group}}", "fold": "Hide actions", - "hint": "Ask first: in chat, DocsGPT asks you before running the action. Over an agent's API or widget nobody can answer, so those actions are refused unless the agent's API write allowlist includes them.", + "hint": "“Allow” and “Ask first” apply in chat; with “Ask first”, DocsGPT asks you before running the action. Changes through an agent's API, widget or public link are allowed per agent, in its Access Details.", "actionCount_one": "{{formatted}} action", "actionCount_other": "{{formatted}} actions", "customize_one": "Customize the action", diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index f70bf654..d1d8bc8c 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -767,9 +767,9 @@ "title": "Qué usa este agente", "intro": "Todos los que usan este agente reciben estos elementos, cada uno con el acceso indicado.", "stopped": "Detenido", - "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.", + "writesOff": "No por API", + "writesNote": "«Permitir» en un conector se aplica a los chats. Los cambios marcados «No por API» siguen desactivados para los usuarios de la API, el widget y el enlace público hasta que los permitas en Access Details.", + "writesNoteEditor": "«Permitir» en un conector se aplica a los chats. Los cambios marcados «No por API» siguen desactivados para los usuarios de la API, el widget y el enlace público hasta que el propietario los permita en Access Details.", "access": { "you": "Tu acceso", "person": "Acceso de {{person}}", @@ -789,11 +789,12 @@ "personCredentials": "Credenciales guardadas de {{person}}", "otherCredentials": "Credenciales guardadas de otra persona" }, - "writesSomeOff": "Algunos cambios por API desactivados", + "writesSomeOff": "Algunos no por API", "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." + "writesNoteApi": "«Permitir» en un conector se aplica a los chats. Los cambios marcados «No por API» siguen desactivados para los usuarios de la API y el widget hasta que los permitas en Access Details.", + "writesNoteApiEditor": "«Permitir» en un conector se aplica a los chats. Los cambios marcados «No por API» siguen desactivados para los usuarios de la API y el widget 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.", + "openAccessDetails": "Abrir Access Details" } }, "memberCount_one": "{{formatted}} miembro", @@ -1462,7 +1463,7 @@ "saveFailed": "No se pudo guardar el permiso. Inténtalo de nuevo.", "groupLabel": "Permiso para todo en {{group}}", "fold": "Ocultar acciones", - "hint": "Preguntar antes: en el chat, DocsGPT te pregunta antes de ejecutar la acción. En la API o el widget de un agente nadie puede responder, así que esas acciones se rechazan salvo que la lista de escrituras por API del agente las incluya.", + "hint": "«Permitir» y «Preguntar antes» se aplican en el chat; con «Preguntar antes», DocsGPT te pregunta antes de ejecutar la acción. Los cambios por la API, el widget o el enlace público de un agente se permiten por agente, en su Access Details.", "actionCount_one": "{{formatted}} acción", "actionCount_other": "{{formatted}} acciones", "customize_one": "Ajustar la acción", diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 729d598a..68becf28 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -766,9 +766,9 @@ "title": "このエージェントが使うもの", "intro": "このエージェントを使うすべての人が、表示されたアクセスでこれらを使います。", "stopped": "停止中", - "writesOff": "API からの変更はオフ", - "writesNote": "API、ウィジェット、公開リンクの利用者は、ここで示された変更を行えません。Access Details で許可すると行えます。", - "writesNoteEditor": "API、ウィジェット、公開リンクの利用者は、ここで示された変更を行えません。オーナーが Access Details で許可すると行えます。", + "writesOff": "API 経由は不可", + "writesNote": "コネクタの「許可」はチャットに適用されます。「API 経由は不可」と示された変更は、Access Details で許可するまで、API、ウィジェット、公開リンクの利用者には行えません。", + "writesNoteEditor": "コネクタの「許可」はチャットに適用されます。「API 経由は不可」と示された変更は、オーナーが Access Details で許可するまで、API、ウィジェット、公開リンクの利用者には行えません。", "access": { "you": "あなたのアクセス", "person": "{{person}} のアクセス", @@ -788,11 +788,12 @@ "personCredentials": "{{person}} の保存済み認証情報", "otherCredentials": "他の人の保存済み認証情報" }, - "writesSomeOff": "一部の API からの変更はオフ", + "writesSomeOff": "一部 API 経由は不可", "adminOff": "管理者が変更をオフにしています", - "writesNoteApi": "API とウィジェットの利用者は、ここで示された変更を行えません。Access Details で許可すると行えます。", - "writesNoteApiEditor": "API とウィジェットの利用者は、ここで示された変更を行えません。オーナーが Access Details で許可すると行えます。", - "writesNoteMemberTail": "各自のアカウントを使うツールでは、公開リンクの利用者は自分のアカウントで変更します。" + "writesNoteApi": "コネクタの「許可」はチャットに適用されます。「API 経由は不可」と示された変更は、Access Details で許可するまで、API とウィジェットの利用者には行えません。", + "writesNoteApiEditor": "コネクタの「許可」はチャットに適用されます。「API 経由は不可」と示された変更は、オーナーが Access Details で許可するまで、API とウィジェットの利用者には行えません。", + "writesNoteMemberTail": "各自のアカウントを使うツールでは、公開リンクの利用者は自分のアカウントで変更します。", + "openAccessDetails": "Access Details を開く" } }, "memberCount_one": "{{formatted}}人のメンバー", @@ -1461,7 +1462,7 @@ "saveFailed": "権限を保存できませんでした。もう一度お試しください。", "groupLabel": "{{group}} すべての権限", "fold": "アクションを隠す", - "hint": "事前に確認: チャットでは、DocsGPT がアクションの実行前に確認します。エージェントの API やウィジェットでは誰も応答できないため、エージェントの API 書き込み許可リストに含まれていない限り、これらのアクションは拒否されます。", + "hint": "「許可」と「事前に確認」はチャットに適用されます。「事前に確認」では、DocsGPT がアクションの実行前に確認します。エージェントの API、ウィジェット、公開リンクからの変更は、エージェントごとに Access Details で許可します。", "actionCount_other": "{{formatted}} 件のアクション", "customize_other": "{{formatted}} 件を個別に設定" }, diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index a7e2703b..c0f599ec 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -811,9 +811,9 @@ "title": "Что использует этот агент", "intro": "Все, кто пользуется этим агентом, получают это, каждое — с указанным доступом.", "stopped": "Остановлено", - "writesOff": "Изменения через API выключены", - "writesNote": "Пользователи API, виджета и публичной ссылки не могут вносить отмеченные здесь изменения, пока вы не разрешите их в Access Details.", - "writesNoteEditor": "Пользователи API, виджета и публичной ссылки не могут вносить отмеченные здесь изменения, пока владелец не разрешит их в Access Details.", + "writesOff": "Не через API", + "writesNote": "«Разрешать» у коннектора действует в чатах. Изменения с отметкой «Не через API» остаются выключены для пользователей API, виджета и публичной ссылки, пока вы не разрешите их в Access Details.", + "writesNoteEditor": "«Разрешать» у коннектора действует в чатах. Изменения с отметкой «Не через API» остаются выключены для пользователей API, виджета и публичной ссылки, пока владелец не разрешит их в Access Details.", "access": { "you": "Ваш доступ", "person": "Доступ {{person}}", @@ -833,11 +833,12 @@ "personCredentials": "Сохранённые учётные данные {{person}}", "otherCredentials": "Сохранённые учётные данные другого пользователя" }, - "writesSomeOff": "Часть изменений через API выключена", + "writesSomeOff": "Частично не через API", "adminOff": "Изменения выключены администратором", - "writesNoteApi": "Пользователи API и виджета не могут вносить отмеченные здесь изменения, пока вы не разрешите их в Access Details.", - "writesNoteApiEditor": "Пользователи API и виджета не могут вносить отмеченные здесь изменения, пока владелец не разрешит их в Access Details.", - "writesNoteMemberTail": "В инструментах, где у каждого свой аккаунт, пользователи публичной ссылки вносят изменения через свой аккаунт." + "writesNoteApi": "«Разрешать» у коннектора действует в чатах. Изменения с отметкой «Не через API» остаются выключены для пользователей API и виджета, пока вы не разрешите их в Access Details.", + "writesNoteApiEditor": "«Разрешать» у коннектора действует в чатах. Изменения с отметкой «Не через API» остаются выключены для пользователей API и виджета, пока владелец не разрешит их в Access Details.", + "writesNoteMemberTail": "В инструментах, где у каждого свой аккаунт, пользователи публичной ссылки вносят изменения через свой аккаунт.", + "openAccessDetails": "Открыть Access Details" } }, "memberCount_one": "{{formatted}} участник", @@ -1526,7 +1527,7 @@ "saveFailed": "Не удалось сохранить разрешение. Попробуйте ещё раз.", "groupLabel": "Разрешение для всего в группе «{{group}}»", "fold": "Скрыть действия", - "hint": "Спрашивать: в чате DocsGPT спросит вас перед выполнением действия. Через API или виджет агента ответить некому, поэтому такие действия отклоняются, если их нет в списке разрешённых записей API агента.", + "hint": "«Разрешать» и «Спрашивать» действуют в чате: при «Спрашивать» DocsGPT спросит вас перед выполнением действия. Изменения через API, виджет или публичную ссылку агента разрешаются для каждого агента отдельно, в его Access Details.", "actionCount_one": "{{formatted}} действие", "actionCount_few": "{{formatted}} действия", "actionCount_many": "{{formatted}} действий", diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index ab5bcafc..6c4d0241 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -766,9 +766,9 @@ "title": "此代理使用的內容", "intro": "使用此代理的每個人都會用到這些內容,各自依所示的存取權限執行。", "stopped": "已停止", - "writesOff": "API 變更已關閉", - "writesNote": "在你於 Access Details 中允許之前,API、小工具和公開連結的使用者無法進行此處標示的變更。", - "writesNoteEditor": "在擁有者於 Access Details 中允許之前,API、小工具和公開連結的使用者無法進行此處標示的變更。", + "writesOff": "無法透過 API", + "writesNote": "連接器上的「允許」適用於聊天。標示為「無法透過 API」的變更,在你於 Access Details 中允許之前,API、小工具和公開連結的使用者無法進行。", + "writesNoteEditor": "連接器上的「允許」適用於聊天。標示為「無法透過 API」的變更,在擁有者於 Access Details 中允許之前,API、小工具和公開連結的使用者無法進行。", "access": { "you": "你的存取權限", "person": "{{person}} 的存取權限", @@ -788,11 +788,12 @@ "personCredentials": "{{person}} 儲存的憑證", "otherCredentials": "其他人儲存的憑證" }, - "writesSomeOff": "部分 API 變更已關閉", + "writesSomeOff": "部分無法透過 API", "adminOff": "管理員已關閉變更", - "writesNoteApi": "在你於 Access Details 中允許之前,API 和小工具的使用者無法進行此處標示的變更。", - "writesNoteApiEditor": "在擁有者於 Access Details 中允許之前,API 和小工具的使用者無法進行此處標示的變更。", - "writesNoteMemberTail": "在每個人使用自己帳號的工具上,公開連結的使用者以自己的帳號進行變更。" + "writesNoteApi": "連接器上的「允許」適用於聊天。標示為「無法透過 API」的變更,在你於 Access Details 中允許之前,API 和小工具的使用者無法進行。", + "writesNoteApiEditor": "連接器上的「允許」適用於聊天。標示為「無法透過 API」的變更,在擁有者於 Access Details 中允許之前,API 和小工具的使用者無法進行。", + "writesNoteMemberTail": "在每個人使用自己帳號的工具上,公開連結的使用者以自己的帳號進行變更。", + "openAccessDetails": "開啟 Access Details" } }, "memberCount_one": "{{formatted}} 位成員", @@ -1461,7 +1462,7 @@ "saveFailed": "無法儲存權限。請再試一次。", "groupLabel": "{{group}} 的全部權限", "fold": "隱藏動作", - "hint": "先詢問:在聊天中,DocsGPT 會在執行動作前詢問你。透過代理的 API 或小工具時沒有人能回答,因此除非代理的 API 寫入允許清單包含這些動作,否則它們會被拒絕。", + "hint": "「允許」和「先詢問」適用於聊天:選擇「先詢問」時,DocsGPT 會在執行動作前詢問你。透過代理的 API、小工具或公開連結進行的變更,需在該代理的 Access Details 中逐一代理允許。", "actionCount_other": "{{formatted}} 個動作", "customize_other": "分別設定 {{formatted}} 個" }, diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index 42153a37..457c7338 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -766,9 +766,9 @@ "title": "此智能体使用的内容", "intro": "使用此智能体的每个人都会用到这些内容,各自按所示的访问权限运行。", "stopped": "已停止", - "writesOff": "API 更改已关闭", - "writesNote": "在你于 Access Details 中允许之前,API、小组件和公开链接的用户无法进行此处标记的更改。", - "writesNoteEditor": "在所有者于 Access Details 中允许之前,API、小组件和公开链接的用户无法进行此处标记的更改。", + "writesOff": "不可通过 API", + "writesNote": "连接器上的“允许”适用于聊天。标记为“不可通过 API”的更改,在你于 Access Details 中允许之前,API、小组件和公开链接的用户无法进行。", + "writesNoteEditor": "连接器上的“允许”适用于聊天。标记为“不可通过 API”的更改,在所有者于 Access Details 中允许之前,API、小组件和公开链接的用户无法进行。", "access": { "you": "你的访问权限", "person": "{{person}} 的访问权限", @@ -788,11 +788,12 @@ "personCredentials": "{{person}} 保存的凭据", "otherCredentials": "其他人保存的凭据" }, - "writesSomeOff": "部分 API 更改已关闭", + "writesSomeOff": "部分不可通过 API", "adminOff": "管理员已关闭更改", - "writesNoteApi": "在你于 Access Details 中允许之前,API 和小组件的用户无法进行此处标记的更改。", - "writesNoteApiEditor": "在所有者于 Access Details 中允许之前,API 和小组件的用户无法进行此处标记的更改。", - "writesNoteMemberTail": "在每个人使用自己账号的工具上,公开链接的用户用自己的账号进行更改。" + "writesNoteApi": "连接器上的“允许”适用于聊天。标记为“不可通过 API”的更改,在你于 Access Details 中允许之前,API 和小组件的用户无法进行。", + "writesNoteApiEditor": "连接器上的“允许”适用于聊天。标记为“不可通过 API”的更改,在所有者于 Access Details 中允许之前,API 和小组件的用户无法进行。", + "writesNoteMemberTail": "在每个人使用自己账号的工具上,公开链接的用户用自己的账号进行更改。", + "openAccessDetails": "打开 Access Details" } }, "memberCount_one": "{{formatted}} 名成员", @@ -1461,7 +1462,7 @@ "saveFailed": "无法保存权限。请重试。", "groupLabel": "{{group}} 的全部权限", "fold": "隐藏操作", - "hint": "先询问:在聊天中,DocsGPT 会在执行操作前询问你。通过智能体的 API 或小组件时没有人能回答,因此除非智能体的 API 写入允许列表包含这些操作,否则它们会被拒绝。", + "hint": "“允许”和“先询问”适用于聊天:选择“先询问”时,DocsGPT 会在执行操作前询问你。通过智能体的 API、小组件或公开链接进行的更改,需在该智能体的 Access Details 中逐个智能体允许。", "actionCount_other": "{{formatted}} 个操作", "customize_other": "分别设置 {{formatted}} 个" }, diff --git a/frontend/src/modals/AgentDetailsModal.test.tsx b/frontend/src/modals/AgentDetailsModal.test.tsx index 603e9800..ad3d57e9 100644 --- a/frontend/src/modals/AgentDetailsModal.test.tsx +++ b/frontend/src/modals/AgentDetailsModal.test.tsx @@ -20,7 +20,9 @@ vi.mock('../api/services/userService', () => ({ default: mocks })); // Tested on its own; it needs the store and the tool list. Here it only has // to show up (or not). vi.mock('../agents/ApiWriteAllowlist', () => ({ - default: () =>
, + default: ({ defaultOpen }: { defaultOpen?: boolean }) => ( +
+ ), })); vi.mock('./ConfirmationModal', () => ({ @@ -61,7 +63,7 @@ describe('AgentDetailsModal', () => { document.body.innerHTML = ''; }); - const render = async (agent: Partial) => { + const render = async (agent: Partial, openApiWrites?: boolean) => { await act(async () => { root.render( { mode="edit" modalState="ACTIVE" setModalState={() => undefined} + openApiWrites={openApiWrites} />, ); }); @@ -113,6 +116,13 @@ describe('AgentDetailsModal', () => { expect(allowlist()).not.toBeNull(); }); + it('starts it folded, or open when sent to allow changes', async () => { + await render({ status: 'published', key: 'k-1', access: 'owner' }); + expect(allowlist()?.getAttribute('data-open')).toBe('false'); + await render({ status: 'published', key: 'k-1', access: 'owner' }, true); + expect(allowlist()?.getAttribute('data-open')).toBe('true'); + }); + it('hides it from an editor, even with a key', async () => { await render({ status: 'published', diff --git a/frontend/src/modals/AgentDetailsModal.tsx b/frontend/src/modals/AgentDetailsModal.tsx index cf07c912..e8bc6e57 100644 --- a/frontend/src/modals/AgentDetailsModal.tsx +++ b/frontend/src/modals/AgentDetailsModal.tsx @@ -41,6 +41,8 @@ type AgentDetailsModalProps = { onConfigChange?: (config: AgentConfig) => void; /** The agent's last saved config, which an allowlist change saves onto. */ getSavedConfig?: () => AgentConfig | undefined; + /** Open with the API write allowlist unfolded (sent here to allow changes). */ + openApiWrites?: boolean; }; export default function AgentDetailsModal({ @@ -51,6 +53,7 @@ export default function AgentDetailsModal({ onKeyRegenerated, onConfigChange, getSavedConfig, + openApiWrites = false, }: AgentDetailsModalProps) { const { t } = useTranslation(); const token = useSelector(selectToken); @@ -282,6 +285,7 @@ export default function AgentDetailsModal({ agent={agent} onConfigChange={onConfigChange} getSavedConfig={getSavedConfig} + defaultOpen={openApiWrites} /> )}
diff --git a/frontend/src/teams/ShareToTeamModal.tsx b/frontend/src/teams/ShareToTeamModal.tsx index 439bd84f..7a87b7ab 100644 --- a/frontend/src/teams/ShareToTeamModal.tsx +++ b/frontend/src/teams/ShareToTeamModal.tsx @@ -95,6 +95,11 @@ type Props = { resourceName?: string; credentials?: ShareCredentials; onClose: () => void; + /** + * An agent's: opens its Access details (the API write allowlist) from + * "What this agent uses". The caller closes this dialog for it. + */ + onOpenAccessDetails?: () => void; }; // Member subs (OIDC subs) can be long; there's no display-name endpoint, so we @@ -142,6 +147,7 @@ export default function ShareToTeamModal({ resourceName, credentials, onClose, + onOpenAccessDetails, }: Props) { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -1140,7 +1146,11 @@ export default function ShareToTeamModal({ with, for the people it is shared with (owners and editors only: viewers never open this dialog). */} {resourceType === 'agent' && ( - + )} {accessSettings}