diff --git a/src/lib/agent/base-image-hermes.test.ts b/src/lib/agent/base-image-hermes.test.ts index a75ca8f6d8..c1a7dd1c23 100644 --- a/src/lib/agent/base-image-hermes.test.ts +++ b/src/lib/agent/base-image-hermes.test.ts @@ -7,7 +7,7 @@ import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { makeAgent, withMockedDocker } from "../../../test/helpers/base-image-test-harness"; -import { dockerRunCommandBetween } from "../../../test/helpers/hermes-dockerfile-run"; +import { dockerRunCommandBetween } from "../../../test/helpers/dockerfile-run-shell"; describe("agent base image provisioning", () => { beforeEach(() => { diff --git a/test/helpers/base-apt-security-functions.ts b/test/helpers/base-apt-security-functions.ts index e19c62c350..a0166bff54 100644 --- a/test/helpers/base-apt-security-functions.ts +++ b/test/helpers/base-apt-security-functions.ts @@ -1,59 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { spawnSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; - -export function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - if (start === -1 || end === -1 || end <= start) { - throw new Error(`Expected Dockerfile block between ${startMarker} and ${endMarker}`); - } - const runIndex = dockerfile.indexOf("RUN ", start); - if (runIndex === -1 || runIndex > end) { - throw new Error(`Expected RUN instruction after ${startMarker}`); - } - const runLines: string[] = []; - for (const line of dockerfile.slice(runIndex, end).split("\n")) { - runLines.push(line); - if (!line.trimEnd().endsWith("\\")) { - break; - } - } - const lastLine = runLines[runLines.length - 1]?.trimEnd() ?? ""; - if (lastLine.endsWith("\\")) { - throw new Error(`Expected complete RUN instruction before ${endMarker}`); - } - return runLines - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - -export function runLoggedDockerShell(command: string, tmp: string, functionDefs: string[]) { - const logPath = path.join(tmp, "calls.log"); - const script = [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - ...functionDefs, - command, - ].join("\n"); - const scriptPath = path.join(tmp, "run-docker-block.sh"); - fs.writeFileSync(scriptPath, script, { mode: 0o700 }); - return spawnSync("bash", [scriptPath], { - encoding: "utf-8", - timeout: 15000, - }); -} - type DebianArchitecture = "amd64" | "arm64"; export const BASE_APT_SECURITY_HASHES: Record< diff --git a/test/helpers/dockerfile-run-shell.ts b/test/helpers/dockerfile-run-shell.ts new file mode 100644 index 0000000000..a07bbcad61 --- /dev/null +++ b/test/helpers/dockerfile-run-shell.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { SpawnSyncReturns } from "node:child_process"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const DEFAULT_SHELL_TIMEOUT_MS = 5000; +const CHOWN_LOGGER = 'chown() { printf "chown %s\\n" "$*" >> "$call_log"; }'; + +export interface LoggedDockerShellOptions { + readonly env?: Record; + readonly timeoutMs?: number; +} + +export interface LoggedDockerShellResult { + readonly calls: string; + readonly result: SpawnSyncReturns; +} + +export function dockerRunCommandBetween( + dockerfile: string, + startMarker: string, + endMarker: string, +): string { + const start = dockerfile.indexOf(startMarker); + const end = dockerfile.indexOf(endMarker, start); + if (start === -1 || end === -1 || end <= start) { + throw new Error(`Expected Dockerfile block between ${startMarker} and ${endMarker}`); + } + const runIndex = dockerfile.indexOf("RUN ", start); + if (runIndex === -1 || runIndex > end) { + throw new Error(`Expected RUN instruction after ${startMarker}`); + } + const blockLines = dockerfile.slice(runIndex, end).split("\n"); + const finalLineIndex = blockLines.findIndex((line) => !line.trimEnd().endsWith("\\")); + if (finalLineIndex === -1) { + throw new Error(`Expected complete RUN instruction before ${endMarker}`); + } + return blockLines + .slice(0, finalLineIndex + 1) + .join("\n") + .trim() + .replace(/^RUN\s+/, "") + .replace(/\\\n/g, " "); +} + +function shellEnvironment( + overrides: Record | undefined, +): NodeJS.ProcessEnv | undefined { + if (overrides === undefined) { + return undefined; + } + const childEnv = { ...process.env }; + for (const [key, value] of Object.entries(overrides)) { + if (value === undefined) { + delete childEnv[key]; + } else { + childEnv[key] = value; + } + } + return childEnv; +} + +export function runLoggedDockerShell( + command: string, + tmp: string, + functionDefs: readonly string[] = [], + options: LoggedDockerShellOptions = {}, +): LoggedDockerShellResult { + const logPath = path.join(tmp, "calls.log"); + fs.rmSync(logPath, { force: true }); + const script = [ + "#!/usr/bin/env bash", + "set -euo pipefail", + `call_log=${JSON.stringify(logPath)}`, + ...functionDefs, + command, + ].join("\n"); + const scriptPath = path.join(tmp, "run-docker-block.sh"); + fs.writeFileSync(scriptPath, script, { mode: 0o700 }); + const result = spawnSync("bash", [scriptPath], { + encoding: "utf-8", + env: shellEnvironment(options.env), + timeout: options.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS, + }); + const calls = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf-8") : ""; + return { calls, result }; +} + +export function runDockerShell(command: string, sandboxRoot: string): LoggedDockerShellResult { + return runLoggedDockerShell(command.replaceAll("/sandbox", sandboxRoot), sandboxRoot, [ + CHOWN_LOGGER, + ]); +} diff --git a/test/helpers/hermes-dockerfile-run.ts b/test/helpers/hermes-dockerfile-run.ts deleted file mode 100644 index 4f8881c6fc..0000000000 --- a/test/helpers/hermes-dockerfile-run.ts +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { spawnSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; - -export function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - if (start === -1 || end === -1 || end <= start) { - throw new Error(`Expected Dockerfile block between ${startMarker} and ${endMarker}`); - } - const runIndex = dockerfile.indexOf("RUN ", start); - if (runIndex === -1 || runIndex > end) { - throw new Error(`Expected RUN instruction after ${startMarker}`); - } - const runLines: string[] = []; - for (const line of dockerfile.slice(runIndex, end).split("\n")) { - runLines.push(line); - if (!line.trimEnd().endsWith("\\")) break; - } - const lastLine = runLines[runLines.length - 1]?.trimEnd() ?? ""; - if (lastLine.endsWith("\\")) { - throw new Error(`Expected complete RUN instruction before ${endMarker}`); - } - return runLines - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - -export function runDockerShell(command: string, sandboxRoot: string) { - const logPath = path.join(sandboxRoot, "calls.log"); - fs.rmSync(logPath, { force: true }); - const rewritten = command.replaceAll("/sandbox", sandboxRoot); - const script = [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - 'chown() { printf "chown %s\\n" "$*" >> "$call_log"; }', - rewritten, - ].join("\n"); - const scriptPath = path.join(sandboxRoot, "run-docker-block.sh"); - fs.writeFileSync(scriptPath, script, { mode: 0o700 }); - const result = spawnSync("bash", [scriptPath], { encoding: "utf-8", timeout: 5000 }); - return { result }; -} diff --git a/test/hermes-dashboard-provisioning.test.ts b/test/hermes-dashboard-provisioning.test.ts index 20356394c0..fc28556d18 100644 --- a/test/hermes-dashboard-provisioning.test.ts +++ b/test/hermes-dashboard-provisioning.test.ts @@ -1,44 +1,16 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import type { SpawnSyncReturns } from "node:child_process"; -import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { dockerRunCommandBetween } from "./helpers/hermes-dockerfile-run"; +import { dockerRunCommandBetween, runLoggedDockerShell } from "./helpers/dockerfile-run-shell"; const ROOT = path.resolve(import.meta.dirname, ".."); const HERMES_DOCKERFILE = path.join(ROOT, "agents", "hermes", "Dockerfile"); -interface LoggedDockerShellResult { - calls: string; - result: SpawnSyncReturns; -} - -function runLoggedDockerShell( - command: string, - tmp: string, - functionDefs: string[] = [], -): LoggedDockerShellResult { - const logPath = path.join(tmp, "calls.log"); - fs.rmSync(logPath, { force: true }); - const script = [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - ...functionDefs, - command, - ].join("\n"); - const scriptPath = path.join(tmp, "run-docker-block.sh"); - fs.writeFileSync(scriptPath, script, { mode: 0o700 }); - const result = spawnSync("bash", [scriptPath], { encoding: "utf-8", timeout: 5000 }); - const calls = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf-8") : ""; - return { result, calls }; -} - function dashboardBuildCommand(hermesRoot: string, rootCache: string): string { const dockerfile = fs.readFileSync(HERMES_DOCKERFILE, "utf-8"); return dockerRunCommandBetween( diff --git a/test/hermes-doctor-config-hash.test.ts b/test/hermes-doctor-config-hash.test.ts index 53d9e4fc83..cc089157ed 100644 --- a/test/hermes-doctor-config-hash.test.ts +++ b/test/hermes-doctor-config-hash.test.ts @@ -6,7 +6,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { dockerRunCommandBetween, runDockerShell } from "./helpers/hermes-dockerfile-run"; +import { dockerRunCommandBetween, runDockerShell } from "./helpers/dockerfile-run-shell"; const ROOT = path.resolve(import.meta.dirname, ".."); const HERMES_DOCKERFILE = path.join(ROOT, "agents", "hermes", "Dockerfile"); diff --git a/test/hermes-final-image-layout.test.ts b/test/hermes-final-image-layout.test.ts index c98cb3b64d..af0a9e0f47 100644 --- a/test/hermes-final-image-layout.test.ts +++ b/test/hermes-final-image-layout.test.ts @@ -7,7 +7,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { requireSingleReviewedDockerfileRunCommand } from "./helpers/dockerfile-run-commands"; -import { dockerRunCommandBetween, runDockerShell } from "./helpers/hermes-dockerfile-run"; +import { dockerRunCommandBetween, runDockerShell } from "./helpers/dockerfile-run-shell"; import { expectManagedBootstrapNativeImageContract } from "./support/managed-bootstrap-image-contract"; const ROOT = path.resolve(import.meta.dirname, ".."); diff --git a/test/hermes-mcp-runtime-capability.test.ts b/test/hermes-mcp-runtime-capability.test.ts index 8562207f11..adfb816802 100644 --- a/test/hermes-mcp-runtime-capability.test.ts +++ b/test/hermes-mcp-runtime-capability.test.ts @@ -6,33 +6,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { dockerRunCommandBetween } from "./helpers/dockerfile-run-shell"; const ROOT = path.resolve(import.meta.dirname, ".."); const HERMES_DOCKERFILE = path.join(ROOT, "agents", "hermes", "Dockerfile"); -function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - expect(start, `Expected Dockerfile start marker ${startMarker}`).toBeGreaterThanOrEqual(0); - expect(end, `Expected Dockerfile end marker ${endMarker}`).toBeGreaterThan(start); - const runIndex = dockerfile.indexOf("RUN ", start); - expect(runIndex, `Expected RUN instruction after ${startMarker}`).toBeGreaterThanOrEqual(start); - expect(runIndex, `Expected RUN instruction before ${endMarker}`).toBeLessThan(end); - const blockLines = dockerfile.slice(runIndex, end).split("\n"); - const runEnd = blockLines.findIndex((line) => !line.trimEnd().endsWith("\\")); - expect(runEnd, `Expected complete RUN instruction before ${endMarker}`).toBeGreaterThanOrEqual(0); - const runLines = blockLines.slice(0, runEnd + 1); - return runLines - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - function runHermesMcpClientImportValidation({ mcpAvailable, httpAvailable, diff --git a/test/hermes-runtime-api-key.test.ts b/test/hermes-runtime-api-key.test.ts index 2712a22aa0..2ca2ab2c98 100644 --- a/test/hermes-runtime-api-key.test.ts +++ b/test/hermes-runtime-api-key.test.ts @@ -9,7 +9,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { shellQuote } from "../src/lib/core/shell-quote"; -import { dockerRunCommandBetween } from "./helpers/hermes-dockerfile-run"; +import { dockerRunCommandBetween } from "./helpers/dockerfile-run-shell"; const START_SCRIPT = path.join(import.meta.dirname, "..", "agents", "hermes", "start.sh"); const HERMES_DOCKERFILE = path.join(import.meta.dirname, "..", "agents", "hermes", "Dockerfile"); diff --git a/test/sandbox-base-runtime-tools.test.ts b/test/sandbox-base-runtime-tools.test.ts index cf21c93492..8ab186edb2 100644 --- a/test/sandbox-base-runtime-tools.test.ts +++ b/test/sandbox-base-runtime-tools.test.ts @@ -5,11 +5,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { - BASE_APT_SECURITY_FUNCTIONS, - dockerRunCommandBetween, - runLoggedDockerShell, -} from "./helpers/base-apt-security-functions"; +import { BASE_APT_SECURITY_FUNCTIONS } from "./helpers/base-apt-security-functions"; +import { dockerRunCommandBetween, runLoggedDockerShell } from "./helpers/dockerfile-run-shell"; import { stageFixedParser, useRealPatchedParser } from "./helpers/python-parser-security-fixture"; const ROOT = path.resolve(import.meta.dirname, ".."); @@ -53,13 +50,17 @@ function runBaseAptLayer(prefix: string) { .replaceAll("/usr/local/bin/python", fakePythonLink) .replaceAll("/usr/bin/python3", pythonShim) .replaceAll("/usr/lib/python3.13/html/parser.py", fixedParser); - const result = runLoggedDockerShell(command, tmp, [ - 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; }', - 'install() { [[ "$#" -eq 8 && "$1" == "-d" && "$2" == "-o" && "$3" == "root" && "$4" == "-g" && "$5" == "root" && "$6" == "-m" && "$7" == "0755" ]] || return 64; mkdir -p "$8"; }', - 'chown() { [[ "$#" -eq 2 && "$1" == "root:root" ]] || return 64; }', - ...useRealPatchedParser(BASE_APT_SECURITY_FUNCTIONS, pythonShim), - ]); - const calls = fs.readFileSync(path.join(tmp, "calls.log"), "utf-8"); + const { calls, result } = runLoggedDockerShell( + command, + tmp, + [ + 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; }', + 'install() { [[ "$#" -eq 8 && "$1" == "-d" && "$2" == "-o" && "$3" == "root" && "$4" == "-g" && "$5" == "root" && "$6" == "-m" && "$7" == "0755" ]] || return 64; mkdir -p "$8"; }', + 'chown() { [[ "$#" -eq 2 && "$1" == "root:root" ]] || return 64; }', + ...useRealPatchedParser(BASE_APT_SECURITY_FUNCTIONS, pythonShim), + ], + { timeoutMs: 15_000 }, + ); return { calls, fakePythonLink, pythonShim, result }; } diff --git a/test/sandbox-base-security-packages.test.ts b/test/sandbox-base-security-packages.test.ts index 9b0663988a..aa1091b98f 100644 --- a/test/sandbox-base-security-packages.test.ts +++ b/test/sandbox-base-security-packages.test.ts @@ -9,9 +9,8 @@ import { SANDBOX_BASE_SECURITY_PACKAGE_INVENTORY } from "../src/lib/sandbox-base import { BASE_APT_SECURITY_HASHES, baseAptSecurityFunctions, - dockerRunCommandBetween, - runLoggedDockerShell, } from "./helpers/base-apt-security-functions"; +import { dockerRunCommandBetween, runLoggedDockerShell } from "./helpers/dockerfile-run-shell"; import { stageFixedParser, useRealPatchedParser } from "./helpers/python-parser-security-fixture"; const ROOT = path.resolve(import.meta.dirname, ".."); @@ -143,16 +142,20 @@ describe("sandbox base security packages", () => { const prepared = sandboxSecurityCommand(image, tmp); try { - const result = runLoggedDockerShell(prepared.command, tmp, [ - "perl_base_installed=0", - "perl_installed=0", - 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; [[ "$*" != *"/perl-base.deb"* ]] || perl_base_installed=1; [[ "$*" != *"/perl.deb"* ]] || perl_installed=1; }', - 'install() { [[ "$#" -eq 8 && "$1" == "-d" && "$2" == "-o" && "$3" == "root" && "$4" == "-g" && "$5" == "root" && "$6" == "-m" && "$7" == "0755" ]] || return 64; mkdir -p "$8"; }', - 'chown() { [[ "$#" -eq 2 && "$1" == "root:root" ]] || return 64; }', - ...useRealPatchedParser(baseAptSecurityFunctions(architecture), prepared.pythonShim), - ]); + const { calls, result } = runLoggedDockerShell( + prepared.command, + tmp, + [ + "perl_base_installed=0", + "perl_installed=0", + 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; [[ "$*" != *"/perl-base.deb"* ]] || perl_base_installed=1; [[ "$*" != *"/perl.deb"* ]] || perl_installed=1; }', + 'install() { [[ "$#" -eq 8 && "$1" == "-d" && "$2" == "-o" && "$3" == "root" && "$4" == "-g" && "$5" == "root" && "$6" == "-m" && "$7" == "0755" ]] || return 64; mkdir -p "$8"; }', + 'chown() { [[ "$#" -eq 2 && "$1" == "root:root" ]] || return 64; }', + ...useRealPatchedParser(baseAptSecurityFunctions(architecture), prepared.pythonShim), + ], + { timeoutMs: 15_000 }, + ); expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: "" }); - const calls = fs.readFileSync(path.join(tmp, "calls.log"), "utf-8"); expect(calls).toContain("dpkg-install"); expect(fs.readFileSync(prepared.inventory, "utf-8")).toBe(securityInventory(architecture)); expect(fs.statSync(prepared.inventory).mode & 0o777).toBe(0o444); @@ -184,17 +187,22 @@ describe("sandbox base security packages", () => { const prepared = completedImageSecurityCommand(image, tmp, architecture); try { - const result = runLoggedDockerShell(prepared.command, tmp, [ - "perl_base_installed=1", - "perl_installed=1", + const { result } = runLoggedDockerShell( + prepared.command, + tmp, [ - "stat() {", - ` [[ "$#" -eq 3 && "$1" == "-c" && "$2" == "%u:%g:%a" && "$3" == ${JSON.stringify(prepared.inventory)} ]] || return 64`, - ' printf "0:0:444\\n"', - "}", - ].join("\n"), - ...useRealPatchedParser(baseAptSecurityFunctions(architecture), prepared.pythonShim), - ]); + "perl_base_installed=1", + "perl_installed=1", + [ + "stat() {", + ` [[ "$#" -eq 3 && "$1" == "-c" && "$2" == "%u:%g:%a" && "$3" == ${JSON.stringify(prepared.inventory)} ]] || return 64`, + ' printf "0:0:444\\n"', + "}", + ].join("\n"), + ...useRealPatchedParser(baseAptSecurityFunctions(architecture), prepared.pythonShim), + ], + { timeoutMs: 15_000 }, + ); expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: "" }); } finally { fs.rmSync(tmp, { recursive: true, force: true }); @@ -212,12 +220,17 @@ describe("sandbox base security packages", () => { ); try { - const result = runLoggedDockerShell(command, tmp, [ - 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; }', - ...useRealPatchedParser(baseAptSecurityFunctions(architecture), prepared.pythonShim), - ]); + const { calls, result } = runLoggedDockerShell( + command, + tmp, + [ + 'apt-get() { printf "apt-get %s\\n" "$*" >> "$call_log"; }', + ...useRealPatchedParser(baseAptSecurityFunctions(architecture), prepared.pythonShim), + ], + { timeoutMs: 15_000 }, + ); expect(result.status).not.toBe(0); - expect(fs.readFileSync(path.join(tmp, "calls.log"), "utf-8")).not.toContain("dpkg-install"); + expect(calls).not.toContain("dpkg-install"); expect(fs.existsSync(prepared.debianSecurityDebs)).toBe(true); expect(fs.existsSync(prepared.nativeSecurityDebs)).toBe(true); } finally { diff --git a/test/sandbox-provisioning-helper-permissions.test.ts b/test/sandbox-provisioning-helper-permissions.test.ts index 5f8a36a7a7..0d411300f1 100644 --- a/test/sandbox-provisioning-helper-permissions.test.ts +++ b/test/sandbox-provisioning-helper-permissions.test.ts @@ -1,59 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { dockerRunCommandBetween, runLoggedDockerShell } from "./helpers/dockerfile-run-shell"; const ROOT = path.resolve(import.meta.dirname, ".."); const DOCKERFILE = path.join(ROOT, "Dockerfile"); -function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - expect(start, `Expected Dockerfile start marker: ${startMarker}`).toBeGreaterThanOrEqual(0); - expect(end, `Expected Dockerfile end marker: ${endMarker}`).toBeGreaterThan(start); - const runIndex = dockerfile.indexOf("RUN ", start); - expect(runIndex, `Expected RUN instruction after ${startMarker}`).toBeGreaterThanOrEqual(start); - expect(runIndex, `Expected RUN instruction before ${endMarker}`).toBeLessThan(end); - const runBlock = dockerfile.slice(runIndex, end).split("\n"); - const completeLineIndex = runBlock.findIndex((line) => !line.trimEnd().endsWith("\\")); - expect( - completeLineIndex, - `Expected complete RUN instruction before ${endMarker}`, - ).toBeGreaterThanOrEqual(0); - const runLines = runBlock.slice(0, completeLineIndex + 1); - return runLines - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - -function runLoggedDockerShell(command: string, tmp: string, functionDefs: string[] = []) { - const logPath = path.join(tmp, "calls.log"); - const scriptPath = path.join(tmp, "run-docker-block.sh"); - fs.writeFileSync( - scriptPath, - [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - ...functionDefs, - command, - ].join("\n"), - { mode: 0o700 }, - ); - const result = spawnSync("bash", [scriptPath], { encoding: "utf-8", timeout: 5000 }); - return { result }; -} - describe("sandbox provisioning: copied OpenClaw helper permissions (#2861)", () => { it("normalizes copied blueprint permissions before non-root config generation", () => { const dockerfile = fs.readFileSync(DOCKERFILE, "utf-8"); diff --git a/test/sandbox-provisioning-tavily.test.ts b/test/sandbox-provisioning-tavily.test.ts index 33597b4131..255a2975af 100644 --- a/test/sandbox-provisioning-tavily.test.ts +++ b/test/sandbox-provisioning-tavily.test.ts @@ -1,43 +1,22 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { + dockerRunCommandBetween, + type LoggedDockerShellResult, + runLoggedDockerShell, +} from "./helpers/dockerfile-run-shell"; const DOCKERFILE = path.join(import.meta.dirname, "..", "Dockerfile"); -function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - assert( - start !== -1 && end !== -1 && end > start, - `Expected Dockerfile block between ${startMarker} and ${endMarker}`, - ); - const runIndex = dockerfile.indexOf("RUN ", start); - assert(runIndex !== -1 && runIndex <= end, `Expected RUN instruction after ${startMarker}`); - const runLines = dockerfile.slice(runIndex, end).split("\n"); - const finalLine = runLines.findIndex((line) => !line.trimEnd().endsWith("\\")); - assert(finalLine !== -1, `Expected terminated RUN instruction after ${startMarker}`); - return runLines - .slice(0, finalLine + 1) - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - function runPluginInstallBlock( functionDefinition: string, env: Record, -): { calls: string; result: ReturnType } { +): LoggedDockerShellResult { const dockerfile = fs.readFileSync(DOCKERFILE, "utf-8"); const command = dockerRunCommandBetween( dockerfile, @@ -45,28 +24,9 @@ function runPluginInstallBlock( "# The reviewed cache stays root-owned and immutable to the sandbox user.", ); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-tavily-plugin-")); - const logPath = path.join(tmp, "calls.log"); - const scriptPath = path.join(tmp, "run-docker-block.sh"); - fs.writeFileSync( - scriptPath, - [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - functionDefinition, - command, - ].join("\n"), - { mode: 0o700 }, - ); try { - const result = spawnSync("bash", [scriptPath], { - encoding: "utf-8", - env: { ...process.env, ...env }, - timeout: 5000, - }); - const calls = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf-8") : ""; - return { calls, result }; + return runLoggedDockerShell(command, tmp, [functionDefinition], { env }); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } diff --git a/test/sandbox-provisioning.test.ts b/test/sandbox-provisioning.test.ts index 77e66a207e..1008f88292 100644 --- a/test/sandbox-provisioning.test.ts +++ b/test/sandbox-provisioning.test.ts @@ -14,6 +14,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { + dockerRunCommandBetween, + runDockerShell, + runLoggedDockerShell, +} from "./helpers/dockerfile-run-shell"; const ROOT = path.resolve(import.meta.dirname, ".."); const DOCKERFILE = path.join(ROOT, "Dockerfile"); @@ -33,38 +38,6 @@ function completedDockerStage(dockerfile: string): string { return start >= 0 ? dockerfile.slice(start) : dockerfile; } -function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - if (start === -1 || end === -1 || end <= start) { - throw new Error(`Expected Dockerfile block between ${startMarker} and ${endMarker}`); - } - const runIndex = dockerfile.indexOf("RUN ", start); - if (runIndex === -1 || runIndex > end) { - throw new Error(`Expected RUN instruction after ${startMarker}`); - } - const runLines: string[] = []; - for (const line of dockerfile.slice(runIndex, end).split("\n")) { - runLines.push(line); - if (!line.trimEnd().endsWith("\\")) { - break; - } - } - const lastLine = runLines[runLines.length - 1]?.trimEnd() ?? ""; - if (lastLine.endsWith("\\")) { - throw new Error(`Expected complete RUN instruction before ${endMarker}`); - } - return runLines - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - function dockerHealthCommandBetween( dockerfile: string, startMarker: string, @@ -99,58 +72,6 @@ function dockerHealthCommandBetween( return command.trim(); } -function runDockerShell(command: string, sandboxRoot: string) { - const logPath = path.join(sandboxRoot, "calls.log"); - fs.rmSync(logPath, { force: true }); - const rewritten = command.replaceAll("/sandbox", sandboxRoot); - const script = [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - 'chown() { printf "chown %s\\n" "$*" >> "$call_log"; }', - rewritten, - ].join("\n"); - const scriptPath = path.join(sandboxRoot, "run-docker-block.sh"); - fs.writeFileSync(scriptPath, script, { mode: 0o700 }); - const result = spawnSync("bash", [scriptPath], { encoding: "utf-8", timeout: 5000 }); - const calls = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf-8") : ""; - return { result, calls }; -} - -function runLoggedDockerShell( - command: string, - tmp: string, - functionDefs: string[] = [], - env: Record = {}, -) { - const logPath = path.join(tmp, "calls.log"); - fs.rmSync(logPath, { force: true }); - const script = [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - ...functionDefs, - command, - ].join("\n"); - const scriptPath = path.join(tmp, "run-docker-block.sh"); - fs.writeFileSync(scriptPath, script, { mode: 0o700 }); - const childEnv = { ...process.env }; - for (const [key, value] of Object.entries(env)) { - if (value === undefined) { - delete childEnv[key]; - } else { - childEnv[key] = value; - } - } - const result = spawnSync("bash", [scriptPath], { - encoding: "utf-8", - env: childEnv, - timeout: 5000, - }); - const calls = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf-8") : ""; - return { result, calls }; -} - function runOpenclawRepairLayoutCase(legacy: boolean) { const dockerfile = fs.readFileSync(DOCKERFILE, "utf-8"); const cleanupBlock = dockerRunCommandBetween( @@ -395,10 +316,12 @@ describe("sandbox provisioning: image health checks (#1430)", () => { tmp, ['curl() { printf "%s\\n" "$*" >> "$call_log"; }'], { - NEMOCLAW_DASHBOARD_PORT: undefined, - OPENCLAW_GATEWAY_PORT: undefined, - CHAT_UI_URL: undefined, - ...env, + env: { + NEMOCLAW_DASHBOARD_PORT: undefined, + OPENCLAW_GATEWAY_PORT: undefined, + CHAT_UI_URL: undefined, + ...env, + }, }, ); diff --git a/test/sandbox-rlimit-hooks.test.ts b/test/sandbox-rlimit-hooks.test.ts index 83e5683e2c..cf9b3108b5 100644 --- a/test/sandbox-rlimit-hooks.test.ts +++ b/test/sandbox-rlimit-hooks.test.ts @@ -6,6 +6,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { dockerRunCommandBetween, runLoggedDockerShell } from "./helpers/dockerfile-run-shell"; const ROOT = path.resolve(import.meta.dirname, ".."); const DOCKERFILE = path.join(ROOT, "Dockerfile"); @@ -19,49 +20,6 @@ const DCODE_DOCKERFILE_BASE = path.join( ); const SANDBOX_RLIMITS = path.join(ROOT, "scripts", "lib", "sandbox-rlimits.sh"); -function dockerRunCommandBetween( - dockerfile: string, - startMarker: string, - endMarker: string, -): string { - const start = dockerfile.indexOf(startMarker); - const end = dockerfile.indexOf(endMarker, start); - expect(start, `Expected Dockerfile block start marker ${startMarker}`).not.toBe(-1); - expect(end, `Expected Dockerfile block end marker ${endMarker}`).toBeGreaterThan(start); - const runIndex = dockerfile.indexOf("RUN ", start); - expect(runIndex, `Expected RUN instruction after ${startMarker}`).not.toBe(-1); - expect(runIndex, `Expected RUN instruction before ${endMarker}`).toBeLessThanOrEqual(end); - const sourceLines = dockerfile.slice(runIndex, end).split("\n"); - const finalLineIndex = sourceLines.findIndex((line) => !line.trimEnd().endsWith("\\")); - expect( - finalLineIndex, - `Expected complete RUN instruction before ${endMarker}`, - ).toBeGreaterThanOrEqual(0); - const runLines = sourceLines.slice(0, finalLineIndex + 1); - return runLines - .join("\n") - .trim() - .replace(/^RUN\s+/, "") - .replace(/\\\n/g, " "); -} - -function runLoggedDockerShell(command: string, tmp: string) { - const logPath = path.join(tmp, "calls.log"); - fs.rmSync(logPath, { force: true }); - const scriptPath = path.join(tmp, "run-docker-block.sh"); - fs.writeFileSync( - scriptPath, - [ - "#!/usr/bin/env bash", - "set -euo pipefail", - `call_log=${JSON.stringify(logPath)}`, - command, - ].join("\n"), - { mode: 0o700 }, - ); - return spawnSync("bash", [scriptPath], { encoding: "utf-8", timeout: 5000 }); -} - function copyRlimitFixture(rlimitLib: string): void { // TEST-ONLY OVERRIDE: production remains 512 in scripts/lib/sandbox-rlimits.sh. // RLIMIT_NPROC is shared by the real user, so that default can starve this @@ -451,7 +409,7 @@ describe("sandbox rlimit system hooks (#2173)", () => { .replaceAll("/etc/profile.d/nemoclaw-proxy.sh", profileHook) .replaceAll("/etc/bash.bashrc", bashrc); - const result = runLoggedDockerShell(command, tmp); + const { result } = runLoggedDockerShell(command, tmp); expect(result.status, result.stderr).toBe(0); expect(fs.readFileSync(rlimitHook, "utf-8")).toContain(expectedRlimitShim); expect(fs.readFileSync(bashrc, "utf-8")).toContain(expectedRlimitShim); @@ -485,7 +443,7 @@ describe("sandbox rlimit system hooks (#2173)", () => { .replaceAll("/etc/profile.d/nemoclaw-rlimits.sh", rlimitHook) .replaceAll("/etc/bash.bashrc", bashrc); - const result = runLoggedDockerShell(command, tmp); + const { result } = runLoggedDockerShell(command, tmp); expect(result.status, result.stderr).toBe(0); expect(fs.readFileSync(rlimitHook, "utf-8")).toContain(expectedRlimitShim); expect(fs.readFileSync(bashrc, "utf-8")).toContain(expectedRlimitShim); @@ -532,7 +490,7 @@ describe("sandbox rlimit system hooks (#2173)", () => { .replaceAll("/etc/profile.d/nemoclaw-proxy.sh", profileHook) .replaceAll("/etc/bash.bashrc", bashrc); - const result = runLoggedDockerShell(command, tmp); + const { result } = runLoggedDockerShell(command, tmp); expect(result.status, result.stderr).toBe(0); const bashrcBody = fs.readFileSync(bashrc, "utf-8"); expect(occurrenceCount(bashrcBody, expectedProxyShim)).toBe(1); @@ -675,7 +633,7 @@ describe("sandbox rlimit system hooks (#2173)", () => { // "wheel", so stub chown while preserving every chmod and hook write. const command = ["chown() { :; }", replay].join("\n"); - const result = runLoggedDockerShell(command, tmp); + const { result } = runLoggedDockerShell(command, tmp); expect(result.status, result.stderr).toBe(0); expect(fs.readFileSync(profileHook, "utf-8")).toContain(expectedRlimitShim); expect(fs.readFileSync(bashrc, "utf-8")).toContain(expectedRlimitShim);