Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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" \
Expand Down
5 changes: 3 additions & 2 deletions ci/protected-managed-image-runtime-activation-v1.json
Original file line number Diff line number Diff line change
@@ -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"
}
21 changes: 17 additions & 4 deletions scripts/checks/build-protected-managed-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
set -euo pipefail

usage() {
echo "usage: $0 --output <json> --revision <sha> --cohort <id> --platform <linux/amd64|linux/arm64> --openclaw-base <exact-ref> --hermes-base <exact-ref> --dcode-base <exact-ref> [--source-root <absolute-dir>] [--cache-to <absolute-dir>] [--cache-from <absolute-dir> --audit-evidence-from <absolute-dir>]" >&2
echo "usage: $0 --output <json> --revision <sha> --cohort <id> --platform <linux/amd64|linux/arm64> --openclaw-base <exact-ref> --hermes-base <exact-ref> --dcode-base <exact-ref> [--runtime-user <root|sandbox>] [--source-root <absolute-dir>] [--cache-to <absolute-dir>] [--cache-from <absolute-dir> --audit-evidence-from <absolute-dir>]" >&2
exit 2
}

Expand All @@ -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)
Expand All @@ -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"
Expand Down Expand Up @@ -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" ;;
Expand Down Expand Up @@ -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
Expand All @@ -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[@]}"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).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 = {
Expand Down
74 changes: 69 additions & 5 deletions test/platform/images/protected-managed-image-build-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
`,
Expand All @@ -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
Expand Down Expand Up @@ -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",
[
Expand Down Expand Up @@ -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,
Expand All @@ -291,6 +305,7 @@ beforeEach(() => {
registryLog = path.join(testRoot, "registry.log");
registryStatus = "404";
teeFailureMode = "";
imageUser = "root";
mkdirSync(stubBin);
writeExecutable(
"docker",
Expand Down Expand Up @@ -338,6 +353,55 @@ 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");
imageUser = "sandbox";
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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Exercise rejection of a mismatched final image user. stubBuildInvocation() returns [] for docker image inspect, while its final jq -e --arg agent ... branch returns success without consuming Config.User, so these new root/sandbox cases prove only the build argument. If v2 selects sandbox but a Dockerfile or validation regression leaves Config.User as root, this suite remains green and the protected E2E can accept a root-running image. Make the fixture expose the inspected user, assert exact root and sandbox success, and add a selected/actual mismatch that must fail.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 632e6ea. The Docker stub now returns a complete inspected-image record with the exact Config.User and contract labels, and the final jq branch delegates to the real jq binary so it executes the production predicate against that record. The root and sandbox success paths provide their exact expected users, and a new sandbox-selected/root-inspected case fails on the first image as required. Verification: build-script 35/35 locally; adjacent protected workflow + build-script 79/79 on Brev; all pre-commit and pre-push gates passed.

});

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) => {
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();

Expand All @@ -347,12 +411,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",
Expand All @@ -361,7 +425,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",
);
});

Expand Down
12 changes: 12 additions & 0 deletions tools/e2e/managed-image-protected-runtime-workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"',
Expand All @@ -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");
Expand Down
Loading