diff --git a/ci/e2e-assertion-budget.json b/ci/e2e-assertion-budget.json index 7fca917cd9d..2c0f63acb6a 100644 --- a/ci/e2e-assertion-budget.json +++ b/ci/e2e-assertion-budget.json @@ -15,28 +15,28 @@ "testFileCount": 86, "liveFileCount": 222, "direct": { - "expectCalls": 1882, - "matcherAssertions": 1851, + "expectCalls": 1815, + "matcherAssertions": 1784, "nodeAssertions": 100, - "namedAssertionHelpers": 627, + "namedAssertionHelpers": 610, "failCalls": 8, "throwGuards": 87, "objectFieldAssertions": 245, - "assertionPoints": 2918, - "generatedProbeBlocks": 136, - "generatedProbeConditions": 354 + "assertionPoints": 2834, + "generatedProbeBlocks": 126, + "generatedProbeConditions": 340 }, "unique": { - "expectCalls": 2374, - "matcherAssertions": 2338, + "expectCalls": 2307, + "matcherAssertions": 2271, "nodeAssertions": 119, - "namedAssertionHelpers": 930, + "namedAssertionHelpers": 913, "failCalls": 38, - "throwGuards": 638, + "throwGuards": 636, "objectFieldAssertions": 348, - "assertionPoints": 4411, - "generatedProbeBlocks": 290, - "generatedProbeConditions": 975 + "assertionPoints": 4325, + "generatedProbeBlocks": 280, + "generatedProbeConditions": 961 }, "fileMetricOrder": [ "directExpectCalls", @@ -106,7 +106,7 @@ "test/e2e/live/openclaw-tui-chat-correlation.test.ts": [40,46,40,52,4], "test/e2e/live/openshell-credential-generation-window.test.ts": [44,94,44,113,18], "test/e2e/live/openshell-gateway-auth-source-contract.test.ts": [0,0,16,28,2], - "test/e2e/live/openshell-gateway-upgrade.test.ts": [77,109,77,121,17], + "test/e2e/live/openshell-gateway-upgrade.test.ts": [10,25,10,35,7], "test/e2e/live/overlayfs-autofix.test.ts": [22,25,22,28,2], "test/e2e/live/pi-agent-qualification.test.ts": [30,37,30,52,4], "test/e2e/live/podman-cpu-lifecycle.test.ts": [40,67,53,102,1], @@ -121,7 +121,7 @@ "test/e2e/live/sandbox-rlimits-connect.test.ts": [1,1,1,1,0], "test/e2e/live/sandbox-survival.test.ts": [10,27,10,27,1], "test/e2e/live/sessions-agents-cli.test.ts": [20,29,20,31,0], - "test/e2e/live/shields-retirement-upgrade.test.ts": [14,29,14,41,5], + "test/e2e/live/shields-retirement-upgrade.test.ts": [14,29,14,39,5], "test/e2e/live/skill-agent.test.ts": [9,11,9,11,0], "test/e2e/live/snapshot-commands.test.ts": [26,39,26,41,3], "test/e2e/live/spark-express-vllm.test.ts": [35,45,47,65,4], diff --git a/internal/security-reviews/openclaw-2026.7.1-dependency-review.md b/internal/security-reviews/openclaw-2026.7.1-dependency-review.md index 234f28becd6..ddda12ae7e0 100644 --- a/internal/security-reviews/openclaw-2026.7.1-dependency-review.md +++ b/internal/security-reviews/openclaw-2026.7.1-dependency-review.md @@ -673,20 +673,23 @@ follows: - a root entrypoint starts the `gateway` user with `HOME=/sandbox`, so startup migrations do not probe the inaccessible `/root/.openclaw` path. -Installed-base coverage is the `v0.0.89-x86_64` row in the -`openshell-gateway-upgrade` E2E matrix. It installs the immutable v0.0.89 -release with OpenClaw `2026.6.10`, seeds its legacy Memory Core SQLite and -update-check state plus a durable marker in the per-agent database materialized -by the legacy CLI, then upgrades through the current installer. The row proves -the per-agent database survives intact, the global database remains healthy, -the legacy sidecar migration and `2026.7.1` startup checkpoint complete, and -the restored `apiKey: "unused"` config still receives its gateway-held -credential only at the OpenShell boundary. -This custom route supplies `COMPATIBLE_API_KEY` only to the frozen v0.0.89 -install, then deliberately withholds it from the current installer so the -post-upgrade turn proves the existing gateway-held credential was reused. The -frozen runtime intentionally creates no NVIDIA auth-profile key reference; the -E2E preserves any references that do exist without inventing one for this route. +Installed-base coverage is the `openshell-gateway-upgrade-v0-0-89-x86-64` E2E +target. It installs the immutable v0.0.89 +release with OpenClaw `2026.6.10`, upgrades through the current installer, and +proves that the surviving sandbox reaches `Ready`, preserves a workspace marker, +keeps the raw gateway credential out of the sandbox environment, +`openclaw.json`, and recursive `auth-profiles.json` files, and completes +authenticated agent turns before and after the upgrade. + +Deterministic tests own the implementation details. Legacy `update-check.json` +cleanup and unsafe-file rejection live in +`test/agents/openclaw/openclaw-2026-7-startup-compat.test.ts`. OpenClaw's +shared and per-agent SQLite permission patches, update-check migration bypass, +and SQLite sidecar permission handling live in +`test/agents/openclaw/openclaw-shared-state-permissions-patch.test.ts`. The live +test does not assert database schemas or third-party migration checkpoints. It +verifies the pinned historical fixture inputs and current OpenShell version. +Deterministic tests own other installer and platform-selection details. During image assembly, the shared-state repair rejects symbolic links, non-regular entries, and multiply linked files before it changes the ownership diff --git a/test/automation/e2e/e2e-recommendations.test.ts b/test/automation/e2e/e2e-recommendations.test.ts index 79d1574a489..e9d54d9965f 100644 --- a/test/automation/e2e/e2e-recommendations.test.ts +++ b/test/automation/e2e/e2e-recommendations.test.ts @@ -127,6 +127,7 @@ describe("E2E recommendation normalizer", () => { "tools/e2e/execution-coverage.mts", "tools/e2e/gateway-runtime.mts", "tools/e2e/onboard-timeout-contract.mts", + "tools/e2e/openshell-gateway-upgrade-fixture.mts", "tools/e2e/selector-aliases.mts", "tools/e2e/target-catalogue.mts", "scripts/checks/llama-cpp-dgx-spark-qualification-paths.mts", diff --git a/test/e2e/README.md b/test/e2e/README.md index df2970c63ba..32413c76657 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -177,13 +177,23 @@ The historical fixtures retain these version boundaries: | Fixture | Required boundary | | --- | --- | -| `openshell-gateway-upgrade` | Retain the historical installer commit and SHA-256 digest, sandbox image digest, and reviewed OpenClaw npm URL and SHA-512 integrity. Install the historical package before testing the candidate upgrade path. | +| `openshell-gateway-upgrade` | Retain one v0.0.89 fixture with a pinned installer commit and digest, sandbox image digest, and reviewed OpenClaw archive. Prove that the current gateway upgrade leaves its sandbox Ready, preserves a workspace marker, keeps the raw gateway credential out of the sandbox environment, `/sandbox/.openclaw/openclaw.json`, and recursive `auth-profiles.json` files below `/sandbox/.openclaw/agents`, and supports authenticated agent turns before and after the upgrade. | | `rebuild-openclaw` | Retain the reviewed old-base build in the target. Build and create the old sandbox before testing the candidate rebuild path. | These targets may restore the shared artifact for the candidate CLI. They must not replace a historical installer, package, image, or version boundary with that artifact. The gateway fixture already binds its remote historical inputs to immutable commits and cryptographic digests. The workflow does not republish those inputs as artifacts. +Deterministic tests own installer identity, OpenShell release asset selection, +NemoClaw restore behavior, and Dockerfile patch behavior. The live target does not +assert OpenClaw database tables, migration checkpoints, or other third-party +storage details. + +The retained live target owns the released-gateway upgrade and usable-survivor +boundary. Deterministic rebuild-flow tests own post-backup recreate failure, +preserved backup and registry state, recovery-journal retention, and successful +retry; stale-recovery tests own fail-closed behavior when no authoritative live +policy remains. ### Hermes Sandbox Image Artifact diff --git a/test/e2e/live/openshell-gateway-upgrade-helpers.ts b/test/e2e/live/openshell-gateway-upgrade-helpers.ts index 2f734442172..00fea678ee6 100644 --- a/test/e2e/live/openshell-gateway-upgrade-helpers.ts +++ b/test/e2e/live/openshell-gateway-upgrade-helpers.ts @@ -2,17 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import { shellQuote } from "../fixtures/clients/command.ts"; +import { REVIEWED_GATEWAY_UPGRADE_FIXTURE } from "../../../tools/e2e/openshell-gateway-upgrade-fixture.mts"; import { reviewedOldInstallerProfile } from "./openshell-gateway-upgrade-old-installer.ts"; const NON_INTERACTIVE_INSTALLER_ARGS = ["--non-interactive", "--yes-i-accept-third-party-software"]; const GATEWAY_VOLUME_PREFIX = "openshell-cluster-nemoclaw"; -const LEGACY_GATEWAY_DOCKER_NETWORK = "openshell-cluster-nemoclaw"; export const GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS = 35 * 60_000; export interface LegacyGatewayUpgradeFixture { nemoclawRef: string; nemoclawCommit: string; installerSha256: string; + openShellVersion: string; openclawVersion: string; sandboxBaseImageRef: string; } @@ -28,23 +29,33 @@ export function validateLegacyGatewayUpgradeFixture(fixture: LegacyGatewayUpgrad `NEMOCLAW_OLD_NEMOCLAW_COMMIT must be a full lowercase commit SHA; got ${fixture.nemoclawCommit}`, ); } - if (!/^[0-9a-f]{64}$/.test(fixture.installerSha256)) { + if ( + !/^[0-9a-f]{64}$/.test(fixture.installerSha256) || + fixture.installerSha256 !== REVIEWED_GATEWAY_UPGRADE_FIXTURE.installerSha256 + ) { throw new Error( - `NEMOCLAW_OLD_INSTALLER_SHA256 must be a lowercase SHA-256 digest; got ${fixture.installerSha256}`, + `NEMOCLAW_OLD_INSTALLER_SHA256 must match the reviewed descriptor's lowercase SHA-256 digest; got ${fixture.installerSha256}`, ); } - if (!/^\d{4}\.\d{1,2}\.\d{1,2}$/.test(fixture.openclawVersion)) { + if ( + !/^\d{4}\.\d{1,2}\.\d{1,2}$/.test(fixture.openclawVersion) || + !/^\d+\.\d+\.\d+$/.test(fixture.openShellVersion) || + fixture.openShellVersion !== REVIEWED_GATEWAY_UPGRADE_FIXTURE.openShellVersion + ) { throw new Error( - `NEMOCLAW_OLD_OPENCLAW_VERSION must use the YYYY.M.D release format; got ${fixture.openclawVersion}`, + `NEMOCLAW_OLD_OPENCLAW_VERSION and NEMOCLAW_OLD_OPENSHELL_VERSION must match the reviewed descriptor; got ${fixture.openclawVersion}/${fixture.openShellVersion}`, ); } reviewedOldInstallerProfile(fixture); const sandboxBaseDigest = fixture.sandboxBaseImageRef.match( /^[^@\s]+@sha256:([0-9a-f]{64})$/, )?.[1]; - if (!sandboxBaseDigest) { + if ( + !sandboxBaseDigest || + fixture.sandboxBaseImageRef !== REVIEWED_GATEWAY_UPGRADE_FIXTURE.sandboxBaseImageRef + ) { throw new Error( - `NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF must be digest-pinned; got ${fixture.sandboxBaseImageRef}`, + `NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF must match the reviewed descriptor and use a digest pin; got ${fixture.sandboxBaseImageRef}`, ); } return { sandboxBaseDigest }; @@ -54,11 +65,8 @@ export function oldGatewayUpgradeInstallerArgs(installer: string): string[] { return [installer, ...NON_INTERACTIVE_INSTALLER_ARGS, "--fresh"]; } -export function currentGatewayUpgradeInstallerArgs( - installer: string, - options: { interactive?: boolean } = {}, -): string[] { - return options.interactive ? [installer] : [installer, ...NON_INTERACTIVE_INSTALLER_ARGS]; +export function currentGatewayUpgradeInstallerArgs(installer: string): string[] { + return [installer, ...NON_INTERACTIVE_INSTALLER_ARGS]; } export function currentNemoclawUpgradeRef(env: NodeJS.ProcessEnv): string { @@ -72,29 +80,13 @@ export function currentNemoclawUpgradeRef(env: NodeJS.ProcessEnv): string { return "HEAD"; } -export function legacyGatewayUpgradeHostFirewallOptions(nemoclawRef: string): { +export function legacyGatewayUpgradeHostFirewallOptions(): { networkName: string | undefined; waitForNetworkMs: number; } { - let networkName: string | undefined; - switch (nemoclawRef) { - case "v0.0.36": - // This cluster-era gateway names its bridge after the gateway; newer - // Docker gateways use the host fixture's openshell-docker default. - networkName = LEGACY_GATEWAY_DOCKER_NETWORK; - break; - case "v0.0.55": - case "v0.0.74": - case "v0.0.89": - case "v0.0.115": - networkName = undefined; - break; - default: - throw new Error(`Unsupported gateway-upgrade network fixture: ${nemoclawRef}`); - } // The historical install creates its network after fetching and building // its payload, so keep the parallel probe alive for the full install budget. - return { networkName, waitForNetworkMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS }; + return { networkName: undefined, waitForNetworkMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS }; } export function throwGatewayUpgradeSetupFailures( @@ -109,23 +101,6 @@ export function throwGatewayUpgradeSetupFailures( } } -export function expectedLegacyRegistryMetadata(nemoclawRef: string): { - nemoclawVersion: string | undefined; - fromDockerfile: null | undefined; -} { - switch (nemoclawRef) { - case "v0.0.36": - case "v0.0.55": - return { nemoclawVersion: undefined, fromDockerfile: undefined }; - case "v0.0.74": - return { nemoclawVersion: "0.0.74", fromDockerfile: null }; - case "v0.0.89": - return { nemoclawVersion: "0.0.89", fromDockerfile: null }; - default: - throw new Error(`Unsupported gateway-upgrade registry fixture: ${nemoclawRef}`); - } -} - export function upgradeGatewayStateCleanupScript(pidFile: string): string { return `set -e volume_prefix=${GATEWAY_VOLUME_PREFIX} diff --git a/test/e2e/live/openshell-gateway-upgrade-old-installer.ts b/test/e2e/live/openshell-gateway-upgrade-old-installer.ts index f95ce3ae86c..9a4e1c3e084 100644 --- a/test/e2e/live/openshell-gateway-upgrade-old-installer.ts +++ b/test/e2e/live/openshell-gateway-upgrade-old-installer.ts @@ -2,56 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; +import { REVIEWED_GATEWAY_UPGRADE_FIXTURE } from "../../../tools/e2e/openshell-gateway-upgrade-fixture.mts"; -export type ReviewedOldOpenClawArchive = Readonly<{ - expectedIntegrity: string; - label: string; - packageSpec: string; - tarballUrl: string; -}>; - -export type OldInstallerFixtureIdentity = Readonly<{ +type ReviewedOldOpenClawArchive = typeof REVIEWED_GATEWAY_UPGRADE_FIXTURE.openClawArchive; +type OldInstallerFixtureIdentity = Readonly<{ nemoclawCommit: string; nemoclawRef: string; openclawVersion: string; }>; - -type ReviewedOldInstallerProfile = OldInstallerFixtureIdentity & - Readonly<{ - expectedAdvisoryAuditCount: 0 | 1; - }>; - -const REVIEWED_OLD_OPENCLAW_ARCHIVES: Readonly> = - Object.freeze({ - "2026.4.24": { - expectedIntegrity: - "sha512-W6u4XeIIP4+uG4DYV9G3JeS6QNuKwfhQIej1GIoL4BdcnUFgrnB8kHYNXL3MxiHRKuhZB9OYwUMGs8jKFZR/Vg==", - label: "historical fixture OpenClaw 2026.4.24", - packageSpec: "openclaw@2026.4.24", - tarballUrl: "https://registry.npmjs.org/openclaw/-/openclaw-2026.4.24.tgz", - }, - "2026.5.22": { - expectedIntegrity: - "sha512-m+zgBELGbCHjWB1IWF5WSWNPr480cMKOMff2OF72c8A0AMD4hC/9+qwYtzjYmGkETcffnB711JymlVsQnh2Tow==", - label: "historical fixture OpenClaw 2026.5.22", - packageSpec: "openclaw@2026.5.22", - tarballUrl: "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.22.tgz", - }, - "2026.5.27": { - expectedIntegrity: - "sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==", - label: "historical fixture OpenClaw 2026.5.27", - packageSpec: "openclaw@2026.5.27", - tarballUrl: "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.27.tgz", - }, - "2026.6.10": { - expectedIntegrity: - "sha512-LcooND2tBQw8A+kc1Ujltu3lg30bJ0w7XaeRy7eYzobb8BBdcW6DOGbwJL4vpj1vl9+gjRceOtlh5nh9OARcug==", - label: "historical fixture OpenClaw 2026.6.10", - packageSpec: "openclaw@2026.6.10", - tarballUrl: "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.10.tgz", - }, - }); +type ReviewedOldInstallerProfile = Pick< + typeof REVIEWED_GATEWAY_UPGRADE_FIXTURE, + "expectedAdvisoryAuditCount" | "nemoclawCommit" | "nemoclawRef" | "openclawVersion" +>; export const OLD_INSTALLER_BOOTSTRAP_NEEDLE = ' legacy_script="${source_root}/install.sh"\n'; export const OLD_INSTALLER_CLONE_NEEDLE = @@ -60,53 +22,24 @@ export const OLD_INSTALLER_ADVISORY_AUDIT = " npm --prefix /usr/local/lib/nemoclaw/mcporter-runtime audit --omit=dev --audit-level=low; \\\n"; export const OLD_INSTALLER_ARCHIVE_CONTEXT_PATH = "nemoclaw/src/.nemoclaw-e2e-old-openclaw.tgz"; -const REVIEWED_OLD_INSTALLER_PROFILES: Readonly> = - Object.freeze({ - "v0.0.36": Object.freeze({ - expectedAdvisoryAuditCount: 0, - nemoclawCommit: "3351fbdd4eb7d9b80ec471545083956327da2b10", - nemoclawRef: "v0.0.36", - openclawVersion: "2026.4.24", - }), - "v0.0.55": Object.freeze({ - expectedAdvisoryAuditCount: 0, - nemoclawCommit: "95d483fe2b6569d68e59493c60f19df09a068e8f", - nemoclawRef: "v0.0.55", - openclawVersion: "2026.5.22", - }), - "v0.0.74": Object.freeze({ - expectedAdvisoryAuditCount: 1, - nemoclawCommit: "3a05b54e8ec3e1d5550ec5c728de54af872bffe3", - nemoclawRef: "v0.0.74", - openclawVersion: "2026.5.27", - }), - "v0.0.89": Object.freeze({ - expectedAdvisoryAuditCount: 1, - nemoclawCommit: "1143aa5cce77f3bad1b3b5588bd7fddbe438237e", - nemoclawRef: "v0.0.89", - openclawVersion: "2026.6.10", - }), - }); - export function reviewedOldOpenClawArchive(version: string): ReviewedOldOpenClawArchive { - const reviewedArchive = REVIEWED_OLD_OPENCLAW_ARCHIVES[version]; - if (!reviewedArchive) { + if (version !== REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion) { throw new Error(`Historical gateway upgrade OpenClaw ${version} has no reviewed archive pin`); } - return reviewedArchive; + return REVIEWED_GATEWAY_UPGRADE_FIXTURE.openClawArchive; } export function reviewedOldInstallerProfile( identity: OldInstallerFixtureIdentity, ): ReviewedOldInstallerProfile { - const profile = REVIEWED_OLD_INSTALLER_PROFILES[identity.nemoclawRef]; + const profile = REVIEWED_GATEWAY_UPGRADE_FIXTURE; if ( - !profile || + profile.nemoclawRef !== identity.nemoclawRef || profile.nemoclawCommit !== identity.nemoclawCommit || profile.openclawVersion !== identity.openclawVersion ) { throw new Error( - `Historical gateway upgrade fixture must match an exact reviewed ref/commit/OpenClaw profile; got ${identity.nemoclawRef}/${identity.nemoclawCommit}/${identity.openclawVersion}`, + `Historical gateway upgrade fixture must match the reviewed descriptor's ref, commit, and OpenClaw version; got ${identity.nemoclawRef}/${identity.nemoclawCommit}/${identity.openclawVersion}`, ); } return profile; diff --git a/test/e2e/live/openshell-gateway-upgrade.test.ts b/test/e2e/live/openshell-gateway-upgrade.test.ts index 1a1d32a5d1e..a9a4373becb 100644 --- a/test/e2e/live/openshell-gateway-upgrade.test.ts +++ b/test/e2e/live/openshell-gateway-upgrade.test.ts @@ -2,25 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 /** - * - * Preserves the PR #3001 contract at the same boundary as the former shell - * lane: install an old NemoClaw/OpenShell gateway, create a real OpenClaw - * sandbox, seed durable workspace + live process state, run the current - * installer upgrade path, then assert the gateway reports the current - * OpenShell version. Fixtures whose live OpenShell policy survives the gateway - * transition restore the claw; the cluster-era fixture fails closed with its - * backup intact because NemoClaw has no policy shadow from which to recreate it. - * After the outer rebuild destroys the source sandbox, the inner onboarding flow - * must continue the upgrade-owned recreation journal without opening a second transaction. - * - * The macOS regressions from the shell script remain hermetic installer-script - * probes in this file: fake Darwin arm64 PATH, fake existing OpenShell tools, - * real scripts/install-openshell.sh execution, and static Dockerfile guard - * assertions. No new fixture family or migration ledger is introduced. + * Installs one reviewed historical NemoClaw/OpenShell gateway, creates a real + * OpenClaw sandbox, seeds durable workspace state, and runs the current + * installer upgrade path. The survivor must remain usable and retain its + * workspace state across the gateway transition. */ -import { Buffer } from "node:buffer"; -import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; @@ -30,6 +17,7 @@ import { removeReviewedNpmArchive, } from "../../../scripts/lib/reviewed-npm-archive.mts"; import { shellQuote } from "../../../src/lib/core/shell-quote"; +import { REVIEWED_GATEWAY_UPGRADE_FIXTURE } from "../../../tools/e2e/openshell-gateway-upgrade-fixture.mts"; import { type ArtifactSink } from "../fixtures/artifacts.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import { assertExitZero as expectExitZero } from "../fixtures/clients/command.ts"; @@ -48,7 +36,6 @@ import type { ShellProbeResult } from "../fixtures/shell-probe.ts"; import { currentGatewayUpgradeInstallerArgs, currentNemoclawUpgradeRef, - expectedLegacyRegistryMetadata, GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, legacyGatewayUpgradeHostFirewallOptions, oldGatewayUpgradeInstallerArgs, @@ -62,7 +49,6 @@ import { reviewedOldOpenClawArchive, } from "./openshell-gateway-upgrade-old-installer.ts"; -const INSTALL_OPENSHELL = path.join(REPO_ROOT, "scripts", "install-openshell.sh"); const STATE_DIR = path.join( os.homedir(), ".local", @@ -71,42 +57,36 @@ const STATE_DIR = path.join( "openshell-docker-gateway", ); const PID_FILE = path.join(STATE_DIR, "openshell-gateway.pid"); -const OLD_NEMOCLAW_REF = process.env.NEMOCLAW_OLD_NEMOCLAW_REF ?? "v0.0.36"; +const OLD_NEMOCLAW_REF = + process.env.NEMOCLAW_OLD_NEMOCLAW_REF ?? REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef; const OLD_NEMOCLAW_COMMIT = - process.env.NEMOCLAW_OLD_NEMOCLAW_COMMIT ?? "3351fbdd4eb7d9b80ec471545083956327da2b10"; + process.env.NEMOCLAW_OLD_NEMOCLAW_COMMIT ?? REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawCommit; const OLD_INSTALLER_SHA256 = - process.env.NEMOCLAW_OLD_INSTALLER_SHA256 ?? - "0c42400a0d3867739f1d75d612e069967be4506e169974bbbebf14b7af39144f"; -const OLD_OPENSHELL_VERSION = process.env.NEMOCLAW_OLD_OPENSHELL_VERSION ?? "0.0.36"; + process.env.NEMOCLAW_OLD_INSTALLER_SHA256 ?? REVIEWED_GATEWAY_UPGRADE_FIXTURE.installerSha256; +const OLD_OPENSHELL_VERSION = + process.env.NEMOCLAW_OLD_OPENSHELL_VERSION ?? REVIEWED_GATEWAY_UPGRADE_FIXTURE.openShellVersion; const CURRENT_OPENSHELL_VERSION = process.env.NEMOCLAW_CURRENT_OPENSHELL_VERSION ?? "0.0.106"; const OLD_SANDBOX_BASE_IMAGE_REF = process.env.NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF ?? - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6"; -const OLD_OPENCLAW_VERSION = process.env.NEMOCLAW_OLD_OPENCLAW_VERSION ?? "2026.4.24"; -const CURRENT_OPENCLAW_VERSION = process.env.NEMOCLAW_CURRENT_OPENCLAW_VERSION ?? ""; -const OPENCLAW_STATE_UPGRADE_PROOF = process.env.NEMOCLAW_OPENCLAW_STATE_UPGRADE_PROOF === "1"; -const LEGACY_GATEWAY_PRESERVES_LIVE_POLICY = OLD_NEMOCLAW_REF !== "v0.0.36"; + REVIEWED_GATEWAY_UPGRADE_FIXTURE.sandboxBaseImageRef; +const OLD_OPENCLAW_VERSION = + process.env.NEMOCLAW_OLD_OPENCLAW_VERSION ?? REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion; const OLD_INSTALLER_FIXTURE_IDENTITY = Object.freeze({ nemoclawCommit: OLD_NEMOCLAW_COMMIT, nemoclawRef: OLD_NEMOCLAW_REF, openclawVersion: OLD_OPENCLAW_VERSION, }); -const { sandboxBaseDigest: OLD_SANDBOX_BASE_DIGEST } = validateLegacyGatewayUpgradeFixture({ +validateLegacyGatewayUpgradeFixture({ ...OLD_INSTALLER_FIXTURE_IDENTITY, installerSha256: OLD_INSTALLER_SHA256, + openShellVersion: OLD_OPENSHELL_VERSION, sandboxBaseImageRef: OLD_SANDBOX_BASE_IMAGE_REF, }); const SURVIVOR_SANDBOX = process.env.NEMOCLAW_GATEWAY_UPGRADE_SURVIVOR_NAME ?? `e2e-gw-${process.pid}`; const SURVIVOR_MARKER = `gateway-upgrade-survivor-${Date.now()}`; const SURVIVOR_MARKER_PATH = "/sandbox/.openclaw/workspace/nemoclaw-gateway-upgrade-marker"; -const INSTALLED_AGENT_DB_MARKER = `openclaw-2026-6-agent-db-${Date.now()}`; -const LEGACY_MEMORY_MARKER = `openclaw-2026-6-memory-${Date.now()}`; -const LEGACY_MEMORY_SIDECAR = "/sandbox/.openclaw/memory/main.sqlite"; -const OPENCLAW_GLOBAL_STATE_DB = "/sandbox/.openclaw/state/openclaw.sqlite"; -const OPENCLAW_MAIN_AGENT_DB = "/sandbox/.openclaw/agents/main/agent/openclaw-agent.sqlite"; -const LEGACY_UPDATE_CHECK_PATH = "/sandbox/.openclaw/update-check.json"; -const REGISTRY_FILE = path.join(os.homedir(), ".nemoclaw", "sandboxes.json"); +const GATEWAY_CREDENTIAL = "nemoclaw-gateway-upgrade-fixture-key"; const TEST_TIMEOUT_MS = 65 * 60_000; const OPENSHELL_TIMEOUT_MS = 2 * 60_000; @@ -116,17 +96,6 @@ expect( `openshell-gateway-upgrade live test only accepts survivor sandbox names with prefix e2e-gw-; got ${SURVIVOR_SANDBOX}`, ).toBe(true); expect(SURVIVOR_SANDBOX.length).toBeLessThanOrEqual(19); -const stateUpgradeFixtureExpectations: ReadonlyArray = - OPENCLAW_STATE_UPGRADE_PROOF - ? [ - [OLD_NEMOCLAW_REF, "v0.0.89"], - [OLD_OPENCLAW_VERSION, "2026.6.10"], - [CURRENT_OPENCLAW_VERSION, "2026.7.1"], - ] - : []; -for (const [actual, expected] of stateUpgradeFixtureExpectations) { - expect(actual).toBe(expected); -} function writeExecutable(target: string, contents: string): void { fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 }); @@ -148,7 +117,7 @@ function withoutEnvKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): NodeJS return Object.fromEntries(Object.entries(env).filter(([key]) => !excluded.has(key))); } -function shellLoginPrefix(hiddenOpenShellDir?: string): string { +function shellLoginPrefix(): string { const lines = [ "set -euo pipefail", 'if [ -f "$HOME/.bashrc" ]; then', @@ -161,21 +130,7 @@ function shellLoginPrefix(hiddenOpenShellDir?: string): string { ' . "$NVM_DIR/nvm.sh"', "fi", ]; - lines.push( - ...(hiddenOpenShellDir - ? [ - '_path_without_user_local=""', - "while IFS= read -r _path_entry; do", - ' [ "$_path_entry" = "$HOME/.local/bin" ] && continue', - ` [ "$_path_entry" = ${shellQuote(hiddenOpenShellDir)} ] && continue`, - ' _path_without_user_local="${_path_without_user_local:+${_path_without_user_local}:}${_path_entry}"', - 'done < <(tr ":" "\\n" <<<"$PATH")', - 'export PATH="$_path_without_user_local"', - "unset _path_without_user_local _path_entry", - "hash -r", - ] - : ['export PATH="$HOME/.local/bin:$PATH"']), - ); + lines.push('export PATH="$HOME/.local/bin:$PATH"'); return lines.join("\n"); } @@ -183,65 +138,26 @@ function expectOutputContains(result: ShellProbeResult, value: string, label: st expect(resultText(result), label).toContain(value); } -function escapeRegExpLiteral(value: string): string { - return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"); -} - -function expectFullGitSha(result: ShellProbeResult, label: string): string { - expectExitZero(result, label); - const sha = result.stdout.trim(); - expect(sha, `${label} must produce a full git commit SHA:\n${resultText(result)}`).toMatch( - /^[0-9a-f]{40}$/, - ); - return sha; -} - async function bash( host: HostCliClient, script: string, options: { artifactName: string; + captureLimitBytes?: number; env?: NodeJS.ProcessEnv; timeoutMs?: number; cwd?: string; - hiddenOpenShellDir?: string; redactionValues?: string[]; }, ): Promise { - return host.command( - "bash", - ["-lc", `${shellLoginPrefix(options.hiddenOpenShellDir)}\n${script}`], - { - cwd: options.cwd ?? REPO_ROOT, - artifactName: options.artifactName, - env: options.env ?? liveEnv(), - redactionValues: options.redactionValues, - timeoutMs: options.timeoutMs ?? OPENSHELL_TIMEOUT_MS, - }, - ); -} - -interface OpenClawStateContract { - agentDbIntegrity: string; - apiKey: unknown; - globalDbIntegrity: string; - installedAgentDbMarker: string | null; - keyRefIds: string[]; - legacyMemoryMarker: string | null; - legacyMemorySidecarArchived: boolean; - legacyMemorySidecarPresent: boolean; - literalSecretEnvKeys: string[]; - literalSecretInState: boolean; - placeholderEnvKeys: string[]; - startupCheckpoint: string | null; - uid: number; - updateCheckPresent: boolean; - version: string; -} - -function encodedNodeCommand(source: string): string { - const payload = Buffer.from(source, "utf8").toString("base64"); - return `printf '%s' ${shellQuote(payload)} | base64 -d | NODE_NO_WARNINGS=1 node`; + return host.command("bash", ["-lc", `${shellLoginPrefix()}\n${script}`], { + cwd: options.cwd ?? REPO_ROOT, + artifactName: options.artifactName, + captureLimitBytes: options.captureLimitBytes, + env: options.env ?? liveEnv(), + redactionValues: options.redactionValues, + timeoutMs: options.timeoutMs ?? OPENSHELL_TIMEOUT_MS, + }); } async function runInSurvivorSandbox( @@ -254,204 +170,61 @@ async function runInSurvivorSandbox( : `openshell sandbox exec --name ${shellQuote(SURVIVOR_SANDBOX)} --`; return bash(host, `${prefix} sh -lc ${shellQuote(command)}`, { artifactName: options.artifactName, - redactionValues: ["dummy"], + redactionValues: [GATEWAY_CREDENTIAL], timeoutMs: options.timeoutMs ?? 60_000, }); } -async function inspectOpenClawStateContract( +async function assertOpenClawAgentSecretBoundary( host: HostCliClient, + fake: FakeOpenAiCompatibleServer, phase: "legacy" | "upgraded", -): Promise { - const seedLegacyUpdateCheck = phase === "legacy"; - const source = String.raw` +): Promise { + const secretNonExposure = await runInSurvivorSandbox( + host, + `node <<'NODE' const fs = require("node:fs"); const path = require("node:path"); -const { execFileSync } = require("node:child_process"); -const { DatabaseSync } = require("node:sqlite"); -const configPath = "/sandbox/.openclaw/openclaw.json"; -const authPath = "/sandbox/.openclaw/agents/main/agent/auth-profiles.json"; -const globalDbPath = ${JSON.stringify(OPENCLAW_GLOBAL_STATE_DB)}; -const agentDbPath = ${JSON.stringify(OPENCLAW_MAIN_AGENT_DB)}; -const installedAgentDbMarkerValue = ${JSON.stringify(INSTALLED_AGENT_DB_MARKER)}; -const legacyMemoryPath = ${JSON.stringify(LEGACY_MEMORY_SIDECAR)}; -const legacyMemoryMarkerValue = ${JSON.stringify(LEGACY_MEMORY_MARKER)}; -const updateCheckPath = ${JSON.stringify(LEGACY_UPDATE_CHECK_PATH)}; -const configText = fs.readFileSync(configPath, "utf8"); -const authText = fs.existsSync(authPath) ? fs.readFileSync(authPath, "utf8") : "{}"; -const config = JSON.parse(configText); -const auth = JSON.parse(authText); -if (${JSON.stringify(seedLegacyUpdateCheck)}) { - fs.writeFileSync(updateCheckPath, JSON.stringify({ - lastCheckedAt: "2026-07-20T00:00:00.000Z", - lastAvailableVersion: "2026.7.1", - }) + "\n", { mode: 0o600 }); -} -const globalDb = new DatabaseSync(globalDbPath, { readOnly: true }); -globalDb.exec("PRAGMA busy_timeout = 5000"); -const globalDbIntegrity = globalDb.prepare("PRAGMA integrity_check").get().integrity_check; -const hasSchemaMeta = globalDb - .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") - .get("schema_meta"); -const startupCheckpoint = hasSchemaMeta - ? globalDb - .prepare("SELECT app_version AS appVersion FROM schema_meta WHERE meta_key = ?") - .get("startup-migrations")?.appVersion ?? null - : null; -globalDb.close(); -if (${JSON.stringify(seedLegacyUpdateCheck)}) { - // A normal agent turn need not touch agent-local SQLite. Materialize the - // real 6.10 schema through its public, offline memory command before probing - // it; a writable DatabaseSync open on a missing path would create an empty - // file and manufacture state that no legacy OpenClaw process initialized. - execFileSync("openclaw", ["memory", "status", "--json", "--agent", "main"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }); -} -if (!fs.existsSync(agentDbPath)) { - throw new Error("OpenClaw agent database was not materialized at " + agentDbPath); -} -const agentDb = new DatabaseSync(agentDbPath, { - readOnly: !${JSON.stringify(seedLegacyUpdateCheck)}, -}); -agentDb.exec("PRAGMA busy_timeout = 5000"); -if (${JSON.stringify(seedLegacyUpdateCheck)}) { - agentDb - .prepare("INSERT OR REPLACE INTO cache_entries (scope, key, value_json, blob, expires_at, updated_at) VALUES (?, ?, ?, NULL, NULL, ?)") - .run( - "nemoclaw-e2e-state-upgrade", - "installed-agent-db-marker", - JSON.stringify(installedAgentDbMarkerValue), - Date.now(), - ); -} -const agentDbIntegrity = agentDb.prepare("PRAGMA integrity_check").get().integrity_check; -const installedAgentDbMarkerJson = agentDb - .prepare("SELECT value_json AS valueJson FROM cache_entries WHERE scope = ? AND key = ?") - .get("nemoclaw-e2e-state-upgrade", "installed-agent-db-marker")?.valueJson; -const installedAgentDbMarker = - typeof installedAgentDbMarkerJson === "string" ? JSON.parse(installedAgentDbMarkerJson) : null; -let legacyMemoryMarker = null; -if (${JSON.stringify(seedLegacyUpdateCheck)}) { - fs.mkdirSync(path.dirname(legacyMemoryPath), { recursive: true }); - const legacyMemoryDb = new DatabaseSync(legacyMemoryPath); - legacyMemoryDb.exec([ - "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)", - "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, source TEXT NOT NULL, hash TEXT NOT NULL, mtime INTEGER NOT NULL, size INTEGER NOT NULL)", - "CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, path TEXT NOT NULL, source TEXT NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, hash TEXT NOT NULL, model TEXT NOT NULL, text TEXT NOT NULL, embedding TEXT NOT NULL, updated_at INTEGER NOT NULL)", - ].join(";\n")); - legacyMemoryDb.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)") - .run("nemoclaw-e2e-state-upgrade", legacyMemoryMarkerValue); - legacyMemoryDb.prepare("INSERT OR REPLACE INTO files (path, source, hash, mtime, size) VALUES (?, ?, ?, ?, ?)") - .run("memory/nemoclaw-e2e.md", "memory", "nemoclaw-e2e-hash", 1, legacyMemoryMarkerValue.length); - legacyMemoryDb.prepare("INSERT OR REPLACE INTO chunks (id, path, source, start_line, end_line, hash, model, text, embedding, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") - .run("nemoclaw-e2e-legacy-memory", "memory/nemoclaw-e2e.md", "memory", 1, 1, "nemoclaw-e2e-hash", "none", legacyMemoryMarkerValue, "[]", 1); - legacyMemoryDb.close(); - fs.chmodSync(legacyMemoryPath, 0o600); - legacyMemoryMarker = legacyMemoryMarkerValue; -} else { - legacyMemoryMarker = agentDb - .prepare("SELECT text FROM memory_index_chunks WHERE id = ?") - .get("nemoclaw-e2e-legacy-memory")?.text ?? null; -} -agentDb.close(); -const keyRefIds = []; -function collectKeyRefs(value) { - if (!value || typeof value !== "object") return; - if (value.keyRef?.source === "env" && typeof value.keyRef.id === "string") { - keyRefIds.push(value.keyRef.id); + +const gatewayCredential = String.fromCharCode(${Array.from(GATEWAY_CREDENTIAL) + .map((character) => character.charCodeAt(0)) + .join(", ")}); +if (Object.values(process.env).some((value) => value.includes(gatewayCredential))) { + process.exit(41); +} + +const managedFiles = ["/sandbox/.openclaw/openclaw.json"]; +const agentsRoot = "/sandbox/.openclaw/agents"; +if (fs.existsSync(agentsRoot)) { + const pending = [agentsRoot]; + while (pending.length > 0) { + const current = pending.pop(); + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const entryPath = path.join(current, entry.name); + if (entry.isDirectory()) pending.push(entryPath); + if (entry.isFile() && entry.name === "auth-profiles.json") managedFiles.push(entryPath); + } } - for (const child of Object.values(value)) collectKeyRefs(child); -} -collectKeyRefs(config); -collectKeyRefs(auth); -const envEntries = Object.entries(process.env); -console.log(JSON.stringify({ - agentDbIntegrity, - apiKey: config.models?.providers?.inference?.apiKey, - globalDbIntegrity, - installedAgentDbMarker, - keyRefIds: [...new Set(keyRefIds)].sort(), - legacyMemoryMarker, - legacyMemorySidecarArchived: fs.existsSync(legacyMemoryPath + ".migrated"), - legacyMemorySidecarPresent: fs.existsSync(legacyMemoryPath), - literalSecretEnvKeys: envEntries.filter(([, value]) => value === "dummy").map(([key]) => key), - literalSecretInState: configText.includes("dummy") || authText.includes("dummy"), - placeholderEnvKeys: envEntries - .filter(([, value]) => typeof value === "string" && value.startsWith("openshell:resolve:env:")) - .map(([key]) => key) - .sort(), - startupCheckpoint, - uid: process.getuid(), - updateCheckPresent: fs.existsSync(updateCheckPath), - version: execFileSync("openclaw", ["--version"], { encoding: "utf8" }).trim(), -})); -`; - const result = await runInSurvivorSandbox(host, encodedNodeCommand(source), { - artifactName: `state-upgrade-${phase}-contract`, - currentCli: phase === "upgraded", - }); - expectExitZero(result, `${phase} OpenClaw state contract inspection`); - const json = result.stdout.trim().split("\n").at(-1) ?? ""; - expect(json, `${phase} OpenClaw state contract must emit JSON`).not.toBe(""); - const summary = JSON.parse(json) as OpenClawStateContract; - expect(summary.uid, `${phase} contract must run as the sandbox user`).toBeGreaterThan(0); - expect(summary.apiKey, `${phase} custom-provider config must retain the proxy sentinel`).toBe( - "unused", - ); - expect(summary.literalSecretEnvKeys).toEqual([]); - expect(summary.literalSecretInState).toBe(false); - expect(summary.updateCheckPresent).toBe(seedLegacyUpdateCheck); - expect(summary.globalDbIntegrity).toBe("ok"); - expect(summary.agentDbIntegrity).toBe("ok"); - expect(summary.installedAgentDbMarker).toBe(INSTALLED_AGENT_DB_MARKER); - const expectedPhaseContract = { - legacy: { - legacyMemorySidecarArchived: false, - legacyMemorySidecarPresent: true, - startupCheckpoint: null, - }, - upgraded: { - legacyMemorySidecarArchived: true, - legacyMemorySidecarPresent: false, - startupCheckpoint: CURRENT_OPENCLAW_VERSION, - }, - }[phase]; - expect(summary.startupCheckpoint).toBe(expectedPhaseContract.startupCheckpoint); - expect(summary.legacyMemoryMarker).toBe(LEGACY_MEMORY_MARKER); - expect(summary.legacyMemorySidecarPresent).toBe(expectedPhaseContract.legacyMemorySidecarPresent); - expect(summary.legacyMemorySidecarArchived).toBe( - expectedPhaseContract.legacyMemorySidecarArchived, - ); - const versionToken = summary.version.match(/\b\d{4}\.\d{1,2}\.\d{1,2}\b/)?.[0]; - expect(versionToken).toBe(phase === "legacy" ? OLD_OPENCLAW_VERSION : CURRENT_OPENCLAW_VERSION); - return summary; } -function expectStatePreservedAcrossUpgrade( - legacy: OpenClawStateContract, - upgraded: OpenClawStateContract, -): void { - expect(legacy.placeholderEnvKeys).toContain("COMPATIBLE_API_KEY"); - expect(upgraded.placeholderEnvKeys).toEqual([]); - - // The current rebuild intentionally omits COMPATIBLE_API_KEY from its host - // environment. After trusted post-restore finalization (#9946), the - // credential remains gateway-held instead of being projected back into the - // sandbox environment. The upgraded agent turn below proves that the exact - // credential still reaches the compatible endpoint. Preserve any key refs - // the frozen runtime emitted without inventing one for this route. - for (const keyRefId of legacy.keyRefIds) { - expect(upgraded.keyRefIds).toContain(keyRefId); - } +if ( + managedFiles.some( + (file) => fs.existsSync(file) && fs.readFileSync(file, "utf8").includes(gatewayCredential), + ) +) { + process.exit(42); } +NODE`, + { + artifactName: `state-upgrade-${phase}-secret-non-exposure`, + currentCli: phase === "upgraded", + }, + ); + expectExitZero( + secretNonExposure, + `${phase} gateway credential must not be projected into sandbox environment or managed OpenClaw files`, + ); -async function assertOpenClawAgentSecretBoundary( - host: HostCliClient, - fake: FakeOpenAiCompatibleServer, - phase: "legacy" | "upgraded", -): Promise { const requestOffset = fake.requests().length; const agent = await runInSurvivorSandbox( host, @@ -475,54 +248,13 @@ async function assertOpenClawAgentSecretBoundary( ); // The fake endpoint deliberately records only the validated auth result, not // the bearer value. With requireAuth enabled, "ok" means the request carried - // the exact gateway-held `dummy` credential; `unused`, a placeholder, or a + // the exact gateway-held fixture credential; `unused`, a placeholder, or a // missing header would receive 401 and could not complete this agent turn. expect( requests.every((request) => request.auth === "ok" && request.authorizationSent === true), ).toBe(true); } -async function captureLegacyOpenClawStateUpgradeProof( - host: HostCliClient, - fake: FakeOpenAiCompatibleServer, - artifacts: ArtifactSink, -): Promise { - await assertOpenClawAgentSecretBoundary(host, fake, "legacy"); - const legacyStateContract = await inspectOpenClawStateContract(host, "legacy"); - await artifacts.writeJson("openclaw-2026-6-state-contract.json", legacyStateContract); - return legacyStateContract; -} - -async function verifyUpgradedOpenClawStateUpgradeProof( - host: HostCliClient, - fake: FakeOpenAiCompatibleServer, - artifacts: ArtifactSink, - legacyStateContract: OpenClawStateContract | undefined, -): Promise { - expect(legacyStateContract).toBeDefined(); - const upgradedStateContract = await inspectOpenClawStateContract(host, "upgraded"); - expectStatePreservedAcrossUpgrade(legacyStateContract!, upgradedStateContract); - await artifacts.writeJson("openclaw-2026-7-state-contract.json", upgradedStateContract); - await assertOpenClawAgentSecretBoundary(host, fake, "upgraded"); -} - -const captureOpenClawStateUpgradeProof: ( - host: HostCliClient, - fake: FakeOpenAiCompatibleServer, - artifacts: ArtifactSink, -) => Promise = OPENCLAW_STATE_UPGRADE_PROOF - ? captureLegacyOpenClawStateUpgradeProof - : () => Promise.resolve(undefined); - -const verifyOpenClawStateUpgradeProof: ( - host: HostCliClient, - fake: FakeOpenAiCompatibleServer, - artifacts: ArtifactSink, - legacyStateContract: OpenClawStateContract | undefined, -) => Promise = OPENCLAW_STATE_UPGRADE_PROOF - ? verifyUpgradedOpenClawStateUpgradeProof - : () => Promise.resolve(); - function createOldDockerWrapper(artifacts: ArtifactSink): string { const wrapperDir = artifacts.pathFor("old-docker-wrapper"); const logFile = artifacts.pathFor("old-docker-wrapper.log"); @@ -619,91 +351,29 @@ async function waitForSurvivorReady(host: HostCliClient, labelPrefix: string): P expect(ready, `survivor sandbox ${SURVIVOR_SANDBOX} did not become Ready`).toBe(true); } -async function survivorAgentProbe( - host: HostCliClient, - artifactName: string, -): Promise { - const probe = [ - 'pid="$(cat /tmp/nemoclaw-e2e-agent.pid 2>/dev/null || true)"', - '[ -n "$pid" ] || exit 1', - 'kill -0 "$pid" 2>/dev/null || exit 1', - "counter=\"$(sed -n 's/^[^ ]* \\([0-9][0-9]*\\).*/\\1/p' /tmp/nemoclaw-e2e-agent.heartbeat 2>/dev/null | head -1)\"", - "cmdline=\"$(tr '\\000' ' ' <\"/proc/${pid}/cmdline\" 2>/dev/null || true)\"", - 'case "$cmdline" in *nemoclaw-e2e-agent*) ;; *) exit 1 ;; esac', - 'printf "%s %s %s\\n" "$pid" "${counter:-0}" "$cmdline"', - ].join("; "); - return bash( - host, - `openshell sandbox exec --name ${shellQuote(SURVIVOR_SANDBOX)} -- sh -lc ${shellQuote(probe)}`, - { artifactName, timeoutMs: 30_000 }, - ); -} - -async function waitForSurvivorAgentReady(host: HostCliClient): Promise { - let last: ShellProbeResult | undefined; - let attempt = 0; - while (attempt < 60 && last?.exitCode !== 0) { - last = await survivorAgentProbe(host, `survivor-agent-probe-${attempt}`); - attempt += 1; - last.exitCode === 0 || (await new Promise((resolve) => setTimeout(resolve, 1_000))); - } - expect( - last?.exitCode, - `survivor agent did not become healthy: ${last ? resultText(last) : "no probe"}`, - ).toBe(0); - return last!; -} - async function runInstallerPayload( host: HostCliClient, label: string, installerArgs: readonly string[], - logFile: string, + artifacts: ArtifactSink, + logName: string, env: NodeJS.ProcessEnv, redactionValues: string[] = [], - options: { - expectedExitCode?: number; - hiddenOpenShellDir?: string; - interactiveInput?: string; - } = {}, ): Promise { const quotedInstallerArgs = installerArgs.map(shellQuote).join(" "); - const installerCommand = `bash ${quotedInstallerArgs} >${shellQuote(logFile)} 2>&1`; - // The live command runner closes stdin. util-linux `script` supplies the - // /dev/tty that the ordinary curl|bash confirmation path expects. - const installerInvocation = options.interactiveInput - ? `printf '%s\\n' ${shellQuote(options.interactiveInput)} | script --quiet --return --command ${shellQuote(installerCommand)} /dev/null` - : installerCommand; - const hiddenOpenShellPreflight = options.hiddenOpenShellDir - ? [ - 'test -x "$HOME/.local/bin/openshell"', - "if command -v openshell >/dev/null 2>&1; then", - ' echo "Expected the v0.0.55 user-local OpenShell binary to be absent from PATH" >&2', - " exit 1", - "fi", - ].join("\n") - : ""; - const result = await bash( - host, - `${hiddenOpenShellPreflight} -rm -f ${shellQuote(logFile)} -${installerInvocation}`, - { - artifactName: `${label.replace(/[^a-z0-9_.-]+/gi, "-")}-installer`, - env, - hiddenOpenShellDir: options.hiddenOpenShellDir, - redactionValues, - timeoutMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, - }, - ); - const tail = await bash(host, `tail -160 ${shellQuote(logFile)} 2>/dev/null || true`, { - artifactName: `${label}-installer-tail`, - timeoutMs: 30_000, + const result = await bash(host, `bash ${quotedInstallerArgs}`, { + artifactName: `${label.replace(/[^a-z0-9_.-]+/gi, "-")}-installer`, + captureLimitBytes: 1024 * 1024, + env, + redactionValues, + timeoutMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, }); + artifacts.addRedactionValues(redactionValues); + await artifacts.writeText(logName, resultText(result)); expect( result.exitCode, - `${label} NemoClaw installer returned an unexpected exit code:\n${resultText(tail)}`, - ).toBe(options.expectedExitCode ?? 0); + `${label} NemoClaw installer returned an unexpected exit code:\n${resultText(result)}`, + ).toBe(0); return result; } @@ -721,7 +391,6 @@ async function installOldNemoclawAndClaw( fakeBaseUrl: string, ): Promise { const oldInstaller = artifacts.pathFor("old-install.sh"); - const oldInstallLog = artifacts.pathFor("old-install.log"); const oldDockerLog = artifacts.pathFor("old-docker-wrapper.log"); const wrapperDir = createOldDockerWrapper(artifacts); fs.rmSync(oldDockerLog, { force: true }); @@ -750,7 +419,7 @@ async function installOldNemoclawAndClaw( // from replacing that fixture at the ShellProbe boundary. E2E_WORKLOAD_SOURCE: "", PATH: `${wrapperDir}:${process.env.PATH ?? "/usr/bin:/bin"}`, - COMPATIBLE_API_KEY: "dummy", + COMPATIBLE_API_KEY: GATEWAY_CREDENTIAL, NEMOCLAW_REAL_DOCKER: process.env.NEMOCLAW_REAL_DOCKER ?? "/usr/bin/docker", NEMOCLAW_SANDBOX_BASE_IMAGE_REF: OLD_SANDBOX_BASE_IMAGE_REF, NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF: OLD_SANDBOX_BASE_IMAGE_REF, @@ -778,8 +447,10 @@ async function installOldNemoclawAndClaw( host, `old-${OLD_NEMOCLAW_REF}`, oldGatewayUpgradeInstallerArgs(oldInstaller), - oldInstallLog, + artifacts, + "old-install.log", installEnv, + [GATEWAY_CREDENTIAL], ); } finally { removeReviewedNpmArchive(reviewedOpenClaw); @@ -789,25 +460,6 @@ async function installOldNemoclawAndClaw( fs.existsSync(oldDockerLog) ? fs.readFileSync(oldDockerLog, "utf8") : "", ); - const oldLog = fs.readFileSync(oldInstallLog, "utf8"); - const oldSandboxBasePinPrefix = `sha256:${OLD_SANDBOX_BASE_DIGEST}`.slice(0, 19); - expect(oldLog, `old fixture must pin sandbox base image ${OLD_SANDBOX_BASE_IMAGE_REF}`).toContain( - `Pinning base image to ${oldSandboxBasePinPrefix}`, - ); - const oldOpenClawVersionPattern = escapeRegExpLiteral(OLD_OPENCLAW_VERSION); - const wrongOldOpenClaw = oldLog.match( - new RegExp( - `OpenClaw ((?!${oldOpenClawVersionPattern})[0-9]{4}\\.[0-9]+\\.[0-9]+) is current \\(>= ${oldOpenClawVersionPattern}\\)`, - ), - ); - expect( - wrongOldOpenClaw?.[1], - `old fixture log must not use an unexpected OpenClaw version:\n${oldLog}`, - ).toBeUndefined(); - expect(oldLog, `old fixture must show pinned OpenClaw ${OLD_OPENCLAW_VERSION}`).toMatch( - new RegExp(`OpenClaw ${oldOpenClawVersionPattern}|openclaw@${oldOpenClawVersionPattern}`), - ); - const openshellVersion = await bash(host, `openshell --version`, { artifactName: "old-openshell-version", timeoutMs: 30_000, @@ -818,220 +470,52 @@ async function installOldNemoclawAndClaw( OLD_OPENSHELL_VERSION, `old NemoClaw install must leave OpenShell ${OLD_OPENSHELL_VERSION}`, ); - - const sourceHead = await bash( - host, - `test -d "$HOME/.nemoclaw/source/.git" -git -C "$HOME/.nemoclaw/source" rev-parse --verify HEAD`, - { artifactName: "old-source-head", timeoutMs: 30_000 }, - ); - const actualSourceHead = expectFullGitSha(sourceHead, "read old source head"); - expect(actualSourceHead).toBe(OLD_NEMOCLAW_COMMIT); - - await waitForSurvivorReady(host, "old-install"); - const list = await bash(host, `nemoclaw list`, { - artifactName: "old-nemoclaw-list", - timeoutMs: 60_000, - }); - expectExitZero(list, "old nemoclaw list"); - expectOutputContains(list, SURVIVOR_SANDBOX, "old NemoClaw install must register survivor claw"); - - const oldRegistry = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf8")) as { - sandboxes?: Record; - }; - expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]).toBeDefined(); - const expectedRegistryMetadata = expectedLegacyRegistryMetadata(OLD_NEMOCLAW_REF); - expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]?.nemoclawVersion).toBe( - expectedRegistryMetadata.nemoclawVersion, - ); - expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]?.fromDockerfile).toBe( - expectedRegistryMetadata.fromDockerfile, - ); } -async function stageOldOpenShellInUserLocalBin(host: HostCliClient): Promise { - const result = await bash( - host, - `active_openshell="$(command -v openshell)" -active_dir="$(dirname "$active_openshell")" -user_local_bin="$HOME/.local/bin" -mkdir -p "$user_local_bin" -for component in openshell openshell-gateway openshell-sandbox; do - test -x "$active_dir/$component" - if [ "$active_dir" != "$user_local_bin" ]; then - install -m 755 "$active_dir/$component" "$user_local_bin/$component" - fi -done -"$user_local_bin/openshell" --version -printf '%s\n' "$active_dir"`, - { artifactName: "stage-old-openshell-user-local", timeoutMs: 30_000 }, - ); - expectExitZero(result, "stage the v0.0.55 OpenShell layout in ~/.local/bin"); - expectOutputContains( - result, - OLD_OPENSHELL_VERSION, - `staged user-local OpenShell must remain ${OLD_OPENSHELL_VERSION}`, - ); - const activeDir = result.stdout.trim().split("\n").at(-1) ?? ""; - expect(path.isAbsolute(activeDir), `old OpenShell directory must be absolute: ${activeDir}`).toBe( - true, - ); - return activeDir; -} - -async function startSurvivorAgentInExistingClaw(host: HostCliClient): Promise { +async function writeSurvivorMarker(host: HostCliClient): Promise { const markerResult = await bash( host, `openshell sandbox exec --name ${shellQuote(SURVIVOR_SANDBOX)} -- sh -lc ${shellQuote(`mkdir -p /sandbox/.openclaw/workspace && printf '%s\\n' ${shellQuote(SURVIVOR_MARKER)} >${shellQuote(SURVIVOR_MARKER_PATH)}`)}`, { artifactName: "write-survivor-marker", timeoutMs: 60_000 }, ); expectExitZero(markerResult, "write survivor marker before gateway upgrade"); - - const agentPayload = Buffer.from( - [ - "#!/bin/sh", - "set -eu", - 'pid_file="/tmp/nemoclaw-e2e-agent.pid"', - 'heartbeat_file="/tmp/nemoclaw-e2e-agent.heartbeat"', - 'events_file="/tmp/nemoclaw-e2e-agent.events"', - 'printf \'%s\\n\' "$$" >"$pid_file"', - 'printf \'started %s\\n\' "$$" >>"$events_file"', - "counter=0", - 'trap \'printf "stopped %s\\n" "$$" >>"$events_file"; exit 0\' TERM INT', - "while true; do", - " counter=$((counter + 1))", - ' printf \'%s %s %s\\n\' "$$" "$counter" "$(date +%s)" >"$heartbeat_file"', - " sleep 1", - "done", - "", - ].join("\n"), - "utf8", - ).toString("base64"); - const remoteSetup = `printf '%s' ${shellQuote(agentPayload)} | base64 -d >/tmp/nemoclaw-e2e-agent; chmod 755 /tmp/nemoclaw-e2e-agent; rm -f /tmp/nemoclaw-e2e-agent.pid /tmp/nemoclaw-e2e-agent.heartbeat /tmp/nemoclaw-e2e-agent.events /tmp/nemoclaw-e2e-agent.log; nohup /tmp/nemoclaw-e2e-agent >/tmp/nemoclaw-e2e-agent.log 2>&1 &`; - const startResult = await bash( - host, - `openshell sandbox exec --name ${shellQuote(SURVIVOR_SANDBOX)} -- sh -lc ${shellQuote(remoteSetup)}`, - { artifactName: "start-survivor-agent", timeoutMs: 60_000 }, - ); - expectExitZero(startResult, "start survivor agent before gateway upgrade"); - const probe = await waitForSurvivorAgentReady(host); - const pid = Number.parseInt(probe.stdout.trim().split(/\s+/)[0] ?? "", 10); - expect( - Number.isInteger(pid) && pid > 0, - `survivor agent pid must be present:\n${probe.stdout}`, - ).toBe(true); - return pid; } async function installCurrentNemoclawUpgrade( host: HostCliClient, + artifacts: ArtifactSink, fakeBaseUrl: string, - currentInstallLog: string, - hiddenOldOpenShellDir?: string, ): Promise { const currentRef = currentNemoclawUpgradeRef(process.env); - const currentRefResult = await bash( - host, - currentRef === "HEAD" ? "git rev-parse HEAD" : `printf '%s' ${shellQuote(currentRef)}`, - { - artifactName: "current-ref", - timeoutMs: 30_000, - }, + const currentEnv = withoutEnvKeys( + liveEnv({ + GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "", + NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE: "1", + NEMOCLAW_BOOTSTRAP_PAYLOAD: "1", + NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE: JSON.stringify([SURVIVOR_SANDBOX]), + NEMOCLAW_INSTALL_REF: currentRef, + NEMOCLAW_INSTALL_TAG: currentRef, + NEMOCLAW_PROVIDER: "custom", + NEMOCLAW_ENDPOINT_URL: fakeBaseUrl, + NEMOCLAW_MODEL: "test-model", + NEMOCLAW_SANDBOX_NAME: SURVIVOR_SANDBOX, + NEMOCLAW_POLICY_MODE: "skip", + NEMOCLAW_DASHBOARD_PORT: "", + CHAT_UI_URL: "", + }), + ["COMPATIBLE_API_KEY"], ); - expectExitZero(currentRefResult, "resolve current NemoClaw ref"); - const resolvedRef = currentRefResult.stdout.trim(); - expect(resolvedRef.length).toBeGreaterThan(0); - const exerciseOrdinaryUpgrade = OLD_NEMOCLAW_REF === "v0.0.55"; - const expectsLegacyManagedConfirmation = - expectedLegacyRegistryMetadata(OLD_NEMOCLAW_REF).nemoclawVersion === undefined; - expect( - !exerciseOrdinaryUpgrade || Boolean(hiddenOldOpenShellDir), - "the v0.0.55 fixture must record the original OpenShell directory before hiding it", - ).toBe(true); - const baseCurrentEnv = liveEnv({ - COMPATIBLE_API_KEY: "dummy", - GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "", - NEMOCLAW_BOOTSTRAP_PAYLOAD: "1", - NEMOCLAW_INSTALL_REF: resolvedRef, - NEMOCLAW_INSTALL_TAG: resolvedRef, - NEMOCLAW_PROVIDER: "custom", - NEMOCLAW_ENDPOINT_URL: fakeBaseUrl, - NEMOCLAW_MODEL: "test-model", - NEMOCLAW_SANDBOX_NAME: SURVIVOR_SANDBOX, - NEMOCLAW_POLICY_MODE: "skip", - NEMOCLAW_DASHBOARD_PORT: "", - CHAT_UI_URL: "", - }); - const credentialScopedCurrentEnv = OPENCLAW_STATE_UPGRADE_PROOF - ? withoutEnvKeys(baseCurrentEnv, ["COMPATIBLE_API_KEY"]) - : baseCurrentEnv; - const currentEnv = exerciseOrdinaryUpgrade - ? withoutEnvKeys(credentialScopedCurrentEnv, [ - "ACCEPT_THIRD_PARTY_SOFTWARE", - "NON_INTERACTIVE", - "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", - "NEMOCLAW_NON_INTERACTIVE", - "NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE", - "NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE", - ]) - : { - ...credentialScopedCurrentEnv, - NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE: "1", - NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE: JSON.stringify([SURVIVOR_SANDBOX]), - }; - expect( - currentEnv.COMPATIBLE_API_KEY, - "installed-base upgrade must not re-seed the gateway credential", - ).toBe(OPENCLAW_STATE_UPGRADE_PROOF ? undefined : "dummy"); const redactionValues = [process.env.GITHUB_TOKEN ?? ""].filter(Boolean); await runInstallerPayload( host, - `current-${resolvedRef.slice(0, 12)}`, - currentGatewayUpgradeInstallerArgs(path.join(REPO_ROOT, "scripts", "install.sh"), { - interactive: exerciseOrdinaryUpgrade, - }), - currentInstallLog, + `current-${currentRef.slice(0, 12)}`, + currentGatewayUpgradeInstallerArgs(path.join(REPO_ROOT, "scripts", "install.sh")), + artifacts, + "current-install.log", currentEnv, redactionValues, - { - expectedExitCode: LEGACY_GATEWAY_PRESERVES_LIVE_POLICY ? 0 : 1, - hiddenOpenShellDir: exerciseOrdinaryUpgrade ? hiddenOldOpenShellDir : undefined, - // One answer covers a changed usage notice, when present, and the other - // confirms the legacy managed-image recovery prompt. - interactiveInput: exerciseOrdinaryUpgrade ? "yes\nyes" : undefined, - }, ); - const currentLog = fs.readFileSync(currentInstallLog, "utf8"); - const expectedConfirmation = exerciseOrdinaryUpgrade - ? "Confirmed legacy managed-image recovery" - : expectsLegacyManagedConfirmation - ? "Confirmed 1 exact pre-fingerprint sandbox name(s)" - : null; - expect( - expectedConfirmation === null - ? !currentLog.includes("exact pre-fingerprint sandbox name(s)") - : currentLog.includes(expectedConfirmation), - ).toBe(true); - expect(currentLog).toContain("Pre-upgrade backup: 1 backed up, 0 failed, 0 skipped"); - const assertRecoveredInstaller = (): void => { - expect(currentLog).toContain("Existing sandboxes recovered; skipping generic onboarding"); - }; - const assertFailClosedInstaller = (): void => { - expect(currentLog).not.toContain("Existing sandboxes recovered; skipping generic onboarding"); - expect(currentLog).toContain( - "Rebuild cannot recover its missing OpenShell policy or live workspace from NemoClaw registry metadata.", - ); - expect(currentLog).toContain( - "Cannot rebuild an absent sandbox without its authoritative OpenShell policy.", - ); - expect(currentLog).toContain("Generic onboarding will not run"); - expect(currentLog).toContain( - "Installation incomplete: one or more existing sandboxes failed to upgrade.", - ); - }; - (LEGACY_GATEWAY_PRESERVES_LIVE_POLICY ? assertRecoveredInstaller : assertFailClosedInstaller)(); - const openshellVersion = await bash(host, `openshell --version`, { artifactName: "current-openshell-version", redactionValues, @@ -1043,15 +527,6 @@ async function installCurrentNemoclawUpgrade( CURRENT_OPENSHELL_VERSION, `current NemoClaw install must upgrade OpenShell to ${CURRENT_OPENSHELL_VERSION}`, ); - - const status = await bash(host, `openshell status`, { - artifactName: "current-openshell-status", - timeoutMs: 60_000, - }); - expectExitZero(status, "openshell status after current install"); - expect(resultText(status)).toMatch( - new RegExp(`Version:.*${escapeRegExpLiteral(CURRENT_OPENSHELL_VERSION)}`), - ); } async function assertSurvivorSandboxAfterUpgrade(host: HostCliClient): Promise { @@ -1064,143 +539,21 @@ async function assertSurvivorSandboxAfterUpgrade(host: HostCliClient): Promise/dev/null && test -s /sandbox/.openclaw/openclaw.json && openclaw --version 2>/dev/null")}`, - { artifactName: "post-upgrade-openclaw-agent", timeoutMs: 60_000 }, - ); - expectExitZero( - agentCheck, - "OpenClaw agent must remain installed/configured after gateway upgrade", - ); - expect(agentCheck.stdout.trim().length).toBeGreaterThan(0); - - expect(fs.existsSync(REGISTRY_FILE), `${REGISTRY_FILE} must exist after upgrade`).toBe(true); - expect(fs.readFileSync(REGISTRY_FILE, "utf8")).toContain(`"${SURVIVOR_SANDBOX}"`); - - const list = await bash(host, `nemoclaw list`, { - artifactName: "post-upgrade-nemoclaw-list", - timeoutMs: 60_000, - }); - expectExitZero(list, "nemoclaw list after gateway upgrade"); - expectOutputContains(list, SURVIVOR_SANDBOX, "nemoclaw list must still show survivor sandbox"); -} - -async function assertMissingSurvivorFailsClosedAfterUpgrade( - host: HostCliClient, - currentInstallLog: string, -): Promise { - const currentLog = fs.readFileSync(currentInstallLog, "utf8"); - const backupLine = currentLog - .split(/\r?\n/u) - .find((line) => line.includes(`✓ ${SURVIVOR_SANDBOX}:`) && line.includes("→ ")); - const backupPath = backupLine?.split("→ ").at(-1)?.trim() ?? ""; - expect(path.isAbsolute(backupPath), `upgrade backup path must be absolute: ${backupLine}`).toBe( - true, - ); - expect(fs.existsSync(path.join(backupPath, "rebuild-manifest.json"))).toBe(true); - const manifest = JSON.parse( - fs.readFileSync(path.join(backupPath, "rebuild-manifest.json"), "utf8"), - ) as Record; - expect(manifest.rebuildPolicyHandoff).toBeUndefined(); - expect( - fs.readdirSync(backupPath).some((entry) => entry.startsWith("rebuild-policy-handoff.")), - ).toBe(false); - - expect(fs.existsSync(REGISTRY_FILE), `${REGISTRY_FILE} must remain after failed recovery`).toBe( - true, - ); - expect(fs.readFileSync(REGISTRY_FILE, "utf8")).toContain(`"${SURVIVOR_SANDBOX}"`); - - const liveList = await bash(host, "openshell sandbox list", { - artifactName: "post-upgrade-openshell-sandbox-list", - timeoutMs: 60_000, - }); - expectExitZero(liveList, "OpenShell sandbox list after fail-closed upgrade"); - expect(resultText(liveList)).not.toContain(SURVIVOR_SANDBOX); - - const registryList = await bash(host, "nemoclaw list", { - artifactName: "post-upgrade-nemoclaw-list", - timeoutMs: 60_000, - }); - expectExitZero(registryList, "nemoclaw list after fail-closed upgrade"); - expectOutputContains( - registryList, - SURVIVOR_SANDBOX, - "failed recovery must preserve the stranded registry record for explicit cleanup", - ); } -function runMacInstallerProbe( - artifacts: ArtifactSink, - name: string, - setup: (fakeBin: string, tmp: string) => Record, -): ReturnType { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-${name}-`)); - const fakeBin = path.join(tmp, "bin"); - fs.mkdirSync(fakeBin, { recursive: true }); - const extraEnv = setup(fakeBin, tmp); - const result = spawnSync("bash", [INSTALL_OPENSHELL], { - env: { - ...process.env, - ...extraEnv, - NEMOCLAW_OPENSHELL_CHANNEL: "stable", - PATH: `${fakeBin}:/usr/bin:/bin`, - }, - encoding: "utf8", - killSignal: "SIGKILL", - timeout: 60_000, - }); - fs.mkdirSync(artifacts.pathFor(`macos-${name}`), { recursive: true }); - fs.writeFileSync(artifacts.pathFor(`macos-${name}/stdout.txt`), result.stdout ?? "", "utf8"); - fs.writeFileSync(artifacts.pathFor(`macos-${name}/stderr.txt`), result.stderr ?? "", "utf8"); - return result; -} - -function writeFakeDarwinUname(fakeBin: string): void { - writeExecutable( - path.join(fakeBin, "uname"), - `#!/usr/bin/env bash -if [ "\${1:-}" = "-m" ]; then - printf 'arm64\n' -else - printf 'Darwin\n' -fi -`, - ); -} - -function writeFakeCurrentOpenshell(fakeBin: string): void { - writeExecutable( - path.join(fakeBin, "openshell"), - `#!/usr/bin/env bash -# request-body-credential-rewrite -# websocket-credential-rewrite -if [ "\${1:-}" = "--version" ]; then - printf 'openshell ${CURRENT_OPENSHELL_VERSION}\n' - exit 0 -fi -exit 99 -# request-body-credential-rewrite websocket-credential-rewrite -`, - ); -} - -const runOpenShellGatewayUpgrade = test; const runLinuxOpenShellGatewayUpgrade = test.skipIf(process.platform !== "linux"); runLinuxOpenShellGatewayUpgrade( - "openshell-gateway-upgrade: preserves live OpenShell state or fails closed without it", + "openshell-gateway-upgrade: preserves a usable sandbox and workspace state (#10517)", { timeout: TEST_TIMEOUT_MS, meta: { e2ePhases: [ "clear the prior gateway and start compatible inference", "install pinned legacy NemoClaw and its sandbox", - "start the survivor agent and workspace marker", + "verify the legacy agent and write workspace state", "upgrade to the current OpenShell gateway", - "verify version-specific upgrade outcome", + "verify the upgraded agent and preserved workspace state", ], }, }, @@ -1213,10 +566,9 @@ runLinuxOpenShellGatewayUpgrade( "real Docker/OpenShell gateway and OpenClaw sandbox", "exact-name confirmation for the known-managed legacy fixture", "current scripts/install.sh gateway upgrade path", - "sandbox exec /proc process probe", - LEGACY_GATEWAY_PRESERVES_LIVE_POLICY - ? "NemoClaw registry and durable workspace restore" - : "fail-closed missing-policy diagnostics and preserved backup", + "authenticated OpenClaw turns before and after upgrade", + "raw gateway credential absent from sandbox environment and managed OpenClaw files", + "durable workspace restore and survivor discovery through the current CLI", ], oldNemoclawRef: OLD_NEMOCLAW_REF, oldNemoclawCommit: OLD_NEMOCLAW_COMMIT, @@ -1225,12 +577,6 @@ runLinuxOpenShellGatewayUpgrade( oldOpenClawVersion: OLD_OPENCLAW_VERSION, oldSandboxBaseImageRef: OLD_SANDBOX_BASE_IMAGE_REF, currentOpenShellVersion: CURRENT_OPENSHELL_VERSION, - ...(OPENCLAW_STATE_UPGRADE_PROOF - ? { - currentOpenClawVersion: CURRENT_OPENCLAW_VERSION, - openClawStateUpgrade: "2026.6.10 installed state to 2026.7.1", - } - : {}), survivorSandbox: SURVIVOR_SANDBOX, }); @@ -1260,13 +606,13 @@ runLinuxOpenShellGatewayUpgrade( await preCleanUpgradeGateway(host, "pre-cleanup-gateway"); const fake = await startFakeOpenAiCompatibleServer({ - apiKey: "dummy", + apiKey: GATEWAY_CREDENTIAL, host: "0.0.0.0", model: "test-model", progress, publicHost: "host.openshell.internal", - requireAuth: OPENCLAW_STATE_UPGRADE_PROOF, - requireAuthModels: OPENCLAW_STATE_UPGRADE_PROOF, + requireAuth: true, + requireAuthModels: true, responseText: "ok", }); let firewallSetup: ReturnType; @@ -1275,7 +621,7 @@ runLinuxOpenShellGatewayUpgrade( cleanup, host, port: Number(new URL(fake.baseUrl).port), - ...legacyGatewayUpgradeHostFirewallOptions(OLD_NEMOCLAW_REF), + ...legacyGatewayUpgradeHostFirewallOptions(), }); } catch (error) { await fake.close(); @@ -1295,188 +641,16 @@ runLinuxOpenShellGatewayUpgrade( firewallSetup.then((result) => artifacts.writeJson("host-mock-firewall.json", result)), ]); throwGatewayUpgradeSetupFailures(setupResults); - const legacyStateContract = await captureOpenClawStateUpgradeProof(host, fake, artifacts); - const hiddenOldOpenShellDir = - OLD_NEMOCLAW_REF === "v0.0.55" ? await stageOldOpenShellInUserLocalBin(host) : undefined; - progress.phase("start the survivor agent and workspace marker"); - const survivorPid = await startSurvivorAgentInExistingClaw(host); - expect(Number.isInteger(survivorPid) && survivorPid > 0).toBe(true); + progress.phase("verify the legacy agent and write workspace state"); + await assertOpenClawAgentSecretBoundary(host, fake, "legacy"); + await writeSurvivorMarker(host); progress.phase("upgrade to the current OpenShell gateway"); - const currentInstallLog = artifacts.pathFor("current-install.log"); - await installCurrentNemoclawUpgrade( - host, - fake.baseUrl, - currentInstallLog, - hiddenOldOpenShellDir, - ); - - const assertRecoveredUpgrade = async (): Promise => { - await assertSurvivorSandboxAfterUpgrade(host); - await verifyOpenClawStateUpgradeProof(host, fake, artifacts, legacyStateContract); - }; - const assertFailClosedUpgrade = async (): Promise => { - await assertMissingSurvivorFailsClosedAfterUpgrade(host, currentInstallLog); - }; - progress.phase("verify version-specific upgrade outcome"); - await ( - LEGACY_GATEWAY_PRESERVES_LIVE_POLICY ? assertRecoveredUpgrade : assertFailClosedUpgrade - )(); - }, -); - -runOpenShellGatewayUpgrade( - "openshell-gateway-upgrade: macOS incomplete current install fetches Darwin gateway asset", - { - meta: { - e2ePhases: [ - "stage a Darwin install with the gateway missing", - "run the installer asset recovery path", - "inspect the requested Darwin gateway assets", - ], - }, - }, - async ({ artifacts, progress }) => { - const curlLog = artifacts.pathFor("macos-missing-gateway/curl.log"); - progress.phase("run the installer asset recovery path"); - const result = runMacInstallerProbe(artifacts, "missing-gateway", (fakeBin) => { - fs.mkdirSync(path.dirname(curlLog), { recursive: true }); - writeFakeDarwinUname(fakeBin); - writeFakeCurrentOpenshell(fakeBin); - writeExecutable(path.join(fakeBin, "gh"), "#!/usr/bin/env bash\nexit 1\n"); - writeExecutable( - path.join(fakeBin, "curl"), - `#!/usr/bin/env bash -out="" -prev="" -for arg in "$@"; do - if [ "$prev" = "-o" ]; then - out="$arg" - break - fi - prev="$arg" -done -printf '%s\n' "$*" >>${shellQuote(curlLog)} -if [ -n "$out" ]; then - printf 'fake payload\n' >"$out" -fi -exit 0 -`, - ); - return { NEMOCLAW_FAKE_CURL_LOG: curlLog }; - }); - const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`; - expect(result.status, output).not.toBe(0); - expect(result.stdout).toContain("missing Docker-driver binaries"); - progress.phase("inspect the requested Darwin gateway assets"); - const downloads = fs.readFileSync(curlLog, "utf8"); - expect(downloads).toContain("openshell-gateway-aarch64-apple-darwin.tar.gz"); - expect(downloads).not.toContain("openshell-driver-vm-aarch64-apple-darwin.tar.gz"); - }, -); - -runOpenShellGatewayUpgrade( - "openshell-gateway-upgrade: macOS installer does not require VM driver Hypervisor entitlement", - { - meta: { - e2ePhases: [ - "stage a Darwin install with current binaries", - "run the installer entitlement path", - "confirm the VM driver remains unsigned", - ], - }, - }, - async ({ artifacts, progress }) => { - const signLog = artifacts.pathFor("macos-vm-driver-entitlement/codesign.log"); - const stateFile = artifacts.pathFor("macos-vm-driver-entitlement/codesign-state"); - progress.phase("run the installer entitlement path"); - const result = runMacInstallerProbe(artifacts, "vm-driver-entitlement", (fakeBin) => { - fs.mkdirSync(path.dirname(signLog), { recursive: true }); - writeFakeDarwinUname(fakeBin); - writeFakeCurrentOpenshell(fakeBin); - writeExecutable( - path.join(fakeBin, "openshell-gateway"), - `#!/usr/bin/env bash -if [ "\${1:-}" = "--version" ]; then - printf 'openshell-gateway ${CURRENT_OPENSHELL_VERSION}\n' - exit 0 -fi -# allow_all_known_mcp_methods -exit 0 -`, - ); - writeExecutable(path.join(fakeBin, "openshell-driver-vm"), "#!/usr/bin/env bash\nexit 0\n"); - writeExecutable( - path.join(fakeBin, "codesign"), - `#!/usr/bin/env bash -if [ "\${1:-}" = "-d" ]; then - if [ -f ${shellQuote(stateFile)} ]; then - printf '%s\n' 'com.apple.security.hypervisor' - fi - exit 0 -fi -printf '%s\n' "$*" >>${shellQuote(signLog)} -: >${shellQuote(stateFile)} -exit 0 -`, - ); - return { - NEMOCLAW_FAKE_CODESIGN_LOG: signLog, - NEMOCLAW_FAKE_CODESIGN_STATE: stateFile, - }; - }); - const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`; - expect(result.status, output).toBe(0); - progress.phase("confirm the VM driver remains unsigned"); - const signLogText = fs.existsSync(signLog) ? fs.readFileSync(signLog, "utf8") : ""; - expect(signLogText).not.toContain("--force --sign - --entitlements"); - expect(result.stdout).not.toContain("Installing OpenShell from release"); - }, -); - -runOpenShellGatewayUpgrade( - "openshell-gateway-upgrade: macOS Docker sandbox builds keep VM rootfs compatibility disabled", - { - meta: { - e2ePhases: [ - "read the Docker compatibility sources", - "confirm OpenClaw Docker defaults disable Darwin VM mode", - "confirm Hermes Docker defaults disable Darwin VM mode", - ], - }, - }, - async ({ artifacts, progress }) => { - await artifacts.writeJson("macos-docker-rootfs-permissions-target.json", { - id: "openshell-gateway-upgrade-macos-docker-rootfs-permissions", - runner: "vitest", - boundary: "static Dockerfile and Dockerfile patch contract", - }); - const dockerfile = fs.readFileSync(path.join(REPO_ROOT, "Dockerfile"), "utf8"); - const patchFlow = fs.readFileSync( - path.join(REPO_ROOT, "src/lib/onboard/sandbox-dockerfile-patch-flow.ts"), - "utf8", - ); - const dockerfilePatch = fs.readFileSync( - path.join(REPO_ROOT, "src/lib/onboard/dockerfile-patch.ts"), - "utf8", - ); - const hermesDockerfile = fs.readFileSync( - path.join(REPO_ROOT, "agents/hermes/Dockerfile"), - "utf8", - ); - - progress.phase("confirm OpenClaw Docker defaults disable Darwin VM mode"); - expect(dockerfile).toContain("ARG NEMOCLAW_DARWIN_VM_COMPAT=0"); - expect(dockerfilePatch).toContain( - 'ARG NEMOCLAW_DARWIN_VM_COMPAT=${sanitizeDockerArg(darwinVmCompat ? "1" : "0")}', - ); - expect(patchFlow).toContain("const darwinVmCompat = false;"); - expect(dockerfile).toContain("chmod -R a+rwX /sandbox/.openclaw"); + await installCurrentNemoclawUpgrade(host, artifacts, fake.baseUrl); - progress.phase("confirm Hermes Docker defaults disable Darwin VM mode"); - expect(hermesDockerfile).toContain("ARG NEMOCLAW_DARWIN_VM_COMPAT=0"); - expect(hermesDockerfile).toContain("chmod -R a+rwX /sandbox/.hermes"); - expect(hermesDockerfile).toContain("chmod a+rw /sandbox/.bashrc /sandbox/.profile"); + progress.phase("verify the upgraded agent and preserved workspace state"); + await assertSurvivorSandboxAfterUpgrade(host); + await assertOpenClawAgentSecretBoundary(host, fake, "upgraded"); }, ); diff --git a/test/e2e/live/shields-retirement-upgrade.test.ts b/test/e2e/live/shields-retirement-upgrade.test.ts index 9a89f249542..b4bbbb28397 100644 --- a/test/e2e/live/shields-retirement-upgrade.test.ts +++ b/test/e2e/live/shields-retirement-upgrade.test.ts @@ -271,7 +271,7 @@ test.skipIf(process.platform !== "linux")( cleanup, host, port: Number(new URL(fake.baseUrl).port), - ...legacyGatewayUpgradeHostFirewallOptions(RELEASE_TAG), + ...legacyGatewayUpgradeHostFirewallOptions(), }); } catch (error) { await fake.close(); diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index d3bd307f025..25332ada683 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -738,11 +738,30 @@ }, { "live": "test/e2e/live/openshell-gateway-upgrade.test.ts", + "liveSources": [ + "test/e2e/live/openshell-gateway-upgrade-helpers.ts", + "test/e2e/live/openshell-gateway-upgrade-old-installer.ts" + ], "fast": [ + "src/lib/actions/sandbox/rebuild-flow-lifecycle.test.ts", + "src/lib/actions/sandbox/rebuild-flow-recovery.test.ts", + "src/lib/actions/sandbox/rebuild-flow-target-image.test.ts", + "src/lib/actions/upgrade-sandboxes-recovery.test.ts", + "src/lib/onboard/dockerfile-patch.test.ts", + "src/lib/state/openclaw-plugin-restore.test.ts", + "test/agents/openclaw/openclaw-2026-7-startup-compat.test.ts", + "test/agents/openclaw/openclaw-shared-state-permissions-patch.test.ts", + "test/install/install-preexisting-sandbox-recovery.test.ts", "test/e2e/support/e2e-live-target-gating.test.ts", "test/e2e/support/e2e-cleanup-resources.test.ts", "test/e2e/support/e2e-clients.test.ts", - "test/e2e/support/openclaw-agent-output.test.ts" + "test/e2e/support/openclaw-agent-output.test.ts", + "test/e2e/support/openshell-gateway-upgrade-old-installer.test.ts", + "test/e2e/support/openshell-gateway-upgrade-workflow-boundary.test.ts", + "test/install/installer-hash-check.test.ts", + "test/installer-integration/install-openshell-version-check.test.ts", + "test/process-recovery/rebuild-stale-recovery.test.ts", + "test/state/snapshot-recovery-validation.test.ts" ] }, { diff --git a/test/e2e/support/e2e-fixture-context.test.ts b/test/e2e/support/e2e-fixture-context.test.ts index e3033ae746f..eef1c43117f 100644 --- a/test/e2e/support/e2e-fixture-context.test.ts +++ b/test/e2e/support/e2e-fixture-context.test.ts @@ -358,7 +358,7 @@ describe("E2E fixture primitives", () => { ).toThrow(/argument cannot contain NUL bytes/); }); - it("shell probe enforces options.redactionValues even when the injected redactor ignores extra values", async () => { + it("redacts ShellProbe output and retained logs when the secret is registered on both surfaces", async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-e2e-shell-probe-enforce-")); try { const artifacts = new ArtifactSink(tmp); @@ -394,6 +394,11 @@ describe("E2E fixture primitives", () => { expect(result.stderr).toContain("[REDACTED]"); expect(result.stdout).not.toContain(secret); expect(result.stderr).not.toContain(secret); + artifacts.addRedactionValues([secret]); + await artifacts.writeText("retained-install.log", `${result.stdout}\n${result.stderr}\n${secret}`); + const retained = fs.readFileSync(artifacts.pathFor("retained-install.log"), "utf8"); + expect(retained).toContain("[REDACTED]"); + expect(retained).not.toContain(secret); const written = fs.readFileSync( artifacts.pathFor("shell/options-redaction-enforced.result.json"), "utf8", diff --git a/test/e2e/support/e2e-live-target-gating.test.ts b/test/e2e/support/e2e-live-target-gating.test.ts index 10486574efd..66c12041f9c 100644 --- a/test/e2e/support/e2e-live-target-gating.test.ts +++ b/test/e2e/support/e2e-live-target-gating.test.ts @@ -238,7 +238,7 @@ describe("live E2E target gating", () => { ], [ "openshell-gateway-upgrade.test.ts", - "openshell-gateway-upgrade: preserves live OpenShell state or fails closed without it", + "openshell-gateway-upgrade: preserves a usable sandbox and workspace state (#10517)", ], ] as const)("applies the Linux gate to %s at real Vitest collection", (file, testName) => { const result = listLiveTests({ diff --git a/test/e2e/support/openshell-gateway-upgrade-old-installer.test.ts b/test/e2e/support/openshell-gateway-upgrade-old-installer.test.ts index 4ad58142b00..bfd8543d634 100644 --- a/test/e2e/support/openshell-gateway-upgrade-old-installer.test.ts +++ b/test/e2e/support/openshell-gateway-upgrade-old-installer.test.ts @@ -6,6 +6,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; +import { REVIEWED_GATEWAY_UPGRADE_FIXTURE } from "../../../tools/e2e/openshell-gateway-upgrade-fixture.mts"; import { REPO_ROOT } from "../fixtures/paths.ts"; import { OLD_INSTALLER_ADVISORY_AUDIT, @@ -17,37 +18,37 @@ import { } from "../live/openshell-gateway-upgrade-old-installer.ts"; const temporaryDirectories: string[] = []; -const HISTORICAL_BUILD_CONTEXT_MODULES = Object.freeze({ - "v0.0.36": "src/lib/sandbox-build-context.ts", - "v0.0.55": "src/lib/sandbox/build-context.ts", - "v0.0.74": "src/lib/sandbox/build-context.ts", - "v0.0.89": "src/lib/sandbox/build-context.ts", -}); -const HISTORICAL_OPENCLAW_VERSIONS = Object.freeze({ - "v0.0.36": "2026.4.24", - "v0.0.55": "2026.5.22", - "v0.0.74": "2026.5.27", - "v0.0.89": "2026.6.10", -}); -const HISTORICAL_NEMOCLAW_COMMITS = Object.freeze({ - "v0.0.36": "3351fbdd4eb7d9b80ec471545083956327da2b10", - "v0.0.55": "95d483fe2b6569d68e59493c60f19df09a068e8f", - "v0.0.74": "3a05b54e8ec3e1d5550ec5c728de54af872bffe3", - "v0.0.89": "1143aa5cce77f3bad1b3b5588bd7fddbe438237e", -}); +const HISTORICAL_BUILD_CONTEXT_MODULE = "src/lib/sandbox/build-context.ts"; -type ReviewedHistoricalRef = keyof typeof HISTORICAL_BUILD_CONTEXT_MODULES; +function historicalReleaseCommitRef(nemoclawRef: string): string { + return `refs/tags/${nemoclawRef}^{commit}`; +} -function historicalFixtureIdentity(nemoclawRef: ReviewedHistoricalRef): { +function assertHistoricalReleaseIdentity(identity: { nemoclawCommit: string; - nemoclawRef: ReviewedHistoricalRef; - openclawVersion: string; -} { - return { - nemoclawCommit: HISTORICAL_NEMOCLAW_COMMITS[nemoclawRef], - nemoclawRef, - openclawVersion: HISTORICAL_OPENCLAW_VERSIONS[nemoclawRef], - }; + nemoclawRef: string; +}): void { + const resolved = spawnSync( + "git", + [ + "-C", + REPO_ROOT, + "rev-parse", + "--verify", + "--end-of-options", + historicalReleaseCommitRef(identity.nemoclawRef), + ], + { encoding: "utf8" }, + ); + expect( + resolved.status, + `Historical NemoClaw release ${identity.nemoclawRef} could not be resolved from its tag`, + ).toBe(0); + + expect( + resolved.stdout.trim(), + `Historical NemoClaw release ${identity.nemoclawRef} must resolve to reviewed commit ${identity.nemoclawCommit}`, + ).toBe(identity.nemoclawCommit); } function writeInstallerHarness(sourceRoot: string): { @@ -109,7 +110,7 @@ function writeHistoricalFixture(advisoryAuditCount = 1): { path.join(sourceRoot, "Dockerfile"), [ "FROM fixture", - "ARG OPENCLAW_VERSION=2026.5.27", + `ARG OPENCLAW_VERSION=${REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion}`, ...Array.from({ length: advisoryAuditCount }, () => OLD_INSTALLER_ADVISORY_AUDIT.trimEnd()), " npm --prefix /usr/local/lib/nemoclaw/mcporter-runtime audit signatures; \\", " true", @@ -119,16 +120,28 @@ function writeHistoricalFixture(advisoryAuditCount = 1): { return writeInstallerHarness(sourceRoot); } -function extractReviewedHistoricalSource(nemoclawRef: ReviewedHistoricalRef): string { +function extractReviewedHistoricalSource(): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-old-upgrade-source-")); temporaryDirectories.push(root); const sourceRoot = path.join(root, "source"); fs.mkdirSync(sourceRoot); - const archive = spawnSync("git", ["-C", REPO_ROOT, "archive", nemoclawRef], { - maxBuffer: 128 * 1024 * 1024, - }); - expect(archive.status, archive.stderr.toString()).toBe(0); + const archive = spawnSync( + "git", + [ + "-C", + REPO_ROOT, + "archive", + historicalReleaseCommitRef(REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef), + ], + { + maxBuffer: 128 * 1024 * 1024, + }, + ); + expect( + archive.status, + `Historical NemoClaw release ${REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef} could not be archived from its tag`, + ).toBe(0); const extract = spawnSync("tar", ["-xf", "-", "-C", sourceRoot], { input: archive.stdout, maxBuffer: 128 * 1024 * 1024, @@ -137,11 +150,8 @@ function extractReviewedHistoricalSource(nemoclawRef: ReviewedHistoricalRef): st return sourceRoot; } -function stageFrozenOptimizedBuildContext( - sourceRoot: string, - nemoclawRef: ReviewedHistoricalRef, -): string { - const modulePath = path.join(sourceRoot, HISTORICAL_BUILD_CONTEXT_MODULES[nemoclawRef]); +function stageFrozenOptimizedBuildContext(sourceRoot: string): string { + const modulePath = path.join(sourceRoot, HISTORICAL_BUILD_CONTEXT_MODULE); const outputPath = path.join(path.dirname(sourceRoot), "staged-context-path.txt"); const runner = String.raw` import { writeFileSync } from "node:fs"; @@ -174,16 +184,16 @@ writeFileSync(outputPath, staged.buildCtx); return fs.readFileSync(outputPath, "utf8"); } -function runReviewedHistoricalFixture(nemoclawRef: ReviewedHistoricalRef): string { - const fixture = writeInstallerHarness(extractReviewedHistoricalSource(nemoclawRef)); - patchOldInstallerFixture(fixture.installer, historicalFixtureIdentity(nemoclawRef)); +function runReviewedHistoricalFixture(): string { + const fixture = writeInstallerHarness(extractReviewedHistoricalSource()); + patchOldInstallerFixture(fixture.installer, REVIEWED_GATEWAY_UPGRADE_FIXTURE); const result = spawnSync("bash", [fixture.installer], { encoding: "utf8", env: { ...process.env, NEMOCLAW_OLD_OPENCLAW_ARCHIVE: fixture.archive, - NEMOCLAW_OLD_OPENCLAW_VERSION: HISTORICAL_OPENCLAW_VERSIONS[nemoclawRef], + NEMOCLAW_OLD_OPENCLAW_VERSION: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion, }, }); expect(result.status, result.stderr).toBe(0); @@ -198,10 +208,10 @@ function runReviewedHistoricalFixture(nemoclawRef: ReviewedHistoricalRef): strin "npm install -g --ignore-scripts --no-audit --no-fund --no-progress /tmp/nemoclaw-e2e-old-openclaw.tgz", ); expect(dockerfile).toContain( - `test "$(openclaw --version | awk '{print $2}')" = "${HISTORICAL_OPENCLAW_VERSIONS[nemoclawRef]}"`, + `test "$(openclaw --version | awk '{print $2}')" = "${REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion}"`, ); - const stagedContext = stageFrozenOptimizedBuildContext(fixture.sourceRoot, nemoclawRef); + const stagedContext = stageFrozenOptimizedBuildContext(fixture.sourceRoot); expect( fs.readFileSync(path.join(stagedContext, OLD_INSTALLER_ARCHIVE_CONTEXT_PATH), "utf8"), ).toBe("reviewed fixture archive"); @@ -215,22 +225,21 @@ afterEach(() => { }); describe("historical OpenShell gateway upgrade installer adapter", () => { - it.each([ - "v0.0.36", - "v0.0.55", - ] as const)("accepts the reviewed %s profile without an advisory audit", (nemoclawRef) => { - const dockerfile = runReviewedHistoricalFixture(nemoclawRef); - expect(dockerfile).not.toContain("audit --omit=dev --audit-level=low"); - expect(dockerfile).not.toContain( - "Skipping current advisory audit for the immutable historical mcporter lock", - ); - }, 30_000); + it("binds the retained NemoClaw release tag to its reviewed commit (#10517)", () => { + expect(() => assertHistoricalReleaseIdentity(REVIEWED_GATEWAY_UPGRADE_FIXTURE)).not.toThrow(); + }); - it.each([ - "v0.0.74", - "v0.0.89", - ] as const)("accepts the reviewed %s advisory and signature audit boundary", (nemoclawRef) => { - const dockerfile = runReviewedHistoricalFixture(nemoclawRef); + it("rejects a retained NemoClaw release tag paired with another commit (#10517)", () => { + expect(() => + assertHistoricalReleaseIdentity({ + ...REVIEWED_GATEWAY_UPGRADE_FIXTURE, + nemoclawCommit: "0".repeat(40), + }), + ).toThrow(/must resolve to reviewed commit 0{40}/u); + }); + + it("accepts the retained fixture advisory and signature audit boundary", () => { + const dockerfile = runReviewedHistoricalFixture(); expect(dockerfile).not.toContain("audit --omit=dev --audit-level=low"); expect(dockerfile).toContain( "Skipping current advisory audit for the immutable historical mcporter lock", @@ -240,7 +249,7 @@ describe("historical OpenShell gateway upgrade installer adapter", () => { it("rejects an ambiguous historical advisory boundary", () => { const fixture = writeHistoricalFixture(2); - patchOldInstallerFixture(fixture.installer, historicalFixtureIdentity("v0.0.74")); + patchOldInstallerFixture(fixture.installer, REVIEWED_GATEWAY_UPGRADE_FIXTURE); const originalDockerfile = fs.readFileSync(fixture.dockerfile, "utf8"); const result = spawnSync("bash", [fixture.installer], { @@ -248,7 +257,7 @@ describe("historical OpenShell gateway upgrade installer adapter", () => { env: { ...process.env, NEMOCLAW_OLD_OPENCLAW_ARCHIVE: fixture.archive, - NEMOCLAW_OLD_OPENCLAW_VERSION: "2026.5.27", + NEMOCLAW_OLD_OPENCLAW_VERSION: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion, }, }); expect(result.status).not.toBe(0); @@ -258,7 +267,7 @@ describe("historical OpenShell gateway upgrade installer adapter", () => { it("rejects a missing historical advisory boundary", () => { const fixture = writeHistoricalFixture(0); - patchOldInstallerFixture(fixture.installer, historicalFixtureIdentity("v0.0.74")); + patchOldInstallerFixture(fixture.installer, REVIEWED_GATEWAY_UPGRADE_FIXTURE); const originalDockerfile = fs.readFileSync(fixture.dockerfile, "utf8"); const result = spawnSync("bash", [fixture.installer], { @@ -266,7 +275,7 @@ describe("historical OpenShell gateway upgrade installer adapter", () => { env: { ...process.env, NEMOCLAW_OLD_OPENCLAW_ARCHIVE: fixture.archive, - NEMOCLAW_OLD_OPENCLAW_VERSION: "2026.5.27", + NEMOCLAW_OLD_OPENCLAW_VERSION: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion, }, }); expect(result.status).not.toBe(0); @@ -274,24 +283,6 @@ describe("historical OpenShell gateway upgrade installer adapter", () => { expect(fs.readFileSync(fixture.dockerfile, "utf8")).toBe(originalDockerfile); }); - it("rejects an advisory audit in a profile that predates the audit", () => { - const fixture = writeHistoricalFixture(1); - patchOldInstallerFixture(fixture.installer, historicalFixtureIdentity("v0.0.36")); - const originalDockerfile = fs.readFileSync(fixture.dockerfile, "utf8"); - - const result = spawnSync("bash", [fixture.installer], { - encoding: "utf8", - env: { - ...process.env, - NEMOCLAW_OLD_OPENCLAW_ARCHIVE: fixture.archive, - NEMOCLAW_OLD_OPENCLAW_VERSION: "2026.4.24", - }, - }); - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("found 1 historical mcporter advisory audits; expected 0"); - expect(fs.readFileSync(fixture.dockerfile, "utf8")).toBe(originalDockerfile); - }); - it("rejects an unreviewed historical installer profile", () => { const fixture = writeHistoricalFixture(); const originalInstaller = fs.readFileSync(fixture.installer, "utf8"); @@ -302,47 +293,14 @@ describe("historical OpenShell gateway upgrade installer adapter", () => { nemoclawRef: "v0.0.75", openclawVersion: "2026.5.28", }), - ).toThrow(/exact reviewed ref\/commit\/OpenClaw profile/u); - expect(fs.readFileSync(fixture.installer, "utf8")).toBe(originalInstaller); - }); - - it("rejects a mixed historical installer profile", () => { - const fixture = writeHistoricalFixture(); - const originalInstaller = fs.readFileSync(fixture.installer, "utf8"); - - expect(() => - patchOldInstallerFixture(fixture.installer, { - ...historicalFixtureIdentity("v0.0.55"), - nemoclawCommit: HISTORICAL_NEMOCLAW_COMMITS["v0.0.36"], - }), - ).toThrow(/exact reviewed ref\/commit\/OpenClaw profile/u); + ).toThrow(/reviewed descriptor/u); expect(fs.readFileSync(fixture.installer, "utf8")).toBe(originalInstaller); }); - it.each([ - [ - "2026.4.24", - "sha512-W6u4XeIIP4+uG4DYV9G3JeS6QNuKwfhQIej1GIoL4BdcnUFgrnB8kHYNXL3MxiHRKuhZB9OYwUMGs8jKFZR/Vg==", - ], - [ - "2026.5.22", - "sha512-m+zgBELGbCHjWB1IWF5WSWNPr480cMKOMff2OF72c8A0AMD4hC/9+qwYtzjYmGkETcffnB711JymlVsQnh2Tow==", - ], - [ - "2026.5.27", - "sha512-2N93zhdAo88KAbHt6T7KvYXf4s7XIkYXBgv1npYpn7e1Y9FvrtgtpsA38my9rtFW+70uXEojRPX5/OqnuDqJPw==", - ], - [ - "2026.6.10", - "sha512-LcooND2tBQw8A+kc1Ujltu3lg30bJ0w7XaeRy7eYzobb8BBdcW6DOGbwJL4vpj1vl9+gjRceOtlh5nh9OARcug==", - ], - ])("binds historical OpenClaw %s to its reviewed archive", (version, expectedIntegrity) => { - expect(reviewedOldOpenClawArchive(version)).toEqual({ - expectedIntegrity, - label: `historical fixture OpenClaw ${version}`, - packageSpec: `openclaw@${version}`, - tarballUrl: `https://registry.npmjs.org/openclaw/-/openclaw-${version}.tgz`, - }); + it("binds the retained historical OpenClaw version to its reviewed archive", () => { + expect(reviewedOldOpenClawArchive(REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion)).toBe( + REVIEWED_GATEWAY_UPGRADE_FIXTURE.openClawArchive, + ); }); it("rejects an unreviewed historical OpenClaw version", () => { diff --git a/test/e2e/support/openshell-gateway-upgrade-workflow-boundary.test.ts b/test/e2e/support/openshell-gateway-upgrade-workflow-boundary.test.ts index 6a0be4a9e51..3aac96d8d50 100644 --- a/test/e2e/support/openshell-gateway-upgrade-workflow-boundary.test.ts +++ b/test/e2e/support/openshell-gateway-upgrade-workflow-boundary.test.ts @@ -8,14 +8,15 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { catalogueTarget, + E2E_TARGET_CATALOGUE, validateE2eTargetCatalogue, } from "../../../tools/e2e/target-catalogue.mts"; +import { REVIEWED_GATEWAY_UPGRADE_FIXTURE } from "../../../tools/e2e/openshell-gateway-upgrade-fixture.mts"; import { validateE2eWorkflow } from "../../../tools/e2e/workflow-boundary.mts"; import { readWorkflow } from "../../helpers/e2e-workflow-contract"; import { currentGatewayUpgradeInstallerArgs, currentNemoclawUpgradeRef, - expectedLegacyRegistryMetadata, GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, legacyGatewayUpgradeHostFirewallOptions, oldGatewayUpgradeInstallerArgs, @@ -25,95 +26,38 @@ import { } from "../live/openshell-gateway-upgrade-helpers.ts"; describe("OpenShell gateway upgrade boundary", () => { - it("pins architecture and immediate-predecessor fixtures in the catalogue (#6114)", () => { - const fixtures = [ - catalogueTarget("openshell-gateway-upgrade-v0-0-36-x86-64"), - catalogueTarget("openshell-gateway-upgrade-v0-0-55-x86-64"), - catalogueTarget("openshell-gateway-upgrade-v0-0-55-aarch64"), - catalogueTarget("openshell-gateway-upgrade-v0-0-74-x86-64"), - catalogueTarget("openshell-gateway-upgrade-v0-0-89-x86-64"), - ]; - + it("pins the retained gateway-upgrade fixture in the catalogue (#10517)", () => { + expect(Object.isFrozen(REVIEWED_GATEWAY_UPGRADE_FIXTURE)).toBe(true); + expect(Object.isFrozen(REVIEWED_GATEWAY_UPGRADE_FIXTURE.openClawArchive)).toBe(true); expect( - fixtures.map(({ environment, runner, shard }) => ({ - runner, - shard, - nemoclawRef: environment.NEMOCLAW_OLD_NEMOCLAW_REF, - commit: environment.NEMOCLAW_OLD_NEMOCLAW_COMMIT, - installerSha256: environment.NEMOCLAW_OLD_INSTALLER_SHA256, - sandboxBaseImageRef: environment.NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF, - openShellVersion: environment.NEMOCLAW_OLD_OPENSHELL_VERSION, - openClawVersion: environment.NEMOCLAW_OLD_OPENCLAW_VERSION, - currentOpenClawVersion: environment.NEMOCLAW_CURRENT_OPENCLAW_VERSION, - stateUpgradeProof: environment.NEMOCLAW_OPENCLAW_STATE_UPGRADE_PROOF, - })), - ).toEqual([ - { - runner: "ubuntu-latest", - shard: "v0-0-36-x86-64", - nemoclawRef: "v0.0.36", - commit: "3351fbdd4eb7d9b80ec471545083956327da2b10", - installerSha256: "0c42400a0d3867739f1d75d612e069967be4506e169974bbbebf14b7af39144f", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6", - openShellVersion: "0.0.36", - openClawVersion: "2026.4.24", - currentOpenClawVersion: "", - stateUpgradeProof: "", - }, - { - runner: "ubuntu-latest", - shard: "v0-0-55-x86-64", - nemoclawRef: "v0.0.55", - commit: "95d483fe2b6569d68e59493c60f19df09a068e8f", - installerSha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108", - openShellVersion: "0.0.44", - openClawVersion: "2026.5.22", - currentOpenClawVersion: "", - stateUpgradeProof: "", - }, - { - runner: "ubuntu-24.04-arm", - shard: "v0-0-55-aarch64", - nemoclawRef: "v0.0.55", - commit: "95d483fe2b6569d68e59493c60f19df09a068e8f", - installerSha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108", - openShellVersion: "0.0.44", - openClawVersion: "2026.5.22", - currentOpenClawVersion: "", - stateUpgradeProof: "", - }, - { - runner: "ubuntu-latest", - shard: "v0-0-74-x86-64", - nemoclawRef: "v0.0.74", - commit: "3a05b54e8ec3e1d5550ec5c728de54af872bffe3", - installerSha256: "a0cd3feca488d247e53d59d7d8246d2b86e75e95acb5e7d78504b3c0c60fd7db", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6", - openShellVersion: "0.0.72", - openClawVersion: "2026.5.27", - currentOpenClawVersion: "", - stateUpgradeProof: "", - }, - { - runner: "ubuntu-latest", - shard: "v0-0-89-x86-64", - nemoclawRef: "v0.0.89", - commit: "1143aa5cce77f3bad1b3b5588bd7fddbe438237e", - installerSha256: "00f24959e5ca68104fe91221c0a015dab6a4154618497fa36b969b661f418cc2", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:3265d482f67c9d81ee3a59b0bbad5eb5ea6c705fea81ece8ae888ed12794f7f1", - openShellVersion: "0.0.85", - openClawVersion: "2026.6.10", - currentOpenClawVersion: "2026.7.1", - stateUpgradeProof: "1", - }, - ]); + E2E_TARGET_CATALOGUE.filter((entry) => entry.targetId === "openshell-gateway-upgrade").map( + (entry) => entry.id, + ), + ).toEqual(["openshell-gateway-upgrade-v0-0-89-x86-64"]); + + const { environment, runner, shard } = catalogueTarget( + "openshell-gateway-upgrade-v0-0-89-x86-64", + ); + + expect({ + runner, + shard, + nemoclawRef: environment.NEMOCLAW_OLD_NEMOCLAW_REF, + commit: environment.NEMOCLAW_OLD_NEMOCLAW_COMMIT, + installerSha256: environment.NEMOCLAW_OLD_INSTALLER_SHA256, + sandboxBaseImageRef: environment.NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF, + openShellVersion: environment.NEMOCLAW_OLD_OPENSHELL_VERSION, + openClawVersion: environment.NEMOCLAW_OLD_OPENCLAW_VERSION, + }).toEqual({ + runner: "ubuntu-latest", + shard: "v0-0-89-x86-64", + nemoclawRef: REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef, + commit: REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawCommit, + installerSha256: REVIEWED_GATEWAY_UPGRADE_FIXTURE.installerSha256, + sandboxBaseImageRef: REVIEWED_GATEWAY_UPGRADE_FIXTURE.sandboxBaseImageRef, + openShellVersion: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openShellVersion, + openClawVersion: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion, + }); }); it("rejects reintroducing the superseded workflow job", () => { @@ -125,12 +69,15 @@ describe("OpenShell gateway upgrade boundary", () => { ); }); - it("rejects drift from every exact reviewed gateway-upgrade fixture field (#6114)", () => { - const fixture = catalogueTarget("openshell-gateway-upgrade-v0-0-55-x86-64"); + it("rejects drift from every exact reviewed gateway-upgrade fixture field (#10517)", () => { + const fixture = catalogueTarget("openshell-gateway-upgrade-v0-0-89-x86-64"); const mutations = [ { runner: "ubuntu-24.04-arm" }, - { shard: "v0-0-55-aarch64" }, + { shard: "v0-0-89-aarch64" }, + { environment: { ...fixture.environment, NEMOCLAW_OLD_NEMOCLAW_REF: "v0.0.88" } }, + { environment: { ...fixture.environment, NEMOCLAW_OLD_NEMOCLAW_COMMIT: "0".repeat(40) } }, { environment: { ...fixture.environment, NEMOCLAW_OLD_OPENSHELL_VERSION: "0.0.45" } }, + { environment: { ...fixture.environment, NEMOCLAW_OLD_OPENCLAW_VERSION: "2026.5.28" } }, { environment: { ...fixture.environment, NEMOCLAW_OLD_INSTALLER_SHA256: "0".repeat(64) } }, { environment: { @@ -159,9 +106,6 @@ describe("OpenShell gateway upgrade boundary", () => { "--non-interactive", "--yes-i-accept-third-party-software", ]); - expect(currentGatewayUpgradeInstallerArgs("current-install.sh", { interactive: true })).toEqual( - ["current-install.sh"], - ); }); it("installs the selected E2E checkout instead of the trusted workflow SHA", () => { @@ -185,24 +129,12 @@ describe("OpenShell gateway upgrade boundary", () => { expect(currentNemoclawUpgradeRef({})).toBe("HEAD"); }); - it.each(["v0.0.55", "v0.0.74", "v0.0.89", "v0.0.115"])( - "waits through the historical install for each gateway network [%s]", - (nemoclawRef) => { - expect(legacyGatewayUpgradeHostFirewallOptions("v0.0.36")).toEqual({ - networkName: "openshell-cluster-nemoclaw", - waitForNetworkMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, - }); - - expect(legacyGatewayUpgradeHostFirewallOptions(nemoclawRef)).toEqual({ - networkName: undefined, - waitForNetworkMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, - }); - - expect(() => legacyGatewayUpgradeHostFirewallOptions("v0.0.90")).toThrow( - /Unsupported gateway-upgrade network fixture/, - ); - }, - ); + it("waits through the historical install for the Docker gateway network", () => { + expect(legacyGatewayUpgradeHostFirewallOptions()).toEqual({ + networkName: undefined, + waitForNetworkMs: GATEWAY_UPGRADE_INSTALL_TIMEOUT_MS, + }); + }); it("accepts successful legacy install and firewall setup results (#8696)", () => { expect(() => @@ -239,70 +171,59 @@ describe("OpenShell gateway upgrade boundary", () => { ); }); - it("pins the registry metadata written by each historical release fixture", () => { - const absentMetadata = { nemoclawVersion: undefined, fromDockerfile: undefined }; - expect(expectedLegacyRegistryMetadata("v0.0.36")).toEqual(absentMetadata); - expect(expectedLegacyRegistryMetadata("v0.0.55")).toEqual(absentMetadata); - expect(expectedLegacyRegistryMetadata("v0.0.74")).toEqual({ - nemoclawVersion: "0.0.74", - fromDockerfile: null, - }); - expect(expectedLegacyRegistryMetadata("v0.0.89")).toEqual({ - nemoclawVersion: "0.0.89", - fromDockerfile: null, - }); - expect(() => expectedLegacyRegistryMetadata("v0.0.90")).toThrow( - /Unsupported gateway-upgrade registry fixture/, - ); - }); - it("rejects mutable or injectable historical fixture inputs before use (#6114)", () => { const fixture = { - nemoclawRef: "v0.0.55", - nemoclawCommit: "95d483fe2b6569d68e59493c60f19df09a068e8f", - installerSha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897", - openclawVersion: "2026.5.22", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108", + nemoclawRef: REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef, + nemoclawCommit: REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawCommit, + installerSha256: REVIEWED_GATEWAY_UPGRADE_FIXTURE.installerSha256, + openShellVersion: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openShellVersion, + openclawVersion: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion, + sandboxBaseImageRef: REVIEWED_GATEWAY_UPGRADE_FIXTURE.sandboxBaseImageRef, }; expect(validateLegacyGatewayUpgradeFixture(fixture)).toEqual({ - sandboxBaseDigest: "10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108", - }); - expect( - validateLegacyGatewayUpgradeFixture({ - nemoclawRef: "v0.0.89", - nemoclawCommit: "1143aa5cce77f3bad1b3b5588bd7fddbe438237e", - installerSha256: "00f24959e5ca68104fe91221c0a015dab6a4154618497fa36b969b661f418cc2", - openclawVersion: "2026.6.10", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:3265d482f67c9d81ee3a59b0bbad5eb5ea6c705fea81ece8ae888ed12794f7f1", - }), - ).toEqual({ - sandboxBaseDigest: "3265d482f67c9d81ee3a59b0bbad5eb5ea6c705fea81ece8ae888ed12794f7f1", + sandboxBaseDigest: REVIEWED_GATEWAY_UPGRADE_FIXTURE.sandboxBaseImageRef.split("@sha256:")[1], }); expect(() => validateLegacyGatewayUpgradeFixture({ ...fixture, nemoclawCommit: "3351fbdd4eb7d9b80ec471545083956327da2b10", }), - ).toThrow(/exact reviewed ref\/commit\/OpenClaw profile/); + ).toThrow(/reviewed descriptor/); expect(() => validateLegacyGatewayUpgradeFixture({ ...fixture, openclawVersion: "2026.4.24", }), - ).toThrow(/exact reviewed ref\/commit\/OpenClaw profile/); + ).toThrow(/reviewed descriptor/); expect(() => validateLegacyGatewayUpgradeFixture({ ...fixture, nemoclawRef: "v0.0.36", }), - ).toThrow(/exact reviewed ref\/commit\/OpenClaw profile/); + ).toThrow(/reviewed descriptor/); + expect(() => + validateLegacyGatewayUpgradeFixture({ + ...fixture, + installerSha256: "1".repeat(64), + }), + ).toThrow(/reviewed descriptor/); + expect(() => + validateLegacyGatewayUpgradeFixture({ + ...fixture, + openShellVersion: "0.0.84", + }), + ).toThrow(/reviewed descriptor/); + expect(() => + validateLegacyGatewayUpgradeFixture({ + ...fixture, + sandboxBaseImageRef: `ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:${"1".repeat(64)}`, + }), + ).toThrow(/reviewed descriptor/); expect(() => validateLegacyGatewayUpgradeFixture({ ...fixture, - nemoclawRef: "v0.0.55; echo injected", + nemoclawRef: `${fixture.nemoclawRef}; echo injected`, }), ).toThrow(/NEMOCLAW_OLD_NEMOCLAW_REF/); expect(() => @@ -320,7 +241,7 @@ describe("OpenShell gateway upgrade boundary", () => { expect(() => validateLegacyGatewayUpgradeFixture({ ...fixture, - openclawVersion: '2026.5.22" && echo injected #', + openclawVersion: `${fixture.openclawVersion}" && echo injected #`, }), ).toThrow(/NEMOCLAW_OLD_OPENCLAW_VERSION/); expect(() => diff --git a/test/e2e/support/shields-retirement-workflow-plan.test.ts b/test/e2e/support/shields-retirement-workflow-plan.test.ts index 7e2a89b31b4..21b9a413762 100644 --- a/test/e2e/support/shields-retirement-workflow-plan.test.ts +++ b/test/e2e/support/shields-retirement-workflow-plan.test.ts @@ -48,10 +48,10 @@ describe("Shields retirement upgrade workflow plan", () => { expect(selectedWorkflowJobs(targeted)).toEqual(["catalogue-github-read"]); const unfiltered = buildE2eWorkflowPlan(); - expect(E2E_TARGET_CATALOGUE).toHaveLength(65); - expect(unfiltered.coverageMatrix).toHaveLength(90); + expect(E2E_TARGET_CATALOGUE).toHaveLength(61); + expect(unfiltered.coverageMatrix).toHaveLength(86); expect(unfiltered.coverageMatrix.filter((row) => row.unresolvedReason === "")).toHaveLength( - 89, + 85, ); expect( Object.values(unfiltered.catalogueMatrices) diff --git a/test/e2e/support/workflow-plan.test.ts b/test/e2e/support/workflow-plan.test.ts index d25b31a80d5..408de549752 100644 --- a/test/e2e/support/workflow-plan.test.ts +++ b/test/e2e/support/workflow-plan.test.ts @@ -89,7 +89,7 @@ describe("E2E workflow plan", () => { }), ]); expect(plan.hermesSelected).toBe(true); - expect(plan.coverageMatrix).toHaveLength(90); + expect(plan.coverageMatrix).toHaveLength(86); expect(selectedWorkflowJobs(plan)).toEqual([ "catalogue-brave-nvidia-inference", "catalogue-github-read", diff --git a/test/installer-integration/install-openshell-version-check.test.ts b/test/installer-integration/install-openshell-version-check.test.ts index 6eef8f45aae..6f3223d1efb 100644 --- a/test/installer-integration/install-openshell-version-check.test.ts +++ b/test/installer-integration/install-openshell-version-check.test.ts @@ -146,7 +146,7 @@ exit 1`, path.join(fakeBin, "curl"), options.homebrewFormulaDownload ? `#!/usr/bin/env bash -out="" +printf '%s\n' "$*" >&2; out="" while [ "$#" -gt 0 ]; do if [ "$1" = "-o" ]; then shift; out="$1"; fi shift || true @@ -402,17 +402,20 @@ describe("install-openshell.sh version check", { timeout: 15_000 }, () => { } }); - it("triggers reinstall when the required OpenShell is missing Docker-driver binaries", () => { + it("requests the Darwin Arm64 gateway archive instead of the VM-driver archive when driver binaries are missing", () => { const result = runWithInstalledVersion( REQUIRED_OPENSHELL_VERSION, {}, - { driverBins: false, os: "Linux" }, - ); - expect(result.status).not.toBe(0); - expect(result.stdout).toMatch(/missing Docker-driver binaries/); - expect(result.stdout).toContain( - `Installing OpenShell from release 'v${REQUIRED_OPENSHELL_VERSION}'`, + { + arch: "arm64", + driverBins: false, + homebrewAvailable: false, + homebrewFormulaDownload: true, + os: "Darwin", + }, ); + expect(result.stderr).toContain("openshell-gateway-aarch64-apple-darwin.tar.gz"); + expect(result.stderr).not.toContain("openshell-driver-vm-aarch64-apple-darwin.tar.gz"); }); it("fails closed when the required OpenShell lacks required messaging rewrite support", () => { diff --git a/tools/e2e/openshell-gateway-upgrade-fixture.mts b/tools/e2e/openshell-gateway-upgrade-fixture.mts new file mode 100644 index 00000000000..a1b4d2c05d1 --- /dev/null +++ b/tools/e2e/openshell-gateway-upgrade-fixture.mts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const REVIEWED_GATEWAY_UPGRADE_FIXTURE = Object.freeze({ + expectedAdvisoryAuditCount: 1, + installerSha256: "00f24959e5ca68104fe91221c0a015dab6a4154618497fa36b969b661f418cc2", + nemoclawCommit: "1143aa5cce77f3bad1b3b5588bd7fddbe438237e", + nemoclawRef: "v0.0.89", + openclawVersion: "2026.6.10", + openClawArchive: Object.freeze({ + expectedIntegrity: + "sha512-LcooND2tBQw8A+kc1Ujltu3lg30bJ0w7XaeRy7eYzobb8BBdcW6DOGbwJL4vpj1vl9+gjRceOtlh5nh9OARcug==", + label: "historical fixture OpenClaw 2026.6.10", + packageSpec: "openclaw@2026.6.10", + tarballUrl: "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.10.tgz", + }), + openShellVersion: "0.0.85", + sandboxBaseImageRef: + "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:3265d482f67c9d81ee3a59b0bbad5eb5ea6c705fea81ece8ae888ed12794f7f1", +} as const); diff --git a/tools/e2e/target-catalogue.mts b/tools/e2e/target-catalogue.mts index e094a3980a6..73fe31fde89 100644 --- a/tools/e2e/target-catalogue.mts +++ b/tools/e2e/target-catalogue.mts @@ -22,6 +22,7 @@ import { ONBOARD_RESUME_TARGET_TIMEOUT_MINUTES, ONBOARD_SINGLE_FINAL_HANDOFF_TARGET_TIMEOUT_MINUTES, } from "./onboard-timeout-contract.mts"; +import { REVIEWED_GATEWAY_UPGRADE_FIXTURE } from "./openshell-gateway-upgrade-fixture.mts"; import { normalizeE2eSelectorId } from "./selector-aliases.mts"; export const E2E_EXECUTION_PROFILES = [ @@ -324,122 +325,37 @@ function commonEgressTarget(options: { }); } -interface GatewayUpgradeTargetOptions { - commit: string; - currentOpenClawVersion?: string; - displayName: string; - installerSha256: string; - nemoclawRef: string; - openClawVersion: string; - openShellVersion: string; - runner?: string; - sandboxBaseImageRef: string; - shard: string; - stateUpgrade?: boolean; -} - -function gatewayUpgradeTarget(options: GatewayUpgradeTargetOptions): E2eCatalogueTarget { - return dockerOnlyTarget(`openshell-gateway-upgrade-${options.shard}`, { - targetId: "openshell-gateway-upgrade", - displayName: options.displayName, - agentRuntime: "openclaw", - environmentOrInferenceEndpoint: - options.runner === "ubuntu-24.04-arm" - ? "Arm64 Ubuntu; GitHub release artifacts; no inference endpoint" - : "x86-64 Ubuntu; GitHub release artifacts; no inference endpoint", - profile: "github-read", - runner: options.runner ?? "ubuntu-latest", - testFile: "test/e2e/live/openshell-gateway-upgrade.test.ts", - timeoutMinutes: 70, - installMode: "none", - restoreCli: true, - exposeCliBin: true, - shard: options.shard, - owningPaths: [ - "test/e2e/live/openshell-gateway-upgrade-helpers.ts", - "test/e2e/live/openshell-gateway-upgrade-old-installer.ts", - ], - environment: { - ...nonInteractive, - NEMOCLAW_GATEWAY_UPGRADE_SURVIVOR_NAME: "e2e-gw-survivor", - NEMOCLAW_OLD_NEMOCLAW_REF: options.nemoclawRef, - NEMOCLAW_OLD_NEMOCLAW_COMMIT: options.commit, - NEMOCLAW_OLD_INSTALLER_SHA256: options.installerSha256, - NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF: options.sandboxBaseImageRef, - NEMOCLAW_OLD_OPENSHELL_VERSION: options.openShellVersion, - NEMOCLAW_OLD_OPENCLAW_VERSION: options.openClawVersion, - NEMOCLAW_CURRENT_OPENCLAW_VERSION: options.currentOpenClawVersion ?? "", - NEMOCLAW_OPENCLAW_STATE_UPGRADE_PROOF: options.stateUpgrade ? "1" : "", - OPENSHELL_GATEWAY: "nemoclaw", - }, - }); -} - -const GATEWAY_UPGRADE_FIXTURES = [ - { - displayName: "Upgrade: preserves v0.0.36 state on x86-64", - shard: "v0-0-36-x86-64", - nemoclawRef: "v0.0.36", - commit: "3351fbdd4eb7d9b80ec471545083956327da2b10", - installerSha256: "0c42400a0d3867739f1d75d612e069967be4506e169974bbbebf14b7af39144f", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6", - openShellVersion: "0.0.36", - openClawVersion: "2026.4.24", - }, - { - displayName: "Upgrade: preserves v0.0.55 state on x86-64", - shard: "v0-0-55-x86-64", - nemoclawRef: "v0.0.55", - commit: "95d483fe2b6569d68e59493c60f19df09a068e8f", - installerSha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108", - openShellVersion: "0.0.44", - openClawVersion: "2026.5.22", - }, - { - displayName: "Upgrade: preserves v0.0.55 state on Arm64", - runner: "ubuntu-24.04-arm", - shard: "v0-0-55-aarch64", - nemoclawRef: "v0.0.55", - commit: "95d483fe2b6569d68e59493c60f19df09a068e8f", - installerSha256: "ff8cf448e4d17b00421545a1f333262b615b1b0aa236d0cc5aeaf4e2cae2d897", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:10433a8cd2f2b809dd0fdf983514679e04c0f8aa1ff5bbff675029046033b108", - openShellVersion: "0.0.44", - openClawVersion: "2026.5.22", +const GATEWAY_UPGRADE_TARGET = dockerOnlyTarget("openshell-gateway-upgrade-v0-0-89-x86-64", { + targetId: "openshell-gateway-upgrade", + displayName: `Upgrade: preserves a ${REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef} sandbox on x86-64`, + agentRuntime: "openclaw", + environmentOrInferenceEndpoint: + "x86-64 Ubuntu; GitHub release artifacts; host-local compatible inference endpoint", + profile: "github-read", + runner: "ubuntu-latest", + testFile: "test/e2e/live/openshell-gateway-upgrade.test.ts", + timeoutMinutes: 70, + installMode: "none", + restoreCli: true, + exposeCliBin: true, + shard: "v0-0-89-x86-64", + owningPaths: [ + "tools/e2e/openshell-gateway-upgrade-fixture.mts", + "test/e2e/live/openshell-gateway-upgrade-helpers.ts", + "test/e2e/live/openshell-gateway-upgrade-old-installer.ts", + ], + environment: { + ...nonInteractive, + NEMOCLAW_GATEWAY_UPGRADE_SURVIVOR_NAME: "e2e-gw-survivor", + NEMOCLAW_OLD_NEMOCLAW_REF: REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawRef, + NEMOCLAW_OLD_NEMOCLAW_COMMIT: REVIEWED_GATEWAY_UPGRADE_FIXTURE.nemoclawCommit, + NEMOCLAW_OLD_INSTALLER_SHA256: REVIEWED_GATEWAY_UPGRADE_FIXTURE.installerSha256, + NEMOCLAW_OLD_SANDBOX_BASE_IMAGE_REF: REVIEWED_GATEWAY_UPGRADE_FIXTURE.sandboxBaseImageRef, + NEMOCLAW_OLD_OPENSHELL_VERSION: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openShellVersion, + NEMOCLAW_OLD_OPENCLAW_VERSION: REVIEWED_GATEWAY_UPGRADE_FIXTURE.openclawVersion, + OPENSHELL_GATEWAY: "nemoclaw", }, - { - displayName: "Upgrade: preserves v0.0.74 state on x86-64", - shard: "v0-0-74-x86-64", - nemoclawRef: "v0.0.74", - commit: "3a05b54e8ec3e1d5550ec5c728de54af872bffe3", - installerSha256: "a0cd3feca488d247e53d59d7d8246d2b86e75e95acb5e7d78504b3c0c60fd7db", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:104151ffadc2ff0b6c815e3c95c2783ced61aee0d0f83fc327cc02be9b7e14e6", - openShellVersion: "0.0.72", - openClawVersion: "2026.5.27", - }, - { - displayName: "Upgrade: migrates v0.0.89 state on x86-64", - shard: "v0-0-89-x86-64", - nemoclawRef: "v0.0.89", - commit: "1143aa5cce77f3bad1b3b5588bd7fddbe438237e", - installerSha256: "00f24959e5ca68104fe91221c0a015dab6a4154618497fa36b969b661f418cc2", - sandboxBaseImageRef: - "ghcr.io/nvidia/nemoclaw/sandbox-base@sha256:3265d482f67c9d81ee3a59b0bbad5eb5ea6c705fea81ece8ae888ed12794f7f1", - openShellVersion: "0.0.85", - openClawVersion: "2026.6.10", - currentOpenClawVersion: "2026.7.1", - stateUpgrade: true, - }, -] as const satisfies readonly GatewayUpgradeTargetOptions[]; - -const GATEWAY_UPGRADE_TARGETS = GATEWAY_UPGRADE_FIXTURES.map(gatewayUpgradeTarget); -const GATEWAY_UPGRADE_TARGET_BY_ID = new Map( - GATEWAY_UPGRADE_TARGETS.map((entry) => [entry.id, entry]), -); +}); export const E2E_CATALOGUE_EXCLUSION_REASONS = { "issue-4434-tui-unreachable-inference": @@ -1228,10 +1144,9 @@ export const E2E_TARGET_CATALOGUE: readonly E2eCatalogueTarget[] = [ OPENSHELL_GATEWAY: "nemoclaw", }, }), - ...GATEWAY_UPGRADE_TARGETS, + GATEWAY_UPGRADE_TARGET, dockerOnlyTarget("shields-retirement-upgrade", { - displayName: - "Upgrade: migrates a v0.0.115 Shields sandbox to the candidate image", + displayName: "Upgrade: migrates a v0.0.115 Shields sandbox to the candidate image", agentRuntime: "openclaw", environmentOrInferenceEndpoint: "x86-64 Ubuntu; pinned v0.0.115 install and candidate managed image; local compatible endpoint", @@ -1254,8 +1169,7 @@ export const E2E_TARGET_CATALOGUE: readonly E2eCatalogueTarget[] = [ ...nonInteractive, NEMOCLAW_AGENT: "openclaw", NEMOCLAW_OLD_NEMOCLAW_REF: "v0.0.115", - NEMOCLAW_OLD_NEMOCLAW_TAG_OBJECT: - "7503e700808655df1303ddc51888bb596c9afa34", + NEMOCLAW_OLD_NEMOCLAW_TAG_OBJECT: "7503e700808655df1303ddc51888bb596c9afa34", NEMOCLAW_OLD_NEMOCLAW_COMMIT: "324a886fd05b01f6756bae0371ea503c651fbd11", NEMOCLAW_OLD_INSTALLER_SHA256: "0ed77ba8cf176641bd3b22cfd89b4977b3d9a6f47b76da8b03bf4091a20d1251", @@ -1724,8 +1638,10 @@ export function validateE2eTargetCatalogue( entry.targetId === "openshell-gateway-upgrade" || entry.id.startsWith("openshell-gateway-upgrade-") ) { - const expected = GATEWAY_UPGRADE_TARGET_BY_ID.get(entry.id); - if (!expected || !isDeepStrictEqual(entry, expected)) { + if ( + entry.id !== GATEWAY_UPGRADE_TARGET.id || + !isDeepStrictEqual(entry, GATEWAY_UPGRADE_TARGET) + ) { throw new Error( `E2E target ${entry.id} must match the exact reviewed gateway-upgrade fixture`, );