diff --git a/scripts/checks/extract-installer-pins.mts b/scripts/checks/extract-installer-pins.mts index a8696cf4f7c..64494b2e010 100644 --- a/scripts/checks/extract-installer-pins.mts +++ b/scripts/checks/extract-installer-pins.mts @@ -196,6 +196,8 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [ runtimeTemplateSha256: [ "c1922eaa4f73c1a05aa8bccf50fc40208d7f71db0e6c110dcd09d0372d1aa068", "abfc1337284d437e71e47945936af7ef0bc6f28ac2495e12fac41894eb24ce3c", + // Allow the gateway-preparation runtime template; image and manifest digest stay unchanged. + "b6e467dd20e1bcb27d26d40ccc56eaef2a0c1321537108884e0941661a840373", ], }, pinLayout: LEGACY_OPENSHELL_PIN_LAYOUT, @@ -315,6 +317,8 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [ runtimeTemplateSha256: [ "c1922eaa4f73c1a05aa8bccf50fc40208d7f71db0e6c110dcd09d0372d1aa068", "abfc1337284d437e71e47945936af7ef0bc6f28ac2495e12fac41894eb24ce3c", + // Allow the gateway-preparation runtime template; image and manifest digest stay unchanged. + "b6e467dd20e1bcb27d26d40ccc56eaef2a0c1321537108884e0941661a840373", ], }, pinLayout: LEGACY_OPENSHELL_PIN_LAYOUT, @@ -366,6 +370,8 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [ runtimeTemplateSha256: [ "c1922eaa4f73c1a05aa8bccf50fc40208d7f71db0e6c110dcd09d0372d1aa068", "abfc1337284d437e71e47945936af7ef0bc6f28ac2495e12fac41894eb24ce3c", + // Allow the gateway-preparation runtime template; image and manifest digest stay unchanged. + "b6e467dd20e1bcb27d26d40ccc56eaef2a0c1321537108884e0941661a840373", ], }, pinLayout: LEGACY_OPENSHELL_PIN_LAYOUT, @@ -414,6 +420,8 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [ runtimeTemplateSha256: [ "c1922eaa4f73c1a05aa8bccf50fc40208d7f71db0e6c110dcd09d0372d1aa068", "abfc1337284d437e71e47945936af7ef0bc6f28ac2495e12fac41894eb24ce3c", + // Allow the gateway-preparation runtime template; image and manifest digest stay unchanged. + "b6e467dd20e1bcb27d26d40ccc56eaef2a0c1321537108884e0941661a840373", ], }, pinLayout: LEGACY_OPENSHELL_PIN_LAYOUT, @@ -482,6 +490,8 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [ runtimeTemplateSha256: [ "c1922eaa4f73c1a05aa8bccf50fc40208d7f71db0e6c110dcd09d0372d1aa068", "abfc1337284d437e71e47945936af7ef0bc6f28ac2495e12fac41894eb24ce3c", + // Allow the gateway-preparation runtime template; image and manifest digest stay unchanged. + "b6e467dd20e1bcb27d26d40ccc56eaef2a0c1321537108884e0941661a840373", ], }, pinLayout: LEGACY_OPENSHELL_PIN_LAYOUT, @@ -543,6 +553,8 @@ const TRUSTED_OPENSHELL_RELEASES: readonly OpenShellReleaseTrust[] = [ runtimeTemplateSha256: [ "c1922eaa4f73c1a05aa8bccf50fc40208d7f71db0e6c110dcd09d0372d1aa068", "abfc1337284d437e71e47945936af7ef0bc6f28ac2495e12fac41894eb24ce3c", + // Allow the gateway-preparation runtime template; image and manifest digest stay unchanged. + "b6e467dd20e1bcb27d26d40ccc56eaef2a0c1321537108884e0941661a840373", // Exact #11251 template after the reviewed 0.0.116-only recovery gate. "593ced09573f8cea5d2323b6d388ebb5d30f6da241d4f511e5364a3057887911", // Exact #11251 template after stable supervisor override binding. diff --git a/test/helpers/openshell-installer-template.ts b/test/helpers/openshell-installer-template.ts index 2d1e3f362c7..ec4c58f0d94 100644 --- a/test/helpers/openshell-installer-template.ts +++ b/test/helpers/openshell-installer-template.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import assert from "node:assert/strict"; + const MACOS_METHOD_START = `MACOS_INSTALL_METHOD="\${_NEMOCLAW_OPENSHELL_INSTALL_METHOD:-auto}"`; const MACOS_METHOD_END = "esac\n"; @@ -16,3 +18,59 @@ export function installerReleaseTemplate(source: string, version: string): strin "test/", ); } + +export function addV00106OperationalTrust(source: string): string { + const withIdentityCheck = source.replace( + "pinned_sandbox_build_version() {", + `is_pinned_openshell_v00106_linux_x86_64_install() { + local openshell_bin="$1" + local gateway_bin="$2" + local sandbox_bin="$3" + local openshell_sha gateway_sha sandbox_sha + + [ "$OS" = "Linux" ] && [ "$ARCH_LABEL" = "x86_64" ] || return 1 + openshell_sha="$(file_sha256 "$openshell_bin")" || return 1 + gateway_sha="$(file_sha256 "$gateway_bin")" || return 1 + sandbox_sha="$(file_sha256 "$sandbox_bin")" || return 1 + [ "$openshell_sha" = "98ecf95113fea999e94a928043e57b04cf58a45a1b66ae8bffc73d1bc8bb1d59" ] \\ + && [ "$gateway_sha" = "e6cde8a54568aa1926ff6584ffd6984314c68dad64d2722509618a74094c622c" ] \\ + && [ "$sandbox_sha" = "019301ec8618abbed8135e8d39dde7bea47e5e92813bbc17768550de34db59f8" ] +} + +pinned_sandbox_build_version() {`, + ); + const capabilityMarker = " # OpenShell #1865 has no authoritative CLI/RPC capability query yet."; + const result = withIdentityCheck.replace( + capabilityMarker, + ` # The v0.0.106 release binaries are stripped and no longer retain every + # source-level capability marker used by the development-build fallback + # below. Accept only the reviewed executable byte identities as the stable + # release capability proof; arbitrary binaries that merely report 0.0.106 + # must still pass the fail-closed marker checks. + if is_pinned_openshell_v00106_linux_x86_64_install \\ + "$openshell_bin" "$gateway_bin" "$sandbox_bin"; then + return 0 + fi + +${capabilityMarker}`, + ); + assert.notEqual(withIdentityCheck, source, "v0.0.106 executable identity helper"); + assert.notEqual(result, withIdentityCheck, "v0.0.106 capability proof"); + return result; +} + +export function removeV00106OperationalTrust(source: string): string { + const identityStart = source.indexOf("is_pinned_openshell_v00106_linux_x86_64_install() {"); + const sandboxStart = source.indexOf("pinned_sandbox_build_version() {", identityStart); + assert.ok(![identityStart, sandboxStart].includes(-1), "v0.0.106 helper boundaries"); + const withoutIdentity = `${source.slice(0, identityStart)}${source.slice(sandboxStart)}`; + const capabilityStart = withoutIdentity.indexOf( + " # The v0.0.106 release binaries are stripped and no longer retain every", + ); + const fallbackStart = withoutIdentity.indexOf( + " # OpenShell #1865 has no authoritative CLI/RPC capability query yet.", + capabilityStart, + ); + assert.ok(![capabilityStart, fallbackStart].includes(-1), "v0.0.106 proof boundaries"); + return `${withoutIdentity.slice(0, capabilityStart)}${withoutIdentity.slice(fallbackStart)}`; +} diff --git a/test/helpers/prepared-gateway-runtime.ts b/test/helpers/prepared-gateway-runtime.ts new file mode 100644 index 00000000000..53effa49170 --- /dev/null +++ b/test/helpers/prepared-gateway-runtime.ts @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; + +export function selectPreparedGatewayRuntime(source: string): string { + return [ + [ + " getDockerDriverGatewayPid(): number | null;", + ` getDockerDriverGatewayPreparation( + versionOutput?: string | null, + platform?: NodeJS.Platform, + ): import("./docker-driver-gateway-env").DockerDriverGatewayPreparation; + getDockerDriverGatewayPid(): number | null;`, + ], + [ + ` function getDockerDriverGatewayEnv( + versionOutput: string | null = null, + platform: NodeJS.Platform = process.platform, + ): Record {`, + ` function getDockerDriverGatewayPreparation( + versionOutput: string | null = null, + platform: NodeJS.Platform = process.platform, + ): import("./docker-driver-gateway-env").DockerDriverGatewayPreparation {`, + ], + [ + "const gatewayEnv = dockerDriverGatewayEnv.buildDockerDriverGatewayEnv({", + "const preparation = dockerDriverGatewayEnv.prepareDockerDriverGatewayEnv({", + ], + [ + ` if (gatewayEnv.OPENSHELL_LOCAL_TLS_DIR) { + process.env.OPENSHELL_LOCAL_TLS_DIR = gatewayEnv.OPENSHELL_LOCAL_TLS_DIR; + } + return gatewayEnv;`, + ` if (preparation.gatewayEnv.OPENSHELL_LOCAL_TLS_DIR) { + process.env.OPENSHELL_LOCAL_TLS_DIR = preparation.gatewayEnv.OPENSHELL_LOCAL_TLS_DIR; + } + return preparation; + } + + function getDockerDriverGatewayEnv( + versionOutput: string | null = null, + platform: NodeJS.Platform = process.platform, + ): Record { + return getDockerDriverGatewayPreparation(versionOutput, platform).gatewayEnv;`, + ], + [ + " getDockerDriverGatewayEnv,", + " getDockerDriverGatewayEnv,\n getDockerDriverGatewayPreparation,", + ], + ].reduce((result, [expected, replacement]) => { + assert.ok(result.includes(expected), `preparation fixture must contain ${expected}`); + return result.replaceAll(expected, replacement); + }, source); +} diff --git a/test/install/installer-hash-check.test.ts b/test/install/installer-hash-check.test.ts index 5a4479164a0..04624e6a1bd 100644 --- a/test/install/installer-hash-check.test.ts +++ b/test/install/installer-hash-check.test.ts @@ -30,7 +30,13 @@ import { V00116_SANDBOX_BUILD_DIGESTS, v00116Pins, } from "../helpers/openshell-release-fixtures"; -import { installerReleaseTemplate } from "../helpers/openshell-installer-template"; +import { + addV00106OperationalTrust, + installerReleaseTemplate, + removeV00106OperationalTrust, +} from "../helpers/openshell-installer-template"; + +import { selectPreparedGatewayRuntime } from "../helpers/prepared-gateway-runtime"; const REPO_ROOT = path.join(import.meta.dirname, "../.."); const INSTALLER_TEMPLATE = fs.readFileSync( @@ -615,62 +621,6 @@ function replacePinFunction( return `${source.slice(0, start)}${replacement}${source.slice(next)}`; } -function addV00106OperationalTrust(source: string): string { - const withIdentityCheck = source.replace( - "pinned_sandbox_build_version() {", - `is_pinned_openshell_v00106_linux_x86_64_install() { - local openshell_bin="$1" - local gateway_bin="$2" - local sandbox_bin="$3" - local openshell_sha gateway_sha sandbox_sha - - [ "$OS" = "Linux" ] && [ "$ARCH_LABEL" = "x86_64" ] || return 1 - openshell_sha="$(file_sha256 "$openshell_bin")" || return 1 - gateway_sha="$(file_sha256 "$gateway_bin")" || return 1 - sandbox_sha="$(file_sha256 "$sandbox_bin")" || return 1 - [ "$openshell_sha" = "98ecf95113fea999e94a928043e57b04cf58a45a1b66ae8bffc73d1bc8bb1d59" ] \\ - && [ "$gateway_sha" = "e6cde8a54568aa1926ff6584ffd6984314c68dad64d2722509618a74094c622c" ] \\ - && [ "$sandbox_sha" = "019301ec8618abbed8135e8d39dde7bea47e5e92813bbc17768550de34db59f8" ] -} - -pinned_sandbox_build_version() {`, - ); - const capabilityMarker = " # OpenShell #1865 has no authoritative CLI/RPC capability query yet."; - const result = withIdentityCheck.replace( - capabilityMarker, - ` # The v0.0.106 release binaries are stripped and no longer retain every - # source-level capability marker used by the development-build fallback - # below. Accept only the reviewed executable byte identities as the stable - # release capability proof; arbitrary binaries that merely report 0.0.106 - # must still pass the fail-closed marker checks. - if is_pinned_openshell_v00106_linux_x86_64_install \\ - "$openshell_bin" "$gateway_bin" "$sandbox_bin"; then - return 0 - fi - -${capabilityMarker}`, - ); - expect(withIdentityCheck, "v0.0.106 executable identity helper").not.toBe(source); - expect(result, "v0.0.106 capability proof").not.toBe(withIdentityCheck); - return result; -} - -function removeV00106OperationalTrust(source: string): string { - const identityStart = source.indexOf("is_pinned_openshell_v00106_linux_x86_64_install() {"); - const sandboxStart = source.indexOf("pinned_sandbox_build_version() {", identityStart); - expect([identityStart, sandboxStart], "v0.0.106 helper boundaries").not.toContain(-1); - const withoutIdentity = `${source.slice(0, identityStart)}${source.slice(sandboxStart)}`; - const capabilityStart = withoutIdentity.indexOf( - " # The v0.0.106 release binaries are stripped and no longer retain every", - ); - const fallbackStart = withoutIdentity.indexOf( - " # OpenShell #1865 has no authoritative CLI/RPC capability query yet.", - capabilityStart, - ); - expect([capabilityStart, fallbackStart], "v0.0.106 proof boundaries").not.toContain(-1); - return `${withoutIdentity.slice(0, capabilityStart)}${withoutIdentity.slice(fallbackStart)}`; -} - function renderInstallerTemplate(openshellVersion: string, pinFunction: string): string { const selected = installerReleaseTemplate(INSTALLER_TEMPLATE, openshellVersion) .replace(/^MIN_VERSION="[0-9]+\.[0-9]+\.[0-9]+"$/m, `MIN_VERSION="${openshellVersion}"`) @@ -1117,6 +1067,41 @@ describe("installer hash verification", () => { }, ); + it.each(["0.0.72", "0.0.99", "0.0.101", "0.0.103", "0.0.106", "0.0.116"])( + "accepts the gateway-preparation template with the selected OpenShell %s release (#11212)", + (version) => { + const root = createFixture(version); + const runtimePath = "src/lib/onboard/docker-driver-gateway-runtime.ts"; + const candidatePins = fs.readFileSync(path.join(root, runtimePath), "utf8"); + const source = fs.readFileSync(path.join(REPO_ROOT, runtimePath), "utf8"); + const prepared = selectPreparedGatewayRuntime(source).replace( + /const OPENSHELL_SUPERVISOR_MANIFEST_DIGESTS: Readonly> = \{[\s\S]*?\n\};/, + candidatePins.trim(), + ); + fs.writeFileSync(path.join(root, runtimePath), prepared); + const result = spawnSync( + "node", + [ + "--no-warnings", + path.join(REPO_ROOT, "scripts/checks/extract-installer-pins.mts"), + "--blueprint", + path.join(root, "nemoclaw-blueprint/blueprint.yaml"), + "--installer", + path.join(root, "scripts/install-openshell.sh"), + "--brev-installer", + path.join(root, "scripts/brev-launchable-ci-cpu.sh"), + "--supervisor-runtime", + path.join(root, runtimePath), + "--format", + "tsv", + ], + { encoding: "utf8" }, + ); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain(version); + }, + ); + it("rejects v0.0.116 pins when the stable selector requests a GNU sandbox (#10790)", () => { const result = runFixture("stable-gnu-v00116", "0.0.116", true); diff --git a/test/install/installer-supervisor-manifest-trust.test.ts b/test/install/installer-supervisor-manifest-trust.test.ts index e1416760674..311f308be78 100644 --- a/test/install/installer-supervisor-manifest-trust.test.ts +++ b/test/install/installer-supervisor-manifest-trust.test.ts @@ -15,6 +15,8 @@ import { V00106_SUPERVISOR_MANIFEST_DIGEST, } from "../helpers/openshell-release-fixtures"; +import { selectPreparedGatewayRuntime } from "../helpers/prepared-gateway-runtime"; + const REPO_ROOT = path.join(import.meta.dirname, "../.."); const PARSER = path.join(REPO_ROOT, "scripts/checks/extract-installer-pins.mts"); const INSTALLER_TEMPLATE = fs.readFileSync( @@ -220,6 +222,23 @@ function runParser(options: RunOptions = {}) { } describe("OpenShell supervisor manifest trust", () => { + it("accepts the gateway runtime template that prepares the Docker driver environment (#11212)", () => { + const result = runParser({ transformSupervisor: selectPreparedGatewayRuntime }); + expect(result.status, result.stderr).toBe(0); + }); + + it("rejects a repository mutation of the gateway-preparation runtime template (#11212)", () => { + const result = runParser({ + transformSupervisor: (source) => + selectPreparedGatewayRuntime(source).replace( + "ghcr.io/nvidia/openshell/supervisor@${manifestDigest}", + "registry.invalid/openshell/supervisor@${manifestDigest}", + ), + }); + expect(result.status).toBe(1); + expect(result.stderr).toContain("supervisor runtime operational template is not base-trusted"); + }); + it("accepts the selected base-trusted OpenShell 0.0.106 supervisor identity (#6256)", () => { const result = runParser();