diff --git a/dify-agent/docker/local-sandbox/Dockerfile b/dify-agent/docker/local-sandbox/Dockerfile index f6da6bf8240..955ed68a599 100644 --- a/dify-agent/docker/local-sandbox/Dockerfile +++ b/dify-agent/docker/local-sandbox/Dockerfile @@ -9,45 +9,79 @@ FROM python:3.12-slim-bookworm AS base +ARG NODE_VERSION=22.22.1 +ARG PNPM_VERSION=11.9.0 +ARG UV_VERSION=0.8.9 +ARG DIFY_AGENT_TOOL_SPEC=.[grpc] +ARG SHELL_SESSION_MANAGER_TOOL_SPEC=shell-session-manager + ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ - DIFY_AGENT_STUB_DRIVE_BASE=/mnt/drive + DIFY_AGENT_STUB_DRIVE_BASE=/mnt/drive \ + UV_TOOL_DIR=/opt/dify-agent-tools/envs \ + UV_TOOL_BIN_DIR=/opt/dify-agent-tools/bin +ENV PATH="${UV_TOOL_BIN_DIR}:${PATH}" RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ + file \ + git \ + jq \ + less \ + openssh-client \ + procps \ + ripgrep \ tmux \ + unzip \ + xz-utils \ + zip \ + && node_arch="$(dpkg --print-architecture)" \ + && case "${node_arch}" in \ + amd64) node_arch="x64" ;; \ + arm64) node_arch="arm64" ;; \ + *) echo "Unsupported Node.js architecture: ${node_arch}" >&2; exit 1 ;; \ + esac \ + && node_dist="node-v${NODE_VERSION}-linux-${node_arch}" \ + && curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" \ + && curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/${node_dist}.tar.xz" \ + && grep " ${node_dist}.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ + && tar -xJf "${node_dist}.tar.xz" -C /usr/local --strip-components=1 \ + && rm -f SHASUMS256.txt "${node_dist}.tar.xz" \ + && npm install --global "pnpm@${PNPM_VERSION}" \ + && npm cache clean --force \ && rm -rf /var/lib/apt/lists/* -ENV UV_VERSION=0.8.9 RUN python -m pip install --no-cache-dir "uv==${UV_VERSION}" WORKDIR /opt/dify-agent -FROM base AS packages +FROM base AS tools -ENV SHELL_SESSION_MANAGER_VERSION=2.3.0 +ARG DIFY_AGENT_TOOL_SPEC +ARG SHELL_SESSION_MANAGER_TOOL_SPEC COPY pyproject.toml uv.lock README.md ./ COPY src ./src -RUN uv sync --frozen --no-dev --no-editable --extra grpc \ - && uv pip install --python .venv/bin/python "shell-session-manager==${SHELL_SESSION_MANAGER_VERSION}" +RUN uv export --frozen --no-dev --all-extras --no-emit-project --no-hashes \ + > /tmp/dify-agent-constraints.txt \ + && uv tool install --force --python /usr/local/bin/python --no-python-downloads \ + --constraints /tmp/dify-agent-constraints.txt --link-mode=copy "${DIFY_AGENT_TOOL_SPEC}" \ + && uv tool install --force --python /usr/local/bin/python --no-python-downloads \ + --constraints /tmp/dify-agent-constraints.txt --link-mode=copy "${SHELL_SESSION_MANAGER_TOOL_SPEC}" \ + && rm -f /tmp/dify-agent-constraints.txt FROM base AS production -ENV VIRTUAL_ENV=/opt/dify-agent/.venv -ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" +COPY --from=tools ${UV_TOOL_DIR} ${UV_TOOL_DIR} +COPY --from=tools ${UV_TOOL_BIN_DIR} ${UV_TOOL_BIN_DIR} -COPY --from=packages ${VIRTUAL_ENV} ${VIRTUAL_ENV} - -RUN ln -s ${VIRTUAL_ENV}/bin/dify-agent /usr/local/bin/dify-agent \ - && ln -s ${VIRTUAL_ENV}/bin/shellctl /usr/local/bin/shellctl \ - && useradd --create-home --shell /bin/sh dify \ +RUN useradd --create-home --shell /bin/sh dify \ && mkdir -p /mnt/drive \ && chown -R dify:dify /home/dify /mnt/drive 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 91842f05dbc..a915031fe1d 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 @@ -228,8 +228,14 @@ and pre-creates that directory with write access for the same user. The provided `docker/local-sandbox/Dockerfile` installs: - `tmux`, required by `shellctl` to manage shell jobs; -- `shell-session-manager==2.2.1`, which provides the `shellctl` CLI/server; +- common shell workspace tools: `git`, `openssh-client`, `jq`, `ripgrep`, + `unzip`, `zip`, `file`, `procps`, and `less`; +- `shell-session-manager==2.3.0` as a standalone uv tool, which provides the + `shellctl` CLI/server; - `uv`, so uv shebang scripts with PEP 723 metadata can run inside the shell - workspace; -- the `dify-agent` Agent Stub client CLI, including its gRPC transport extra; + workspace and Python CLI tools can be installed with isolated tool + environments; +- `node==22.22.1` and `pnpm==11.9.0`, so JavaScript and TypeScript tooling can + run inside the shell workspace without per-job installation; +- the `dify-agent[grpc]` Agent Stub client CLI as a standalone uv tool; - a non-root default user named `dify`. diff --git a/dify-agent/tests/local/test_packaging.py b/dify-agent/tests/local/test_packaging.py index 84ee53ba2dd..89a43935f5f 100644 --- a/dify-agent/tests/local/test_packaging.py +++ b/dify-agent/tests/local/test_packaging.py @@ -23,7 +23,7 @@ SERVER_RUNTIME_DEPENDENCIES = { "pydantic-ai-slim[anthropic,google,openai]>=1.85.1,<2.0.0", "pydantic-settings>=2.12.0,<3.0.0", "redis>=7.4.0,<8.0.0", - "shell-session-manager==2.2.1", + "shell-session-manager==2.3.0", "uvicorn[standard]==0.46.0", } @@ -66,24 +66,43 @@ def test_default_package_discovery_excludes_example_packages() -> None: assert "dify_agent_examples*" not in find_config["include"] -def test_project_declares_console_script_and_local_sandbox_docker_version() -> None: +def test_project_declares_console_scripts() -> None: pyproject = _read_pyproject() scripts = pyproject["project"]["scripts"] - dockerfile = (PROJECT_ROOT / "docker" / "local-sandbox" / "Dockerfile").read_text(encoding="utf-8") assert scripts["dify-agent"] == "dify_agent.agent_stub.cli.main:main" assert scripts["dify-agent-stub-server"] == "dify_agent.agent_stub.server.cli:main" - assert "SHELL_SESSION_MANAGER_VERSION=2.2.1" in dockerfile def test_local_sandbox_dockerfile_installs_stub_client_and_shellctl() -> None: dockerfile = (PROJECT_ROOT / "docker" / "local-sandbox" / "Dockerfile").read_text(encoding="utf-8") - assert "uv sync --frozen --no-dev --no-editable --extra grpc" in dockerfile - assert "SHELL_SESSION_MANAGER_VERSION=2.2.1" in dockerfile - assert "shell-session-manager==${SHELL_SESSION_MANAGER_VERSION}" in dockerfile + assert "ARG NODE_VERSION=22.22.1" in dockerfile + assert "ARG PNPM_VERSION=11.9.0" in dockerfile + assert "ARG UV_VERSION=0.8.9" in dockerfile + assert "ARG DIFY_AGENT_TOOL_SPEC=.[grpc]" in dockerfile + assert "ARG SHELL_SESSION_MANAGER_TOOL_SPEC=shell-session-manager" in dockerfile + assert "UV_TOOL_DIR=/opt/dify-agent-tools/envs" in dockerfile + assert "UV_TOOL_BIN_DIR=/opt/dify-agent-tools/bin" in dockerfile + assert "git" in dockerfile + assert "jq" in dockerfile + assert "openssh-client" in dockerfile + assert "ripgrep" in dockerfile + assert "node_dist=\"node-v${NODE_VERSION}-linux-${node_arch}\"" in dockerfile + assert 'curl -fsSLO "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt"' in dockerfile + assert 'tar -xJf "${node_dist}.tar.xz" -C /usr/local --strip-components=1' in dockerfile + assert 'npm install --global "pnpm@${PNPM_VERSION}"' in dockerfile + assert "uv export --frozen --no-dev --all-extras --no-emit-project --no-hashes" in dockerfile + assert "> /tmp/dify-agent-constraints.txt" in dockerfile + assert '--constraints /tmp/dify-agent-constraints.txt --link-mode=copy "${DIFY_AGENT_TOOL_SPEC}"' in dockerfile + assert ( + '--constraints /tmp/dify-agent-constraints.txt --link-mode=copy "${SHELL_SESSION_MANAGER_TOOL_SPEC}"' + in dockerfile + ) assert "DIFY_AGENT_STUB_DRIVE_BASE=/mnt/drive" in dockerfile - assert "ln -s ${VIRTUAL_ENV}/bin/dify-agent /usr/local/bin/dify-agent" in dockerfile - assert "ln -s ${VIRTUAL_ENV}/bin/shellctl /usr/local/bin/shellctl" in dockerfile + assert "COPY --from=tools ${UV_TOOL_DIR} ${UV_TOOL_DIR}" in dockerfile + assert "COPY --from=tools ${UV_TOOL_BIN_DIR} ${UV_TOOL_BIN_DIR}" in dockerfile + assert "VIRTUAL_ENV" not in dockerfile + assert "uv sync" not in dockerfile assert "mkdir -p /mnt/drive" in dockerfile assert '["shellctl", "serve", "--listen", "0.0.0.0:5004"]' in dockerfile