From 98abfba3a4c31c821018e0a73490eb602c7400ad Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 25 Jun 2026 12:34:43 -0700 Subject: [PATCH 1/4] fix(dcode): use managed Python venv on PATH --- agents/langchain-deepagents-code/Dockerfile | 5 +- .../langchain-deepagents-code/Dockerfile.base | 15 ++++-- .../dcode-wrapper.sh | 2 +- .../langchain-deepagents-code/manifest.yaml | 2 +- .../policy-additions.yaml | 11 +++-- agents/langchain-deepagents-code/start.sh | 5 +- .../quickstart-langchain-deepagents-code.mdx | 7 +++ docs/security/best-practices.mdx | 2 +- .../05-deepagents-code-landlock-readonly.sh | 9 +++- .../06-deepagents-code-python-egress.sh | 13 ++++++ test/langchain-deepagents-code-image.test.ts | 46 ++++++++++++++++++- 11 files changed, 96 insertions(+), 21 deletions(-) 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..65a8623edf0 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,9 @@ 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* } + - { path: /opt/venv/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..d086e212b61 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 @@ -79,6 +79,19 @@ fi info "Running Deep Agents Code arbitrary-Python egress checks in sandbox: $SANDBOX_NAME" +# 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 -q '^PATH=/usr/local/bin:/opt/venv/bin:' \ + && 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 "GitHub" "https://api.github.com/" expect_reached "PyPI" "https://pypi.org/" expect_blocked "Tavily" "https://api.tavily.com/" diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index d1d1184c6b8..67e6b58265a 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,10 @@ 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).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,10 +280,16 @@ 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('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("python3 - ${url@Q} <<'PY'"); expect(pythonEgressCheck).toContain('expect_reached "GitHub" "https://api.github.com/"'); expect(pythonEgressCheck).toContain('expect_reached "PyPI" "https://pypi.org/"'); @@ -270,12 +304,20 @@ describe("LangChain Deep Agents Code image contracts", () => { 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==', ); From de1403116fa9e3d699a4073a3bb2d53b2d1c58db Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 25 Jun 2026 12:48:29 -0700 Subject: [PATCH 2/4] fix(dcode): allow project venv PyPI installs --- agents/langchain-deepagents-code/policy-additions.yaml | 5 ++++- .../checks/06-deepagents-code-python-egress.sh | 3 ++- test/langchain-deepagents-code-image.test.ts | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/agents/langchain-deepagents-code/policy-additions.yaml b/agents/langchain-deepagents-code/policy-additions.yaml index 65a8623edf0..129f4501e16 100644 --- a/agents/langchain-deepagents-code/policy-additions.yaml +++ b/agents/langchain-deepagents-code/policy-additions.yaml @@ -93,6 +93,9 @@ network_policies: - { 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. + # 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/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 d086e212b61..d9d18432bd6 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,7 @@ 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" ok() { printf '%s\n' "${PREFIX}: OK ($*)"; } info() { printf '%s\n' "${PREFIX}: $*"; } @@ -81,7 +82,7 @@ info "Running Deep Agents Code arbitrary-Python egress checks in sandbox: $SANDB # 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 -q '^PATH=/usr/local/bin:/opt/venv/bin:' \ +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/' \ diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 67e6b58265a..ad538345849 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -249,6 +249,8 @@ describe("LangChain Deep Agents Code image contracts", () => { ); 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 }"); }); @@ -286,6 +288,8 @@ describe("LangChain Deep Agents Code image contracts", () => { 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(`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$"); From e4828f4c4dba79ab711e18a430c752d2e9034ab4 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 25 Jun 2026 14:18:06 -0700 Subject: [PATCH 3/4] test(e2e): cover dcode project venv egress --- .../06-deepagents-code-python-egress.sh | 66 ++++++++++++++----- test/langchain-deepagents-code-image.test.ts | 22 +++++-- 2 files changed, 64 insertions(+), 24 deletions(-) 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 d9d18432bd6..e1cb7a9f618 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 @@ -15,6 +15,9 @@ 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}: $*"; } @@ -32,8 +35,9 @@ 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.request url = sys.argv[1] @@ -47,29 +51,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 @@ -77,6 +89,7 @@ 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" @@ -93,12 +106,29 @@ else fail_test "sandbox Python PATH does not resolve through the managed venv: $OUT" fi -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/" +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_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 ad538345849..3d5978691c7 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -294,16 +294,26 @@ describe("LangChain Deep Agents Code image contracts", () => { 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("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("${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_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", () => { From a1295209bf1a39aa1a9db959e571e646a8576aa2 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 25 Jun 2026 14:53:36 -0700 Subject: [PATCH 4/4] test(e2e): cover dcode artifact host egress --- .../checks/06-deepagents-code-python-egress.sh | 4 ++++ test/langchain-deepagents-code-image.test.ts | 5 +++++ 2 files changed, 9 insertions(+) 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 e1cb7a9f618..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 @@ -39,11 +39,14 @@ python_probe() { 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 @@ -122,6 +125,7 @@ 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" diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 3d5978691c7..0ff2add83f5 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -294,6 +294,8 @@ describe("LangChain Deep Agents Code image contracts", () => { 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/"', @@ -306,6 +308,9 @@ describe("LangChain Deep Agents Code image contracts", () => { 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"', );