diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index 2106063048b..ede9bcd0b73 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -30,7 +30,7 @@ RUN chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts /opt/nemoclaw-dee && chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/dcode-wrapper.sh \ && chmod -R a+rX /opt/nemoclaw-blueprint /src/lib/messaging \ && python3 /opt/nemoclaw-deepagents-code/patch-managed-deepagents-code.py \ - && rm -f /usr/local/bin/dcode /usr/local/bin/deepagents-code \ + && rm -f /usr/local/bin/dcode /usr/local/bin/deepagents-code /opt/venv/bin/dcode /opt/venv/bin/deepagents-code \ && install -m 0755 /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/bin/dcode \ && install -m 0755 /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/bin/dcode.real \ && install -m 0755 /usr/local/lib/nemoclaw/dcode-wrapper.sh /usr/local/bin/deepagents-code \ @@ -48,7 +48,8 @@ ARG NEMOCLAW_MESSAGING_PLAN_B64= ARG NEMOCLAW_DARWIN_VM_COMPAT=0 ENV HOME=/sandbox \ - PATH="/usr/local/bin:${PATH}" \ + VIRTUAL_ENV=/opt/venv \ + PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" \ NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \ NEMOCLAW_PROVIDER_KEY=${NEMOCLAW_PROVIDER_KEY} \ NEMOCLAW_UPSTREAM_PROVIDER=${NEMOCLAW_UPSTREAM_PROVIDER} \ diff --git a/agents/langchain-deepagents-code/Dockerfile.base b/agents/langchain-deepagents-code/Dockerfile.base index 2e234261ca9..bc9d9883e28 100644 --- a/agents/langchain-deepagents-code/Dockerfile.base +++ b/agents/langchain-deepagents-code/Dockerfile.base @@ -9,7 +9,9 @@ FROM node:22-trixie-slim@sha256:2d9f5c76c8f4dd36e8f253bee5d828a83a6c09f36188f0b0414325232e0b175d -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND=noninteractive \ + VIRTUAL_ENV=/opt/venv \ + PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" RUN apt-get update && apt-get install -y --no-install-recommends \ python3=3.13.5-1 \ @@ -46,26 +48,29 @@ RUN printf '%s\n' \ '# Source runtime proxy + Deep Agents Code config' \ '[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \ 'export HOME=/sandbox' \ - 'export PATH="/usr/local/bin:${PATH}"' \ + 'export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"' \ > /sandbox/.bashrc \ && printf '%s\n' \ '# Source runtime proxy + Deep Agents Code config' \ '[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \ 'export HOME=/sandbox' \ - 'export PATH="/usr/local/bin:${PATH}"' \ + 'export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"' \ > /sandbox/.profile \ && chown root:root /sandbox/.bashrc /sandbox/.profile \ && chmod 444 /sandbox/.bashrc /sandbox/.profile COPY agents/langchain-deepagents-code/requirements.lock /tmp/deepagents-code-requirements.lock -RUN pip3 install --no-cache-dir --break-system-packages --ignore-installed --require-hashes \ +RUN python3 -m venv --copies "$VIRTUAL_ENV" \ + && "$VIRTUAL_ENV/bin/pip3" install --no-cache-dir --require-hashes \ -r /tmp/deepagents-code-requirements.lock \ + && ln -sf "$VIRTUAL_ENV/bin/dcode" /usr/local/bin/dcode \ + && ln -sf "$VIRTUAL_ENV/bin/deepagents-code" /usr/local/bin/deepagents-code \ && rm -f /tmp/deepagents-code-requirements.lock \ && /usr/local/bin/dcode --version ENV HOME=/sandbox \ - PATH="/usr/local/bin:${PATH}" \ + PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" \ DEEPAGENTS_CODE_NO_UPDATE_CHECK=1 WORKDIR /sandbox diff --git a/agents/langchain-deepagents-code/dcode-wrapper.sh b/agents/langchain-deepagents-code/dcode-wrapper.sh index 06f64e5e0a3..643c925bbbf 100755 --- a/agents/langchain-deepagents-code/dcode-wrapper.sh +++ b/agents/langchain-deepagents-code/dcode-wrapper.sh @@ -7,7 +7,7 @@ set -euo pipefail export HOME=/sandbox -export PATH="/usr/local/bin:${PATH}" +export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" export DEEPAGENTS_CODE_NO_UPDATE_CHECK=1 export DEEPAGENTS_CODE_AUTO_UPDATE=0 export DEEPAGENTS_CODE_OPENAI_API_KEY="${DEEPAGENTS_CODE_OPENAI_API_KEY:-nemoclaw-managed-inference}" diff --git a/agents/langchain-deepagents-code/manifest.yaml b/agents/langchain-deepagents-code/manifest.yaml index 2501656537d..f151cd9acc9 100644 --- a/agents/langchain-deepagents-code/manifest.yaml +++ b/agents/langchain-deepagents-code/manifest.yaml @@ -72,4 +72,4 @@ package_registry: hosts: - pypi.org - files.pythonhosted.org - binary: /usr/local/bin/pip3 + binary: /opt/venv/bin/pip3 diff --git a/agents/langchain-deepagents-code/policy-additions.yaml b/agents/langchain-deepagents-code/policy-additions.yaml index ae0a5a59851..129f4501e16 100644 --- a/agents/langchain-deepagents-code/policy-additions.yaml +++ b/agents/langchain-deepagents-code/policy-additions.yaml @@ -13,6 +13,7 @@ filesystem_policy: include_workdir: true read_only: - /usr + - /opt/venv - /lib - /proc - /dev/urandom @@ -52,8 +53,8 @@ network_policies: - allow: { method: GET, path: "/v1/models/**" } binaries: - { path: /usr/local/bin/dcode } - - { path: /usr/bin/python3* } - - { path: /usr/local/lib/python3.13/** } + - { path: /opt/venv/bin/python3* } + - { path: /opt/venv/lib/python3.13/** } github: name: github @@ -71,7 +72,7 @@ network_policies: # interpreter, not only as the /usr/local/bin/dcode shell wrapper. Keep # this broad Python boundary limited to approved GitHub hosts; optional # Tavily, LangSmith, MCP, and arbitrary hosts are intentionally absent. - - { path: /usr/bin/python3* } + - { path: /opt/venv/bin/python3* } pypi: name: pypi @@ -89,9 +90,12 @@ network_policies: rules: - allow: { method: GET, path: "/**" } binaries: - - { path: /usr/local/bin/pip3 } + - { path: /opt/venv/bin/pip3 } # pip and dcode package-install traffic execute through Python. This is # intentionally process-wide only for the read-only PyPI hosts listed - # above; optional service egress must be added explicitly by policy. - - { path: /usr/bin/python3* } + # above, including project venvs under /sandbox; optional service egress + # must be added explicitly by policy. + - { path: /sandbox/**/bin/pip3 } + - { path: /opt/venv/bin/python3* } + - { path: /sandbox/**/bin/python3* } - { path: /usr/local/bin/dcode } diff --git a/agents/langchain-deepagents-code/start.sh b/agents/langchain-deepagents-code/start.sh index 822501e0322..1a74f57c3ba 100755 --- a/agents/langchain-deepagents-code/start.sh +++ b/agents/langchain-deepagents-code/start.sh @@ -7,7 +7,7 @@ set -euo pipefail export HOME=/sandbox -export PATH="/usr/local/bin:${PATH}" +export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" export DEEPAGENTS_CODE_NO_UPDATE_CHECK=1 export DEEPAGENTS_CODE_AUTO_UPDATE=0 export DEEPAGENTS_CODE_OPENAI_API_KEY="${DEEPAGENTS_CODE_OPENAI_API_KEY:-nemoclaw-managed-inference}" @@ -84,8 +84,7 @@ prepare_runtime_env() { tmp="$(mktemp /tmp/nemoclaw-proxy-env.XXXXXX)" { printf '%s\n' 'export HOME=/sandbox' - # shellcheck disable=SC2016 - printf '%s\n' 'export PATH="/usr/local/bin:${PATH}"' + printf '%s\n' 'export PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"' printf '%s\n' 'export DEEPAGENTS_CODE_NO_UPDATE_CHECK=1' printf '%s\n' 'export DEEPAGENTS_CODE_AUTO_UPDATE=0' # shellcheck disable=SC2016 diff --git a/docs/get-started/quickstart-langchain-deepagents-code.mdx b/docs/get-started/quickstart-langchain-deepagents-code.mdx index 63835007024..e5cc2fb9a3a 100644 --- a/docs/get-started/quickstart-langchain-deepagents-code.mdx +++ b/docs/get-started/quickstart-langchain-deepagents-code.mdx @@ -47,6 +47,13 @@ dcode -n "Summarize this repository" The managed wrapper launches Deep Agents Code with `HOME=/sandbox`, update checks disabled, remote Deep Agents sandbox providers disabled, MCP auto-loading disabled, and shell allow-list overrides blocked. +## Python Environment + +Deep Agents Code runs from a NemoClaw-managed Python virtual environment at `/opt/venv`. +The sandbox puts `/opt/venv/bin` on `PATH` before the system Python directories, so `python3` and `pip3` resolve to that managed environment. +NemoClaw keeps `/opt/venv` read-only to protect the installed harness. +For project-specific Python dependencies, create a separate virtual environment under `/sandbox` and activate it before installing packages. + ## State and Backup Deep Agents Code state lives under `/sandbox/.deepagents`. diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index bab83df3776..64f37ebc4ef 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -381,7 +381,7 @@ The entrypoint locks the `PATH` environment variable to system directories, prev | Aspect | Detail | |---|---| -| Default | The entrypoint sets `PATH` to `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` at startup. | +| Default | The default sandbox entrypoint sets `PATH` to `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` at startup. Agent-specific images may add locked read-only runtime directories, such as `/opt/venv/bin` for LangChain Deep Agents Code. | | What you can change | This is not a user-facing knob. The entrypoint enforces it. | | Risk if relaxed | Without PATH hardening, the agent could create an executable named `curl` or `git` in a writable directory earlier in the PATH, intercepting commands run by the entrypoint or other processes. | | Recommendation | No action needed. The entrypoint handles this automatically. | diff --git a/test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh b/test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh index 069ce7cd8be..11e016937a6 100755 --- a/test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh +++ b/test/e2e/e2e-cloud-experimental/checks/05-deepagents-code-landlock-readonly.sh @@ -52,6 +52,13 @@ else fail_test "/usr is writable under the Deep Agents Code policy: $OUT" fi +OUT=$(sandbox_exec "touch /opt/venv/deepagents-landlock-test 2>&1 || echo BLOCKED" || true) +if echo "$OUT" | grep -qi "BLOCKED\|Permission denied\|Read-only\|EACCES"; then + pass "/opt/venv is Landlock read-only for Deep Agents Code" +else + fail_test "/opt/venv is writable under the Deep Agents Code policy: $OUT" +fi + OUT=$(sandbox_exec "touch /etc/deepagents-landlock-test 2>&1 || echo BLOCKED" || true) if echo "$OUT" | grep -qi "BLOCKED\|Permission denied\|Read-only\|EACCES"; then pass "/etc is Landlock read-only for Deep Agents Code" @@ -66,7 +73,7 @@ else fail_test "/tmp is NOT writable under Landlock: $OUT" fi -sandbox_exec "rm -f /sandbox/.deepagents/deepagents-landlock-test /usr/deepagents-landlock-test /etc/deepagents-landlock-test /tmp/deepagents-landlock-test 2>/dev/null || true" || true +sandbox_exec "rm -f /sandbox/.deepagents/deepagents-landlock-test /usr/deepagents-landlock-test /opt/venv/deepagents-landlock-test /etc/deepagents-landlock-test /tmp/deepagents-landlock-test 2>/dev/null || true" || true printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed" [ "$FAILED" -eq 0 ] || exit 1 diff --git a/test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh b/test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh index 1495004f890..970e1090d10 100755 --- a/test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh +++ b/test/e2e/e2e-cloud-experimental/checks/06-deepagents-code-python-egress.sh @@ -14,6 +14,10 @@ set -euo pipefail SANDBOX_NAME="${SANDBOX_NAME:-${NEMOCLAW_SANDBOX_NAME:-e2e-cloud-onboard}}" PREFIX="06-deepagents-code-python-egress" +DCODE_CANONICAL_PATH="/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin" +PROJECT_VENV="/sandbox/.nemoclaw-e2e-project-venv" +PROJECT_PYTHON="${PROJECT_VENV}/bin/python3" +PROJECT_PIP="${PROJECT_VENV}/bin/pip3" ok() { printf '%s\n' "${PREFIX}: OK ($*)"; } info() { printf '%s\n' "${PREFIX}: $*"; } @@ -31,14 +35,18 @@ sandbox_exec() { } python_probe() { - local url="$1" - sandbox_exec "python3 - ${url@Q} <<'PY' + local python_bin="$1" + local url="$2" + sandbox_exec "${python_bin@Q} - ${url@Q} <<'PY' import sys +import urllib.error import urllib.request url = sys.argv[1] try: with urllib.request.urlopen(url, timeout=8) as response: print(f'REACHED:{response.status}') +except urllib.error.HTTPError as exc: + print(f'REACHED:{exc.code}') except Exception as exc: print(f'BLOCKED:{type(exc).__name__}:{exc}') PY @@ -46,29 +54,37 @@ PY } expect_reached() { - local label="$1" - local url="$2" + local actor="$1" + local label="$2" + local url="$3" + local python_bin="${4:-python3}" local output - output="$(python_probe "$url")" + output="$(python_probe "$python_bin" "$url")" if echo "$output" | grep -q "REACHED:"; then - pass "arbitrary Python can reach approved ${label} host" + pass "${actor} can reach approved ${label} host" else - fail_test "arbitrary Python could not reach approved ${label} host: $output" + fail_test "${actor} could not reach approved ${label} host: $output" fi } expect_blocked() { - local label="$1" - local url="$2" + local actor="$1" + local label="$2" + local url="$3" + local python_bin="${4:-python3}" local output - output="$(python_probe "$url")" + output="$(python_probe "$python_bin" "$url")" if echo "$output" | grep -q "BLOCKED:" && ! echo "$output" | grep -q "REACHED:"; then - pass "arbitrary Python cannot reach ${label} without explicit policy" + pass "${actor} cannot reach ${label} without explicit policy" else - fail_test "arbitrary Python reached ${label} unexpectedly: $output" + fail_test "${actor} reached ${label} unexpectedly: $output" fi } +cleanup_project_venv() { + sandbox_exec "rm -rf ${PROJECT_VENV@Q}" >/dev/null || true +} + PASSED=0 FAILED=0 @@ -76,15 +92,47 @@ if ! sandbox_exec "test -d /sandbox/.deepagents && command -v dcode >/dev/null 2 info "SKIP: sandbox '${SANDBOX_NAME}' is not a Deep Agents Code sandbox" exit 0 fi +trap cleanup_project_venv EXIT info "Running Deep Agents Code arbitrary-Python egress checks in sandbox: $SANDBOX_NAME" -expect_reached "GitHub" "https://api.github.com/" -expect_reached "PyPI" "https://pypi.org/" -expect_blocked "Tavily" "https://api.tavily.com/" -expect_blocked "LangSmith" "https://api.smith.langchain.com/" -expect_blocked "MCP hosts" "https://modelcontextprotocol.io/" -expect_blocked "unapproved hosts" "https://example.com/" +# shellcheck disable=SC2016 +OUT=$(sandbox_exec 'printf "PATH=%s\n" "$PATH"; printf "PYTHON=%s\n" "$(command -v python3)"; printf "PIP=%s\n" "$(command -v pip3)"; printf "PYTHON_REAL=%s\n" "$(readlink -f "$(command -v python3)")"; printf "PIP_REAL=%s\n" "$(readlink -f "$(command -v pip3)")"; printf "USRLOCAL_COUNT=%s\n" "$(printf "%s" "$PATH" | tr ":" "\n" | grep -cx "/usr/local/bin")"' || true) +if echo "$OUT" | grep -Fxq "PATH=${DCODE_CANONICAL_PATH}" \ + && echo "$OUT" | grep -q '^PYTHON=/opt/venv/bin/python3$' \ + && echo "$OUT" | grep -q '^PIP=/opt/venv/bin/pip3$' \ + && echo "$OUT" | grep -q '^PYTHON_REAL=/opt/venv/' \ + && echo "$OUT" | grep -q '^PIP_REAL=/opt/venv/' \ + && echo "$OUT" | grep -q '^USRLOCAL_COUNT=1$'; then + pass "sandbox Python and pip resolve to the managed venv before system paths" +else + fail_test "sandbox Python PATH does not resolve through the managed venv: $OUT" +fi + +expect_reached "arbitrary Python" "GitHub" "https://api.github.com/" +expect_reached "arbitrary Python" "PyPI" "https://pypi.org/" +expect_blocked "arbitrary Python" "Tavily" "https://api.tavily.com/" +expect_blocked "arbitrary Python" "LangSmith" "https://api.smith.langchain.com/" +expect_blocked "arbitrary Python" "MCP hosts" "https://modelcontextprotocol.io/" +expect_blocked "arbitrary Python" "unapproved hosts" "https://example.com/" + +# Exercise the writable-project-venv allowlist entries directly. The managed +# /opt/venv Python creates the project venv, then the probes run through the +# /sandbox/.../bin/python3 executable path that policy-additions.yaml allows +# for PyPI only. +PROJECT_OUT="$(sandbox_exec "rm -rf ${PROJECT_VENV@Q} && python3 -m venv --copies ${PROJECT_VENV@Q} && test -x ${PROJECT_PYTHON@Q} && test -x ${PROJECT_PIP@Q} && printf 'PROJECT_PYTHON=%s\n' \"\$(readlink -f ${PROJECT_PYTHON@Q})\" && printf 'PROJECT_PIP=%s\n' \"\$(readlink -f ${PROJECT_PIP@Q})\"" || true)" +if echo "$PROJECT_OUT" | grep -Fxq "PROJECT_PYTHON=${PROJECT_PYTHON}" \ + && echo "$PROJECT_OUT" | grep -Fxq "PROJECT_PIP=${PROJECT_PIP}"; then + pass "project venv under /sandbox exposes python3 and pip3 executables" + expect_reached "project venv Python under /sandbox" "PyPI" "https://pypi.org/" "$PROJECT_PYTHON" + expect_reached "project venv Python under /sandbox" "files.pythonhosted.org" "https://files.pythonhosted.org/" "$PROJECT_PYTHON" + expect_blocked "project venv Python under /sandbox" "Tavily" "https://api.tavily.com/" "$PROJECT_PYTHON" + expect_blocked "project venv Python under /sandbox" "LangSmith" "https://api.smith.langchain.com/" "$PROJECT_PYTHON" + expect_blocked "project venv Python under /sandbox" "MCP hosts" "https://modelcontextprotocol.io/" "$PROJECT_PYTHON" + expect_blocked "project venv Python under /sandbox" "unapproved hosts" "https://example.com/" "$PROJECT_PYTHON" +else + fail_test "project venv under /sandbox did not create usable python3/pip3 executables: $PROJECT_OUT" +fi printf '%s\n' "${PREFIX}: $PASSED passed, $FAILED failed" [ "$FAILED" -eq 0 ] || exit 1 diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index d1d1184c6b8..0ff2add83f5 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -9,6 +9,8 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; const agentDir = path.join(process.cwd(), "agents", "langchain-deepagents-code"); +const DCODE_CANONICAL_PATH = + "/usr/local/bin:/opt/venv/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"; function readAgentFile(name: string): string { return fs.readFileSync(path.join(agentDir, name), "utf8"); @@ -86,6 +88,8 @@ describe("LangChain Deep Agents Code image contracts", () => { }); const envFileText = fs.readFileSync(envFile, "utf8"); + expect(envFileText).toContain(`export PATH="${DCODE_CANONICAL_PATH}"`); + expect(envFileText.match(/\/usr\/local\/bin/g)).toHaveLength(1); expect(envFileText).toContain("export HTTP_PROXY=http://proxy.example:8080"); expect(envFileText).toContain("export https_proxy=https://safe-proxy.example:8443"); }); @@ -186,7 +190,9 @@ describe("LangChain Deep Agents Code image contracts", () => { const wrapper = readAgentFile("dcode-wrapper.sh"); const policy = readAgentFile("policy-additions.yaml"); - expect(dockerfile).toContain("rm -f /usr/local/bin/dcode /usr/local/bin/deepagents-code"); + expect(dockerfile).toContain( + "rm -f /usr/local/bin/dcode /usr/local/bin/deepagents-code /opt/venv/bin/dcode /opt/venv/bin/deepagents-code", + ); expect(dockerfile).toContain("patch-managed-deepagents-code.py"); expect(dockerfile).not.toContain("NEMOCLAW_WEB_SEARCH_ENABLED"); expect(wrapper).toContain("unset DEEPAGENTS_CODE_SHELL_ALLOW_LIST"); @@ -207,12 +213,30 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(policy).not.toContain("dcode.upstream"); }); + it("puts the managed Python venv before system Python in every dcode entry path", () => { + const baseDockerfile = readAgentFile("Dockerfile.base"); + const dockerfile = readAgentFile("Dockerfile"); + const startScript = readAgentFile("start.sh"); + const wrapper = readAgentFile("dcode-wrapper.sh"); + const pathContractFiles = [baseDockerfile, dockerfile, startScript, wrapper].join("\n"); + + expect(baseDockerfile).toContain("VIRTUAL_ENV=/opt/venv"); + expect(dockerfile).toContain("VIRTUAL_ENV=/opt/venv"); + expect(baseDockerfile).toContain(`PATH="${DCODE_CANONICAL_PATH}"`); + expect(dockerfile).toContain(`PATH="${DCODE_CANONICAL_PATH}"`); + expect(startScript).toContain(`export PATH="${DCODE_CANONICAL_PATH}"`); + expect(startScript).toContain(`printf '%s\\n' 'export PATH="${DCODE_CANONICAL_PATH}"'`); + expect(wrapper).toContain(`export PATH="${DCODE_CANONICAL_PATH}"`); + expect(pathContractFiles).not.toContain('PATH="/usr/local/bin:${PATH}"'); + }); + it("keeps optional service egress out of the default policy and requires Landlock", () => { const policy = readAgentFile("policy-additions.yaml"); expect(policy).not.toContain("api.tavily.com"); expect(policy).not.toContain("api.smith.langchain.com"); expect(policy).toContain(" - /usr\n"); + expect(policy).toContain(" - /opt/venv\n"); expect(policy).toContain(" - /etc\n"); expect(policy).toContain("compatibility: strict"); expect(policy).not.toContain("compatibility: best_effort"); @@ -223,6 +247,12 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(policy).toContain( "Tavily, LangSmith, MCP, and arbitrary hosts are intentionally absent", ); + expect(policy).toContain("- { path: /opt/venv/bin/python3* }"); + expect(policy).toContain("- { path: /opt/venv/bin/pip3 }"); + expect(policy).toContain("- { path: /sandbox/**/bin/python3* }"); + expect(policy).toContain("- { path: /sandbox/**/bin/pip3 }"); + expect(policy).not.toContain("- { path: /usr/bin/python3* }"); + expect(policy).not.toContain("- { path: /usr/local/bin/pip3 }"); }); it("ships live policy behavior checks for Deep Agents Code", () => { @@ -252,30 +282,61 @@ describe("LangChain Deep Agents Code image contracts", () => { expect(landlockCheck).toContain("test -d /sandbox/.deepagents && command -v dcode"); expect(landlockCheck).toContain("touch /sandbox/.deepagents/deepagents-landlock-test"); expect(landlockCheck).toContain("touch /usr/deepagents-landlock-test"); + expect(landlockCheck).toContain("touch /opt/venv/deepagents-landlock-test"); expect(landlockCheck).toContain("touch /etc/deepagents-landlock-test"); expect(landlockCheck).toContain("touch /tmp/deepagents-landlock-test"); expect(landlockCheck).toContain("/usr is Landlock read-only for Deep Agents Code"); + expect(landlockCheck).toContain("/opt/venv is Landlock read-only for Deep Agents Code"); expect(landlockCheck).toContain("/etc is Landlock read-only for Deep Agents Code"); - expect(pythonEgressCheck).toContain("python3 - ${url@Q} <<'PY'"); - expect(pythonEgressCheck).toContain('expect_reached "GitHub" "https://api.github.com/"'); - expect(pythonEgressCheck).toContain('expect_reached "PyPI" "https://pypi.org/"'); + expect(pythonEgressCheck).toContain(`DCODE_CANONICAL_PATH="${DCODE_CANONICAL_PATH}"`); + expect(pythonEgressCheck).toContain('grep -Fxq "PATH=${DCODE_CANONICAL_PATH}"'); + expect(pythonEgressCheck).toContain('printf "PYTHON_REAL=%s\\n"'); + expect(pythonEgressCheck).toContain("^PYTHON=/opt/venv/bin/python3$"); + expect(pythonEgressCheck).toContain("^PIP=/opt/venv/bin/pip3$"); + expect(pythonEgressCheck).toContain("^USRLOCAL_COUNT=1$"); + expect(pythonEgressCheck).toContain("import urllib.error"); + expect(pythonEgressCheck).toContain("except urllib.error.HTTPError as exc:"); + expect(pythonEgressCheck).toContain("${python_bin@Q} - ${url@Q} <<'PY'"); + expect(pythonEgressCheck).toContain( + 'expect_reached "arbitrary Python" "GitHub" "https://api.github.com/"', + ); + expect(pythonEgressCheck).toContain( + 'expect_reached "arbitrary Python" "PyPI" "https://pypi.org/"', + ); + expect(pythonEgressCheck).toContain('PROJECT_VENV="/sandbox/.nemoclaw-e2e-project-venv"'); + expect(pythonEgressCheck).toContain("python3 -m venv --copies"); + expect(pythonEgressCheck).toContain( + 'expect_reached "project venv Python under /sandbox" "PyPI" "https://pypi.org/" "$PROJECT_PYTHON"', + ); + expect(pythonEgressCheck).toContain( + 'expect_reached "project venv Python under /sandbox" "files.pythonhosted.org" "https://files.pythonhosted.org/" "$PROJECT_PYTHON"', + ); + expect(pythonEgressCheck).toContain( + 'expect_blocked "project venv Python under /sandbox" "Tavily" "https://api.tavily.com/" "$PROJECT_PYTHON"', + ); expect(pythonEgressCheck).toContain("https://api.tavily.com/"); expect(pythonEgressCheck).toContain("https://api.smith.langchain.com/"); expect(pythonEgressCheck).toContain("https://modelcontextprotocol.io/"); expect(pythonEgressCheck).toContain("https://example.com/"); - expect(pythonEgressCheck).toContain( - "arbitrary Python cannot reach ${label} without explicit policy", - ); + expect(pythonEgressCheck).toContain("${actor} cannot reach ${label} without explicit policy"); }); it("hash-locks Deep Agents Code base image PyPI installs", () => { const baseDockerfile = readAgentFile("Dockerfile.base"); + const manifest = readAgentFile("manifest.yaml"); const requirementsLock = readAgentFile("requirements.lock"); expect(baseDockerfile).toContain("COPY agents/langchain-deepagents-code/requirements.lock"); + expect(baseDockerfile).toContain('python3 -m venv --copies "$VIRTUAL_ENV"'); + expect(baseDockerfile).toContain( + '"$VIRTUAL_ENV/bin/pip3" install --no-cache-dir --require-hashes', + ); expect(baseDockerfile).toContain("--require-hashes"); - expect(baseDockerfile).toContain("--ignore-installed"); expect(baseDockerfile).toContain("-r /tmp/deepagents-code-requirements.lock"); + expect(baseDockerfile).not.toContain("--break-system-packages"); + expect(baseDockerfile).not.toContain("--ignore-installed"); + expect(manifest).toContain("binary: /opt/venv/bin/pip3"); + expect(manifest).not.toContain("binary: /usr/local/bin/pip3"); expect(baseDockerfile).not.toContain( 'pip3 install --no-cache-dir --break-system-packages \\"uv==', );