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
3 changes: 2 additions & 1 deletion Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 41 additions & 1 deletion test/sandbox-provisioning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, [
Expand All @@ -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-"));
Expand Down
Loading