Files
dify/dify-agent/src/dify_agent/server/settings.py
T

257 lines
12 KiB
Python

"""Configuration for the FastAPI run server.
Outbound HTTP client settings describe the FastAPI lifespan-owned
``httpx.AsyncClient`` instances shared by local run tasks for plugin-daemon and
Dify API inner calls. Layers and Agenton providers do not own those clients, so
these settings are process resource limits rather than per-run lifecycle knobs.
Endpoint URLs and API keys stay service-specific. The Agent Stub also uses this
settings model directly: the public Agent Stub API base URL, server secret,
and optional Dify inner API bridge settings live here under the
``DIFY_AGENT_...`` environment-variable namespace.
"""
import httpx
from typing import ClassVar, Literal, cast
from pydantic import AliasChoices, AnyHttpUrl, Field, TypeAdapter, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from dify_agent.agent_stub.protocol.agent_stub import normalize_agent_stub_api_base_url
from dify_agent.agent_stub.server.agent_stub_config import DifyApiAgentStubConfigRequestHandler
from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler
from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec, decode_server_secret_key
from dify_agent.runtime.runner import DEFAULT_AGENT_RUN_TIMEOUT_SECONDS
from dify_agent.runtime_backend import RuntimeBackendProfile
from dify_agent.runtime_backend.e2b import E2B_MAX_ACTIVE_TIMEOUT_SECONDS
from dify_agent.runtime_backend.profile import (
DEFAULT_LOCAL_HOME_SNAPSHOT_ROOT,
DEFAULT_LOCAL_MATERIALIZED_HOME_ROOT,
DEFAULT_LOCAL_WORKSPACE_ROOT,
RuntimeBackendSettings,
create_runtime_backend_profile,
)
DEFAULT_RUN_RETENTION_SECONDS = 3 * 24 * 60 * 60
class ServerSettings(BaseSettings):
"""Environment settings for scheduling, outbound HTTP, and runtime resources."""
redis_url: str = "redis://localhost:6379/0"
redis_prefix: str = "dify-agent"
shutdown_grace_seconds: float = 30
run_retention_seconds: int = Field(default=DEFAULT_RUN_RETENTION_SECONDS, ge=1)
run_timeout_seconds: float = Field(default=DEFAULT_AGENT_RUN_TIMEOUT_SECONDS, gt=0)
plugin_daemon_url: str = "http://localhost:5002"
plugin_daemon_api_key: str = ""
inner_api_url: str = "http://localhost:5001"
inner_api_key: str | None = None
runtime_backend: Literal["local", "enterprise", "e2b"] = "local"
local_sandbox_endpoint: str | None = Field(
default=None,
validation_alias=AliasChoices("DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT", "DIFY_AGENT_SHELLCTL_ENTRYPOINT"),
)
local_sandbox_auth_token: str | None = Field(
default=None,
validation_alias=AliasChoices("DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN", "DIFY_AGENT_SHELLCTL_AUTH_TOKEN"),
)
local_sandbox_materialized_home_root: str = DEFAULT_LOCAL_MATERIALIZED_HOME_ROOT
local_sandbox_workspace_root: str = DEFAULT_LOCAL_WORKSPACE_ROOT
local_sandbox_home_snapshot_root: str = DEFAULT_LOCAL_HOME_SNAPSHOT_ROOT
enterprise_sandbox_gateway_endpoint: str | None = None
enterprise_sandbox_gateway_auth_token: str | None = None
enterprise_sandbox_gateway_timeout: float = Field(default=30.0, gt=0)
enterprise_sandbox_proxy_timeout: float = Field(default=60.0, gt=0)
e2b_api_key: str | None = None
e2b_template: str = "difys-default-team/dify-agent-local-sandbox"
e2b_active_timeout_seconds: int = Field(
default=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
ge=1,
le=E2B_MAX_ACTIVE_TIMEOUT_SECONDS,
)
e2b_shellctl_port: int = Field(default=5004, ge=1, le=65535)
agent_stub_api_base_url: str | None = Field(default=None, validation_alias="DIFY_AGENT_STUB_API_BASE_URL")
sandbox_files_base_url: str | None = Field(
default=None,
validation_alias="DIFY_AGENT_SANDBOX_FILES_BASE_URL",
)
stub_upload_file_size_limit: int = Field(
default=50,
ge=0,
description="Maximum Agent Stub upload size in MiB",
validation_alias="DIFY_AGENT_STUB_UPLOAD_FILE_SIZE_LIMIT",
)
binding_file_download_command_timeout_seconds: float = Field(default=210.0, gt=0)
server_secret_key: str | None = None
api_token: str | None = None
shell_redact_patterns: str = ""
outbound_http_connect_timeout: float = Field(default=10.0, ge=0)
outbound_http_read_timeout: float = Field(default=600.0, ge=0)
outbound_http_write_timeout: float = Field(default=30.0, ge=0)
outbound_http_pool_timeout: float = Field(default=10.0, ge=0)
outbound_http_max_connections: int = Field(default=100, ge=1)
outbound_http_max_keepalive_connections: int = Field(default=20, ge=0)
outbound_http_keepalive_expiry: float = Field(default=30.0, ge=0)
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
env_prefix="DIFY_AGENT_",
env_file=(".env", "dify-agent/.env"),
extra="ignore",
populate_by_name=True,
)
@field_validator("agent_stub_api_base_url")
@classmethod
def normalize_agent_stub_api_base_url_value(cls, value: str | None) -> str | None:
"""Normalize the public Agent Stub URL while still validating its scheme."""
if value is None:
return None
stripped = value.strip()
if not stripped:
return None
if stripped.startswith(("http://", "https://")):
validated = str(TypeAdapter(AnyHttpUrl).validate_python(stripped))
return normalize_agent_stub_api_base_url(validated)
return normalize_agent_stub_api_base_url(stripped)
@field_validator("sandbox_files_base_url")
@classmethod
def normalize_sandbox_files_base_url_value(cls, value: str | None) -> str | None:
"""Normalize the Dify API base URL reachable from the Sandbox."""
if value is None:
return None
stripped = value.strip()
if not stripped:
return None
validated = str(TypeAdapter(AnyHttpUrl).validate_python(stripped))
parsed = validated.rstrip("/")
if "?" in parsed or "#" in parsed:
raise ValueError("DIFY_AGENT_SANDBOX_FILES_BASE_URL must not include a query string or fragment")
return parsed
@field_validator("server_secret_key")
@classmethod
def validate_server_secret_key(cls, value: str | None) -> str | None:
"""Validate the configured base64url-encoded server root secret."""
if value is None:
return None
stripped = value.strip()
if not stripped:
return None
_ = decode_server_secret_key(stripped)
return stripped
@field_validator("inner_api_url")
@classmethod
def normalize_inner_api_url(cls, value: str) -> str:
"""Normalize the trusted Dify API base URL used for inner API calls."""
stripped = value.strip()
if not stripped:
raise ValueError("DIFY_AGENT_INNER_API_URL must not be empty")
validated = str(TypeAdapter(AnyHttpUrl).validate_python(stripped))
parsed = validated.rstrip("/")
if "?" in parsed or "#" in parsed:
raise ValueError("DIFY_AGENT_INNER_API_URL must not include a query string or fragment")
return parsed
@field_validator("inner_api_key", "api_token")
@classmethod
def normalize_optional_api_token(cls, value: str | None) -> str | None:
"""Normalize optional API authentication tokens."""
if value is None:
return None
return value.strip() or None
def get_shell_redact_patterns(self) -> list[str]:
"""Parse the JSON array from shell_redact_patterns; empty/blank → empty list."""
stripped = self.shell_redact_patterns.strip()
if not stripped:
return []
import json as _json
parsed = cast(object, _json.loads(stripped))
if not isinstance(parsed, list):
raise ValueError("DIFY_AGENT_SHELL_REDACT_PATTERNS must be a JSON array of strings")
return TypeAdapter(list[str]).validate_python(parsed)
@model_validator(mode="after")
def validate_agent_stub_requirements(self) -> "ServerSettings":
"""Require Agent Stub settings while allowing deployments without inner API calls."""
if self.agent_stub_api_base_url is not None and self.server_secret_key is None:
raise ValueError("DIFY_AGENT_SERVER_SECRET_KEY is required when DIFY_AGENT_STUB_API_BASE_URL is set.")
if (
self.agent_stub_api_base_url is not None
and self.inner_api_key is not None
and self.sandbox_files_base_url is None
):
raise ValueError(
"DIFY_AGENT_SANDBOX_FILES_BASE_URL is required for Agent Stub file transfers and Config downloads."
)
return self
def build_runtime_backend_profile(self) -> RuntimeBackendProfile | None:
"""Build the deployment-selected resource backend without adding service state."""
if self.runtime_backend == "local" and not self.local_sandbox_endpoint:
return None
return create_runtime_backend_profile(
RuntimeBackendSettings(
runtime_backend=self.runtime_backend,
local_sandbox_endpoint=self.local_sandbox_endpoint,
local_sandbox_auth_token=self.local_sandbox_auth_token,
local_sandbox_materialized_home_root=self.local_sandbox_materialized_home_root,
local_sandbox_workspace_root=self.local_sandbox_workspace_root,
local_sandbox_home_snapshot_root=self.local_sandbox_home_snapshot_root,
enterprise_sandbox_gateway_endpoint=self.enterprise_sandbox_gateway_endpoint,
enterprise_sandbox_gateway_auth_token=self.enterprise_sandbox_gateway_auth_token,
enterprise_sandbox_gateway_timeout=self.enterprise_sandbox_gateway_timeout,
enterprise_sandbox_proxy_timeout=self.enterprise_sandbox_proxy_timeout,
e2b_api_key=self.e2b_api_key,
e2b_template=self.e2b_template,
e2b_active_timeout_seconds=self.e2b_active_timeout_seconds,
e2b_shellctl_port=self.e2b_shellctl_port,
)
)
def create_agent_stub_token_codec(self) -> AgentStubTokenCodec | None:
"""Return the Agent Stub token codec when the server secret is configured."""
if self.server_secret_key is None:
return None
return AgentStubTokenCodec.from_server_secret(self.server_secret_key)
def create_agent_stub_file_request_handler(self) -> DifyApiAgentStubFileRequestHandler | None:
"""Return the file bridge when inner API and Sandbox data-plane settings are configured."""
if self.inner_api_key is None or self.sandbox_files_base_url is None:
return None
return DifyApiAgentStubFileRequestHandler(
inner_api_url=self.inner_api_url,
inner_api_key=self.inner_api_key,
sandbox_files_base_url=self.sandbox_files_base_url,
max_upload_size_bytes=self.stub_upload_file_size_limit * 1024 * 1024,
timeout=self.create_outbound_http_timeout(),
)
def create_agent_stub_config_request_handler(self) -> DifyApiAgentStubConfigRequestHandler | None:
"""Return the Config bridge when inner API and Sandbox data-plane settings are configured."""
if self.inner_api_key is None or self.sandbox_files_base_url is None:
return None
return DifyApiAgentStubConfigRequestHandler(
inner_api_url=self.inner_api_url,
inner_api_key=self.inner_api_key,
sandbox_files_base_url=self.sandbox_files_base_url,
timeout=self.create_outbound_http_timeout(),
)
def create_outbound_http_timeout(self) -> httpx.Timeout:
"""Build one shared outbound HTTP timeout object from server settings."""
return httpx.Timeout(
connect=self.outbound_http_connect_timeout,
read=self.outbound_http_read_timeout,
write=self.outbound_http_write_timeout,
pool=self.outbound_http_pool_timeout,
)
__all__ = ["DEFAULT_RUN_RETENTION_SECONDS", "ServerSettings"]