Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7120139
fix(sandbox): retry cold glibc probes
senthilr-nv Aug 5, 2026
316159d
test(sandbox): keep glibc retry test linear
senthilr-nv Aug 5, 2026
8619d8d
fix(sandbox): make glibc probe names collision resistant
senthilr-nv Aug 5, 2026
4c19d40
merge(pr): refresh #8389 onto current main
cjagwani Aug 5, 2026
87ac99d
merge(main): refresh PR #8389
cjagwani Aug 5, 2026
488f272
merge(main): refresh PR #8389
cjagwani Aug 5, 2026
5849048
merge(main): refresh PR #8389
cv Aug 6, 2026
aac9287
merge(main): refresh PR #8389
cv Aug 6, 2026
fd0b2ad
test(sandbox): cover retained glibc probe cleanup
senthilr-nv Aug 6, 2026
bcc8c25
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
b5366ea
test(sandbox): keep glibc cleanup linear
senthilr-nv Aug 7, 2026
70582e1
test(sandbox): harden glibc probe lifecycle
senthilr-nv Aug 7, 2026
6099743
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
021ea74
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
2e90b7e
Merge branch 'main' into codex/fix-cold-glibc-probe
senthilr-nv Aug 7, 2026
b804cac
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
f6576e9
Merge remote-tracking branch 'upstream/codex/fix-cold-glibc-probe' in…
senthilr-nv Aug 7, 2026
20e0ca4
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
833fc10
test(sandbox): cover terminal glibc probe cleanup
senthilr-nv Aug 7, 2026
ed3c5e3
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
106f760
fix(images): verify glibc probe cleanup
senthilr-nv Aug 7, 2026
4be7327
Merge remote-tracking branch 'upstream/main' into codex/fix-cold-glib…
senthilr-nv Aug 7, 2026
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
2 changes: 1 addition & 1 deletion src/lib/agent/base-image-hermes-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe("Hermes base-image resolver integration", () => {
(inspectOutputByKey.get(`${format}\0${ref}`) ?? "").trim(),
);
dockerMocks.capture.mockImplementation(
(args: string[]) => captureByEntrypoint.get(args[3]) ?? "",
(args: string[]) => captureByEntrypoint.get(args[args.indexOf("--entrypoint") + 1]) ?? "",
);
});

Expand Down
49 changes: 46 additions & 3 deletions src/lib/sandbox-base-image/image-compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
describe("sandbox base-image glibc compatibility", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.dockerCapture.mockImplementation((args: readonly string[]) =>
args[0] === "run" ? "ldd (GNU libc) 2.41" : "",
);
});

