mirror of
https://github.com/langgenius/dify.git
synced 2026-09-21 05:11:22 +08:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
265 lines
12 KiB
Python
265 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_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, decode_server_secret_key
|
|
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)
|
|
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_auth_token: str = ""
|
|
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",
|
|
)
|
|
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")
|
|
@classmethod
|
|
def normalize_inner_api_key(cls, value: str | None) -> str | None:
|
|
"""Normalize the optional trusted Dify inner API key."""
|
|
if value is None:
|
|
return None
|
|
stripped = value.strip()
|
|
return stripped 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_auth_token=self.e2b_shellctl_auth_token,
|
|
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,
|
|
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_agent_stub_drive_request_handler(self) -> DifyApiAgentStubDriveRequestHandler | None:
|
|
"""Return the Dify API drive bridge when both Dify API settings are configured.
|
|
|
|
Drive manifest and commit requests should honor the same outbound timeout
|
|
settings as the server's other trusted Dify API HTTP calls.
|
|
"""
|
|
if self.inner_api_key is None:
|
|
return None
|
|
return DifyApiAgentStubDriveRequestHandler(
|
|
inner_api_url=self.inner_api_url,
|
|
inner_api_key=self.inner_api_key,
|
|
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"]
|