diff --git a/src/lib/onboard/runtime-provider/docker-operation-authority.test.ts b/src/lib/onboard/runtime-provider/docker-operation-authority.test.ts index 0a3815cbc33..e84514543aa 100644 --- a/src/lib/onboard/runtime-provider/docker-operation-authority.test.ts +++ b/src/lib/onboard/runtime-provider/docker-operation-authority.test.ts @@ -343,6 +343,16 @@ describe("Docker operation authority", () => { ); }); + it("fails closed when the fixed PATH has no Docker executable", () => { + expect(() => + createDockerOperationAuthority("sandbox-lifecycle", { + HOME: "/tmp/nemoclaw-home", + DOCKER_HOST: "unix:///tmp/nemoclaw-docker.sock", + PATH: fakeExecutableRoot(), + }), + ).toThrow("Docker operation could not resolve one absolute Docker executable"); + }); + it("rejects plaintext remote TCP before issuing a daemon command", () => { const capture = contextCapture("unix:///var/run/docker.sock"); diff --git a/src/lib/onboard/runtime-provider/docker-state-mutation.test.ts b/src/lib/onboard/runtime-provider/docker-state-mutation.test.ts index 540fd49319e..c4a1c8d55ef 100644 --- a/src/lib/onboard/runtime-provider/docker-state-mutation.test.ts +++ b/src/lib/onboard/runtime-provider/docker-state-mutation.test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; +import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { @@ -51,6 +52,16 @@ afterEach(() => { }); describe("Docker runtime-provider state mutation surface", () => { + it("uses one harness-owned absolute Docker executable", () => { + const runtime = harness(); + runtime.authority.engine.capture(["version"]); + const executable = runtime.capture.mock.calls[0]?.[0] as string; + + expect(path.isAbsolute(executable)).toBe(true); + expect(executable).toBe(fs.realpathSync(path.join(runtime.root, "bin", "docker"))); + expect(runtime.context.environment).toMatchObject({ PATH: path.join(runtime.root, "bin") }); + }); + it("resolves one full labeled runtime and records authority only on synchronous acquire", () => { const runtime = harness(); const surface = createDockerStateMutationSurface({ diff --git a/test/helpers/docker-state-mutation-harness.ts b/test/helpers/docker-state-mutation-harness.ts index 748ac6e04e3..be13ef0be52 100644 --- a/test/helpers/docker-state-mutation-harness.ts +++ b/test/helpers/docker-state-mutation-harness.ts @@ -32,6 +32,7 @@ export const DOCKER_STATE_MUTATION_PROJECTION_SHA256 = "b".repeat(64); export const DOCKER_STATE_MUTATION_STATE_ROOT = "/sandbox/.hermes"; export const DOCKER_STATE_MUTATION_LIFECYCLE_GENERATION = "generation-7"; const SANDBOX_ID = "sandbox-alpha-id"; +const DOCKER_EXECUTABLE_SOURCE = "#!/bin/sh\nexit 1\n"; const PODMAN_EXECUTABLE_BYTES = Buffer.from("qualified-podman-state-mutation", "utf8"); const PODMAN_SOCKET_AUTHORITY = { directoryChain: [], @@ -82,6 +83,13 @@ function temporaryRoot(): string { return root; } +function createDockerExecutableSearchPath(root: string): string { + const directory = path.join(root, "bin"); + fs.mkdirSync(directory, { mode: 0o700 }); + fs.writeFileSync(path.join(directory, "docker"), DOCKER_EXECUTABLE_SOURCE, { mode: 0o700 }); + return directory; +} + export function cleanupDockerStateMutationRoots(): void { for (const root of roots.splice(0)) fs.rmSync(root, { force: true, recursive: true }); } @@ -375,6 +383,7 @@ function createContainerStateMutationHarness( HOME: "/tmp/nemoclaw-home", DOCKER_CONFIG: "/tmp/nemoclaw-docker", DOCKER_HOST: "unix:///tmp/nemoclaw-docker.sock", + PATH: createDockerExecutableSearchPath(root), } : { HOME: "/tmp/nemoclaw-home" }; const dockerAuthority =