diff --git a/test/e2e/live/gateway-guard-legacy-keepalive-fixture.ts b/test/e2e/live/gateway-guard-legacy-keepalive-fixture.ts index 5a07a978b43..be0389fe925 100644 --- a/test/e2e/live/gateway-guard-legacy-keepalive-fixture.ts +++ b/test/e2e/live/gateway-guard-legacy-keepalive-fixture.ts @@ -4,10 +4,15 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; +import * as dockerRunNamespace from "../../../src/lib/adapters/docker/run.ts"; +import type { DockerGpuPatchDeps } from "../../../src/lib/onboard/docker-gpu-patch-types.ts"; import * as startupCommandPatchNamespace from "../../../src/lib/onboard/docker-startup-command-patch.ts"; import { redactString } from "../fixtures/redaction.ts"; const LEGACY_KEEPALIVE_COMMAND = ["sleep", "infinity"] as const; +const MANAGED_IMAGE_ENTRYPOINT = ["/usr/local/bin/nemoclaw-start"] as const; +const MANAGED_IMAGE_COMMAND = ["/bin/bash"] as const; +const LEGACY_OPENSHELL_ENTRYPOINT = ["/opt/openshell/bin/openshell-sandbox"] as const; const DEFAULT_RECREATE_TIMEOUT_SECS = 180; const DOCKER_CONTAINER_ID_PATTERN = /^[0-9a-f]{64}$/i; const startupCommandPatch = ( @@ -16,8 +21,13 @@ const startupCommandPatch = ( : startupCommandPatchNamespace ) as typeof import("../../../src/lib/onboard/docker-startup-command-patch.ts"); const { recreateOpenShellDockerSandboxWithStartupCommand } = startupCommandPatch; +const dockerRun = ( + "default" in dockerRunNamespace ? dockerRunNamespace.default : dockerRunNamespace +) as typeof import("../../../src/lib/adapters/docker/run.ts"); +const { dockerCapture: defaultDockerCapture } = dockerRun; type StartupCommandRecreate = typeof recreateOpenShellDockerSandboxWithStartupCommand; +type DockerCapture = NonNullable; export type LegacyKeepaliveFixtureOptions = { sandboxName: string; @@ -27,19 +37,91 @@ export type LegacyKeepaliveFixtureOptions = { export type LegacyKeepaliveFixtureDeps = { recreate: StartupCommandRecreate; + dockerCapture: DockerCapture; }; const defaultDeps: LegacyKeepaliveFixtureDeps = { recreate: recreateOpenShellDockerSandboxWithStartupCommand, + dockerCapture: defaultDockerCapture, }; function requireFixtureInput(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message); } +function hasExactTokens(value: unknown, expected: readonly string[]): boolean { + return ( + Array.isArray(value) && + value.length === expected.length && + value.every((token, index) => token === expected[index]) + ); +} + +export function rewriteManagedInspectForLegacyKeepalive( + output: string, + expectedContainerId: string, +): string { + let parsed: unknown; + try { + parsed = JSON.parse(output); + } catch { + throw new Error("legacy keepalive fixture could not parse Docker inspect output"); + } + requireFixtureInput( + Array.isArray(parsed) && parsed.length === 1, + "legacy keepalive fixture requires one Docker inspect record", + ); + const inspect = parsed[0]; + requireFixtureInput( + typeof inspect === "object" && inspect !== null, + "legacy keepalive fixture requires a Docker inspect object", + ); + const record = inspect as Record; + requireFixtureInput( + record.Id === expectedContainerId, + "legacy keepalive fixture Docker inspect identity changed", + ); + const config = record.Config; + requireFixtureInput( + typeof config === "object" && config !== null, + "legacy keepalive fixture requires Docker configuration", + ); + const configRecord = config as Record; + requireFixtureInput( + hasExactTokens(configRecord.Entrypoint, MANAGED_IMAGE_ENTRYPOINT) && + hasExactTokens(configRecord.Cmd, MANAGED_IMAGE_COMMAND), + "legacy keepalive fixture requires the reviewed managed-image process contract", + ); + + // The replacement container runs the exact pre-0.0.99 OpenShell supervisor + // contract. The production recreation helper still rejects other shapes. + configRecord.Entrypoint = [...LEGACY_OPENSHELL_ENTRYPOINT]; + configRecord.Cmd = []; + return JSON.stringify(parsed); +} + +function legacyKeepaliveDockerCapture( + expectedContainerId: string, + capture: DockerCapture, +): DockerCapture { + return (args, options) => { + const output = capture(args, options); + if ( + args.length === 4 && + args[0] === "inspect" && + args[1] === "--type" && + args[2] === "container" && + args[3] === expectedContainerId + ) { + return rewriteManagedInspectForLegacyKeepalive(output, expectedContainerId); + } + return output; + }; +} + export function createLegacyKeepaliveFixture( options: LegacyKeepaliveFixtureOptions, - deps: LegacyKeepaliveFixtureDeps = defaultDeps, + deps: Partial = defaultDeps, ): ReturnType { requireFixtureInput(options.sandboxName.trim() !== "", "sandbox name is required"); requireFixtureInput( @@ -47,12 +129,19 @@ export function createLegacyKeepaliveFixture( "expected container ID must be a full Docker container ID", ); - const result = deps.recreate({ - sandboxName: options.sandboxName, - expectedOldContainerId: options.expectedContainerId, - openshellSandboxCommand: LEGACY_KEEPALIVE_COMMAND, - timeoutSecs: options.timeoutSecs ?? DEFAULT_RECREATE_TIMEOUT_SECS, - }); + const recreate = deps.recreate ?? defaultDeps.recreate; + const dockerCapture = deps.dockerCapture ?? defaultDeps.dockerCapture; + const result = recreate( + { + sandboxName: options.sandboxName, + expectedOldContainerId: options.expectedContainerId, + openshellSandboxCommand: LEGACY_KEEPALIVE_COMMAND, + timeoutSecs: options.timeoutSecs ?? DEFAULT_RECREATE_TIMEOUT_SECS, + }, + { + dockerCapture: legacyKeepaliveDockerCapture(options.expectedContainerId, dockerCapture), + }, + ); requireFixtureInput( result.oldContainerId === options.expectedContainerId, diff --git a/test/e2e/support/gateway-guard-legacy-keepalive-fixture.test.ts b/test/e2e/support/gateway-guard-legacy-keepalive-fixture.test.ts index 6613bb45abf..af6645d7a17 100644 --- a/test/e2e/support/gateway-guard-legacy-keepalive-fixture.test.ts +++ b/test/e2e/support/gateway-guard-legacy-keepalive-fixture.test.ts @@ -6,9 +6,14 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it, vi } from "vitest"; +import { + buildDockerGpuCloneRunArgs, + buildDockerGpuMode, +} from "../../../src/lib/onboard/docker-gpu-patch.ts"; import { createLegacyKeepaliveFixture, type LegacyKeepaliveFixtureDeps, + rewriteManagedInspectForLegacyKeepalive, } from "../live/gateway-guard-legacy-keepalive-fixture.ts"; const OLD_CONTAINER_ID = "a".repeat(64); @@ -34,26 +39,110 @@ function successfulResult() { }; } +function managedImageInspect( + entrypoint: string[] = ["/usr/local/bin/nemoclaw-start"], + containerId = OLD_CONTAINER_ID, + command: string[] = ["/bin/bash"], +): string { + return JSON.stringify([ + { + Id: containerId, + Image: `sha256:${"c".repeat(64)}`, + Name: "/openshell-e2e-2701", + Config: { + Image: "nemoclaw-managed:test", + Entrypoint: entrypoint, + Cmd: command, + Env: ["OPENSHELL_SANDBOX_COMMAND=env /usr/local/bin/nemoclaw-start"], + }, + HostConfig: {}, + }, + ]); +} + describe("gateway guard legacy keepalive fixture", () => { - it("recreates only the pinned sandbox container with the legacy startup command", () => { - const recreate = vi.fn(() => successfulResult()); + it("recreates only the pinned sandbox container with the reviewed legacy supervisor contract (#9364)", () => { + const dockerCapture = vi.fn(() => managedImageInspect()); + const recreate = vi.fn((_, deps: Parameters[1]) => { + const rewritten = JSON.parse( + deps?.dockerCapture?.(["inspect", "--type", "container", OLD_CONTAINER_ID], { + ignoreError: true, + }) ?? "null", + ); + expect(rewritten[0].Config).toMatchObject({ + Entrypoint: ["/opt/openshell/bin/openshell-sandbox"], + Cmd: [], + }); + return successfulResult(); + }); const result = createLegacyKeepaliveFixture( { sandboxName: "e2e-2701", expectedContainerId: OLD_CONTAINER_ID, }, - { recreate }, + { recreate, dockerCapture }, ); expect(result.newContainerId).toBe(NEW_CONTAINER_ID); expect(recreate).toHaveBeenCalledOnce(); - expect(recreate).toHaveBeenCalledWith({ - sandboxName: "e2e-2701", - expectedOldContainerId: OLD_CONTAINER_ID, + expect(recreate).toHaveBeenCalledWith( + { + sandboxName: "e2e-2701", + expectedOldContainerId: OLD_CONTAINER_ID, + openshellSandboxCommand: ["sleep", "infinity"], + timeoutSecs: 180, + }, + { dockerCapture: expect.any(Function) }, + ); + }); + + it("rejects an unreviewed managed-image entrypoint before legacy recreation (#9364)", () => { + expect(() => + rewriteManagedInspectForLegacyKeepalive( + managedImageInspect(["/unreviewed/supervisor"]), + OLD_CONTAINER_ID, + ), + ).toThrow("requires the reviewed managed-image process contract"); + }); + + it("rejects an unreviewed managed-image command before legacy recreation (#9364)", () => { + expect(() => + rewriteManagedInspectForLegacyKeepalive( + managedImageInspect(["/usr/local/bin/nemoclaw-start"], OLD_CONTAINER_ID, ["/bin/sh"]), + OLD_CONTAINER_ID, + ), + ).toThrow("requires the reviewed managed-image process contract"); + }); + + it("rejects Docker inspect output for a different container before legacy recreation (#9364)", () => { + expect(() => + rewriteManagedInspectForLegacyKeepalive( + managedImageInspect(["/usr/local/bin/nemoclaw-start"], NEW_CONTAINER_ID), + OLD_CONTAINER_ID, + ), + ).toThrow("Docker inspect identity changed"); + }); + + it("produces a clone contract accepted by production startup-command validation (#9364)", () => { + const rewritten = JSON.parse( + rewriteManagedInspectForLegacyKeepalive(managedImageInspect(), OLD_CONTAINER_ID), + ); + const immutableImage = `sha256:${"c".repeat(64)}`; + const args = buildDockerGpuCloneRunArgs(rewritten[0], buildDockerGpuMode("startup-command"), { + image: immutableImage, openshellSandboxCommand: ["sleep", "infinity"], - timeoutSecs: 180, }); + + expect(args).toEqual( + expect.arrayContaining([ + "--entrypoint", + "/opt/openshell/bin/openshell-sandbox", + "--env", + "OPENSHELL_SANDBOX_COMMAND=sleep infinity", + ]), + ); + expect(args.slice(args.indexOf(immutableImage))).toEqual([immutableImage]); }); it.each([