From 6dabaefa94db41422501c8a42a5da525461046f0 Mon Sep 17 00:00:00 2001 From: Charan Jagwani Date: Wed, 20 May 2026 13:56:44 -0700 Subject: [PATCH] fix(sandbox): add python -> python3 symlink in base image (#1452) Bare `python` invocations from agent tool calls hit "command not found" in the sandbox because Debian trixie ships only `python3`. Symlinking `/usr/local/bin/python -> /usr/bin/python3` from Dockerfile.base removes the trigger described in #1452. Verified empirically (Ubuntu 22.04 amd64, Brev): `docker run` of the built base image runs `python --version` and `python -c 'print(...)'` successfully. Reproduces the original #1452 failure mode on an unpatched main and confirms the symlink resolves on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Charan Jagwani --- Dockerfile.base | 3 ++- test/sandbox-provisioning.test.ts | 42 ++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Dockerfile.base b/Dockerfile.base index 4d920cb614..08876f33cb 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -68,7 +68,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ jq=1.7.1-6+deb13u2 \ vim-tiny=2:9.1.1230-2 \ openssh-sftp-server=1:10.0p1-7+deb13u4 \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && ln -s /usr/bin/python3 /usr/local/bin/python # gosu for privilege separation (gateway vs sandbox user). # Install from GitHub release with checksum verification instead of diff --git a/test/sandbox-provisioning.test.ts b/test/sandbox-provisioning.test.ts index dcdef43c5d..6251e5f83f 100644 --- a/test/sandbox-provisioning.test.ts +++ b/test/sandbox-provisioning.test.ts @@ -192,12 +192,20 @@ describe("sandbox provisioning: base runtime tools", () => { const dockerfile = fs.readFileSync(DOCKERFILE_BASE, "utf-8"); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-base-apt-")); const lists = path.join(tmp, "apt-lists"); + const fakePy3 = path.join(tmp, "usr-bin", "python3"); + const fakePyLink = path.join(tmp, "usr-local-bin", "python"); fs.mkdirSync(lists); + fs.mkdirSync(path.dirname(fakePy3), { recursive: true }); + fs.mkdirSync(path.dirname(fakePyLink), { recursive: true }); + fs.writeFileSync(fakePy3, "#!/bin/sh\n", { mode: 0o755 }); const command = dockerRunCommandBetween( dockerfile, "RUN apt-get update", "# gosu for privilege separation", - ).replaceAll("/var/lib/apt/lists", lists); + ) + .replaceAll("/var/lib/apt/lists", lists) + .replaceAll("/usr/local/bin/python", fakePyLink) + .replaceAll("/usr/bin/python3", fakePy3); try { const { result, calls } = runLoggedDockerShell(command, tmp, [ @@ -213,6 +221,38 @@ describe("sandbox provisioning: base runtime tools", () => { } }); + it("symlinks bare `python` to python3 so agent tool calls don't fail with command-not-found (#1452)", () => { + const dockerfile = fs.readFileSync(DOCKERFILE_BASE, "utf-8"); + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-base-pysymlink-")); + const lists = path.join(tmp, "apt-lists"); + const fakePy3 = path.join(tmp, "usr-bin", "python3"); + const fakePyLink = path.join(tmp, "usr-local-bin", "python"); + fs.mkdirSync(lists, { recursive: true }); + fs.mkdirSync(path.dirname(fakePy3), { recursive: true }); + fs.mkdirSync(path.dirname(fakePyLink), { recursive: true }); + fs.writeFileSync(fakePy3, "#!/bin/sh\necho 3.13\n", { mode: 0o755 }); + + const command = dockerRunCommandBetween( + dockerfile, + "RUN apt-get update", + "# gosu for privilege separation", + ) + .replaceAll("/var/lib/apt/lists", lists) + .replaceAll("/usr/local/bin/python", fakePyLink) + .replaceAll("/usr/bin/python3", fakePy3); + + try { + const { result } = runLoggedDockerShell(command, tmp, [ + 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; }', + ]); + expect(result.status).toBe(0); + expect(fs.lstatSync(fakePyLink).isSymbolicLink()).toBe(true); + expect(fs.readlinkSync(fakePyLink)).toBe(fakePy3); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + it("runtime hardening installs procps and e2fsprogs when a stale base lacks ps and chattr", () => { const dockerfile = fs.readFileSync(DOCKERFILE, "utf-8"); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-procps-"));