from __future__ import annotations from pathlib import Path import secrets from typing import cast import httpx import pytest from pydantic import ValidationError from dify_agent.agent_stub.server.agent_stub_drive import DifyApiAgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec from dify_agent.server.settings import ServerSettings from dify_agent.runtime_backend.e2b import E2BExecutionBindingBackend from dify_agent.runtime_backend.enterprise import EnterpriseExecutionBindingBackend from dify_agent.runtime_backend.local import LocalExecutionBindingBackend, LocalHomeSnapshotBackend def _base64url_secret(value: bytes) -> str: import base64 return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") def test_server_settings_reads_shellctl_entrypoint_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_SHELLCTL_ENTRYPOINT", "http://shellctl.example") settings = ServerSettings() assert settings.local_sandbox_endpoint == "http://shellctl.example" def test_server_settings_reads_shellctl_auth_token_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_SHELLCTL_AUTH_TOKEN", "shell-secret") settings = ServerSettings() assert settings.local_sandbox_auth_token == "shell-secret" def test_server_settings_reads_enterprise_timeouts_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_TIMEOUT", "45") monkeypatch.setenv("DIFY_AGENT_ENTERPRISE_SANDBOX_PROXY_TIMEOUT", "90") settings = ServerSettings() assert settings.enterprise_sandbox_gateway_timeout == 45 assert settings.enterprise_sandbox_proxy_timeout == 90 def test_server_settings_reads_e2b_active_timeout_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_E2B_ACTIVE_TIMEOUT_SECONDS", "900") settings = ServerSettings() assert settings.e2b_active_timeout_seconds == 900 def test_server_settings_defaults_shellctl_auth_token_to_none( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: monkeypatch.delenv("DIFY_AGENT_SHELLCTL_AUTH_TOKEN", raising=False) monkeypatch.chdir(tmp_path) settings = ServerSettings() assert settings.local_sandbox_auth_token is None def test_server_settings_reads_agent_stub_settings_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_STUB_API_BASE_URL", "https://agent.example.com/agent-stub/") monkeypatch.setenv("DIFY_AGENT_SERVER_SECRET_KEY", _base64url_secret(secrets.token_bytes(32))) settings = ServerSettings() assert settings.agent_stub_api_base_url == "https://agent.example.com/agent-stub" def test_server_settings_normalizes_agent_stub_service_root_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_STUB_API_BASE_URL", "https://agent.example.com") monkeypatch.setenv("DIFY_AGENT_SERVER_SECRET_KEY", _base64url_secret(secrets.token_bytes(32))) settings = ServerSettings() assert settings.agent_stub_api_base_url == "https://agent.example.com/agent-stub" def test_server_settings_ignores_obsolete_legacy_settings_namespace(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_SHELL_BACK_PROXY_PUBLIC_URL", "https://agent.example.com/back-proxy/") monkeypatch.setenv("DIFY_AGENT_BACK_PROXY_URL", "https://agent.example.com/back-proxy/") settings = ServerSettings() assert settings.agent_stub_api_base_url is None def test_server_settings_rejects_agent_stub_api_base_url_with_query_or_fragment() -> None: secret = _base64url_secret(secrets.token_bytes(32)) with pytest.raises(ValidationError, match="query string or fragment"): _ = ServerSettings( agent_stub_api_base_url="https://agent.example.com/agent-stub?x=1", server_secret_key=secret, ) with pytest.raises(ValidationError, match="query string or fragment"): _ = ServerSettings( agent_stub_api_base_url="https://agent.example.com/agent-stub#fragment", server_secret_key=secret, ) def test_server_settings_rejects_agent_stub_api_base_url_with_unexpected_path() -> None: with pytest.raises(ValidationError, match="empty or /agent-stub"): _ = ServerSettings( agent_stub_api_base_url="https://agent.example.com/foo", server_secret_key=_base64url_secret(secrets.token_bytes(32)), ) def test_server_settings_rejects_public_agent_stub_api_base_url_without_secret_key() -> None: with pytest.raises(ValidationError, match="DIFY_AGENT_SERVER_SECRET_KEY"): _ = ServerSettings(agent_stub_api_base_url="https://agent.example.com/agent-stub") def test_server_settings_accepts_grpc_agent_stub_api_base_url_and_bind_override() -> None: settings = ServerSettings( agent_stub_api_base_url="grpc://agent.example.com:9091", agent_stub_grpc_bind_address="0.0.0.0:9191", server_secret_key=_base64url_secret(secrets.token_bytes(32)), ) assert settings.agent_stub_api_base_url == "grpc://agent.example.com:9091" assert settings.agent_stub_grpc_bind_address == "0.0.0.0:9191" def test_server_settings_rejects_grpc_bind_override_without_grpc_url() -> None: with pytest.raises(ValidationError, match="grpc://"): _ = ServerSettings( agent_stub_api_base_url="https://agent.example.com/agent-stub", agent_stub_grpc_bind_address="0.0.0.0:9191", server_secret_key=_base64url_secret(secrets.token_bytes(32)), ) def test_server_settings_rejects_invalid_server_secret_key() -> None: with pytest.raises(ValidationError, match="32 decoded bytes"): _ = ServerSettings(server_secret_key=_base64url_secret(b"short")) def test_server_settings_rejects_padded_or_quoted_server_secret_key() -> None: secret = _base64url_secret(secrets.token_bytes(32)) with pytest.raises(ValidationError, match="unpadded base64url"): _ = ServerSettings(server_secret_key=f"{secret}=") with pytest.raises(ValidationError, match="unpadded base64url"): _ = ServerSettings(server_secret_key=f'"{secret}"') def test_server_settings_normalizes_inner_api_url_from_env(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_INNER_API_URL", "https://api.example.com/") monkeypatch.setenv("DIFY_AGENT_INNER_API_KEY", "inner-secret") settings = ServerSettings() assert settings.inner_api_url == "https://api.example.com" assert settings.inner_api_key == "inner-secret" def test_server_settings_allows_inner_api_url_without_key_until_a_bridge_is_used() -> None: settings = ServerSettings(inner_api_key="inner-secret") assert settings.inner_api_key == "inner-secret" assert settings.inner_api_url == "http://localhost:5001" def test_server_settings_rejects_inner_api_url_with_query_or_fragment() -> None: with pytest.raises(ValidationError, match="query string or fragment"): _ = ServerSettings( inner_api_url="https://api.example.com?x=1", inner_api_key="inner-secret", ) with pytest.raises(ValidationError, match="query string or fragment"): _ = ServerSettings( inner_api_url="https://api.example.com#frag", inner_api_key="inner-secret", ) def test_server_settings_create_agent_stub_token_codec_returns_none_without_secret() -> None: assert ServerSettings().create_agent_stub_token_codec() is None def test_server_settings_create_agent_stub_token_codec_returns_codec_when_secret_is_configured() -> None: settings = ServerSettings(server_secret_key=_base64url_secret(secrets.token_bytes(32))) codec = settings.create_agent_stub_token_codec() assert isinstance(codec, AgentStubTokenCodec) def test_server_settings_create_agent_stub_file_request_handler_returns_none_without_full_settings() -> None: assert ServerSettings().create_agent_stub_file_request_handler() is None def test_server_settings_create_agent_stub_file_request_handler_returns_handler_when_configured() -> None: settings = ServerSettings( inner_api_url="https://api.example.com", inner_api_key="inner-secret", ) handler = settings.create_agent_stub_file_request_handler() assert isinstance(handler, DifyApiAgentStubFileRequestHandler) assert handler.inner_api_url == "https://api.example.com" assert handler.inner_api_key == "inner-secret" def test_server_settings_create_agent_stub_drive_request_handler_returns_none_without_full_settings() -> None: assert ServerSettings().create_agent_stub_drive_request_handler() is None def test_server_settings_create_agent_stub_drive_request_handler_returns_handler_when_configured() -> None: settings = ServerSettings( inner_api_url="https://api.example.com", inner_api_key="inner-secret", outbound_http_connect_timeout=11, outbound_http_read_timeout=22, outbound_http_write_timeout=33, outbound_http_pool_timeout=44, ) handler = settings.create_agent_stub_drive_request_handler() assert isinstance(handler, DifyApiAgentStubDriveRequestHandler) assert handler.inner_api_url == "https://api.example.com" assert handler.inner_api_key == "inner-secret" timeout = cast(httpx.Timeout, handler.timeout) assert timeout.connect == 11 assert timeout.read == 22 assert timeout.write == 33 assert timeout.pool == 44 def test_build_runtime_backend_profile_returns_none_when_local_endpoint_is_unset( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: monkeypatch.delenv("DIFY_AGENT_SHELLCTL_ENTRYPOINT", raising=False) monkeypatch.chdir(tmp_path) assert ServerSettings().build_runtime_backend_profile() is None def test_build_runtime_backend_profile_returns_local_drivers_when_configured() -> None: settings = ServerSettings( runtime_backend="local", local_sandbox_endpoint="http://shellctl.example", local_sandbox_auth_token="shell-secret", local_sandbox_materialized_home_root="/tmp/dify/homes", local_sandbox_workspace_root="/tmp/dify/workspaces", local_sandbox_home_snapshot_root="/tmp/dify/snapshots", ) profile = settings.build_runtime_backend_profile() assert profile is not None assert isinstance(profile.execution_bindings, LocalExecutionBindingBackend) assert isinstance(profile.home_snapshots, LocalHomeSnapshotBackend) assert profile.execution_bindings.endpoint == "http://shellctl.example" assert profile.execution_bindings.auth_token == "shell-secret" assert profile.execution_bindings.materialized_home_root == "/tmp/dify/homes" assert profile.execution_bindings.workspace_root == "/tmp/dify/workspaces" assert profile.execution_bindings.snapshot_root == "/tmp/dify/snapshots" assert profile.home_snapshots.snapshot_root == "/tmp/dify/snapshots" def test_build_runtime_backend_profile_returns_enterprise_drivers_when_selected() -> None: settings = ServerSettings( runtime_backend="enterprise", enterprise_sandbox_gateway_endpoint="https://gateway.example", enterprise_sandbox_gateway_auth_token="gateway-secret", enterprise_sandbox_gateway_timeout=45, enterprise_sandbox_proxy_timeout=90, ) profile = settings.build_runtime_backend_profile() assert profile is not None assert isinstance(profile.execution_bindings, EnterpriseExecutionBindingBackend) assert profile.execution_bindings.gateway_endpoint == "https://gateway.example" assert profile.execution_bindings.auth_token == "gateway-secret" assert profile.execution_bindings.gateway_timeout == 45 assert profile.execution_bindings.proxy_timeout == 90 def test_build_runtime_backend_profile_passes_e2b_active_timeout() -> None: settings = ServerSettings( runtime_backend="e2b", e2b_api_key="e2b-secret", e2b_active_timeout_seconds=900, ) profile = settings.build_runtime_backend_profile() assert profile is not None assert isinstance(profile.execution_bindings, E2BExecutionBindingBackend) assert profile.execution_bindings.active_timeout_seconds == 900 assert profile.execution_bindings.template == "difys-default-team/dify-agent-local-sandbox" def test_sandbox_file_upload_limit_defaults_to_tool_file_limit() -> None: settings = ServerSettings() assert settings.sandbox_file_upload_max_bytes == 50 * 1024 * 1024 def test_build_runtime_backend_profile_rejects_missing_enterprise_endpoint( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: monkeypatch.delenv("DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_ENDPOINT", raising=False) monkeypatch.chdir(tmp_path) with pytest.raises(ValidationError, match="enterprise_sandbox_gateway_endpoint is required"): _ = ServerSettings(runtime_backend="enterprise").build_runtime_backend_profile() def test_build_runtime_backend_profile_rejects_blank_local_endpoint() -> None: with pytest.raises(ValidationError, match="local_sandbox_endpoint is required"): _ = ServerSettings(runtime_backend="local", local_sandbox_endpoint=" ").build_runtime_backend_profile() def test_server_settings_parses_shell_redact_patterns_json_array(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", '["sk-[A-Za-z0-9]+","ghp_[A-Za-z0-9]{36}"]') settings = ServerSettings() assert settings.get_shell_redact_patterns() == ["sk-[A-Za-z0-9]+", "ghp_[A-Za-z0-9]{36}"] def test_server_settings_shell_redact_patterns_empty_string_yields_empty_list(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", "") settings = ServerSettings() assert settings.get_shell_redact_patterns() == [] def test_server_settings_shell_redact_patterns_defaults_to_empty_list( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: monkeypatch.delenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", raising=False) monkeypatch.chdir(tmp_path) settings = ServerSettings() assert settings.get_shell_redact_patterns() == [] def test_server_settings_rejects_non_array_shell_redact_patterns(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", '{"key": "value"}') settings = ServerSettings() with pytest.raises(ValueError, match="must be a JSON array"): _ = settings.get_shell_redact_patterns()