From 3bb9c4b28062ff40a588f4fdf9c54ef0ec30422f Mon Sep 17 00:00:00 2001 From: Harry Date: Mon, 19 Jan 2026 14:18:50 +0800 Subject: [PATCH] feat(constants): introduce DIFY_CLI_ROOT and update paths for Dify CLI and app assets - Added DIFY_CLI_ROOT constant for the root directory of Dify CLI. - Updated DIFY_CLI_PATH and DIFY_CLI_CONFIG_PATH to use absolute paths. - Modified app asset initialization to create directories under DIFY_CLI_ROOT. - Enhanced Docker and E2B environment file handling to use workspace paths. --- api/core/sandbox/__init__.py | 2 ++ api/core/sandbox/constants.py | 10 +++--- .../initializer/app_assets_initializer.py | 4 +-- .../initializer/dify_cli_initializer.py | 10 +++++- .../providers/docker_daemon_sandbox.py | 29 +++++++++++++++-- .../providers/e2b_sandbox.py | 32 ++++++++++++------- 6 files changed, 67 insertions(+), 20 deletions(-) diff --git a/api/core/sandbox/__init__.py b/api/core/sandbox/__init__.py index 6d309e1af1..c7fcd8c899 100644 --- a/api/core/sandbox/__init__.py +++ b/api/core/sandbox/__init__.py @@ -11,6 +11,7 @@ from .constants import ( DIFY_CLI_CONFIG_PATH, DIFY_CLI_PATH, DIFY_CLI_PATH_PATTERN, + DIFY_CLI_ROOT, ) from .initializer import AppAssetsInitializer, DifyCliInitializer, SandboxInitializer from .manager import SandboxManager @@ -26,6 +27,7 @@ __all__ = [ "DIFY_CLI_CONFIG_PATH", "DIFY_CLI_PATH", "DIFY_CLI_PATH_PATTERN", + "DIFY_CLI_ROOT", "AppAssetsInitializer", "ArchiveSandboxStorage", "DifyCliBinary", diff --git a/api/core/sandbox/constants.py b/api/core/sandbox/constants.py index 35a43e850d..35227227f5 100644 --- a/api/core/sandbox/constants.py +++ b/api/core/sandbox/constants.py @@ -1,11 +1,13 @@ from typing import Final -DIFY_CLI_PATH: Final[str] = ".dify/bin/dify" +# Dify CLI (absolute path - hidden in /tmp, not in sandbox workdir) +DIFY_CLI_ROOT: Final[str] = "/tmp/.dify" +DIFY_CLI_PATH: Final[str] = "/tmp/.dify/bin/dify" DIFY_CLI_PATH_PATTERN: Final[str] = "dify-cli-{os}-{arch}" -DIFY_CLI_CONFIG_PATH: Final[str] = ".dify_cli.json" +DIFY_CLI_CONFIG_PATH: Final[str] = "/tmp/.dify/.dify_cli.json" -# App Assets +# App Assets (relative path - stays in sandbox workdir) APP_ASSETS_PATH: Final[str] = "assets" -APP_ASSETS_ZIP_PATH: Final[str] = ".dify/tmp/assets.zip" +APP_ASSETS_ZIP_PATH: Final[str] = "/tmp/.dify/tmp/assets.zip" diff --git a/api/core/sandbox/initializer/app_assets_initializer.py b/api/core/sandbox/initializer/app_assets_initializer.py index a7f0d2fedd..4e0fd24307 100644 --- a/api/core/sandbox/initializer/app_assets_initializer.py +++ b/api/core/sandbox/initializer/app_assets_initializer.py @@ -10,7 +10,7 @@ from extensions.ext_database import db from extensions.ext_storage import storage from models.app_asset import AppAssets -from ..constants import APP_ASSETS_PATH, APP_ASSETS_ZIP_PATH +from ..constants import APP_ASSETS_PATH, APP_ASSETS_ZIP_PATH, DIFY_CLI_ROOT from .base import SandboxInitializer logger = logging.getLogger(__name__) @@ -44,7 +44,7 @@ class AppAssetsInitializer(SandboxInitializer): with with_connection(env) as conn: execute( env, - ["mkdir", "-p", ".dify/tmp"], + ["mkdir", "-p", f"{DIFY_CLI_ROOT}/tmp"], connection=conn, error_message="Failed to create temp directory", ) diff --git a/api/core/sandbox/initializer/dify_cli_initializer.py b/api/core/sandbox/initializer/dify_cli_initializer.py index 243d925322..68c02c1c3b 100644 --- a/api/core/sandbox/initializer/dify_cli_initializer.py +++ b/api/core/sandbox/initializer/dify_cli_initializer.py @@ -6,7 +6,7 @@ from core.virtual_environment.__base.helpers import execute from core.virtual_environment.__base.virtual_environment import VirtualEnvironment from ..bash.dify_cli import DifyCliLocator -from ..constants import DIFY_CLI_PATH +from ..constants import DIFY_CLI_PATH, DIFY_CLI_ROOT from .base import SandboxInitializer logger = logging.getLogger(__name__) @@ -18,6 +18,14 @@ class DifyCliInitializer(SandboxInitializer): def initialize(self, env: VirtualEnvironment) -> None: binary = self._locator.resolve(env.metadata.os, env.metadata.arch) + + execute( + env, + ["mkdir", "-p", f"{DIFY_CLI_ROOT}/bin"], + timeout=10, + error_message="Failed to create dify CLI directory", + ) + env.upload_file(DIFY_CLI_PATH, BytesIO(binary.path.read_bytes())) execute( diff --git a/api/core/virtual_environment/providers/docker_daemon_sandbox.py b/api/core/virtual_environment/providers/docker_daemon_sandbox.py index ccbc9699d2..12561d2357 100644 --- a/api/core/virtual_environment/providers/docker_daemon_sandbox.py +++ b/api/core/virtual_environment/providers/docker_daemon_sandbox.py @@ -369,8 +369,33 @@ class DockerDaemonEnvironment(VirtualEnvironment): return self._working_dir return f"{self._working_dir}/{relative.as_posix()}" + def _workspace_path(self, path: str) -> str: + """ + Convert a path to an absolute path in the Docker container. + Absolute paths are returned as-is, relative paths are joined with _working_dir. + """ + normalized = PurePosixPath(path) + if normalized.is_absolute(): + return str(normalized) + return self._container_path(path) + def upload_file(self, path: str, content: BytesIO) -> None: container = self._get_container() + normalized = PurePosixPath(path) + + if normalized.is_absolute(): + parent_dir = str(normalized.parent) + file_name = normalized.name + payload = content.getvalue() + tar_stream = BytesIO() + with tarfile.open(fileobj=tar_stream, mode="w") as tar: + tar_info = tarfile.TarInfo(name=file_name) + tar_info.size = len(payload) + tar.addfile(tar_info, BytesIO(payload)) + tar_stream.seek(0) + container.put_archive(parent_dir, tar_stream.read()) # pyright: ignore[reportUnknownMemberType] # + return + relative_path = self._relative_path(path) if not relative_path.parts: raise ValueError("Upload path must point to a file within the workspace.") @@ -386,7 +411,7 @@ class DockerDaemonEnvironment(VirtualEnvironment): def download_file(self, path: str) -> BytesIO: container = self._get_container() - container_path = self._container_path(path) + container_path = self._workspace_path(path) stream, _ = container.get_archive(container_path) tar_stream = BytesIO() for chunk in stream: @@ -469,7 +494,7 @@ class DockerDaemonEnvironment(VirtualEnvironment): raise RuntimeError("Docker container ID is not available for exec.") api_client = self.get_docker_api_client(self.get_docker_sock()) - working_dir = self._container_path(cwd) if cwd else self._working_dir + working_dir = self._workspace_path(cwd) if cwd else self._working_dir exec_info: dict[str, object] = cast( dict[str, object], diff --git a/api/core/virtual_environment/providers/e2b_sandbox.py b/api/core/virtual_environment/providers/e2b_sandbox.py index 58386585e7..2f63f9b92d 100644 --- a/api/core/virtual_environment/providers/e2b_sandbox.py +++ b/api/core/virtual_environment/providers/e2b_sandbox.py @@ -1,4 +1,4 @@ -import os +import posixpath import shlex import threading from collections.abc import Mapping, Sequence @@ -171,10 +171,9 @@ class E2BEnvironment(VirtualEnvironment): path (str): The path to upload the file to. content (BytesIO): The content of the file. """ - path = os.path.join(self._WORKDIR, path.lstrip("/")) - + remote_path = self._workspace_path(path) sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX] - sandbox.files.write(path, content) # pyright: ignore[reportUnknownMemberType] # + sandbox.files.write(remote_path, content) # pyright: ignore[reportUnknownMemberType] # def download_file(self, path: str) -> BytesIO: """ @@ -185,10 +184,9 @@ class E2BEnvironment(VirtualEnvironment): Returns: BytesIO: The content of the file. """ - path = os.path.join(self._WORKDIR, path.lstrip("/")) - + remote_path = self._workspace_path(path) sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX] - content = sandbox.files.read(path) + content = sandbox.files.read(remote_path) return BytesIO(content.encode()) def list_files(self, directory_path: str, limit: int) -> Sequence[FileState]: @@ -196,11 +194,11 @@ class E2BEnvironment(VirtualEnvironment): List files in a directory of the E2B virtual environment. """ sandbox: Sandbox = self.metadata.store[self.StoreKey.SANDBOX] - directory_path = os.path.join(self._WORKDIR, directory_path.lstrip("/")) - files_info = sandbox.files.list(directory_path, depth=self.options.get(self.OptionsKey.E2B_LIST_FILE_DEPTH, 3)) + remote_dir = self._workspace_path(directory_path) + files_info = sandbox.files.list(remote_dir, depth=self.options.get(self.OptionsKey.E2B_LIST_FILE_DEPTH, 3)) return [ FileState( - path=os.path.relpath(file_info.path, self._WORKDIR), + path=posixpath.relpath(file_info.path, self._WORKDIR), size=file_info.size, created_at=int(file_info.modified_time.timestamp()), updated_at=int(file_info.modified_time.timestamp()), @@ -225,7 +223,7 @@ class E2BEnvironment(VirtualEnvironment): stdout_stream = QueueTransportReadCloser() stderr_stream = QueueTransportReadCloser() - working_dir = os.path.join(self._WORKDIR, cwd) if cwd else self._WORKDIR + working_dir = self._workspace_path(cwd) if cwd else self._WORKDIR threading.Thread( target=self._cmd_thread, @@ -282,6 +280,18 @@ class E2BEnvironment(VirtualEnvironment): """ return self.options.get(self.OptionsKey.API_KEY, "") + def _workspace_path(self, path: str) -> str: + """ + Convert a path to an absolute path in the E2B environment. + Absolute paths are returned as-is, relative paths are joined with _WORKDIR. + """ + normalized = posixpath.normpath(path) + if normalized in ("", "."): + return self._WORKDIR + if normalized.startswith("/"): + return normalized + return posixpath.join(self._WORKDIR, normalized) + def _convert_architecture(self, arch_str: str) -> Arch: arch_map = { "x86_64": Arch.AMD64,