it.each([
Expand All @@ -44,21 +47,61 @@ describe("sandbox base-image glibc compatibility", () => {
});

it("reads the image glibc version through the Docker adapter", () => {
mocks.dockerCapture.mockReturnValue("ldd (GNU libc) 2.41\nCopyright notice");
mocks.dockerCapture.mockImplementation((args: readonly string[]) =>
args[0] === "run" ? "ldd (GNU libc) 2.41\nCopyright notice" : "",
);

expect(getImageGlibcVersion("nemoclaw:test")).toBe("2.41");
expect(mocks.dockerCapture).toHaveBeenCalledWith(
["run", "--rm", "--entrypoint", "/usr/bin/ldd", "nemoclaw:test", "--version"],
[
"run",
"--rm",
"--name",
expect.stringMatching(/^nemoclaw-glibc-probe-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/),
"--entrypoint",
"/usr/bin/ldd",
"nemoclaw:test",
"--version",
],
{ ignoreError: true, timeout: 20_000 },
);
});

it("retries a probe with missing output and removes its retained container (#8375)", () => {
mocks.dockerCapture
.mockReturnValueOnce("")
.mockReturnValueOnce("")
.mockReturnValueOnce("ldd (Debian GLIBC 2.41-12+deb13u3) 2.41");

expect(getImageGlibcVersion("nemoclaw:cold")).toBe("2.41");

const probeCalls = mocks.dockerCapture.mock.calls.filter((call) => call[0]?.[0] === "run");
expect(probeCalls.map((call) => call[1]?.timeout)).toEqual([20_000, 120_000]);
const containerNames = probeCalls.map((call) => call[0]?.[3]);
expect(new Set(containerNames)).toHaveProperty("size", 2);
const cleanupCalls = mocks.dockerCapture.mock.calls.filter((call) => call[0]?.[0] === "rm");
expect(cleanupCalls).toEqual([
[["rm", "-f", containerNames[0]], { ignoreError: true, timeout: 20_000 }],
]);
});

it("does not retry non-empty incompatible output", () => {
mocks.dockerCapture.mockImplementation((args: readonly string[]) =>
args[0] === "run" ? "musl libc (x86_64)\nVersion 1.2.5" : "",
);

expect(getImageGlibcVersion("nemoclaw:musl")).toBeNull();
expect(mocks.dockerCapture.mock.calls.filter((call) => call[0]?.[0] === "run")).toHaveLength(1);
});

it.each([
["ldd (GNU libc) 2.41", "2.39", { ok: true, version: "2.41" }],
["ldd (GNU libc) 2.36", "2.39", { ok: false, version: "2.36" }],
["musl libc (x86_64)\nVersion 1.2.5", "2.39", { ok: false, version: null }],
])("enforces the minimum glibc version %#", (output, minimum, expected) => {
mocks.dockerCapture.mockReturnValue(output);
mocks.dockerCapture.mockImplementation((args: readonly string[]) =>
args[0] === "run" ? output : "",
);

expect(imageMeetsMinimumGlibc("nemoclaw:test", minimum)).toEqual(expected);
});
Expand Down
36 changes: 31 additions & 5 deletions src/lib/sandbox-base-image/image-compatibility.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { randomUUID } from "node:crypto";
import { dockerCapture } from "../adapters/docker";
import { OPENSHELL_SANDBOX_MIN_GLIBC } from "./types";

const GLIBC_PROBE_TIMEOUTS_MS = [20_000, 120_000] as const;

export function parseGlibcVersion(output: string | null | undefined): string | null {
const text = String(output || "");
const firstLine = text.split(/\r?\n/).find((line) => line.trim());
Expand All @@ -30,11 +33,34 @@ export function versionGte(left = "0.0.0", right = "0.0.0"): boolean {
}

export function getImageGlibcVersion(imageRef: string): string | null {
const output = dockerCapture(
["run", "--rm", "--entrypoint", "/usr/bin/ldd", imageRef, "--version"],
{ ignoreError: true, timeout: 20_000 },
);
return parseGlibcVersion(output);
for (const timeout of GLIBC_PROBE_TIMEOUTS_MS) {
const containerName = `nemoclaw-glibc-probe-${randomUUID()}`;
let output = "";
try {
output = dockerCapture(
[
"run",
"--rm",
"--name",
containerName,
"--entrypoint",
"/usr/bin/ldd",
imageRef,
"--version",
],
{ ignoreError: true, timeout },
);
} finally {
if (!output) {
dockerCapture(["rm", "-f", containerName], {
ignoreError: true,
timeout: 20_000,
});
}
}
if (output) return parseGlibcVersion(output);
}
return null;
}

export function imageMeetsMinimumGlibc(
Expand Down
6 changes: 5 additions & 1 deletion test/helpers/onboard-script-mocks.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ function mockOnboardRunCapture(command, options = {}) {
if (isOpenClawSecurityInventoryProbe(command)) {
return "nemoclaw-security-inventory-ok";
}
if (/^docker run --rm --entrypoint \/usr\/bin\/ldd \S+ --version$/.test(normalized)) {
if (
normalized.startsWith("docker run ") &&
normalized.includes(" --entrypoint /usr/bin/ldd ") &&
normalized.endsWith(" --version")
) {
return "ldd (GNU libc) 2.41";
}
return mockSandboxExecCurl(command, options);
Expand Down
Loading