From 080a1d42f2387c6c9729ff49d71a6344c40d3b0e Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 29 Jul 2026 20:27:38 +0800 Subject: [PATCH] fix: fix hardcode /home/dify (#39767) --- .../docs/dify-agent/get-started/index.md | 4 ++ dify-agent/docs/dify-agent/guide/index.md | 9 +++- .../user-manual/shell-layer/index.md | 4 ++ .../src/dify_agent/runtime_backend/profile.py | 47 +++++++++++++++++-- dify-agent/src/dify_agent/server/settings.py | 14 +++++- .../runtime_backend/test_profile.py | 35 +++++++++++++- .../local/dify_agent/server/test_settings.py | 10 +++- 7 files changed, 116 insertions(+), 7 deletions(-) diff --git a/dify-agent/docs/dify-agent/get-started/index.md b/dify-agent/docs/dify-agent/get-started/index.md index cb3e893c380..bcad90cf144 100644 --- a/dify-agent/docs/dify-agent/get-started/index.md +++ b/dify-agent/docs/dify-agent/get-started/index.md @@ -80,6 +80,10 @@ Binding backend. A standalone Local server normally uses: DIFY_AGENT_RUNTIME_BACKEND=local DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT=http://127.0.0.1:5004 DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN= +# Optional when shellctl runs directly on a host without /home/dify: +# DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT=/tmp/dify-agent/materialized-homes +# DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT=/tmp/dify-agent/workspaces +# DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT=/tmp/dify-agent/home-snapshots DIFY_AGENT_SANDBOX_FILE_UPLOAD_MAX_BYTES=52428800 ``` diff --git a/dify-agent/docs/dify-agent/guide/index.md b/dify-agent/docs/dify-agent/guide/index.md index fceba47b3b9..da0df3b4fdd 100644 --- a/dify-agent/docs/dify-agent/guide/index.md +++ b/dify-agent/docs/dify-agent/guide/index.md @@ -42,6 +42,9 @@ also reads `.env` and `dify-agent/.env` when present. | `DIFY_AGENT_RUNTIME_BACKEND` | `local` | Selects one coherent `local`, `enterprise`, or `e2b` Home Snapshot + Execution Binding backend profile. | | `DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT` | empty | Local shellctl data-plane URL. With the default Local selection, leaving it empty disables `dify.runtime` and resource endpoints. | | `DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN` | empty | Optional bearer token sent to Local shellctl. | +| `DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT` | `/home/dify/.dify-agent-materialized-homes` | Root directory, on the Local shellctl filesystem, for per-Binding materialized Homes. | +| `DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT` | `/home/dify/.dify-agent-workspaces` | Root directory, on the Local shellctl filesystem, for mutable Workspaces. | +| `DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT` | `/home/dify/.dify-agent-home-snapshots` | Root directory, on the Local shellctl filesystem, for immutable Home Snapshots. | | `DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_ENDPOINT` | empty | Enterprise Gateway endpoint required by configuration. Default-Home Bindings are supported; immutable Home Snapshot operations remain unsupported. | | `DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_AUTH_TOKEN` | empty | Optional `X-Inner-Api-Key` sent to the Enterprise Gateway. | | `DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_TIMEOUT` | `30` | Enterprise control-plane timeout in seconds. | @@ -78,6 +81,10 @@ DIFY_AGENT_INNER_API_KEY=replace-with-dify-inner-api-key-for-plugin DIFY_AGENT_RUNTIME_BACKEND=local DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT=http://127.0.0.1:5004 DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN=replace-with-shellctl-token +# Set these when shellctl runs directly on a host that does not have /home/dify. +DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT=/tmp/dify-agent/materialized-homes +DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT=/tmp/dify-agent/workspaces +DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT=/tmp/dify-agent/home-snapshots DIFY_AGENT_SANDBOX_FILE_UPLOAD_MAX_BYTES=52428800 DIFY_AGENT_STUB_API_BASE_URL=https://agent.example.com/agent-stub # This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens. @@ -90,7 +97,7 @@ DIFY_AGENT_SERVER_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY accepted only as legacy aliases for the two Local settings. New deployments must use `DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT` and `DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN`. There is no compatibility setting for -the removed shell-provider selector or Shell-owned Home root. +the removed shell-provider selector. The example above is for a standalone Dify Agent process, where the byte limit can be set directly. In a Docker deployment, set `PLUGIN_MAX_FILE_SIZE` in diff --git a/dify-agent/docs/dify-agent/user-manual/shell-layer/index.md b/dify-agent/docs/dify-agent/user-manual/shell-layer/index.md index b03a48f3c22..188add6cfc3 100644 --- a/dify-agent/docs/dify-agent/user-manual/shell-layer/index.md +++ b/dify-agent/docs/dify-agent/user-manual/shell-layer/index.md @@ -71,6 +71,10 @@ Equivalent standalone environment settings are: DIFY_AGENT_RUNTIME_BACKEND=local DIFY_AGENT_LOCAL_SANDBOX_ENDPOINT=http://127.0.0.1:5004 DIFY_AGENT_LOCAL_SANDBOX_AUTH_TOKEN=replace-with-shellctl-token +# Optional when shellctl runs directly on a host without /home/dify: +# DIFY_AGENT_LOCAL_SANDBOX_MATERIALIZED_HOME_ROOT=/tmp/dify-agent/materialized-homes +# DIFY_AGENT_LOCAL_SANDBOX_WORKSPACE_ROOT=/tmp/dify-agent/workspaces +# DIFY_AGENT_LOCAL_SANDBOX_HOME_SNAPSHOT_ROOT=/tmp/dify-agent/home-snapshots ``` The auth token may be empty when shellctl authentication is disabled. E2B uses diff --git a/dify-agent/src/dify_agent/runtime_backend/profile.py b/dify-agent/src/dify_agent/runtime_backend/profile.py index c0c4e93d756..28bcf6bd01e 100644 --- a/dify-agent/src/dify_agent/runtime_backend/profile.py +++ b/dify-agent/src/dify_agent/runtime_backend/profile.py @@ -2,6 +2,7 @@ from __future__ import annotations +import posixpath from typing import ClassVar, Literal, Self from urllib.parse import urlparse @@ -19,6 +20,9 @@ from dify_agent.runtime_backend.local import LocalExecutionBindingBackend, Local from dify_agent.runtime_backend.protocols import RuntimeBackendProfile DEFAULT_E2B_TEMPLATE = "difys-default-team/dify-agent-local-sandbox" +DEFAULT_LOCAL_MATERIALIZED_HOME_ROOT = "/home/dify/.dify-agent-materialized-homes" +DEFAULT_LOCAL_WORKSPACE_ROOT = "/home/dify/.dify-agent-workspaces" +DEFAULT_LOCAL_HOME_SNAPSHOT_ROOT = "/home/dify/.dify-agent-home-snapshots" class RuntimeBackendSettings(BaseSettings): @@ -40,6 +44,9 @@ class RuntimeBackendSettings(BaseSettings): "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 @@ -70,6 +77,18 @@ class RuntimeBackendSettings(BaseSettings): if not self.local_sandbox_endpoint or not self.local_sandbox_endpoint.strip(): raise ValueError("local_sandbox_endpoint is required for the local runtime backend") _validate_http_url(self.local_sandbox_endpoint, field_name="local_sandbox_endpoint") + _validate_absolute_posix_path( + self.local_sandbox_materialized_home_root, + field_name="local_sandbox_materialized_home_root", + ) + _validate_absolute_posix_path( + self.local_sandbox_workspace_root, + field_name="local_sandbox_workspace_root", + ) + _validate_absolute_posix_path( + self.local_sandbox_home_snapshot_root, + field_name="local_sandbox_home_snapshot_root", + ) case "enterprise": endpoint = self.enterprise_sandbox_gateway_endpoint if not endpoint or not endpoint.strip(): @@ -92,8 +111,18 @@ def create_runtime_backend_profile(settings: RuntimeBackendSettings) -> RuntimeB endpoint = settings.local_sandbox_endpoint or "" token = settings.local_sandbox_auth_token or "" return RuntimeBackendProfile( - home_snapshots=LocalHomeSnapshotBackend(endpoint=endpoint, auth_token=token), - execution_bindings=LocalExecutionBindingBackend(endpoint=endpoint, auth_token=token), + home_snapshots=LocalHomeSnapshotBackend( + endpoint=endpoint, + auth_token=token, + snapshot_root=settings.local_sandbox_home_snapshot_root, + ), + execution_bindings=LocalExecutionBindingBackend( + endpoint=endpoint, + auth_token=token, + materialized_home_root=settings.local_sandbox_materialized_home_root, + workspace_root=settings.local_sandbox_workspace_root, + snapshot_root=settings.local_sandbox_home_snapshot_root, + ), ) case "enterprise": endpoint = settings.enterprise_sandbox_gateway_endpoint or "" @@ -129,4 +158,16 @@ def _validate_http_url(value: str, *, field_name: str) -> None: raise ValueError(f"{field_name} must be a valid http(s) URL") -__all__ = ["DEFAULT_E2B_TEMPLATE", "RuntimeBackendSettings", "create_runtime_backend_profile"] +def _validate_absolute_posix_path(value: str, *, field_name: str) -> None: + if not value.strip() or not posixpath.isabs(value): + raise ValueError(f"{field_name} must be an absolute POSIX path") + + +__all__ = [ + "DEFAULT_E2B_TEMPLATE", + "DEFAULT_LOCAL_HOME_SNAPSHOT_ROOT", + "DEFAULT_LOCAL_MATERIALIZED_HOME_ROOT", + "DEFAULT_LOCAL_WORKSPACE_ROOT", + "RuntimeBackendSettings", + "create_runtime_backend_profile", +] diff --git a/dify-agent/src/dify_agent/server/settings.py b/dify-agent/src/dify_agent/server/settings.py index 40403fa7b01..37ad32f37ac 100644 --- a/dify-agent/src/dify_agent/server/settings.py +++ b/dify-agent/src/dify_agent/server/settings.py @@ -25,7 +25,13 @@ from dify_agent.agent_stub.server.grpc_bind import normalize_agent_stub_grpc_bin 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 RuntimeBackendSettings, create_runtime_backend_profile +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 @@ -50,6 +56,9 @@ class ServerSettings(BaseSettings): 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) @@ -178,6 +187,9 @@ class ServerSettings(BaseSettings): 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, diff --git a/dify-agent/tests/local/dify_agent/runtime_backend/test_profile.py b/dify-agent/tests/local/dify_agent/runtime_backend/test_profile.py index 2f827be7897..465731a4c0e 100644 --- a/dify-agent/tests/local/dify_agent/runtime_backend/test_profile.py +++ b/dify-agent/tests/local/dify_agent/runtime_backend/test_profile.py @@ -4,7 +4,12 @@ import pytest from pydantic import ValidationError from dify_agent.runtime_backend.e2b import E2B_MAX_ACTIVE_TIMEOUT_SECONDS -from dify_agent.runtime_backend.profile import DEFAULT_E2B_TEMPLATE, RuntimeBackendSettings +from dify_agent.runtime_backend.local import LocalExecutionBindingBackend, LocalHomeSnapshotBackend +from dify_agent.runtime_backend.profile import ( + DEFAULT_E2B_TEMPLATE, + RuntimeBackendSettings, + create_runtime_backend_profile, +) def test_e2b_backend_uses_prepared_dify_template_by_default() -> None: @@ -32,3 +37,31 @@ def test_e2b_backend_rejects_active_timeout_above_platform_limit() -> None: def test_local_backend_requires_shellctl_endpoint() -> None: with pytest.raises(ValidationError, match="local_sandbox_endpoint"): _ = RuntimeBackendSettings(runtime_backend="local") + + +def test_local_backend_passes_configured_roots_to_drivers() -> None: + settings = RuntimeBackendSettings( + runtime_backend="local", + local_sandbox_endpoint="http://shellctl.example", + local_sandbox_materialized_home_root="/tmp/dify/homes", + local_sandbox_workspace_root="/tmp/dify/workspaces", + local_sandbox_home_snapshot_root="/tmp/dify/snapshots", + ) + + profile = create_runtime_backend_profile(settings) + + assert isinstance(profile.execution_bindings, LocalExecutionBindingBackend) + assert isinstance(profile.home_snapshots, LocalHomeSnapshotBackend) + 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_local_backend_rejects_relative_roots() -> None: + with pytest.raises(ValidationError, match="absolute POSIX path"): + _ = RuntimeBackendSettings( + runtime_backend="local", + local_sandbox_endpoint="http://shellctl.example", + local_sandbox_workspace_root="relative/workspaces", + ) diff --git a/dify-agent/tests/local/dify_agent/server/test_settings.py b/dify-agent/tests/local/dify_agent/server/test_settings.py index c9636ebbf3e..d4c9af76a8b 100644 --- a/dify-agent/tests/local/dify_agent/server/test_settings.py +++ b/dify-agent/tests/local/dify_agent/server/test_settings.py @@ -14,7 +14,7 @@ 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 +from dify_agent.runtime_backend.local import LocalExecutionBindingBackend, LocalHomeSnapshotBackend def _base64url_secret(value: bytes) -> str: @@ -260,14 +260,22 @@ def test_build_runtime_backend_profile_returns_local_drivers_when_configured() - 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: