From 74334541fbbf245273f1ee56060334847545ff31 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Fri, 11 Sep 2026 09:02:08 -0700 Subject: [PATCH 1/2] fix(e2e): admit sandbox runtime transition Signed-off-by: Prekshi Vyas --- .github/workflows/e2e.yaml | 35 ++++++++++---- ...d-managed-image-runtime-activation-v1.json | 5 +- .../checks/build-protected-managed-images.sh | 21 +++++++-- ...d-image-protected-runtime-workflow.test.ts | 47 ++++++++++++++++++- ...otected-managed-image-build-script.test.ts | 41 ++++++++++++++-- ...ge-protected-runtime-workflow-boundary.mts | 12 +++++ 6 files changed, 142 insertions(+), 19 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c7ace9d69fb..3d192b75540 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -4788,7 +4788,8 @@ jobs: - name: Prepare E2E workspace uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75 - - name: Validate protected runtime activation contract + - id: runtime-contract + name: Validate protected runtime activation contract env: CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} shell: bash @@ -4804,17 +4805,31 @@ jobs: echo "::error::Protected managed-image runtime activation contract is absent" >&2 exit 1 } - jq -e ' - (keys | sort) == ["agents", "contractVersion", "jobId", "platform", "providers"] and - .contractVersion == 1 and - .jobId == "managed-image-protected-runtime" and - .agents == ["openclaw", "hermes", "langchain-deepagents-code"] and - .platform == "linux/amd64" and - .providers == ["ollama", "nim", "vllm"] - ' "$activation" >/dev/null || { + runtime_user="$( + jq -er ' + ( + .jobId == "managed-image-protected-runtime" and + .agents == ["openclaw", "hermes", "langchain-deepagents-code"] and + .platform == "linux/amd64" and + .providers == ["ollama", "nim", "vllm"] + ) as $common | + if $common and + (keys | sort) == ["agents", "contractVersion", "jobId", "platform", "providers"] and + .contractVersion == 1 + then "root" + elif $common and + (keys | sort) == ["agents", "contractVersion", "jobId", "platform", "providers", "runtimeUser"] and + .contractVersion == 2 and + .runtimeUser == "sandbox" + then "sandbox" + else error("invalid protected runtime activation contract") + end + ' "$activation" + )" || { echo "::error::Protected managed-image runtime activation contract is invalid" >&2 exit 1 } + printf 'runtime_user=%s\n' "$runtime_user" >> "$GITHUB_OUTPUT" install -d -m 0700 "$E2E_ARTIFACT_DIR" - id: runtime-hermes-base @@ -4916,6 +4931,7 @@ jobs: BASE_HERMES: ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@${{ steps.runtime-hermes-base.outputs.digest }} BASE_OPENCLAW: ${{ steps.runtime-bases.outputs.openclaw }} CHECKOUT_SHA: ${{ inputs.checkout_sha || github.sha }} + RUNTIME_USER: ${{ steps.runtime-contract.outputs.runtime_user }} shell: bash run: | set -euo pipefail @@ -4924,6 +4940,7 @@ jobs: --revision "$CHECKOUT_SHA" \ --cohort "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT" \ --platform linux/amd64 \ + --runtime-user "$RUNTIME_USER" \ --source-root "$GITHUB_WORKSPACE/.candidate-runtime" \ --cache-from "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE" \ --audit-evidence-from "$GITHUB_WORKSPACE/.candidate-runtime/artifacts/reviewed-npm-audit" \ diff --git a/ci/protected-managed-image-runtime-activation-v1.json b/ci/protected-managed-image-runtime-activation-v1.json index 9332f48dcc3..9990e1b8749 100644 --- a/ci/protected-managed-image-runtime-activation-v1.json +++ b/ci/protected-managed-image-runtime-activation-v1.json @@ -1,7 +1,8 @@ { - "contractVersion": 1, + "contractVersion": 2, "jobId": "managed-image-protected-runtime", "platform": "linux/amd64", "providers": ["ollama", "nim", "vllm"], - "agents": ["openclaw", "hermes", "langchain-deepagents-code"] + "agents": ["openclaw", "hermes", "langchain-deepagents-code"], + "runtimeUser": "sandbox" } diff --git a/scripts/checks/build-protected-managed-images.sh b/scripts/checks/build-protected-managed-images.sh index d1b0b0fd9f4..89be5293d64 100755 --- a/scripts/checks/build-protected-managed-images.sh +++ b/scripts/checks/build-protected-managed-images.sh @@ -5,7 +5,7 @@ set -euo pipefail usage() { - echo "usage: $0 --output --revision --cohort --platform --openclaw-base --hermes-base --dcode-base [--source-root ] [--cache-to ] [--cache-from --audit-evidence-from ]" >&2 + echo "usage: $0 --output --revision --cohort --platform --openclaw-base --hermes-base --dcode-base [--runtime-user ] [--source-root ] [--cache-to ] [--cache-from --audit-evidence-from ]" >&2 exit 2 } @@ -20,6 +20,7 @@ source_root="$PWD" cache_to="" cache_from="" audit_evidence_from="" +runtime_user="root" while (($# > 0)); do case "$1" in --audit-evidence-from) @@ -42,6 +43,11 @@ while (($# > 0)); do revision="$2" shift 2 ;; + --runtime-user) + (($# >= 2)) || usage + runtime_user="$2" + shift 2 + ;; --cohort) (($# >= 2)) || usage cohort="$2" @@ -87,6 +93,7 @@ done [[ "$revision" =~ ^[a-f0-9]{40}$ ]] || usage [[ "$cohort" =~ ^protected-[1-9][0-9]{0,19}-[1-9][0-9]{0,9}$ ]] || usage [[ "$platform" == "linux/amd64" || "$platform" == "linux/arm64" ]] || usage +[[ "$runtime_user" == "root" || "$runtime_user" == "sandbox" ]] || usage case "$platform" in linux/amd64) npm_target_cpu="x64" ;; linux/arm64) npm_target_cpu="arm64" ;; @@ -449,7 +456,7 @@ build_agent() { -f "$dockerfile_path" \ --build-arg "BASE_IMAGE=${base_reference}" \ --build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" \ - --build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox" \ + --build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=${runtime_user}" \ --build-arg "TARGETARCH=${target_arch}" local -a build_command=(docker buildx build @@ -474,7 +481,7 @@ build_agent() { # Buildx target explicitly so that default cannot override linux/arm64. --build-arg "TARGETARCH=${platform#linux/}" --build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" - --build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox" + --build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=${runtime_user}" --build-arg "TARGETARCH=${target_arch}" "$source_root") run_build_with_retry "$agent" "$image_repository" "${build_command[@]}" @@ -507,10 +514,16 @@ build_agent() { --arg cohort "$cohort" \ --arg image_id "$image_id" \ --arg platform "$platform" \ + --arg runtime_user "$runtime_user" \ --arg revision "$revision" ' length == 1 and .[0].Id == $image_id and - .[0].Config.User == "sandbox" and + ((.[0].Config.User // "") as $user | + if $runtime_user == "root" then + $user == "" or $user == "root" or $user == "0" + else + $user == "sandbox" + end) and .[0].Config.Labels["io.nvidia.nemoclaw.agent"] == $agent and .[0].Config.Labels["io.nvidia.nemoclaw.managed-image.contract"] == "1" and .[0].Config.Labels["io.nvidia.nemoclaw.managed-image.platform"] == $platform and diff --git a/test/e2e/support/managed-image-protected-runtime-workflow.test.ts b/test/e2e/support/managed-image-protected-runtime-workflow.test.ts index 11a577ff0ca..eb85e057b6b 100644 --- a/test/e2e/support/managed-image-protected-runtime-workflow.test.ts +++ b/test/e2e/support/managed-image-protected-runtime-workflow.test.ts @@ -253,13 +253,58 @@ describe("protected managed-image runtime workflow", () => { expect(activation).toEqual({ agents: ["openclaw", "hermes", "langchain-deepagents-code"], - contractVersion: 1, + contractVersion: 2, jobId: "managed-image-protected-runtime", platform: "linux/amd64", providers: ["ollama", "nim", "vllm"], + runtimeUser: "sandbox", }); }); + it("rejects runtime contract selection drift", () => { + const value = workflow(); + const activation = namedJobStep( + value, + "managed-image-protected-runtime", + "Validate protected runtime activation contract", + ); + activation.run = String(activation.run).replace( + ".contractVersion == 2", + ".contractVersion == 3", + ); + + expect(validateManagedImageProtectedRuntimeWorkflow(value)).toContain( + "managed-image-protected-runtime step 'Validate protected runtime activation contract' must include .contractVersion == 2", + ); + }); + + it("rejects an unbound runtime contract output", () => { + const value = workflow(); + namedJobStep( + value, + "managed-image-protected-runtime", + "Validate protected runtime activation contract", + ).id = "changed-runtime-contract"; + + expect(validateManagedImageProtectedRuntimeWorkflow(value)).toContain( + "managed-image-protected-runtime activation step must expose the reviewed runtime contract", + ); + }); + + it("rejects a protected build that drops the selected runtime user", () => { + const value = workflow(); + const build = namedJobStep( + value, + "managed-image-protected-runtime", + "Build exact all-agent protected runtime images", + ); + (build.env as Record).RUNTIME_USER = "root"; + + expect(validateManagedImageProtectedRuntimeWorkflow(value)).toContain( + "managed-image-protected-runtime protected runtime build bases must bind RUNTIME_USER to ${{ steps.runtime-contract.outputs.runtime_user }}", + ); + }); + it("rejects job-scoped NGC credentials", () => { const value = workflow(); runtimeJob(value).env = { diff --git a/test/platform/images/protected-managed-image-build-script.test.ts b/test/platform/images/protected-managed-image-build-script.test.ts index 3543bd4699b..7344692ff9c 100644 --- a/test/platform/images/protected-managed-image-build-script.test.ts +++ b/test/platform/images/protected-managed-image-build-script.test.ts @@ -338,6 +338,41 @@ describe("protected managed-image source-root boundary", () => { }); describe("protected managed-image build-cache boundary", () => { + it("keeps the legacy root runtime contract unless a reviewed transition selects sandbox", () => { + stubBuildInvocation(); + + const legacy = runBuild(REPO_ROOT); + + expect(legacy.status, legacy.stderr).toBe(0); + expect(recordedBuildInvocations()).toHaveLength(3); + expect( + recordedBuildInvocations().every((invocation) => + invocation.includes("--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root"), + ), + ).toBe(true); + + writeFileSync(dockerLog, "", "utf8"); + const transitioned = runBuild(REPO_ROOT, ["--runtime-user", "sandbox"]); + + expect(transitioned.status, transitioned.stderr).toBe(0); + expect(recordedBuildInvocations()).toHaveLength(3); + expect( + recordedBuildInvocations().every((invocation) => + invocation.includes("--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox"), + ), + ).toBe(true); + }); + + it.each(["0", "Root", "sandbox\nroot"])( + "rejects unreviewed protected runtime user %j before invoking Docker", + (runtimeUser) => { + const result = runBuild(REPO_ROOT, ["--runtime-user", runtimeUser]); + + expect(result.status, result.stderr).toBe(2); + expect(existsSync(dockerLog)).toBe(false); + }, + ); + it("passes the selected Buildx architecture explicitly to every Dockerfile", () => { stubBuildInvocation(); @@ -347,12 +382,12 @@ describe("protected managed-image build-cache boundary", () => { expect(recordedBuildInvocation("openclaw")).toContain("--platform linux/arm64"); expect(recordedBuildInvocation("openclaw")).toContain("--build-arg TARGETARCH=arm64"); expect(recordedBuildInvocation("openclaw")).toContain( - "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox", + "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root", ); expect(recordedBuildInvocation("hermes")).toContain("--platform linux/arm64"); expect(recordedBuildInvocation("hermes")).toContain("--build-arg TARGETARCH=arm64"); expect(recordedBuildInvocation("hermes")).toContain( - "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox", + "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root", ); expect(recordedBuildInvocation("langchain-deepagents-code")).toContain( "--platform linux/arm64", @@ -361,7 +396,7 @@ describe("protected managed-image build-cache boundary", () => { "--build-arg TARGETARCH=arm64", ); expect(recordedBuildInvocation("langchain-deepagents-code")).toContain( - "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox", + "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root", ); }); diff --git a/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts b/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts index e22b77bb6a5..866a32b361b 100644 --- a/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts +++ b/tools/e2e/managed-image-protected-runtime-workflow-boundary.mts @@ -261,15 +261,25 @@ export function validateManagedImageProtectedRuntimeWorkflow(workflow: WorkflowR workflowSteps, "Validate protected runtime activation contract", ); + if (activation?.id !== "runtime-contract") { + errors.push(`${JOB_ID} activation step must expose the reviewed runtime contract`); + } requireFragments(errors, activation, [ 'candidate_root=".candidate-runtime"', `activation="$candidate_root/${ACTIVATION_PATH}"`, '[[ "$(git -C "$candidate_root" rev-parse --verify HEAD)" == "$CHECKOUT_SHA" ]]', '[[ -f "$activation" && ! -L "$activation" ]]', '(keys | sort) == ["agents", "contractVersion", "jobId", "platform", "providers"]', + '(keys | sort) == ["agents", "contractVersion", "jobId", "platform", "providers", "runtimeUser"]', + ".contractVersion == 1", + ".contractVersion == 2", + '.runtimeUser == "sandbox"', + 'then "root"', + 'then "sandbox"', '.agents == ["openclaw", "hermes", "langchain-deepagents-code"]', '.platform == "linux/amd64"', '.providers == ["ollama", "nim", "vllm"]', + 'printf \'runtime_user=%s\\n\' "$runtime_user" >> "$GITHUB_OUTPUT"', ]); const hermesBase = requireStep( @@ -329,6 +339,7 @@ export function validateManagedImageProtectedRuntimeWorkflow(workflow: WorkflowR '--revision "$CHECKOUT_SHA"', '--cohort "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_COHORT"', "--platform linux/amd64", + '--runtime-user "$RUNTIME_USER"', '--source-root "$GITHUB_WORKSPACE/.candidate-runtime"', '--cache-from "$NEMOCLAW_PROTECTED_MANAGED_IMAGE_BUILD_CACHE"', '--audit-evidence-from "$GITHUB_WORKSPACE/.candidate-runtime/artifacts/reviewed-npm-audit"', @@ -344,6 +355,7 @@ export function validateManagedImageProtectedRuntimeWorkflow(workflow: WorkflowR requireValues(errors, `${JOB_ID} protected runtime build bases`, record(build?.env), { BASE_HERMES: "ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@${{ steps.runtime-hermes-base.outputs.digest }}", + RUNTIME_USER: "${{ steps.runtime-contract.outputs.runtime_user }}", }); const install = requireStep(errors, workflowSteps, "Install OpenShell CLI"); From 632e6ead98c5a6c09e995b5210ce0927b61cf4b1 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Fri, 11 Sep 2026 09:57:40 -0700 Subject: [PATCH 2/2] test(images): validate protected runtime user Signed-off-by: Prekshi Vyas --- ...otected-managed-image-build-script.test.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/test/platform/images/protected-managed-image-build-script.test.ts b/test/platform/images/protected-managed-image-build-script.test.ts index 7344692ff9c..f1908755e55 100644 --- a/test/platform/images/protected-managed-image-build-script.test.ts +++ b/test/platform/images/protected-managed-image-build-script.test.ts @@ -36,6 +36,7 @@ let registryCurlExit = ""; let registryLog = ""; let registryStatus = ""; let teeFailureMode = ""; +let imageUser = ""; function writeExecutable(name: string, source: string): void { const target = path.join(stubBin, name); @@ -79,7 +80,16 @@ case "$*" in esac ;; "pull "*) ;; - "image inspect "*) printf '[]\n' ;; + "image inspect "*) + case "$*" in + *"/openclaw@"*) agent=openclaw ;; + *"/hermes@"*) agent=hermes ;; + *"/langchain-deepagents-code@"*) agent=langchain-deepagents-code ;; + *) exit 93 ;; + esac + printf '[{"Id":"sha256:${DIGEST}","Config":{"User":"%s","Labels":{"io.nvidia.nemoclaw.agent":"%s","io.nvidia.nemoclaw.managed-image.contract":"1","io.nvidia.nemoclaw.managed-image.platform":"%s","io.nvidia.nemoclaw.managed-image.startup-profile":"1","io.nvidia.nemoclaw.managed-image.capabilities":"1","io.nvidia.nemoclaw.managed-image.cohort":"protected-1-1","org.opencontainers.image.revision":"${REVISION}"}}}]\n' \ + "$NEMOCLAW_TEST_IMAGE_USER" "$agent" "$NEMOCLAW_TEST_IMAGE_PLATFORM" + ;; *) exit 91 ;; esac `, @@ -90,7 +100,7 @@ esac case "$*" in *containerimage.digest*) printf 'sha256:${DIGEST}\\n' ;; *"if length == 1 then .[0].Id"*) printf 'sha256:${DIGEST}\\n' ;; - *"--arg agent "*) printf '{}\\n' ;; + *"--arg agent "*) PATH="$NEMOCLAW_TEST_REAL_PATH" command jq "$@" ;; *"-se "*) printf '[]\\n' ;; *) ;; esac @@ -235,6 +245,8 @@ function recordedBuildInvocation(agent: string): string { function runBuild(sourceRoot: string, extraArgs: readonly string[] = [], platform = "linux/amd64") { const output = path.join(testRoot, "contracts.json"); + const platformOverride = extraArgs.findIndex((argument) => argument === "--platform"); + const effectivePlatform = platformOverride >= 0 ? extraArgs[platformOverride + 1] : platform; return spawnSync( "bash", [ @@ -265,6 +277,8 @@ function runBuild(sourceRoot: string, extraArgs: readonly string[] = [], platfor NEMOCLAW_TEST_DOCKER_BUILD_COUNT: dockerBuildCount, NEMOCLAW_TEST_DOCKER_BUILD_FAILURE_MODE: dockerBuildFailureMode, NEMOCLAW_TEST_DOCKER_LOG: dockerLog, + NEMOCLAW_TEST_IMAGE_PLATFORM: effectivePlatform, + NEMOCLAW_TEST_IMAGE_USER: imageUser, NEMOCLAW_TEST_REGISTRY_CURL_EXIT: registryCurlExit, NEMOCLAW_TEST_REGISTRY_LOG: registryLog, NEMOCLAW_TEST_REGISTRY_STATUS: registryStatus, @@ -291,6 +305,7 @@ beforeEach(() => { registryLog = path.join(testRoot, "registry.log"); registryStatus = "404"; teeFailureMode = ""; + imageUser = "root"; mkdirSync(stubBin); writeExecutable( "docker", @@ -352,6 +367,7 @@ describe("protected managed-image build-cache boundary", () => { ).toBe(true); writeFileSync(dockerLog, "", "utf8"); + imageUser = "sandbox"; const transitioned = runBuild(REPO_ROOT, ["--runtime-user", "sandbox"]); expect(transitioned.status, transitioned.stderr).toBe(0); @@ -363,6 +379,19 @@ describe("protected managed-image build-cache boundary", () => { ).toBe(true); }); + it("rejects an image whose final Config.User does not match the selected runtime user", () => { + stubBuildInvocation(); + imageUser = "root"; + + const result = runBuild(REPO_ROOT, ["--runtime-user", "sandbox"]); + + expect(result.status, result.stderr).toBe(1); + expect(recordedBuildInvocations()).toHaveLength(1); + expect(recordedBuildInvocation("openclaw")).toContain( + "--build-arg NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=sandbox", + ); + }); + it.each(["0", "Root", "sandbox\nroot"])( "rejects unreviewed protected runtime user %j before invoking Docker", (runtimeUser) => {