Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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} \
Expand Down
15 changes: 10 additions & 5 deletions agents/langchain-deepagents-code/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion agents/langchain-deepagents-code/dcode-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
2 changes: 1 addition & 1 deletion agents/langchain-deepagents-code/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ package_registry:
hosts:
- pypi.org
- files.pythonhosted.org
binary: /usr/local/bin/pip3
binary: /opt/venv/bin/pip3
16 changes: 10 additions & 6 deletions agents/langchain-deepagents-code/policy-additions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ filesystem_policy:
include_workdir: true
read_only:
- /usr
- /opt/venv
- /lib
- /proc
- /dev/urandom
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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* }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- { path: /sandbox/**/bin/python3* }
- { path: /usr/local/bin/dcode }
5 changes: 2 additions & 3 deletions agents/langchain-deepagents-code/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion docs/security/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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}: $*"; }
Expand All @@ -31,60 +35,104 @@ 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
"
}

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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

cleanup_project_venv() {
sandbox_exec "rm -rf ${PROJECT_VENV@Q}" >/dev/null || true
}

PASSED=0
FAILED=0

if ! sandbox_exec "test -d /sandbox/.deepagents && command -v dcode >/dev/null 2>&1" >/dev/null; then
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
Loading
Loading