From 01da88958d490e5006874cce2b05554347301b9b Mon Sep 17 00:00:00 2001 From: Theodore Nastos <15264814+theonastos@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:09:15 +0000 Subject: [PATCH] feat(editor): Show the restriction callout and lock the agent tool configuration form (#40657) --- .../frontend/@n8n/i18n/src/locales/en.json | 3 + .../__tests__/AgentToolConfigModal.test.ts | 90 ++++++++++++++++++- .../AgentToolConfigNodeContent.test.ts | 11 ++- .../AgentToolsConnectionModalWrapper.test.ts | 16 ++++ .../components/AgentApprovalSelector.vue | 3 + .../AgentToolConfigApprovalSetting.vue | 2 + .../agents/components/AgentToolConfigForm.vue | 25 ++++++ .../AgentToolConfigMcpApprovalSetting.vue | 3 + .../components/AgentToolConfigModal.vue | 27 +++++- .../components/AgentToolConfigNodeContent.vue | 2 + .../AgentToolsConnectionModalWrapper.vue | 11 ++- .../toolConfig/NodeToolSettingsContent.vue | 9 +- .../__tests__/NodeToolSettingsContent.test.ts | 26 +++++- .../shared/toolsConnection/ToolRow.vue | 23 +++-- .../toolsConnection/__tests__/ToolRow.test.ts | 10 +++ .../components/RestrictedToolCallout.test.ts | 51 +++++++++++ .../src/components/RestrictedToolCallout.vue | 39 ++++++++ .../useNodeTypeRestriction.test.ts | 13 +++ .../src/composables/useNodeTypeRestriction.ts | 17 ++++ .../frontend/src/index.ts | 1 + 20 files changed, 358 insertions(+), 24 deletions(-) create mode 100644 packages/modules/type-availability-policies/frontend/src/components/RestrictedToolCallout.test.ts create mode 100644 packages/modules/type-availability-policies/frontend/src/components/RestrictedToolCallout.vue diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index c8d1ceefaa3..27463707a8b 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2544,6 +2544,9 @@ "typeAvailabilityPolicies.restrictedNode.scope.project": "Restricted in this project", "typeAvailabilityPolicies.restrictedNode.popover.description": "An administrator blocked this node. To use it in your workflows, contact an instance administrator for access.", "typeAvailabilityPolicies.restrictedNode.replaceNode": "Replace node", + "typeAvailabilityPolicies.restrictedNode.agentTool.instance": "{nodeType} is restricted on this instance. The agent can't use it.", + "typeAvailabilityPolicies.restrictedNode.agentTool.project": "{nodeType} is restricted in this project. The agent can't use it.", + "typeAvailabilityPolicies.restrictedNode.agentTool.generic": "{nodeType} is restricted. The agent can't use it.", "typeAvailabilityPolicies.contactAdmin.title": "Instance admins", "typeAvailabilityPolicies.contactAdmin.description": "Instance admins manage which nodes are available. Contact one of them to request access to '{nodeType}'.", "typeAvailabilityPolicies.contactAdmin.mailSubject": "Access request for the {nodeType} node", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts index 8056ae06603..b6a10ac9b96 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts @@ -3,11 +3,14 @@ import { createComponentRenderer } from '@/__tests__/render'; import { createTestingPinia } from '@pinia/testing'; import type { AgentConfigValidationIssue } from '@n8n/api-types'; import { mockedStore } from '@/__tests__/utils'; +import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useUIStore } from '@/app/stores/ui.store'; import { fireEvent, waitFor } from '@testing-library/vue'; import { defineComponent, onMounted, nextTick } from 'vue'; import { CREDENTIAL_EDIT_MODAL_KEY } from '@/features/credentials/credentials.constants'; import { AgentModalTestStub } from './utils/AgentModalTestStub'; +import { useTypeAvailabilityPoliciesStore } from '@n8n/frontend-module-type-availability-policies'; +import { mockRestrictedNodeTypes } from '@n8n/frontend-module-type-availability-policies/__tests__/mocks'; import AgentToolConfigModal from '../components/AgentToolConfigModal.vue'; import type { AgentJsonToolRef, CustomToolEntry } from '../types'; @@ -64,6 +67,7 @@ function createToolSettingsStub(emitValid: boolean) { 'projectId', 'parameterIssues', 'fromAiDisabledParameters', + 'readOnly', ], emits: ['update:valid', 'update:node-name', 'update:node'], setup(props, { emit, expose }) { @@ -91,7 +95,11 @@ function createToolSettingsStub(emitValid: boolean) { return {}; }, template: ` -
{{ code }}',
},
+ ContactInstanceAdminModal: {
+ props: ['open', 'nodeTypeName'],
+ template: '',
+ },
},
},
});
@@ -232,6 +244,7 @@ describe('AgentToolConfigModal', () => {
beforeEach(() => {
vi.clearAllMocks();
createTestingPinia({ stubActions: false });
+ vi.spyOn(useNodeTypesStore(), 'loadNodeTypesIfNotLoaded').mockResolvedValue(undefined);
uiStore = mockedStore(useUIStore);
uiStore.openModal(MODAL_NAME);
uiStore.closeModal = vi.fn();
@@ -242,6 +255,12 @@ describe('AgentToolConfigModal', () => {
expect(getByTestId('node-tool-settings-content')).toBeTruthy();
});
+ it('loads the node types so the tool shows its display name and parameters', () => {
+ renderModal();
+
+ expect(useNodeTypesStore().loadNodeTypesIfNotLoaded).toHaveBeenCalledOnce();
+ });
+
it('does not open for a persisted node tool without node data', () => {
const invalidRef = {
type: 'node',
@@ -493,4 +512,69 @@ describe('AgentToolConfigModal', () => {
expect(onRemove).toHaveBeenCalledOnce();
expect(uiStore.closeModal).toHaveBeenCalledWith(MODAL_NAME);
});
+
+ describe('restricted node types', () => {
+ beforeEach(() => {
+ mockRestrictedNodeTypes({ 'n8n-nodes-base.slack': 'instance' });
+ });
+
+ it('loads the policy for the agent project', () => {
+ const fetchForProject = vi
+ .spyOn(useTypeAvailabilityPoliciesStore(), 'fetchForProject')
+ .mockResolvedValue(undefined);
+
+ renderModal({ projectId: 'project-1' });
+
+ expect(fetchForProject).toHaveBeenCalledWith('project-1');
+ });
+
+ it('explains the restriction, locks the form and disables Save', async () => {
+ const { container, getByTestId } = renderModal({ valid: true });
+ await nextTick();
+
+ expect(getByTestId('restricted-tool-callout').textContent).toContain(
+ 'typeAvailabilityPolicies.restrictedNode.agentTool.instance',
+ );
+ expect(getByTestId('node-tool-settings-content')).toHaveAttribute('data-read-only', 'true');
+ expect(getByTestId('agent-tool-approval-toggle')).toBeDisabled();
+ expect(queryNativeTestId(container, 'agent-modal-title-input')).toBeNull();
+
+ const saveBtn = getNativeTestId(container, 'agent-tool-config-save') as HTMLButtonElement;
+ expect(saveBtn.disabled).toBe(true);
+ });
+
+ it('opens the contact admin dialog from the callout', async () => {
+ const { getByTestId, queryByTestId } = renderModal();
+ await nextTick();
+
+ expect(queryByTestId('contact-instance-admin-modal')).toBeNull();
+ await fireEvent.click(getByTestId('restricted-tool-contact-admin'));
+
+ expect(getByTestId('contact-instance-admin-modal')).toBeInTheDocument();
+ });
+
+ it('keeps Remove so the agent can be repaired', async () => {
+ const onRemove = vi.fn();
+ const { container } = renderModal({ onRemove });
+ await nextTick();
+
+ await fireEvent.click(getNativeTestId(container, 'agent-tool-config-remove'));
+
+ expect(onRemove).toHaveBeenCalledOnce();
+ expect(uiStore.closeModal).toHaveBeenCalledWith(MODAL_NAME);
+ });
+
+ it('leaves an unrestricted tool editable', async () => {
+ mockRestrictedNodeTypes();
+ const { container, getByTestId, queryByTestId } = renderModal({ valid: true });
+ await nextTick();
+
+ expect(queryByTestId('restricted-tool-callout')).toBeNull();
+ expect(getByTestId('node-tool-settings-content')).toHaveAttribute('data-read-only', 'false');
+ expect(getByTestId('agent-tool-approval-toggle')).not.toBeDisabled();
+ expect(getNativeTestId(container, 'agent-modal-title-input')).toBeInTheDocument();
+ const saveBtn = getNativeTestId(container, 'agent-tool-config-save') as HTMLButtonElement;
+ expect(saveBtn.disabled).toBe(false);
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigNodeContent.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigNodeContent.test.ts
index dd5f24fe786..45691bd6707 100644
--- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigNodeContent.test.ts
+++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigNodeContent.test.ts
@@ -14,13 +14,14 @@ const renderComponent = createComponentRenderer(AgentToolConfigNodeContent, {
stubs: {
NodeToolSettingsContent: {
template:
- '