mirror of
https://github.com/langgenius/dify.git
synced 2026-09-04 16:07:08 +08:00
fix(agent): surface missing tool credentials
This commit is contained in:
parent
0334f4426d
commit
a54222e7fa
@ -42,7 +42,7 @@ from core.tools.entities.tool_entities import (
|
||||
ToolProviderType,
|
||||
emoji_icon_adapter,
|
||||
)
|
||||
from core.tools.errors import ToolProviderNotFoundError
|
||||
from core.tools.errors import ToolProviderCredentialValidationError, ToolProviderNotFoundError
|
||||
from core.tools.mcp_tool.provider import MCPToolProviderController
|
||||
from core.tools.mcp_tool.tool import MCPTool
|
||||
from core.tools.plugin_tool.provider import PluginToolProviderController
|
||||
@ -233,7 +233,10 @@ class ToolManager:
|
||||
builtin_provider = None
|
||||
logger.info("Error getting builtin provider %s:%s", credential_id, e, exc_info=True)
|
||||
if builtin_provider is None:
|
||||
raise ToolProviderNotFoundError(f"provider has been deleted: {credential_id}")
|
||||
raise ToolProviderCredentialValidationError(
|
||||
f"Tool credential {credential_id} has been deleted. "
|
||||
"Select or authorize another credential."
|
||||
)
|
||||
|
||||
if builtin_provider is None:
|
||||
with Session(db.engine) as session:
|
||||
@ -247,7 +250,10 @@ class ToolManager:
|
||||
.order_by(BuiltinToolProvider.is_default.desc(), BuiltinToolProvider.created_at.asc())
|
||||
)
|
||||
if builtin_provider is None:
|
||||
raise ToolProviderNotFoundError(f"no default provider for {provider_id}")
|
||||
raise ToolProviderCredentialValidationError(
|
||||
f"No workspace credential is configured for tool provider {provider_id}. "
|
||||
"Authorize the provider or select a credential."
|
||||
)
|
||||
else:
|
||||
builtin_provider = db.session.scalar(
|
||||
select(BuiltinToolProvider)
|
||||
@ -259,7 +265,10 @@ class ToolManager:
|
||||
)
|
||||
|
||||
if builtin_provider is None:
|
||||
raise ToolProviderNotFoundError(f"builtin provider {provider_id} not found")
|
||||
raise ToolProviderCredentialValidationError(
|
||||
f"No credential is configured for built-in tool provider {provider_id}. "
|
||||
"Authorize the provider or select a credential."
|
||||
)
|
||||
|
||||
from core.helper.credential_utils import runtime_check_credential_policy_compliance
|
||||
|
||||
@ -294,15 +303,24 @@ class ToolManager:
|
||||
system_credentials = BuiltinToolManageService.get_oauth_client(tenant_id, provider_id)
|
||||
|
||||
oauth_handler = OAuthHandler()
|
||||
refreshed_credentials = oauth_handler.refresh_credentials(
|
||||
tenant_id=tenant_id,
|
||||
user_id=builtin_provider.user_id,
|
||||
plugin_id=tool_provider.plugin_id,
|
||||
provider=provider_name,
|
||||
redirect_uri=redirect_uri,
|
||||
system_credentials=system_credentials or {},
|
||||
credentials=decrypted_credentials,
|
||||
)
|
||||
try:
|
||||
refreshed_credentials = oauth_handler.refresh_credentials(
|
||||
tenant_id=tenant_id,
|
||||
user_id=builtin_provider.user_id,
|
||||
plugin_id=tool_provider.plugin_id,
|
||||
provider=provider_name,
|
||||
redirect_uri=redirect_uri,
|
||||
system_credentials=system_credentials or {},
|
||||
credentials=decrypted_credentials,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to refresh OAuth credentials for tool provider %s", provider_id, exc_info=True
|
||||
)
|
||||
raise ToolProviderCredentialValidationError(
|
||||
f"OAuth credential for tool provider {provider_id} could not be refreshed. "
|
||||
"Reauthorize or select another credential."
|
||||
) from exc
|
||||
# update the credentials
|
||||
builtin_provider.encrypted_credentials = json.dumps(
|
||||
encrypter.encrypt(refreshed_credentials.credentials)
|
||||
|
||||
@ -25,7 +25,7 @@ from core.tools.entities.tool_entities import (
|
||||
ToolParameter,
|
||||
ToolProviderType,
|
||||
)
|
||||
from core.tools.errors import ToolProviderNotFoundError
|
||||
from core.tools.errors import ToolProviderCredentialValidationError, ToolProviderNotFoundError
|
||||
from core.tools.plugin_tool.provider import PluginToolProviderController
|
||||
from core.tools.tool_manager import ToolManager
|
||||
from models.base import TypeBase
|
||||
@ -399,7 +399,49 @@ def test_get_tool_runtime_builtin_refreshes_expired_oauth_credentials(
|
||||
cache.delete.assert_called_once()
|
||||
|
||||
|
||||
def test_get_tool_runtime_builtin_plugin_provider_deleted_raises(
|
||||
def test_get_tool_runtime_builtin_maps_oauth_refresh_failure_to_credential_error(
|
||||
monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase
|
||||
):
|
||||
tool = Mock()
|
||||
controller = SimpleNamespace(
|
||||
get_tool=Mock(return_value=tool),
|
||||
need_credentials=True,
|
||||
get_credentials_schema_by_type=Mock(return_value=[]),
|
||||
)
|
||||
tenant_id = "00000000-0000-0000-0000-000000000001"
|
||||
builtin_provider = _builtin_provider(
|
||||
provider_id="00000000-0000-0000-0000-000000000002",
|
||||
tenant_id=tenant_id,
|
||||
credential_type=CredentialType.OAUTH2,
|
||||
expires_at=1,
|
||||
)
|
||||
tool_database.session.add(builtin_provider)
|
||||
tool_database.session.commit()
|
||||
monkeypatch.setattr("core.tools.tool_manager.db", tool_database)
|
||||
|
||||
encrypter = Mock()
|
||||
encrypter.decrypt.return_value = {"token": "expired"}
|
||||
with (
|
||||
patch.object(ToolManager, "get_builtin_provider", return_value=controller),
|
||||
patch("core.tools.tool_manager.create_provider_encrypter", return_value=(encrypter, Mock())),
|
||||
patch("core.tools.tool_manager.time.time", return_value=1000),
|
||||
patch(
|
||||
"services.tools.builtin_tools_manage_service.BuiltinToolManageService.get_oauth_client",
|
||||
return_value={"client_id": "id"},
|
||||
),
|
||||
patch("core.plugin.impl.oauth.OAuthHandler") as oauth_handler_cls,
|
||||
):
|
||||
oauth_handler_cls.return_value.refresh_credentials.side_effect = ValueError("refresh token revoked")
|
||||
with pytest.raises(ToolProviderCredentialValidationError, match="could not be refreshed"):
|
||||
ToolManager.get_tool_runtime(
|
||||
provider_type=ToolProviderType.BUILT_IN,
|
||||
provider_id="time",
|
||||
tool_name="weekday",
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
|
||||
|
||||
def test_get_tool_runtime_builtin_plugin_credential_deleted_raises(
|
||||
monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase
|
||||
):
|
||||
plugin_controller = object.__new__(PluginToolProviderController)
|
||||
@ -409,7 +451,7 @@ def test_get_tool_runtime_builtin_plugin_provider_deleted_raises(
|
||||
|
||||
monkeypatch.setattr("core.tools.tool_manager.db", tool_database)
|
||||
with patch.object(ToolManager, "get_builtin_provider", return_value=plugin_controller):
|
||||
with pytest.raises(ToolProviderNotFoundError, match="provider has been deleted"):
|
||||
with pytest.raises(ToolProviderCredentialValidationError, match="credential .* has been deleted"):
|
||||
ToolManager.get_tool_runtime(
|
||||
provider_type=ToolProviderType.BUILT_IN,
|
||||
provider_id="time",
|
||||
@ -419,6 +461,44 @@ def test_get_tool_runtime_builtin_plugin_provider_deleted_raises(
|
||||
)
|
||||
|
||||
|
||||
def test_get_tool_runtime_builtin_plugin_without_workspace_credential_raises(
|
||||
monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase
|
||||
):
|
||||
plugin_controller = object.__new__(PluginToolProviderController)
|
||||
plugin_controller.entity = SimpleNamespace(credentials_schema=[{"name": "k"}], oauth_schema=None)
|
||||
plugin_controller.get_tool = Mock(return_value=Mock())
|
||||
plugin_controller.get_credentials_schema_by_type = Mock(return_value=[])
|
||||
|
||||
monkeypatch.setattr("core.tools.tool_manager.db", tool_database)
|
||||
with patch.object(ToolManager, "get_builtin_provider", return_value=plugin_controller):
|
||||
with pytest.raises(ToolProviderCredentialValidationError, match="No workspace credential is configured"):
|
||||
ToolManager.get_tool_runtime(
|
||||
provider_type=ToolProviderType.BUILT_IN,
|
||||
provider_id="langgenius/dify-gmail/dify-gmail",
|
||||
tool_name="send_draft",
|
||||
tenant_id="00000000-0000-0000-0000-000000000001",
|
||||
)
|
||||
|
||||
|
||||
def test_get_tool_runtime_hardcoded_provider_without_credential_raises(
|
||||
monkeypatch: pytest.MonkeyPatch, tool_database: _ToolDatabase
|
||||
):
|
||||
controller = SimpleNamespace(
|
||||
get_tool=Mock(return_value=Mock()),
|
||||
need_credentials=True,
|
||||
)
|
||||
|
||||
monkeypatch.setattr("core.tools.tool_manager.db", tool_database)
|
||||
with patch.object(ToolManager, "get_builtin_provider", return_value=controller):
|
||||
with pytest.raises(ToolProviderCredentialValidationError, match="No credential is configured"):
|
||||
ToolManager.get_tool_runtime(
|
||||
provider_type=ToolProviderType.BUILT_IN,
|
||||
provider_id="legacy-provider",
|
||||
tool_name="legacy-tool",
|
||||
tenant_id="00000000-0000-0000-0000-000000000001",
|
||||
)
|
||||
|
||||
|
||||
def test_get_tool_runtime_api_path():
|
||||
api_tool = Mock()
|
||||
api_tool.fork_tool_runtime.return_value = "api-runtime"
|
||||
|
||||
@ -839,6 +839,7 @@ def test_credential_validation_error_maps_to_credential_invalid():
|
||||
with pytest.raises(WorkflowAgentDifyToolsBuildError) as exc_info:
|
||||
_build(builder, _standard_tools_payload())
|
||||
assert exc_info.value.error_code == "agent_tool_credential_invalid"
|
||||
assert "credential validation failed" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_generic_value_error_maps_to_config_invalid():
|
||||
|
||||
@ -243,4 +243,22 @@ describe('AuthorizedInNode Component', () => {
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
|
||||
it('should show unavailable when workspace default credential is missing', async () => {
|
||||
const AuthorizedInNode = (await import('../authorized-in-node')).default
|
||||
mockGetPluginCredentialInfo.mockReturnValue({
|
||||
credentials: [],
|
||||
supported_credential_types: [CredentialTypeEnum.API_KEY],
|
||||
allow_custom_token: true,
|
||||
})
|
||||
const pluginPayload = createPluginPayload()
|
||||
|
||||
render(<AuthorizedInNode pluginPayload={pluginPayload} onAuthorizationItemClick={vi.fn()} />, {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('plugin.auth.workspaceDefault')
|
||||
expect(button.textContent).toContain('plugin.auth.unavailable')
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,7 +48,12 @@ const AuthorizedInNode = ({
|
||||
|
||||
const defaultCredential = credentials.find((c) => c.is_default)
|
||||
|
||||
if (defaultCredential?.not_allowed_to_use) {
|
||||
if (isLoading) {
|
||||
color = 'disabled'
|
||||
} else if (!defaultCredential) {
|
||||
color = 'error'
|
||||
defaultUnavailable = true
|
||||
} else if (defaultCredential.not_allowed_to_use) {
|
||||
color = 'disabled'
|
||||
defaultUnavailable = true
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user