diff --git a/dify-agent-runtime/README.md b/dify-agent-runtime/README.md
index 9e08532a5b5..49ecc5890bf 100644
--- a/dify-agent-runtime/README.md
+++ b/dify-agent-runtime/README.md
@@ -78,12 +78,12 @@ The runner automatically creates `$CWD/.tmp` and sets `TMPDIR`, `TMP`, `TEMP` to
### Environment Variables
-| Variable | Default | Description |
-|----------|---------|-------------|
-| `SHELLCTL_ENABLE_PATH_ISOLATION` | `true` | Set to `false` to disable Landlock entirely |
-| `SHELLCTL_LANDLOCK_RW_PATHS` | *(empty)* | Comma-separated RW directories (besides `$HOME`) |
-| `SHELLCTL_LANDLOCK_RO_PATHS` | `/usr,/bin,...` | Comma-separated RO+exec directories |
-| `SHELLCTL_LANDLOCK_RW_DEV_PATHS` | `/dev/null,...` | Comma-separated device files with RW access |
+| Variable | Default | Description |
+| -------------------------------- | --------------- | ------------------------------------------------ |
+| `SHELLCTL_ENABLE_PATH_ISOLATION` | `true` | Set to `false` to disable Landlock entirely |
+| `SHELLCTL_LANDLOCK_RW_PATHS` | _(empty)_ | Comma-separated RW directories (besides `$HOME`) |
+| `SHELLCTL_LANDLOCK_RO_PATHS` | `/usr,/bin,...` | Comma-separated RO+exec directories |
+| `SHELLCTL_LANDLOCK_RW_DEV_PATHS` | `/dev/null,...` | Comma-separated device files with RW access |
Requires Linux ≥ 5.13. On unsupported kernels, a warning is printed to stderr.
diff --git a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx
index 08a9478412a..899ed6f4d6f 100644
--- a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx
+++ b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx
@@ -8,12 +8,13 @@ import { AuthCategory, CredentialTypeEnum } from '../types'
// ==================== Mock Setup ====================
const mockGetPluginCredentialInfo = vi.fn()
+const mockIsPluginCredentialInfoLoading = vi.fn()
const mockGetPluginOAuthClientSchema = vi.fn()
vi.mock('@/service/use-plugins-auth', () => ({
useGetPluginCredentialInfo: (url: string) => ({
data: url ? mockGetPluginCredentialInfo() : undefined,
- isLoading: false,
+ isLoading: mockIsPluginCredentialInfoLoading(),
}),
useDeletePluginCredential: () => ({ mutateAsync: vi.fn() }),
useSetPluginDefaultCredential: () => ({ mutateAsync: vi.fn() }),
@@ -85,6 +86,7 @@ describe('AuthorizedInNode Component', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsCurrentWorkspaceManager.mockReturnValue(true)
+ mockIsPluginCredentialInfoLoading.mockReturnValue(false)
mockGetPluginCredentialInfo.mockReturnValue({
credentials: [createCredential({ is_default: true })],
supported_credential_types: [CredentialTypeEnum.API_KEY],
@@ -163,6 +165,25 @@ describe('AuthorizedInNode Component', () => {
expect(screen.getByText('plugin.auth.authRemoved'))!.toBeInTheDocument()
})
+ it('should not show auth removed while credential info is loading', async () => {
+ const AuthorizedInNode = (await import('../authorized-in-node')).default
+ mockIsPluginCredentialInfoLoading.mockReturnValue(true)
+ mockGetPluginCredentialInfo.mockReturnValue(undefined)
+ const pluginPayload = createPluginPayload()
+
+ render(
+ ,
+ { wrapper: createWrapper() },
+ )
+
+ expect(screen.queryByText('plugin.auth.authRemoved')).not.toBeInTheDocument()
+ expect(screen.getByText('common.loading')).toBeInTheDocument()
+ })
+
it('should show unavailable when credential is not allowed', async () => {
const AuthorizedInNode = (await import('../authorized-in-node')).default
const credential = createCredential({
diff --git a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx
index ad4117c7e6e..1d996ce4b07 100644
--- a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx
+++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx
@@ -26,6 +26,7 @@ const AuthorizedInNode = ({
canApiKey,
canOAuth,
credentials,
+ isLoading,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, true, credentialId ? [credentialId] : undefined)
@@ -53,8 +54,14 @@ const AuthorizedInNode = ({
}
} else {
const credential = credentials.find((c) => c.id === credentialId)
- label = credential ? credential.name : t(($) => $['auth.authRemoved'], { ns: 'plugin' })
- removed = !credential
+ if (credential) label = credential.name
+ else if (isLoading) {
+ label = t(($) => $.loading, { ns: 'common' })
+ color = 'disabled'
+ } else {
+ label = t(($) => $['auth.authRemoved'], { ns: 'plugin' })
+ removed = true
+ }
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
if (removed) color = 'error'
@@ -86,7 +93,7 @@ const AuthorizedInNode = ({
)
},
- [credentialId, credentials, t],
+ [credentialId, credentials, isLoading, t],
)
const defaultUnavailable = credentials.find((c) => c.is_default)?.not_allowed_to_use
const extraAuthorizationItems: Credential[] = [
diff --git a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts
index e38fb021a3f..2ec1f52f0bd 100644
--- a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts
+++ b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts
@@ -10,7 +10,11 @@ export const usePluginAuth = (
enable?: boolean,
includeCredentialIds?: string[],
) => {
- const { data } = useGetPluginCredentialInfoHook(pluginPayload, enable, includeCredentialIds)
+ const { data, isLoading } = useGetPluginCredentialInfoHook(
+ pluginPayload,
+ enable,
+ includeCredentialIds,
+ )
const isAuthorized = !!data?.credentials.length
const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2)
const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY)
@@ -20,6 +24,7 @@ export const usePluginAuth = (
isAuthorized,
canOAuth,
canApiKey,
+ isLoading,
credentials: data?.credentials || [],
notAllowCustomCredential: data?.allow_custom_token === false,
invalidPluginCredentialInfo,
diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx
index 6988574d113..95f5cb78bea 100644
--- a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx
@@ -86,6 +86,7 @@ const mockAppComposerQueryOptions = vi.hoisted(() =>
}
}
}) => number | false
+ retry?: number
}) => {
const { input } = options
@@ -96,18 +97,37 @@ const mockAppComposerQueryOptions = vi.hoisted(() =>
: ['workflow-agent-composer', input.params.app_id, input.params.node_id],
queryFn: typeof input === 'symbol' ? input : mockAppComposerQueryFn,
refetchInterval: options.refetchInterval,
+ retry: options.retry,
}
},
),
)
const mockSnippetComposerQueryOptions = vi.hoisted(() =>
- vi.fn(({ input }: { input: symbol | { params: { snippet_id: string; node_id: string } } }) => ({
- queryKey:
- typeof input === 'symbol'
- ? ['snippet-agent-composer-disabled']
- : ['snippet-agent-composer', input.params.snippet_id, input.params.node_id],
- queryFn: typeof input === 'symbol' ? input : mockSnippetComposerQueryFn,
- })),
+ vi.fn(
+ (options: {
+ input: symbol | { params: { snippet_id: string; node_id: string } }
+ refetchInterval?: (query: {
+ state: {
+ data?: {
+ agent?: unknown
+ }
+ }
+ }) => number | false
+ retry?: number
+ }) => {
+ const { input } = options
+
+ return {
+ queryKey:
+ typeof input === 'symbol'
+ ? ['snippet-agent-composer-disabled']
+ : ['snippet-agent-composer', input.params.snippet_id, input.params.node_id],
+ queryFn: typeof input === 'symbol' ? input : mockSnippetComposerQueryFn,
+ refetchInterval: options.refetchInterval,
+ retry: options.retry,
+ }
+ },
+ ),
)
vi.mock('@langgenius/dify-ui/toast', () => ({
@@ -187,6 +207,10 @@ vi.mock('@/service/client', () => ({
describe('useWorkflowInlineAgentDetail', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockAppComposerQueryFn.mockReset()
+ mockAppComposerQueryFn.mockResolvedValue({ agent: { id: 'app-agent' } })
+ mockSnippetComposerQueryFn.mockReset()
+ mockSnippetComposerQueryFn.mockResolvedValue({ agent: { id: 'snippet-agent' } })
})
it('loads inline agent detail through the snippet composer API', async () => {
@@ -255,6 +279,41 @@ describe('useWorkflowInlineAgentDetail', () => {
})
expect(result.current.data).toEqual({ agent: { id: 'app-agent' } })
})
+
+ it('retries five times and stops polling when loading the copied inline agent composer fails', async () => {
+ mockAppComposerQueryFn.mockRejectedValue(new Error('Agent composer was not created'))
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ retryDelay: 0,
+ },
+ },
+ })
+ const { result } = renderWorkflowHook(
+ () =>
+ useWorkflowInlineAgentDetail('copied-node', 'source-inline-agent', {
+ pollUntilReady: true,
+ }),
+ {
+ queryClient,
+ hooksStoreProps: {
+ configsMap: {
+ flowId: 'app-1',
+ flowType: FlowType.appFlow,
+ fileSettings: {} as never,
+ },
+ },
+ },
+ )
+
+ await waitFor(() => expect(result.current.isError).toBe(true))
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1200))
+ })
+
+ expect(mockAppComposerQueryFn).toHaveBeenCalledTimes(6)
+ })
})
describe('useCreateInlineAgentBinding', () => {
diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx
index 5829a6e34af..ffd30bcc469 100644
--- a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx
@@ -1023,6 +1023,37 @@ describe('agent/panel', () => {
).not.toBeInTheDocument()
})
+ it('shows a retry action when loading the inline agent composer fails', () => {
+ mockUseWorkflowInlineAgentDetail.mockReturnValue({
+ data: undefined,
+ isError: true,
+ isFetching: false,
+ refetch: mockWorkflowInlineAgentDetailRefetch,
+ })
+
+ const { container } = render(
+ ,
+ )
+
+ expect(container.querySelector('[aria-busy="true"]')).not.toBeInTheDocument()
+ expect(screen.getByRole('alert')).toHaveTextContent(
+ 'agentV2.roster.nodeSelector.createInlineFailed',
+ )
+
+ fireEvent.click(screen.getByRole('button', { name: 'common.operation.retry' }))
+ expect(mockWorkflowInlineAgentDetailRefetch).toHaveBeenCalledTimes(1)
+ })
+
it('recovers the inline setup panel open state from the node open marker', () => {
mockUseWorkflowInlineAgentDetail.mockReturnValue({ data: undefined })
diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx
index 69d28cafd89..7fbdc9d97d7 100644
--- a/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx
@@ -329,11 +329,13 @@ export function AgentRosterField({
agent,
agentId,
canOpenPanel = true,
+ errorMessage,
isPanelOpen,
isPanelCopyPending = false,
isPending = false,
isLoading = false,
isInlineSetup = false,
+ isRetrying = false,
panelBody,
panelMode = 'detail',
showPanelDetailActions = true,
@@ -341,16 +343,19 @@ export function AgentRosterField({
onChange,
onMakeCopy,
onPanelOpenChange,
+ onRetry,
onSaveInlineToRoster,
onStartFromScratch,
}: {
agent?: AgentRosterDisplayData
agentId?: string
canOpenPanel?: boolean
+ errorMessage?: string
isPanelOpen?: boolean
isPanelCopyPending?: boolean
isLoading?: boolean
isInlineSetup?: boolean
+ isRetrying?: boolean
isPending?: boolean
panelBody?: ReactNode
panelMode?: AgentRosterDrawerMode
@@ -359,6 +364,7 @@ export function AgentRosterField({
onChange: (agent: AgentRosterNodeData) => void
onMakeCopy?: () => void
onPanelOpenChange?: (open: boolean) => void
+ onRetry?: () => void
onSaveInlineToRoster?: () => void
onStartFromScratch?: () => void
}) {
@@ -464,7 +470,30 @@ export function AgentRosterField({
- {agent ? (
+ {errorMessage ? (
+
+
+
+
+
+ {errorMessage}
+
+ {onRetry && (
+
+ )}
+
+ ) : agent ? (
canOpenPanel ? (
<>
{isInlineSetup ? (
diff --git a/web/app/components/workflow/nodes/agent-v2/hooks.ts b/web/app/components/workflow/nodes/agent-v2/hooks.ts
index 605a77f6cb5..8a739f8c82d 100644
--- a/web/app/components/workflow/nodes/agent-v2/hooks.ts
+++ b/web/app/components/workflow/nodes/agent-v2/hooks.ts
@@ -21,6 +21,7 @@ type CreateInlineAgentBindingOptions = {
}
const INLINE_AGENT_CREATION_REFETCH_INTERVAL = 1000
+const INLINE_AGENT_CREATION_RETRY_COUNT = 5
export function useAgentRosterDetail(agentId?: string) {
return useQuery(
@@ -46,13 +47,18 @@ export function useWorkflowInlineAgentDetail(
const configsMap = useHooksStore((state) => state.configsMap)
const refetchUntilReady = options?.pollUntilReady
? {
+ retry: INLINE_AGENT_CREATION_RETRY_COUNT,
refetchInterval: (query: {
state: {
data?: {
agent?: unknown
}
+ status: 'error' | 'pending' | 'success'
}
- }) => (query.state.data?.agent ? false : INLINE_AGENT_CREATION_REFETCH_INTERVAL),
+ }) =>
+ query.state.status === 'error' || query.state.data?.agent
+ ? false
+ : INLINE_AGENT_CREATION_REFETCH_INTERVAL,
}
: {}
diff --git a/web/app/components/workflow/nodes/agent-v2/panel.tsx b/web/app/components/workflow/nodes/agent-v2/panel.tsx
index 6941b1125ba..d8984fb9f8d 100644
--- a/web/app/components/workflow/nodes/agent-v2/panel.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/panel.tsx
@@ -159,7 +159,9 @@ export function AgentV2Panel({ id, data }: NodePanelProps) {
? inlineAgentBinding.agent_id
: sourceInlineAgentId
const isInlineAgentCreated = isInlineAgentReady && !!inlineAgent
- const isInlineAgentWaitingForCreation = isInlineAgentReady && !isInlineAgentCreated
+ const isInlineAgentLoadError = isInlineAgentReady && inlineAgentQuery.isError
+ const isInlineAgentWaitingForCreation =
+ isInlineAgentReady && !isInlineAgentCreated && !isInlineAgentLoadError
const isInlineAgentPanelOpen =
(isInlineAgentCreated || isInlineAgentPending) && openInlineAgentPanelNodeId === id
const { isPending: isAppCopyingFromRoster, mutate: copyFromRosterApp } = useMutation(
@@ -619,14 +621,22 @@ export function AgentV2Panel({ id, data }: NodePanelProps) {
/>
$['roster.nodeSelector.createInlineFailed'], { ns: 'agentV2' })
+ : undefined
+ }
isInlineSetup={isInlineAgentReady || isInlineAgentPending}
isLoading={isInlineAgentLoading}
isPanelCopyPending={isCopyingFromRoster}
isPanelOpen={isAgentPanelOpen}
isPending={isAgentBindingPending}
+ isRetrying={isInlineAgentLoadError && inlineAgentQuery.isFetching}
panelBody={
isAgentPanelOpen && displayedAgent ? (
isInlineAgentReady || isInlineAgentPending ? (
@@ -662,6 +672,7 @@ export function AgentV2Panel({ id, data }: NodePanelProps) {
onChange={handleRosterChange}
onMakeCopy={rosterAgentId ? handleMakeRosterCopy : undefined}
onPanelOpenChange={handleAgentPanelOpenChange}
+ onRetry={isInlineAgentLoadError ? () => void inlineAgentQuery.refetch() : undefined}
onSaveInlineToRoster={canSaveInlineToRoster ? handleSaveInlineToRosterOpen : undefined}
onStartFromScratch={canStartFromScratch ? handleStartFromScratch : undefined}
/>
diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/model-compatibility.spec.ts b/web/features/agent-v2/agent-detail/configure/__tests__/model-compatibility.spec.ts
index 296fd60ad7f..25372c6224d 100644
--- a/web/features/agent-v2/agent-detail/configure/__tests__/model-compatibility.spec.ts
+++ b/web/features/agent-v2/agent-detail/configure/__tests__/model-compatibility.spec.ts
@@ -176,6 +176,10 @@ describe('isAgentSuggestedModel', () => {
expect(isAgentSuggestedModel(provider, createModelItem('gpt-5.5'))).toBe(true)
expect(isAgentSuggestedModel(provider, createModelItem('gpt-5.5-pro'))).toBe(true)
+ expect(isAgentSuggestedModel(provider, createModelItem('gpt-5.6'))).toBe(true)
+ expect(isAgentSuggestedModel(provider, createModelItem('gpt-5.6-sol'))).toBe(true)
+ expect(isAgentSuggestedModel(provider, createModelItem('gpt-5.6-terra'))).toBe(true)
+ expect(isAgentSuggestedModel(provider, createModelItem('gpt-5.6-luna'))).toBe(true)
expect(isAgentSuggestedModel(provider, createModelItem('Claude Opus 4.8'))).toBe(true)
expect(isAgentSuggestedModel(provider, createModelItem('opus-4.7'))).toBe(true)
expect(isAgentSuggestedModel(provider, createModelItem('Claude Sonnet 4.6'))).toBe(true)
diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx
index adb2b0ce411..f5ded02fa30 100644
--- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx
@@ -1,4 +1,4 @@
-import type { AddOAuthButtonProps } from '@/app/components/plugins/plugin-auth/types'
+import type { AddOAuthButtonProps, Credential } from '@/app/components/plugins/plugin-auth/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
@@ -6,6 +6,7 @@ import { act, cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createStore, Provider as JotaiProvider } from 'jotai'
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
import { CollectionType } from '@/app/components/tools/types'
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
@@ -21,6 +22,13 @@ import { AgentTools } from '../index'
const toolProviderState = vi.hoisted(() => ({
builtInTools: [] as ToolWithProvider[],
}))
+const pluginAuthState = vi.hoisted(() => ({
+ canOAuth: true as boolean | undefined,
+ canApiKey: false as boolean | undefined,
+ credentials: [] as Credential[],
+ notAllowCustomCredential: false,
+ invalidPluginCredentialInfo: vi.fn(),
+}))
vi.mock('@/app/components/workflow/block-selector/tool-picker', () => ({
ToolPickerContent: () => Mock tool picker
,
@@ -51,6 +59,21 @@ vi.mock('@/app/components/plugins/plugin-auth/authorize/add-oauth-button', () =>
},
}))
+vi.mock('@/app/components/plugins/plugin-auth/hooks/use-plugin-auth', () => ({
+ usePluginAuth: () => ({
+ ...pluginAuthState,
+ isAuthorized: pluginAuthState.credentials.length > 0,
+ }),
+}))
+
+vi.mock('@/hooks/use-credential-permissions', () => ({
+ useCredentialPermissions: () => ({
+ canUseCredential: true,
+ canCreateCredential: true,
+ canManageCredential: true,
+ }),
+}))
+
vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => ({
default: ({
formSchemas,
@@ -333,6 +356,10 @@ describe('AgentTools', () => {
cleanup()
vi.clearAllMocks()
toolProviderState.builtInTools = []
+ pluginAuthState.canOAuth = true
+ pluginAuthState.canApiKey = false
+ pluginAuthState.credentials = []
+ pluginAuthState.notAllowCustomCredential = false
})
describe('User Interactions', () => {
@@ -521,7 +548,8 @@ describe('AgentTools', () => {
expect(store.get(isAgentComposerDirtyAtom)).toBe(false)
})
- it('should show authorization action for reflected OAuth provider tools with unauthorized credential type', () => {
+ it('should open authorization actions for reflected OAuth provider tools', async () => {
+ const user = userEvent.setup()
toolProviderState.builtInTools = [
{
...googleProvider,
@@ -537,7 +565,54 @@ describe('AgentTools', () => {
name: 'tools.notAuthorized',
}),
).toBeInTheDocument()
- expect(screen.queryByText('plugin.auth.setupOAuth')).not.toBeInTheDocument()
+ expect(
+ screen.queryByRole('button', { name: 'plugin.auth.useOAuthAuth' }),
+ ).not.toBeInTheDocument()
+
+ await user.click(screen.getByRole('button', { name: 'tools.notAuthorized' }))
+
+ expect(
+ screen.getByRole('button', {
+ name: 'plugin.auth.useOAuthAuth',
+ }),
+ ).toBeInTheDocument()
+ })
+
+ it('should bind an existing credential selected from the unauthorized status', async () => {
+ const user = userEvent.setup()
+ toolProviderState.builtInTools = [
+ {
+ ...googleProvider,
+ allow_delete: true,
+ is_team_authorization: false,
+ team_credentials: {},
+ },
+ ]
+ pluginAuthState.credentials = [
+ {
+ id: 'workspace-oauth',
+ name: 'Workspace OAuth',
+ provider: 'google',
+ credential_type: CredentialTypeEnum.OAUTH2,
+ is_default: true,
+ },
+ ]
+ const { store } = renderAgentToolsWithStore(reflectedUnauthorizedOAuthCredentialTypeDraft)
+
+ await user.click(screen.getByRole('button', { name: 'tools.notAuthorized' }))
+
+ expect(store.get(agentComposerDraftAtom).tools[0]).toMatchObject({
+ credentialType: 'unauthorized',
+ credentialVariant: 'none',
+ })
+
+ await user.click(screen.getByText('Workspace OAuth'))
+
+ expect(store.get(agentComposerDraftAtom).tools[0]).toMatchObject({
+ credentialId: 'workspace-oauth',
+ credentialType: 'oauth2',
+ credentialVariant: 'authorized',
+ })
})
it('should open provider tool settings with catalog icon and parameters', async () => {
diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/item.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/item.tsx
index df5c7045bbd..74b179a3efe 100644
--- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/item.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/item.tsx
@@ -17,11 +17,8 @@ import {
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
-import { AuthCategory } from '@/app/components/plugins/plugin-auth'
-import AddOAuthButton from '@/app/components/plugins/plugin-auth/authorize/add-oauth-button'
-import ApiKeyModal from '@/app/components/plugins/plugin-auth/authorize/api-key-modal'
+import { AuthCategory, Authorized, usePluginAuth } from '@/app/components/plugins/plugin-auth'
import AuthorizedInNode from '@/app/components/plugins/plugin-auth/authorized-in-node'
-import { useInvalidPluginCredentialInfoHook } from '@/app/components/plugins/plugin-auth/hooks/use-credential'
import { CollectionType } from '@/app/components/tools/types'
import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
@@ -58,7 +55,7 @@ function UnauthorizedCredentialStatus({
) => void
}) {
const { t } = useTranslation()
- const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
+ const [isOpen, setIsOpen] = useState(false)
const pluginPayload = useMemo(
() => ({
provider: tool.id,
@@ -67,53 +64,58 @@ function UnauthorizedCredentialStatus({
}),
[tool.id, tool.providerType],
)
- const invalidPluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload)
- const handleApiKeyModalOpen = useCallback(() => {
- setIsApiKeyModalOpen(true)
- }, [])
- const handleApiKeyModalClose = useCallback(() => {
- setIsApiKeyModalOpen(false)
- }, [])
+ const {
+ canApiKey,
+ canOAuth,
+ credentials,
+ invalidPluginCredentialInfo,
+ notAllowCustomCredential,
+ } = usePluginAuth(pluginPayload, true)
const handleCredentialUpdate = useCallback(() => {
invalidPluginCredentialInfo()
onCredentialChange(undefined, tool.credentialType)
}, [invalidPluginCredentialInfo, onCredentialChange, tool.credentialType])
-
- if (tool.credentialType === 'oauth2') {
- return (
- (
-
- )}
- />
- )
- }
-
- return (
- <>
-