Skip to content

fix(portable): separate Podman activation readiness #2621

fix(portable): separate Podman activation readiness

fix(portable): separate Podman activation readiness #2621

Workflow file for this run

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Publish complete managed images from exact base-image contracts. Every
# agent/platform lane pushes and validates only an immutable digest. The
# aggregate publisher then proves that all three agents have both supported
# Linux architectures before it stages any cohort alias. Only after all staged
# aliases resolve to the exact validated manifests does the single OpenClaw
# cohort pointer move. Consumers use that pointer to discover the matching
# Hermes and Deep Agents Code cohort aliases, so a failed run cannot expose a
# mixed-agent or mixed-architecture cohort.
#
# The Pi candidate lane builds and validates its own per-architecture images and
# publishes exact candidate digests under a separate contract artifact name. It
# never contributes to the all-agent cohort, so Pi cannot reach onboarding agent
# selection before the activation change merges.
name: Images / Managed Images
on:
workflow_call:
pull_request:
paths:
- ".github/actions/ci-reviewed-npm-audit/**"
- ".github/workflows/managed-images.yaml"
- ".dockerignore"
- "Dockerfile"
- "agents/**"
- "ci/npm-audit-exceptions.json"
- "ci/reviewed-npm-audit.json"
- "nemoclaw/**"
- "nemoclaw-blueprint/**"
- "scripts/**"
- "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json"
- "src/lib/core/json-types.ts"
- "src/lib/core/ports.ts"
- "src/lib/messaging/**"
- "src/lib/onboard/**"
- "src/lib/security/credential-hash.ts"
- "src/lib/state/paths.ts"
- "src/lib/state/state-root.ts"
- "test/e2e/live/managed-image-activation-e2e*.ts"
- "src/lib/tool-disclosure.ts"
- "tools/mcp-tool-discovery-runtime/**"
- "tsconfig.runtime-preloads.json"
permissions:
contents: read
packages: write
concurrency:
group: managed-images-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
REGISTRY: ghcr.io
jobs:
pr-reviewed-npm-audit:
name: PR reviewed npm audit
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout commit under review
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
path: candidate
persist-credentials: false
- name: Checkout trusted reviewed npm audit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
path: .trusted-reviewed-npm-audit
persist-credentials: false
sparse-checkout: |
.github/actions/ci-reviewed-npm-audit
ci/npm-audit-exceptions.json
ci/reviewed-npm-audit.json
scripts/audit-reviewed-npm-graph.mts
scripts/lib/openclaw-npm-remediation.mts
scripts/lib/reviewed-npm-archive.mts
scripts/lib/reviewed-npm-audit.mts
sparse-checkout-cone-mode: false
- name: Verify exact audit source and target
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
[[ "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]]
[[ "$CANDIDATE_SHA" =~ ^[a-f0-9]{40}$ ]]
test "$(git -C .trusted-reviewed-npm-audit rev-parse --verify HEAD)" = "$BASE_SHA"
test "$(git -C candidate rev-parse --verify HEAD)" = "$CANDIDATE_SHA"
- name: Audit exact PR production npm graphs
uses: ./.trusted-reviewed-npm-audit/.github/actions/ci-reviewed-npm-audit
with:
target-root: ${{ github.workspace }}/candidate
report-dir: artifacts/reviewed-npm-audit
pr-staging-qa-deep-code:
name: Staging QA base permission regression (Deep Agents Code)
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
timeout-minutes: 90
permissions:
contents: read
env:
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
# Retains the reviewed discovery-permission repair and the current
# managed-image security inventory. The previous staging source pinned
# libssh2 nemoclaw1, which cannot satisfy the candidate's nemoclaw2 contract.
STAGING_QA_SOURCE_SHA: ce96811ddb418ad01c040521a1fe912b5bcb405e
STAGING_QA_BASE_IMAGE: nemoclaw-deepagents-code-base:staging-31396519688
STAGING_QA_FINAL_IMAGE: nemoclaw-managed-pr/langchain-deepagents-code-staging-qa
steps:
- name: Checkout latest PR commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
path: candidate
persist-credentials: false
# Rebuild a security-compatible base from the exact source that retains
# the reviewed discovery-permission repair.
- name: Checkout exact staging QA base source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.STAGING_QA_SOURCE_SHA }}
path: staging-qa-base-source
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.19.0
- name: Validate staging QA source
shell: bash
run: |
set -euo pipefail
source_root="$GITHUB_WORKSPACE/staging-qa-base-source"
actual_source_sha="$(git -C "$source_root" rev-parse HEAD)"
if [ "$actual_source_sha" != "$STAGING_QA_SOURCE_SHA" ]; then
echo "ERROR: staging QA base source does not match the recorded NemoClaw commit." >&2
exit 1
fi
printf '### Staging QA Deep Agents Code base\n\n'
printf 'NemoClaw source commit: %s\n' "$STAGING_QA_SOURCE_SHA"
{
printf '### Staging QA Deep Agents Code base\n\n'
printf 'NemoClaw source commit: %s\n' "$STAGING_QA_SOURCE_SHA"
} >> "$GITHUB_STEP_SUMMARY"
- name: Reproduce staging discovery permission drift
shell: bash
working-directory: candidate
run: |
set -euo pipefail
reviewed_root="tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery"
for artifact in \
BUNDLED_PACKAGES.json \
THIRD_PARTY_LICENSES.txt \
mcp-tool-discovery.bundle
do
artifact_path="${reviewed_root}/${artifact}"
if [ ! -f "$artifact_path" ] || [ -L "$artifact_path" ]; then
echo "ERROR: reviewed discovery permission fixture must be a regular non-symlink: ${artifact_path}" >&2
exit 1
fi
chmod 0664 "$artifact_path"
done
- name: Rebuild staging QA Deep Agents Code base from exact source
id: staging-base
shell: bash
env:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
source_root="$GITHUB_WORKSPACE/staging-qa-base-source"
docker build --progress=plain \
-f "$source_root/agents/langchain-deepagents-code/Dockerfile.base" \
-t "$STAGING_QA_BASE_IMAGE" \
"$source_root"
base_id="$(docker image inspect --format "{{.Id}}" "$STAGING_QA_BASE_IMAGE")"
if [[ ! "$base_id" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: rebuilt staging QA base did not resolve to an immutable local image ID." >&2
exit 1
fi
printf 'id=%s\n' "$base_id" >> "$GITHUB_OUTPUT"
printf '\nRebuilt local image: %s\n' "$base_id" >> "$GITHUB_STEP_SUMMARY"
- name: Build latest PR commit against reproduced staging QA base
id: staging-final
shell: bash
working-directory: candidate
env:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
final_reference="${STAGING_QA_FINAL_IMAGE}:${CANDIDATE_SHA}"
docker build --progress=plain \
-f agents/langchain-deepagents-code/Dockerfile \
--build-arg BASE_IMAGE="$STAGING_QA_BASE_IMAGE" \
--build-arg NEMOCLAW_MODEL=nvidia/nemotron-3-ultra-550b-a55b \
--build-arg NEMOCLAW_INFERENCE_PROVIDER_ID=inference \
--build-arg NEMOCLAW_UPSTREAM_PROVIDER=nvidia-nim \
--build-arg NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1 \
--build-arg NEMOCLAW_INFERENCE_API=openai-completions \
--build-arg NEMOCLAW_DCODE_AUTO_APPROVAL=thread-opt-in \
--build-arg NEMOCLAW_BUILD_ID="$STAGING_QA_SOURCE_SHA" \
--build-arg NEMOCLAW_DARWIN_VM_COMPAT=0 \
-t "$final_reference" \
.
final_id="$(docker image inspect --format "{{.Id}}" "$final_reference")"
if [[ ! "$final_id" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: staging QA final image did not resolve to an immutable local image ID." >&2
exit 1
fi
printf 'id=%s\n' "$final_id" >> "$GITHUB_OUTPUT"
- name: Validate staging QA final image contract
shell: bash
env:
BASE_ID: ${{ steps.staging-base.outputs.id }}
FINAL_ID: ${{ steps.staging-final.outputs.id }}
run: |
set -euo pipefail
base_layers="$(docker image inspect --format "{{json .RootFS.Layers}}" "$BASE_ID")"
final_layers="$(docker image inspect --format "{{json .RootFS.Layers}}" "$FINAL_ID")"
if ! jq -n -e \
--argjson base "$base_layers" \
--argjson final "$final_layers" '
($base | length) > 0 and
($final | length) >= ($base | length) and
all(range(0; $base | length); $final[.] == $base[.])
' >/dev/null; then
echo "ERROR: staging QA final image does not extend the rebuilt QA base." >&2
exit 1
fi
if ! actual_discovery_contract="$(
docker run --rm --platform linux/amd64 --entrypoint /bin/sh "$FINAL_ID" -c '
set -eu
discovery_runtime=/usr/local/lib/nemoclaw/mcp-tool-discovery-runtime
if ! entry_owner_unsafe="$(find -P "$discovery_runtime" ! -user root -print -quit)"; then
echo "ERROR: staging QA discovery entry ownership inspection failed." >&2
exit 1
fi
if [ -n "$entry_owner_unsafe" ]; then
echo "ERROR: staging QA discovery entry is not root-owned: $entry_owner_unsafe" >&2
exit 1
fi
if ! owner_or_write_unsafe="$(
find -L "$discovery_runtime" \
\( ! -user root -o -perm /022 \) -print -quit
)"; then
echo "ERROR: staging QA discovery tree inspection failed." >&2
exit 1
fi
if [ -n "$owner_or_write_unsafe" ]; then
echo "ERROR: staging QA discovery tree is not root-owned and read-only: $owner_or_write_unsafe" >&2
exit 1
fi
if ! mode_unsafe="$(
find -P "$discovery_runtime" \
\( \( -type d ! -perm 0555 \) -o \( -type f ! -perm 0444 \) \) \
-print -quit
)"; then
echo "ERROR: staging QA discovery mode inspection failed." >&2
exit 1
fi
if [ -n "$mode_unsafe" ]; then
echo "ERROR: staging QA discovery path has an unexpected mode: $mode_unsafe" >&2
exit 1
fi
if ! actual_discovery_contract="$(node "$discovery_runtime/mcp-tool-discovery.mjs")"; then
echo "ERROR: staging QA discovery bundle execution failed." >&2
exit 1
fi
printf "%s" "$actual_discovery_contract"
'
)"; then
echo "ERROR: staging QA final image discovery contract probe failed." >&2
exit 1
fi
if ! node -e '
const expected = {
protocol: 1,
ok: false,
detail: "tool discovery received invalid runtime arguments",
};
let result;
try {
result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
} catch {
process.exit(1);
}
const record =
result !== null && typeof result === "object" && !Array.isArray(result)
? result
: undefined;
if (
!record ||
record.protocol !== expected.protocol ||
record.ok !== expected.ok ||
record.detail !== expected.detail
) {
process.exit(1);
}
' <<< "$actual_discovery_contract" >/dev/null; then
echo "ERROR: staging QA discovery bundle contract mismatch." >&2
exit 1
fi
pr-build-and-entrypoint:
name: PR build and direct managed startup (${{ matrix.display_name }})
# External contributor PRs retain the local build/direct-startup lane after
# GitHub's maintainer workflow approval. Exact digest publication is limited
# to branches in NVIDIA/NemoClaw; fork jobs never log in or push.
needs: pr-reviewed-npm-audit
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 90
permissions:
contents: read
packages: write
env:
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
strategy:
fail-fast: false
matrix:
include:
- agent: openclaw
display_name: OpenClaw
dockerfile: Dockerfile
base_dockerfile: Dockerfile.base
base_alias: ghcr.io/nvidia/nemoclaw/sandbox-base:latest
base_repository: ghcr.io/nvidia/nemoclaw/sandbox-base
image: nemoclaw-managed-pr/openclaw
repository: ghcr.io/nvidia/nemoclaw/openclaw-sandbox
- agent: hermes
display_name: Hermes
dockerfile: agents/hermes/Dockerfile
base_dockerfile: agents/hermes/Dockerfile.base
base_alias: ghcr.io/nvidia/nemoclaw/hermes-sandbox-base:latest
base_repository: ghcr.io/nvidia/nemoclaw/hermes-sandbox-base
image: nemoclaw-managed-pr/hermes
repository: ghcr.io/nvidia/nemoclaw/hermes-sandbox
- agent: langchain-deepagents-code
display_name: Deep Agents Code
dockerfile: agents/langchain-deepagents-code/Dockerfile
base_dockerfile: agents/langchain-deepagents-code/Dockerfile.base
base_alias: ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest
base_repository: ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base
image: nemoclaw-managed-pr/langchain-deepagents-code
repository: ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Reproduce reviewed discovery permission drift
shell: bash
run: |
set -euo pipefail
reviewed_root="tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery"
for artifact in \
BUNDLED_PACKAGES.json \
THIRD_PARTY_LICENSES.txt \
mcp-tool-discovery.bundle
do
artifact_path="${reviewed_root}/${artifact}"
if [ ! -f "$artifact_path" ] || [ -L "$artifact_path" ]; then
echo "ERROR: reviewed discovery permission fixture must be a regular non-symlink: ${artifact_path}" >&2
exit 1
fi
chmod 0664 "$artifact_path"
done
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.19.0
- name: Resolve exact linux/amd64 PR base
id: base
shell: bash
env:
BASE_DOCKERFILE: ${{ matrix.base_dockerfile }}
BASE_ALIAS: ${{ matrix.base_alias }}
BASE_REPOSITORY: ${{ matrix.base_repository }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
DISPLAY_NAME: ${{ matrix.display_name }}
LOCAL_BASE_REFERENCE: nemoclaw-managed-pr/${{ matrix.agent }}-base:${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [[ ! "$BASE_SHA" =~ ^[0-9a-f]{40}$ || ! "$CANDIDATE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "ERROR: PR base resolution requires exact base and candidate commit SHAs." >&2
exit 1
fi
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
git fetch --no-tags --depth=1 origin "$BASE_SHA"
fi
diff_status=0
git diff --quiet "$BASE_SHA" "$CANDIDATE_SHA" -- "$BASE_DOCKERFILE" || diff_status=$?
if [ "$diff_status" -gt 1 ]; then
echo "ERROR: PR base Dockerfile comparison failed." >&2
exit "$diff_status"
fi
if [ "$diff_status" -eq 1 ]; then
echo "::notice::${DISPLAY_NAME} base Dockerfile changed; building the exact PR base locally"
local_base_archive="$RUNNER_TEMP/pr-base.docker.tar"
local_base_oci_archive="$RUNNER_TEMP/pr-base.oci.tar"
local_base_oci="$RUNNER_TEMP/pr-base.oci"
docker buildx build \
--platform linux/amd64 \
--provenance=false \
--sbom=false \
--file "$BASE_DOCKERFILE" \
--tag "$LOCAL_BASE_REFERENCE" \
--output "type=docker,dest=${local_base_archive}" \
--output "type=oci,dest=${local_base_oci_archive}" \
.
docker load --input "$local_base_archive"
mkdir -p "$local_base_oci"
tar -C "$local_base_oci" -xf "$local_base_oci_archive"
local_base_oci_digest="$(
jq -er '
.manifests
| if length == 1 then .[0].digest else error("not one image") end
' "$local_base_oci/index.json"
)"
if [[ ! "$local_base_oci_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: local PR base OCI layout returned an invalid digest." >&2
exit 1
fi
printf 'ref=%s\n' "$LOCAL_BASE_REFERENCE" >> "$GITHUB_OUTPUT"
printf 'local=true\n' >> "$GITHUB_OUTPUT"
printf 'oci=%s@%s\n' "$local_base_oci" "$local_base_oci_digest" >> "$GITHUB_OUTPUT"
printf '### %s PR base\n\nLocally built from `%s` at `%s`.\n' \
"$DISPLAY_NAME" "$BASE_DOCKERFILE" "$CANDIDATE_SHA" \
>> "$GITHUB_STEP_SUMMARY"
exit 0
fi
alias_raw="$RUNNER_TEMP/pr-base-alias.raw"
exact_raw="$RUNNER_TEMP/pr-base-exact.raw"
docker buildx imagetools inspect "$BASE_ALIAS" --raw > "$alias_raw"
if ! digest="$(
jq -er '
if (
.mediaType == "application/vnd.oci.image.index.v1+json" or
.mediaType == "application/vnd.docker.distribution.manifest.list.v2+json"
) then
[
.manifests[]
| select(
.platform.os == "linux" and
.platform.architecture == "amd64"
)
]
| if length == 1 then .[0].digest else error("not one linux/amd64 image") end
else
error("base alias is not a platform index")
end
' "$alias_raw"
)"; then
echo "ERROR: PR base alias does not contain exactly one linux/amd64 image." >&2
exit 1
fi
if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: PR base alias returned an invalid linux/amd64 digest." >&2
exit 1
fi
reference="${BASE_REPOSITORY}@${digest}"
docker buildx imagetools inspect "$reference" --raw > "$exact_raw"
actual="sha256:$(sha256sum "$exact_raw" | awk '{print $1}')"
if [ "$actual" != "$digest" ]; then
echo "ERROR: exact PR base bytes do not match the selected descriptor digest." >&2
exit 1
fi
printf 'ref=%s\n' "$reference" >> "$GITHUB_OUTPUT"
printf 'local=false\n' >> "$GITHUB_OUTPUT"
printf '### %s PR base\n\n`%s`\n' "$DISPLAY_NAME" "$reference" \
>> "$GITHUB_STEP_SUMMARY"
- name: Validate PR managed-image build args
shell: bash
env:
BASE_REFERENCE: ${{ steps.base.outputs.ref }}
DOCKERFILE: ${{ matrix.dockerfile }}
run: |
set -euo pipefail
scripts/check-production-build-args.sh \
-f "$DOCKERFILE" \
--build-arg "BASE_IMAGE=${BASE_REFERENCE}" \
--build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" \
--build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root"
- name: Build PR managed image from local base
if: steps.base.outputs.local == 'true'
shell: bash
env:
BASE_IMAGE: ${{ steps.base.outputs.ref }}
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
DOCKERFILE: ${{ matrix.dockerfile }}
IMAGE_REFERENCE: ${{ matrix.image }}:${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
# The base resolver loads a changed base into Docker's local image
# store. Keep this consumer on that same store: a Buildx container
# builder otherwise treats the local-only reference as Docker Hub.
docker build \
--platform linux/amd64 \
--file "$DOCKERFILE" \
--tag "$IMAGE_REFERENCE" \
--label "org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}" \
--label "org.opencontainers.image.revision=${CANDIDATE_SHA}" \
--label "io.nvidia.nemoclaw.agent=${{ matrix.agent }}" \
--label "io.nvidia.nemoclaw.managed-image.contract=1" \
--label "io.nvidia.nemoclaw.managed-image.platform=linux/amd64" \
--label "io.nvidia.nemoclaw.managed-image.startup-profile=1" \
--label "io.nvidia.nemoclaw.managed-image.capabilities=1" \
--label "io.nvidia.nemoclaw.managed-image.cohort=ghrun-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
--build-arg "BASE_IMAGE=${BASE_IMAGE}" \
--build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" \
--build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root" \
.
- name: Build PR managed image from registry base
if: steps.base.outputs.local != 'true'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
load: true
push: false
tags: ${{ matrix.image }}:${{ github.event.pull_request.head.sha }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.event.pull_request.head.sha }}
io.nvidia.nemoclaw.agent=${{ matrix.agent }}
io.nvidia.nemoclaw.managed-image.contract=1
io.nvidia.nemoclaw.managed-image.platform=linux/amd64
io.nvidia.nemoclaw.managed-image.startup-profile=1
io.nvidia.nemoclaw.managed-image.capabilities=1
io.nvidia.nemoclaw.managed-image.cohort=ghrun-${{ github.run_id }}-${{ github.run_attempt }}
build-args: |
BASE_IMAGE=${{ steps.base.outputs.ref }}
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1
NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root
cache-from: type=registry,ref=ghcr.io/nvidia/nemoclaw/${{ matrix.agent }}-sandbox:buildcache-linux-amd64
provenance: false
sbom: false
- name: Validate exact PR managed image contract
id: contract
shell: bash
env:
AGENT: ${{ matrix.agent }}
IMAGE_REFERENCE: ${{ matrix.image }}:${{ github.event.pull_request.head.sha }}
PLATFORM: linux/amd64
PUBLICATION_COHORT: ghrun-${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
image_json="$(docker image inspect "$IMAGE_REFERENCE")"
image_id="$(
jq -er '
if length == 1 and (.[0].Id | type) == "string"
then .[0].Id
else error("expected one local image identity")
end
' <<< "$image_json"
)"
if [[ ! "$image_id" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: PR managed image did not resolve to an immutable local image ID: $image_id" >&2
exit 1
fi
if [ "$(docker image inspect --format '{{.Id}}' "$image_id")" != "$image_id" ]; then
echo "ERROR: PR managed image ID did not resolve to itself." >&2
exit 1
fi
if [[ ! "$PUBLICATION_COHORT" =~ ^ghrun-[1-9][0-9]{0,19}-[1-9][0-9]{0,9}$ ]] ||
! jq -e \
--arg agent "$AGENT" \
--arg cohort "$PUBLICATION_COHORT" \
--arg image_id "$image_id" \
--arg platform "$PLATFORM" \
--arg revision "$CANDIDATE_SHA" '
length == 1 and
.[0].Id == $image_id and
((.[0].Config.User // "") as $user |
$user == "" or $user == "root" or $user == "0") 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
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.startup-profile"] == "1" and
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.capabilities"] == "1" and
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.cohort"] == $cohort and
.[0].Config.Labels["org.opencontainers.image.revision"] == $revision
' <<< "$image_json" >/dev/null; then
echo "ERROR: PR managed image contract does not match the exact build identity." >&2
exit 1
fi
discovery_runtime="/usr/local/lib/nemoclaw/mcp-tool-discovery-runtime"
if ! actual_discovery_contract="$(
docker run --rm --platform "$PLATFORM" --entrypoint /bin/sh "$image_id" -c '
set -eu
discovery_runtime="$1"
if ! entry_owner_unsafe="$(
find -P "$discovery_runtime" ! -user root -print -quit
)"; then
echo "ERROR: final PR managed image discovery entry ownership inspection failed." >&2
exit 1
fi
if [ -n "$entry_owner_unsafe" ]; then
echo "ERROR: final PR managed image discovery entry is not root-owned: $entry_owner_unsafe" >&2
exit 1
fi
if ! owner_or_write_unsafe="$(
find -L "$discovery_runtime" \
\( ! -user root -o -perm /022 \) -print -quit
)"; then
echo "ERROR: final PR managed image discovery tree inspection failed." >&2
exit 1
fi
if [ -n "$owner_or_write_unsafe" ]; then
echo "ERROR: final PR managed image discovery tree is not root-owned and read-only: $owner_or_write_unsafe" >&2
exit 1
fi
if ! mode_unsafe="$(
find -P "$discovery_runtime" \
\( \( -type d ! -perm 0555 \) -o \( -type f ! -perm 0444 \) \) \
-print -quit
)"; then
echo "ERROR: final PR managed image discovery mode inspection failed." >&2
exit 1
fi
if [ -n "$mode_unsafe" ]; then
echo "ERROR: final PR managed image discovery path has an unexpected mode: $mode_unsafe" >&2
exit 1
fi
node "$discovery_runtime/mcp-tool-discovery.mjs"
' -- "$discovery_runtime"
)"; then
echo "ERROR: final PR managed image discovery contract probe failed." >&2
exit 1
fi
if ! node -e '
const expected = {
protocol: 1,
ok: false,
detail: "tool discovery received invalid runtime arguments",
};
let result;
try {
result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
} catch {
process.exit(1);
}
const record =
result !== null && typeof result === "object" && !Array.isArray(result)
? result
: undefined;
if (
!record ||
record.protocol !== expected.protocol ||
record.ok !== expected.ok ||
record.detail !== expected.detail
) {
process.exit(1);
}
' <<< "$actual_discovery_contract" >/dev/null; then
echo "ERROR: final PR managed image discovery bundle contract mismatch." >&2
exit 1
fi
printf 'reference=%s\n' "$image_id" >> "$GITHUB_OUTPUT"
printf '### %s exact PR managed image\n\n`%s`\n' "$AGENT" "$image_id" \
>> "$GITHUB_STEP_SUMMARY"
- name: Install managed-image direct harness dependencies
run: |
npm ci --ignore-scripts
npm run build:policy-boundary
- name: Exercise managed startup root stdin and hold
shell: bash
env:
AGENT: ${{ matrix.agent }}
IMAGE_REFERENCE: ${{ steps.contract.outputs.reference }}
run: |
set -euo pipefail
npx --no-install tsx \
scripts/checks/run-managed-image-direct-e2e.ts \
--agent "$AGENT" \
--image "$IMAGE_REFERENCE" \
--platform linux/amd64
- name: Log in to GHCR for exact same-repository PR digest
if: github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Publish by digest only. No candidate tag, cohort tag, or release alias is
# created, so this proof cannot expose an incomplete all-agent cohort.
- name: Publish exact same-repository PR managed image by digest
id: publish
if: github.event.pull_request.head.repo.full_name == github.repository
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
build-contexts: ${{ steps.base.outputs.local == 'true' && format('nemoclaw-pr-base=oci-layout://{0}', steps.base.outputs.oci) || '' }}
outputs: type=image,name=${{ matrix.repository }},push-by-digest=true,name-canonical=true,push=true
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.event.pull_request.head.sha }}
io.nvidia.nemoclaw.agent=${{ matrix.agent }}
io.nvidia.nemoclaw.managed-image.contract=1
io.nvidia.nemoclaw.managed-image.platform=linux/amd64
io.nvidia.nemoclaw.managed-image.startup-profile=1
io.nvidia.nemoclaw.managed-image.capabilities=1
io.nvidia.nemoclaw.managed-image.cohort=ghrun-${{ github.run_id }}-${{ github.run_attempt }}
build-args: |
BASE_IMAGE=${{ steps.base.outputs.local == 'true' && 'nemoclaw-pr-base' || steps.base.outputs.ref }}
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1
NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root
cache-from: type=registry,ref=ghcr.io/nvidia/nemoclaw/${{ matrix.agent }}-sandbox:buildcache-linux-amd64
provenance: false
sbom: false
- name: Remove PR publication credentials
if: always() && github.event.pull_request.head.repo.full_name == github.repository
shell: bash
run: docker logout ghcr.io
- name: Export exact published PR managed-image contract
if: github.event.pull_request.head.repo.full_name == github.repository
shell: bash
env:
AGENT: ${{ matrix.agent }}
COHORT: ghrun-${{ github.run_id }}-${{ github.run_attempt }}
DIGEST: ${{ steps.publish.outputs.digest }}
IMAGE: ${{ matrix.repository }}
run: |
set -euo pipefail
[[ "$CANDIDATE_SHA" =~ ^[a-f0-9]{40}$ ]] || {
echo "ERROR: exact PR head SHA is invalid" >&2
exit 1
}
[[ "$DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]] || {
echo "ERROR: PR publication did not return an immutable digest" >&2
exit 1
}
reference="${IMAGE}@${DIGEST}"
raw="$RUNNER_TEMP/${AGENT}-published-manifest.raw"
docker buildx imagetools inspect "$reference" --raw > "$raw"
[[ "sha256:$(sha256sum "$raw" | awk '{print $1}')" == "$DIGEST" ]] || {
echo "ERROR: published PR manifest bytes do not match the build digest" >&2
exit 1
}
anonymous_config="$(mktemp -d "$RUNNER_TEMP/managed-pr-anonymous.XXXXXX")"
trap 'rm -rf -- "$anonymous_config"' EXIT
DOCKER_CONFIG="$anonymous_config" docker pull --platform linux/amd64 "$reference"
release="v$(node -p 'require("./package.json").version')"
contract_dir="$RUNNER_TEMP/managed-pr-contract"
mkdir -p "$contract_dir"
jq -n \
--arg agent "$AGENT" \
--arg cohort "$COHORT" \
--arg digest "$DIGEST" \
--arg image "$IMAGE" \
--arg reference "$reference" \
--arg release "$release" \
--arg revision "$CANDIDATE_SHA" \
'{
contractVersion: 1,
agent: $agent,
platform: "linux/amd64",
image: $image,
digest: $digest,
reference: $reference,
source: {
repository: "NVIDIA/NemoClaw",
revision: $revision,
release: $release,
cohort: $cohort
},
startupProfileContractVersion: 1,
capabilityContractVersion: 1
}' > "$contract_dir/contract.json"
- name: Upload exact published PR managed-image contract
if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-pr-contract-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.agent }}
path: ${{ runner.temp }}/managed-pr-contract/contract.json
if-no-files-found: error
retention-days: 1
pr-managed-activation:
name: PR exact all-agent managed runtime activation
needs: pr-build-and-entrypoint
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
timeout-minutes: 90
permissions:
contents: read
env:
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/managed-image-activation
E2E_JOB: "1"
E2E_TARGET_ID: managed-image-activation
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_MANAGED_ACTIVATION_CATALOG: ${{ github.workspace }}/managed-pr-catalog.json
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_RUN_LIVE_E2E: "1"
OPENSHELL_GATEWAY: nemoclaw
steps:
- name: Checkout exact PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.19.0
- name: Download exact published all-agent contracts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: managed-pr-contract-${{ github.run_id }}-${{ github.run_attempt }}-*
path: ${{ runner.temp }}/managed-pr-contracts
merge-multiple: false
- name: Assemble exact all-agent activation catalog
shell: bash
run: |
set -euo pipefail
mapfile -d '' contracts < <(
find "$RUNNER_TEMP/managed-pr-contracts" -type f -name contract.json -print0
)
[[ "${#contracts[@]}" -eq 3 ]] || {
echo "ERROR: exact all-agent activation requires three contracts" >&2
exit 1
}
jq -se \
--arg revision "$CANDIDATE_SHA" '
if (
length == 3 and
([.[].agent] | sort) == ["hermes", "langchain-deepagents-code", "openclaw"] and
([.[].source.revision] | unique) == [$revision] and
([.[].source.cohort] | unique | length) == 1 and
([.[].platform] | unique) == ["linux/amd64"]
)
then map({key: .agent, value: .}) | from_entries
else error("published PR contracts do not form one exact all-agent cohort")
end
' "${contracts[@]}" > "$NEMOCLAW_MANAGED_ACTIVATION_CATALOG"
- name: Build exact candidate CLI
run: |
npm ci --ignore-scripts
npm run build:cli
- name: Install OpenShell CLI
shell: bash
run: env -u GITHUB_TOKEN bash scripts/install-openshell.sh
- name: Run real all-agent managed runtime activation
shell: bash
run: |
set -euo pipefail
[[ "$(git rev-parse --verify HEAD)" == "$CANDIDATE_SHA" ]] || {
echo "ERROR: managed activation checkout does not match the exact PR head" >&2
exit 1
}
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
export OPENSHELL_BIN="$(command -v openshell)"
"$OPENSHELL_BIN" --version
npx tsx tools/e2e/live-vitest-invocation.mts run \
--test-path test/e2e/live/managed-image-activation-e2e.test.ts
- name: Upload managed runtime activation evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-activation-${{ github.run_id }}-${{ github.run_attempt }}
path: e2e-artifacts/live/managed-image-activation/
if-no-files-found: error
retention-days: 1
pi-candidate:
name: Build and validate the Pi candidate managed image (${{ matrix.arch }})
if: github.repository == 'NVIDIA/NemoClaw' && github.event_name == 'pull_request'
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
permissions:
contents: read
strategy: &pi_candidate_strategy
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
env: &pi_candidate_env
BASE_DOCKERFILE: agents/pi/Dockerfile.base
CANDIDATE_IMAGE: nemoclaw-managed-candidate/pi
DOCKERFILE: agents/pi/Dockerfile
LOCAL_BASE_REFERENCE: nemoclaw-managed-candidate/pi-base:${{ github.sha }}
PLATFORM: ${{ matrix.platform }}
PUBLICATION_COHORT: ghrun-${{ github.run_id }}-${{ github.run_attempt }}
REPOSITORY: ghcr.io/nvidia/nemoclaw/pi-sandbox
steps: &pi_candidate_steps
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.19.0
# Pi has no published base alias yet, so the candidate lane builds its own
# base on the target architecture. The activation change adds the shared
# published-base contract with the rest of the release cohort.
- name: Build the exact Pi candidate base
id: base
shell: bash
run: |
set -euo pipefail
local_base_archive="$RUNNER_TEMP/pi-${{ matrix.arch }}-candidate-base.docker.tar"
local_base_oci_archive="$RUNNER_TEMP/pi-${{ matrix.arch }}-candidate-base.oci.tar"
local_base_oci="$RUNNER_TEMP/pi-${{ matrix.arch }}-candidate-base.oci"
docker buildx build \
--platform "$PLATFORM" \
--provenance=false \
--sbom=false \
--file "$BASE_DOCKERFILE" \
--tag "$LOCAL_BASE_REFERENCE" \
--output "type=docker,dest=${local_base_archive}" \
--output "type=oci,dest=${local_base_oci_archive}" \
.
docker load --input "$local_base_archive"
mkdir -p "$local_base_oci"
tar -C "$local_base_oci" -xf "$local_base_oci_archive"
local_base_oci_digest="$(
jq -er '
.manifests
| if length == 1 then .[0].digest else error("not one image") end
' "$local_base_oci/index.json"
)"
if [[ ! "$local_base_oci_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: Pi candidate base OCI layout returned an invalid digest." >&2
exit 1
fi
printf 'oci=%s@%s\n' "$local_base_oci" "$local_base_oci_digest" >> "$GITHUB_OUTPUT"
- name: Validate candidate build args
shell: bash
run: |
set -euo pipefail
scripts/check-production-build-args.sh \
-f "$DOCKERFILE" \
--build-arg "BASE_IMAGE=nemoclaw-pi-base" \
--build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1" \
--build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root"
- name: Build the Pi candidate managed image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: agents/pi/Dockerfile
platforms: ${{ matrix.platform }}
build-contexts: nemoclaw-pi-base=oci-layout://${{ steps.base.outputs.oci }}
load: true
push: false
tags: nemoclaw-managed-candidate/pi:${{ github.sha }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
io.nvidia.nemoclaw.agent=pi
io.nvidia.nemoclaw.managed-image.contract=1
io.nvidia.nemoclaw.managed-image.platform=${{ matrix.platform }}
io.nvidia.nemoclaw.managed-image.startup-profile=1
io.nvidia.nemoclaw.managed-image.capabilities=1
io.nvidia.nemoclaw.managed-image.cohort=ghrun-${{ github.run_id }}-${{ github.run_attempt }}
build-args: |
BASE_IMAGE=nemoclaw-pi-base
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1
NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root
provenance: false
sbom: false
- name: Validate the Pi candidate runtime contract
shell: bash
env:
IMAGE_REFERENCE: nemoclaw-managed-candidate/pi:${{ github.sha }}
run: |
set -euo pipefail
image_json="$(docker image inspect "$IMAGE_REFERENCE")"
if ! jq -e \
--arg platform "$PLATFORM" \
'
length == 1 and (
.[0].Config.Labels["io.nvidia.nemoclaw.agent"] == "pi" and
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.platform"] == $platform and
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.startup-profile"] == "1" and
.[0].Config.Entrypoint == ["/usr/local/bin/nemoclaw-start"]
)
' <<< "$image_json" >/dev/null
then
echo "ERROR: the Pi candidate image does not carry the managed-image runtime contract." >&2
exit 1
fi
version="$(docker run --rm --entrypoint /usr/local/bin/pi "$IMAGE_REFERENCE" --version)"
expected_version="$(node -p 'require("./agents/pi/pi-runtime/package.json").dependencies["@earendil-works/pi-coding-agent"]')"
grep -Fq "$expected_version" <<< "$version"
docker run --rm --entrypoint /bin/bash "$IMAGE_REFERENCE" -c '
set -eu
test "$(stat -c %a /sandbox/.pi/agent/models.json)" = 600
test "$(stat -c %U /sandbox/.pi/agent/models.json)" = sandbox
node -e "const c=require(\"/sandbox/.pi/agent/models.json\"); if (!c.defaultModel || !c.providers.openshell.baseUrl) process.exit(1)"
test -x /usr/local/bin/nemoclaw-managed-bootstrap
'
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Publish only the canonical digest. The candidate repository has no
# consumer alias, so a published candidate stays reachable by digest alone.
- name: Publish the Pi candidate image by digest
id: publish
if: github.event_name != 'pull_request'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: agents/pi/Dockerfile
platforms: ${{ matrix.platform }}
build-contexts: nemoclaw-pi-base=oci-layout://${{ steps.base.outputs.oci }}
outputs: type=image,name=ghcr.io/nvidia/nemoclaw/pi-sandbox,push-by-digest=true,name-canonical=true,push=true
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
io.nvidia.nemoclaw.agent=pi
io.nvidia.nemoclaw.managed-image.contract=1
io.nvidia.nemoclaw.managed-image.platform=${{ matrix.platform }}
io.nvidia.nemoclaw.managed-image.startup-profile=1
io.nvidia.nemoclaw.managed-image.capabilities=1
io.nvidia.nemoclaw.managed-image.cohort=ghrun-${{ github.run_id }}-${{ github.run_attempt }}
build-args: |
BASE_IMAGE=nemoclaw-pi-base
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1
NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root
provenance: false
sbom: false
# The publication above rebuilds the image rather than reusing the
# already-validated local one, so revalidate the exact published digest
# before the contract records it as the qualified candidate.
- name: Validate the published Pi candidate digest
if: github.event_name != 'pull_request'
shell: bash
env:
DIGEST: ${{ steps.publish.outputs.digest }}
run: |
set -euo pipefail
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: the Pi candidate publication did not return an immutable digest." >&2
exit 1
fi
reference="${REPOSITORY}@${DIGEST}"
raw="$RUNNER_TEMP/pi-candidate-published-manifest.raw"
docker buildx imagetools inspect "$reference" --raw > "$raw"
if [ "sha256:$(sha256sum "$raw" | awk '{print $1}')" != "$DIGEST" ]; then
echo "ERROR: published Pi candidate manifest bytes do not match the build digest." >&2
exit 1
fi
anonymous_config="$(mktemp -d "$RUNNER_TEMP/pi-candidate-anonymous.XXXXXX")"
trap 'rm -rf -- "$anonymous_config"' EXIT
DOCKER_CONFIG="$anonymous_config" docker pull --platform "$PLATFORM" "$reference"
image_json="$(docker image inspect "$reference")"
if ! jq -e \
--arg platform "$PLATFORM" \
'
length == 1 and (
.[0].Config.Labels["io.nvidia.nemoclaw.agent"] == "pi" and
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.platform"] == $platform and
.[0].Config.Labels["io.nvidia.nemoclaw.managed-image.startup-profile"] == "1" and
.[0].Config.Entrypoint == ["/usr/local/bin/nemoclaw-start"]
)
' <<< "$image_json" >/dev/null
then
echo "ERROR: the published Pi candidate digest does not carry the managed-image runtime contract." >&2
exit 1
fi
version="$(docker run --rm --entrypoint /usr/local/bin/pi "$reference" --version)"
expected_version="$(node -p 'require("./agents/pi/pi-runtime/package.json").dependencies["@earendil-works/pi-coding-agent"]')"
grep -Fq "$expected_version" <<< "$version"
docker run --rm --entrypoint /bin/bash "$reference" -c '
set -eu
test "$(stat -c %a /sandbox/.pi/agent/models.json)" = 600
test "$(stat -c %U /sandbox/.pi/agent/models.json)" = sandbox
node -e "const c=require(\"/sandbox/.pi/agent/models.json\"); if (!c.defaultModel || !c.providers.openshell.baseUrl) process.exit(1)"
test -x /usr/local/bin/nemoclaw-managed-bootstrap
'
# The checks above bypass /usr/local/bin/nemoclaw-start with a direct
# --entrypoint override, so they never prove the entrypoint itself runs
# correctly. Start the local pull-request image or exact published digest
# through its declared entrypoint with no command, matching a real launch,
# and prove PID 1 drops to the sandbox user, hardens its resource limits,
# and persists the trusted proxy environment before it holds the sandbox
# open.
- name: Exercise the Pi candidate through its declared entrypoint
shell: bash
env:
DIGEST: ${{ steps.publish.outputs.digest }}
EVENT_NAME: ${{ github.event_name }}
IMAGE_REFERENCE: nemoclaw-managed-candidate/pi:${{ github.sha }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "pull_request" ]; then
reference="$IMAGE_REFERENCE"
else
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: the Pi candidate publication did not return an immutable digest." >&2
exit 1
fi
reference="${REPOSITORY}@${DIGEST}"
fi
corporate_ca_dir="$(mktemp -d "$RUNNER_TEMP/pi-candidate-ca.XXXXXX")"
corporate_ca="$corporate_ca_dir/corporate-ca.pem"
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-subj '/CN=NemoClaw Pi candidate CA handoff' \
-addext 'basicConstraints=critical,CA:TRUE' \
-keyout "$corporate_ca_dir/corporate-ca.key" \
-out "$corporate_ca" >/dev/null 2>&1
chmod 0444 "$corporate_ca"
entrypoint_container="$(
docker run -d \
--network none \
--env SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
--mount "type=bind,src=$corporate_ca,dst=/usr/local/share/nemoclaw/corporate-ca.pem,readonly" \
"$reference"
)"
cleanup_entrypoint_container() {
docker rm -f "$entrypoint_container" >/dev/null 2>&1 || true
rm -r -- "$corporate_ca_dir"
}
trap cleanup_entrypoint_container EXIT
entrypoint_deadline=$(($(date +%s) + 30))
while ! docker logs "$entrypoint_container" 2>&1 | grep -q 'Setting up NemoClaw Pi runtime'; do
if [ "$(docker inspect --format '{{.State.Running}}' "$entrypoint_container" 2>/dev/null)" != "true" ]; then
echo "ERROR: the Pi candidate entrypoint exited before reaching its held state." >&2
docker logs "$entrypoint_container" >&2 || true
exit 1
fi
if [ "$(date +%s)" -ge "$entrypoint_deadline" ]; then
echo "ERROR: the Pi candidate entrypoint did not reach its held state in time." >&2
docker logs "$entrypoint_container" >&2 || true
exit 1
fi
sleep 1
done
pid1_status="$(docker exec "$entrypoint_container" cat /proc/1/status)"
pid1_uid="$(printf '%s\n' "$pid1_status" | awk '/^Uid:/ {print $2}')"
if [ "$pid1_uid" != "999" ]; then
echo "ERROR: the Pi candidate entrypoint did not drop PID 1 to the sandbox uid: $pid1_uid" >&2
exit 1
fi
pid1_limits="$(docker exec "$entrypoint_container" cat /proc/1/limits)"
if ! grep -qE '^Max processes +512 +512 ' <<< "$pid1_limits"; then
echo "ERROR: the Pi candidate entrypoint did not harden PID 1 nproc to exactly 512." >&2
exit 1
fi
if ! grep -qE '^Max open files +65536 +65536 ' <<< "$pid1_limits"; then
echo "ERROR: the Pi candidate entrypoint did not harden PID 1 nofile to exactly 65536." >&2
exit 1
fi
runtime_env="$(docker exec "$entrypoint_container" cat /tmp/nemoclaw-proxy-env.sh)"
merged_ca=/tmp/nemoclaw-ca-bundle.pem
merged_ca_status="$(docker exec "$entrypoint_container" stat -c '%u:%g:%a' "$merged_ca")"
if [ "$merged_ca_status" != "0:0:444" ]; then
echo "ERROR: the Pi candidate merged CA bundle is not protected: $merged_ca_status" >&2
exit 1
fi
for ca_variable in \
SSL_CERT_FILE \
CURL_CA_BUNDLE \
REQUESTS_CA_BUNDLE \
GIT_SSL_CAINFO \
NODE_EXTRA_CA_CERTS
do
expected_export="export ${ca_variable}=${merged_ca}"
if ! grep -qF "$expected_export" <<< "$runtime_env"; then
echo "ERROR: the Pi candidate entrypoint did not persist the merged CA variable ($expected_export)." >&2
exit 1
fi
done
docker exec --user 999:999 "$entrypoint_container" node -e '
const fs = require("node:fs");
const { X509Certificate } = require("node:crypto");
const mounted = new X509Certificate(
fs.readFileSync("/usr/local/share/nemoclaw/corporate-ca.pem"),
);
const bundle = fs.readFileSync(process.argv[1], "utf8");
const blocks = bundle.match(
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g,
);
if (!blocks?.some((block) =>
new X509Certificate(block).fingerprint256 === mounted.fingerprint256
)) process.exit(1);
' "$merged_ca"
docker exec --user 999:999 "$entrypoint_container" /bin/bash -ceu '
unset HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy no_proxy
unset SSL_CERT_FILE CURL_CA_BUNDLE REQUESTS_CA_BUNDLE GIT_SSL_CAINFO NODE_EXTRA_CA_CERTS
source /tmp/nemoclaw-proxy-env.sh
proxy_url=http://10.200.0.1:3128
for proxy_variable in HTTP_PROXY HTTPS_PROXY http_proxy https_proxy; do
if [ "${!proxy_variable}" != "$proxy_url" ]; then
echo "ERROR: an independent Pi shell did not load $proxy_variable from the runtime environment." >&2
exit 1
fi
done
no_proxy_value=localhost,127.0.0.1,::1,10.200.0.1
for no_proxy_variable in NO_PROXY no_proxy; do
if [ "${!no_proxy_variable}" != "$no_proxy_value" ]; then
echo "ERROR: an independent Pi shell did not load $no_proxy_variable from the runtime environment." >&2
exit 1
fi
done
merged_ca=/tmp/nemoclaw-ca-bundle.pem
for ca_variable in \
SSL_CERT_FILE \
CURL_CA_BUNDLE \
REQUESTS_CA_BUNDLE \
GIT_SSL_CAINFO \
NODE_EXTRA_CA_CERTS
do
if [ "${!ca_variable}" != "$merged_ca" ]; then
echo "ERROR: an independent Pi shell did not load $ca_variable from the runtime environment." >&2
exit 1
fi
done
'
- name: Record the exact Pi candidate contract
if: github.event_name != 'pull_request'
shell: bash
env:
DIGEST: ${{ steps.publish.outputs.digest }}
run: |
set -euo pipefail
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: the Pi candidate publication did not return an immutable digest." >&2
exit 1
fi
release="v$(node -p 'require("./package.json").version')"
contract_dir="$RUNNER_TEMP/managed-candidate-contract"
mkdir -p "$contract_dir"
jq -n \
--arg cohort "$PUBLICATION_COHORT" \
--arg digest "$DIGEST" \
--arg image "$REPOSITORY" \
--arg platform "$PLATFORM" \
--arg release "$release" \
--arg revision "$GITHUB_SHA" \
--arg reference "${REPOSITORY}@${DIGEST}" \
'{
contractVersion: 1,
agent: "pi",
platform: $platform,
image: $image,
digest: $digest,
reference: $reference,
source: {
repository: "NVIDIA/NemoClaw",
revision: $revision,
release: $release,
cohort: $cohort
},
startupProfileContractVersion: 1,
capabilityContractVersion: 1
}' > "$contract_dir/contract.json"
node --experimental-strip-types tools/managed-images/validate-candidate-contract.mts \
--contract "$contract_dir/contract.json" \
--platform "$PLATFORM"
# The all-agent activation job downloads managed-pr-contract-* artifacts.
# This candidate name is deliberately outside that pattern so a published
# Pi digest cannot enter the atomic release cohort.
- name: Upload the exact Pi candidate contract
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-candidate-contract-${{ github.run_id }}-${{ github.run_attempt }}-pi-${{ matrix.arch }}
path: ${{ runner.temp }}/managed-candidate-contract/contract.json
if-no-files-found: error
retention-days: 7
pi-candidate-publish:
name: Publish and validate the Pi candidate managed image (${{ matrix.arch }})
if: github.repository == 'NVIDIA/NemoClaw' && github.event_name != 'pull_request'
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
permissions:
contents: read
packages: write
strategy: *pi_candidate_strategy
env: *pi_candidate_env
steps: *pi_candidate_steps
build-and-validate:
name: Build and validate ${{ matrix.display_name }} managed image (${{ matrix.arch }})
if: github.event_name != 'pull_request'
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- agent: openclaw
arch: amd64
display_name: OpenClaw
dockerfile: Dockerfile
base_image: nvidia/nemoclaw/sandbox-base
image: nvidia/nemoclaw/openclaw-sandbox
platform: linux/amd64
artifact_platform: linux-amd64
required_binary: /usr/local/bin/openclaw
runner: ubuntu-24.04
- agent: openclaw
arch: arm64
display_name: OpenClaw
dockerfile: Dockerfile
base_image: nvidia/nemoclaw/sandbox-base
image: nvidia/nemoclaw/openclaw-sandbox
platform: linux/arm64
artifact_platform: linux-arm64
required_binary: /usr/local/bin/openclaw
runner: ubuntu-24.04-arm
- agent: hermes
arch: amd64
display_name: Hermes
dockerfile: agents/hermes/Dockerfile
base_image: nvidia/nemoclaw/hermes-sandbox-base
image: nvidia/nemoclaw/hermes-sandbox
platform: linux/amd64
artifact_platform: linux-amd64
required_binary: /usr/local/bin/hermes
runner: ubuntu-24.04
- agent: hermes
arch: arm64
display_name: Hermes
dockerfile: agents/hermes/Dockerfile
base_image: nvidia/nemoclaw/hermes-sandbox-base
image: nvidia/nemoclaw/hermes-sandbox
platform: linux/arm64
artifact_platform: linux-arm64
required_binary: /usr/local/bin/hermes
runner: ubuntu-24.04-arm
- agent: langchain-deepagents-code
arch: amd64
display_name: Deep Agents Code
dockerfile: agents/langchain-deepagents-code/Dockerfile
base_image: nvidia/nemoclaw/langchain-deepagents-code-sandbox-base
image: nvidia/nemoclaw/langchain-deepagents-code-sandbox
platform: linux/amd64
artifact_platform: linux-amd64
required_binary: /usr/local/bin/dcode
runner: ubuntu-24.04
- agent: langchain-deepagents-code
arch: arm64
display_name: Deep Agents Code
dockerfile: agents/langchain-deepagents-code/Dockerfile
base_image: nvidia/nemoclaw/langchain-deepagents-code-sandbox-base
image: nvidia/nemoclaw/langchain-deepagents-code-sandbox
platform: linux/arm64
artifact_platform: linux-arm64
required_binary: /usr/local/bin/dcode
runner: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Download exact base image contract
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: managed-base-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.agent }}
path: ${{ runner.temp }}/managed-base-contract
- name: Validate exact base image contract
id: base
shell: bash
env:
AGENT: ${{ matrix.agent }}
BASE_IMAGE: ${{ env.REGISTRY }}/${{ matrix.base_image }}
CONTRACT: ${{ runner.temp }}/managed-base-contract/contract.json
PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
if [ ! -f "$CONTRACT" ] || [ -L "$CONTRACT" ]; then
echo "ERROR: exact base image contract is missing or is a symlink." >&2
exit 1
fi
if ! jq -e \
--arg agent "$AGENT" \
--arg image "$BASE_IMAGE" \
--arg platform "$PLATFORM" \
--arg revision "$GITHUB_SHA" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
'
(keys | sort) == [
"agent",
"contractVersion",
"digest",
"image",
"platformDigests",
"platformReferences",
"platforms",
"reference",
"run",
"sourceRevision"
]
and .contractVersion == 1
and .agent == $agent
and .image == $image
and (.digest | test("^sha256:[0-9a-f]{64}$"))
and .reference == (.image + "@" + .digest)
and .platforms == ["linux/amd64", "linux/arm64"]
and (.platformDigests | keys | sort) == .platforms
and (.platformReferences | keys | sort) == .platforms
and (.platformDigests[$platform] | test("^sha256:[0-9a-f]{64}$"))
and .platformReferences[$platform] ==
(.image + "@" + .platformDigests[$platform])
and .sourceRevision == $revision
and .run == {id: $runId, attempt: $runAttempt}
' "$CONTRACT" >/dev/null
then
echo "ERROR: exact base image contract failed closed validation." >&2
exit 1
fi
platform_reference="$(
jq -er --arg platform "$PLATFORM" '.platformReferences[$platform]' "$CONTRACT"
)"
docker buildx imagetools inspect "$platform_reference" >/dev/null
printf 'ref=%s\n' "$platform_reference" >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Validate production build args
env:
BASE_IMAGE: ${{ steps.base.outputs.ref }}
DOCKERFILE: ${{ matrix.dockerfile }}
run: |
set -euo pipefail
build_args=(
-f "$DOCKERFILE"
--build-arg "BASE_IMAGE=${BASE_IMAGE}"
--build-arg "NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1"
--build-arg "NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root"
)
scripts/check-production-build-args.sh "${build_args[@]}"
# Push only the canonical digest. Consumer aliases do not exist until the
# exact image has passed anonymous-pull and runtime-contract validation.
- name: Build and push managed image by digest
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
io.nvidia.nemoclaw.agent=${{ matrix.agent }}
io.nvidia.nemoclaw.managed-image.contract=1
io.nvidia.nemoclaw.managed-image.platform=${{ matrix.platform }}
io.nvidia.nemoclaw.managed-image.startup-profile=1
io.nvidia.nemoclaw.managed-image.capabilities=1
io.nvidia.nemoclaw.managed-image.cohort=ghrun-${{ github.run_id }}-${{ github.run_attempt }}
build-args: |
BASE_IMAGE=${{ steps.base.outputs.ref }}
NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=1
NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.artifact_platform }}
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.artifact_platform }},mode=max
provenance: mode=max
sbom: true
- name: Install managed-image publication harness dependencies
run: |
npm ci --ignore-scripts
npm run build:policy-boundary
- name: Validate exact managed image before promotion
id: validate
shell: bash
env:
AGENT: ${{ matrix.agent }}
DIGEST: ${{ steps.build.outputs.digest }}
IMAGE: ${{ env.REGISTRY }}/${{ matrix.image }}
PLATFORM: ${{ matrix.platform }}
PUBLICATION_COHORT: ghrun-${{ github.run_id }}-${{ github.run_attempt }}
REQUIRED_BINARY: ${{ matrix.required_binary }}
run: |
set -euo pipefail
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: managed image build did not return a valid digest: $DIGEST" >&2
exit 1
fi
reference="${IMAGE}@${DIGEST}"
docker buildx imagetools inspect "$reference" >/dev/null
anonymous_config="$(mktemp -d "$RUNNER_TEMP/anonymous-docker-XXXXXX")"
chmod 0700 "$anonymous_config"
if ! DOCKER_CONFIG="$anonymous_config" docker pull --platform "$PLATFORM" "$reference"; then
echo "::error::Anonymous exact-digest pull failed for ${reference}. Before aliases can be promoted, bootstrap the GHCR package ${IMAGE} with public visibility, then rerun this workflow."
exit 1
fi
image_id="$(docker image inspect --format '{{.Id}}' "$reference")"
if [[ ! "$image_id" =~ ^sha256:[0-9a-f]{64}$ ]] ||
[ "$(docker image inspect --format '{{.Id}}' "$image_id")" != "$image_id" ]; then
echo "ERROR: exact managed image did not resolve to one immutable local image ID." >&2
exit 1
fi
image_user="$(docker image inspect --format '{{.Config.User}}' "$reference")"
if [ -n "$image_user" ] && [ "$image_user" != "root" ] && [ "$image_user" != "0" ]; then
echo "ERROR: managed image OCI user must be uid 0 for OpenShell supervisor initialization: $image_user" >&2
exit 1
fi
agent_label="$(
docker image inspect \
--format '{{index .Config.Labels "io.nvidia.nemoclaw.agent"}}' \
"$reference"
)"
contract_label="$(
docker image inspect \
--format '{{index .Config.Labels "io.nvidia.nemoclaw.managed-image.contract"}}' \
"$reference"
)"
startup_profile_label="$(
docker image inspect \
--format '{{index .Config.Labels "io.nvidia.nemoclaw.managed-image.startup-profile"}}' \
"$reference"
)"
capabilities_label="$(
docker image inspect \
--format '{{index .Config.Labels "io.nvidia.nemoclaw.managed-image.capabilities"}}' \
"$reference"
)"
platform_label="$(
docker image inspect \
--format '{{index .Config.Labels "io.nvidia.nemoclaw.managed-image.platform"}}' \
"$reference"
)"
cohort_label="$(
docker image inspect \
--format '{{index .Config.Labels "io.nvidia.nemoclaw.managed-image.cohort"}}' \
"$reference"
)"
revision_label="$(
docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
"$reference"
)"
if [[ ! "$PUBLICATION_COHORT" =~ ^ghrun-[1-9][0-9]{0,19}-[1-9][0-9]{0,9}$ ]] ||
[ "$agent_label" != "$AGENT" ] ||
[ "$contract_label" != "1" ] ||
[ "$startup_profile_label" != "1" ] ||
[ "$capabilities_label" != "1" ] ||
[ "$platform_label" != "$PLATFORM" ] ||
[ "$cohort_label" != "$PUBLICATION_COHORT" ] ||
[ "$revision_label" != "$GITHUB_SHA" ]; then
echo "ERROR: managed image contract labels do not match the build identity." >&2
exit 1
fi
DOCKER_CONFIG="$anonymous_config" docker run --rm -i \
--platform "$PLATFORM" \
--network none \
--env "AGENT=$AGENT" \
--env "REQUIRED_BINARY=$REQUIRED_BINARY" \
--entrypoint /bin/sh \
"$reference" -eu -s <<'VALIDATE_FILESYSTEM'
test -x /usr/local/bin/nemoclaw-start
test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs
test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs
test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs)" = "0:0:444"
test -x "$REQUIRED_BINARY"
test -r /opt/nemoclaw-blueprint/blueprint.yaml
test ! -e /usr/local/share/nemoclaw/corporate-ca.pem
test ! -L /usr/local/share/nemoclaw/corporate-ca.pem
test "$NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION" = "1"
case "$AGENT" in
openclaw)
node <<'VALIDATE_OPENCLAW_UNION'
const fs = require("node:fs");
const path = require("node:path");
const config = JSON.parse(
fs.readFileSync("/sandbox/.openclaw/openclaw.json", "utf8"),
);
const packages = {
"diagnostics-otel": ["@openclaw/diagnostics-otel", "2026.7.1"],
brave: ["@openclaw/brave-plugin", "2026.7.1"],
discord: ["@openclaw/discord", "2026.7.1"],
"openclaw-weixin": ["@tencent-weixin/openclaw-weixin", "2.4.3"],
slack: ["@openclaw/slack", "2026.7.1"],
whatsapp: ["@openclaw/whatsapp", "2026.7.1"],
msteams: ["@openclaw/msteams", "2026.7.1"],
googlechat: ["@openclaw/googlechat", "2026.7.1"],
};
const projectsRoot = "/sandbox/.openclaw/npm/projects";
const projectRoots = fs
.readdirSync(projectsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(projectsRoot, entry.name));
for (const [id, [name, version]] of Object.entries(packages)) {
const matches = projectRoots.flatMap((projectRoot) => {
const nodeModulesRoot = path.join(projectRoot, "node_modules");
const packageRoot = path.join(nodeModulesRoot, ...name.split("/"));
const manifestPath = path.join(packageRoot, "package.json");
if (
!fs.existsSync(nodeModulesRoot) ||
!fs.existsSync(packageRoot) ||
!fs.lstatSync(packageRoot).isDirectory() ||
!fs.existsSync(manifestPath) ||
!fs.lstatSync(manifestPath).isFile()
) return [];
const packageRelative = path.relative(
fs.realpathSync(nodeModulesRoot),
fs.realpathSync(packageRoot),
);
if (
packageRelative === "" ||
packageRelative === ".." ||
packageRelative.startsWith(`..${path.sep}`) ||
path.isAbsolute(packageRelative)
) return [];
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
return manifest.name === name && manifest.version === version ? [manifestPath] : [];
});
if (matches.length !== 1 || config.plugins?.entries?.[id]?.enabled !== false) {
throw new Error(`managed OpenClaw plugin ${id} is missing, duplicated, or active`);
}
}
for (const id of ["telegram", "tavily"]) {
if (config.plugins?.entries?.[id]?.enabled !== false) {
throw new Error(`bundled OpenClaw plugin ${id} is not explicitly disabled`);
}
}
for (const id of [
"telegram",
"discord",
"openclaw-weixin",
"slack",
"whatsapp",
"msteams",
"googlechat",
]) {
if (config.channels?.[id]?.enabled !== false) {
throw new Error(`managed OpenClaw channel ${id} is not explicitly disabled`);
}
}
if (config.tools?.web?.search?.enabled !== false) {
throw new Error("managed OpenClaw web search is not explicitly disabled");
}
VALIDATE_OPENCLAW_UNION
;;
hermes)
/opt/hermes/.venv/bin/python -I <<'VALIDATE_HERMES_UNION'
import importlib.metadata as metadata
from pathlib import Path
import yaml
config = yaml.safe_load(Path("/sandbox/.hermes/config.yaml").read_text())
optional = ("telegram", "discord", "weixin", "slack", "whatsapp", "teams")
if any(config["platforms"].get(name) != {"enabled": False} for name in optional):
raise SystemExit("managed Hermes optional platform is not explicitly disabled")
if metadata.version("microsoft-teams-apps") != "2.0.13.4":
raise SystemExit("managed Hermes microsoft-teams-apps version is wrong")
if metadata.version("aiohttp") != "3.14.3":
raise SystemExit("managed Hermes aiohttp version is wrong")
VALIDATE_HERMES_UNION
;;
langchain-deepagents-code) ;;
*) echo "ERROR: unsupported managed agent $AGENT" >&2; exit 1 ;;
esac
VALIDATE_FILESYSTEM
version_output="$(
DOCKER_CONFIG="$anonymous_config" docker run --rm \
--platform "$PLATFORM" \
--network none \
--entrypoint "$REQUIRED_BINARY" \
"$reference" --version 2>&1
)"
if [ -z "$version_output" ]; then
echo "ERROR: required agent binary returned an empty version." >&2
exit 1
fi
DOCKER_CONFIG="$anonymous_config" \
npx --no-install tsx \
scripts/checks/run-managed-image-direct-e2e.ts \
--agent "$AGENT" \
--image "$reference" \
--platform "$PLATFORM"
printf 'local_id=%s\n' "$image_id" >> "$GITHUB_OUTPUT"
printf 'reference=%s\n' "$reference" >> "$GITHUB_OUTPUT"
- name: Capture exact managed image publication evidence
id: evidence
shell: bash
env:
AGENT: ${{ matrix.agent }}
BASE_REFERENCE: ${{ steps.base.outputs.ref }}
COHORT: ghrun-${{ github.run_id }}-${{ github.run_attempt }}
DIGEST: ${{ steps.build.outputs.digest }}
PLATFORM: ${{ matrix.platform }}
REFERENCE: ${{ steps.validate.outputs.reference }}
run: |
set -euo pipefail
evidence_dir="$RUNNER_TEMP/managed-image-publication-evidence"
evidence="$evidence_dir/evidence.json"
scripts/checks/verify-managed-image-publication-evidence.sh \
--reference "$REFERENCE" \
--digest "$DIGEST" \
--platform "$PLATFORM" \
--agent "$AGENT" \
--base-reference "$BASE_REFERENCE" \
--repository "$GITHUB_REPOSITORY" \
--revision "$GITHUB_SHA" \
--cohort "$COHORT" \
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
--output "$evidence"
printf 'path=%s\n' "$evidence" >> "$GITHUB_OUTPUT"
- name: Export validated managed image candidate
env:
AGENT: ${{ matrix.agent }}
BASE_REFERENCE: ${{ steps.base.outputs.ref }}
DIGEST: ${{ steps.build.outputs.digest }}
EVIDENCE: ${{ steps.evidence.outputs.path }}
IMAGE: ${{ env.REGISTRY }}/${{ matrix.image }}
PLATFORM: ${{ matrix.platform }}
PUBLICATION_COHORT: ghrun-${{ github.run_id }}-${{ github.run_attempt }}
REFERENCE: ${{ steps.validate.outputs.reference }}
run: |
set -euo pipefail
if [[ ! "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ ]] ||
[[ ! "$PUBLICATION_COHORT" =~ ^ghrun-[1-9][0-9]{0,19}-[1-9][0-9]{0,9}$ ]]; then
echo "ERROR: managed image publication identity is invalid." >&2
exit 1
fi
release_tag=""
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
release_tag="${GITHUB_REF#refs/tags/}"
if [[ ! "$release_tag" =~ ^v[0-9]+([.][0-9]+){1,3}([-.][0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then
echo "ERROR: release tag is not a supported version identity: $release_tag" >&2
exit 1
fi
elif [ "$GITHUB_REF" != "refs/heads/main" ]; then
echo "ERROR: managed images may only be published from main or a v* release tag." >&2
exit 1
fi
contract_dir="$RUNNER_TEMP/managed-image-candidate"
mkdir -p "$contract_dir"
if [ ! -f "$EVIDENCE" ] || [ -L "$EVIDENCE" ]; then
echo "ERROR: exact registry publication evidence is missing or unsafe." >&2
exit 1
fi
jq -n \
--arg agent "$AGENT" \
--arg baseReference "$BASE_REFERENCE" \
--arg digest "$DIGEST" \
--arg image "$IMAGE" \
--arg platform "$PLATFORM" \
--arg cohort "$PUBLICATION_COHORT" \
--arg reference "$REFERENCE" \
--arg ref "$GITHUB_REF" \
--arg release "$release_tag" \
--arg repository "$GITHUB_REPOSITORY" \
--arg revision "$GITHUB_SHA" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
--slurpfile publicationEvidence "$EVIDENCE" \
'{
contractVersion: 2,
phase: "candidate",
agent: $agent,
image: $image,
digest: $digest,
reference: $reference,
baseReference: $baseReference,
platform: $platform,
publicationEvidence: $publicationEvidence[0],
source: {
repository: $repository,
revision: $revision,
ref: $ref,
cohort: $cohort
},
run: {
id: $runId,
attempt: $runAttempt
},
release: (if $release == "" then null else $release end)
}' > "$contract_dir/contract.json"
jq -e \
--arg agent "$AGENT" \
--arg image "$IMAGE" \
--arg platform "$PLATFORM" \
--arg cohort "$PUBLICATION_COHORT" \
--arg ref "$GITHUB_REF" \
--arg release "$release_tag" \
--arg repository "$GITHUB_REPOSITORY" \
--arg revision "$GITHUB_SHA" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
'(keys | sort) == [
"agent",
"baseReference",
"contractVersion",
"digest",
"image",
"phase",
"platform",
"publicationEvidence",
"reference",
"release",
"run",
"source"
]
and .contractVersion == 2
and .phase == "candidate"
and .agent == $agent
and .image == $image
and .platform == $platform
and (.digest | test("^sha256:[0-9a-f]{64}$"))
and .reference == (.image + "@" + .digest)
and (.baseReference | test("@sha256:[0-9a-f]{64}$"))
and .publicationEvidence.candidateDescriptor.digest == .digest
and .publicationEvidence.workloadDescriptor.platform == (
$platform
| split("/")
| {os: .[0], architecture: .[1]}
)
and .publicationEvidence.attestations.manifestDescriptor.annotations[
"vnd.docker.reference.digest"
] == .publicationEvidence.workloadDescriptor.digest
and .publicationEvidence.attestations.slsa.descriptor.annotations[
"in-toto.io/predicate-type"
] == "https://slsa.dev/provenance/v1"
and .publicationEvidence.attestations.slsa.statement.subject.digest ==
.publicationEvidence.workloadDescriptor.digest
and .publicationEvidence.attestations.slsa.statement.bindings == {
agent: $agent,
baseReference: .baseReference,
cohort: $cohort,
platform: $platform,
revision: $revision,
source: ("https://github.com/" + $repository)
}
and .publicationEvidence.attestations.slsa.statement.builderId ==
("https://github.com/" + $repository + "/actions/runs/" +
($runId | tostring) + "/attempts/" + ($runAttempt | tostring))
and .publicationEvidence.attestations.spdx.descriptor.annotations[
"in-toto.io/predicate-type"
] == "https://spdx.dev/Document"
and .publicationEvidence.attestations.spdx.statement.subject.digest ==
.publicationEvidence.workloadDescriptor.digest
and .source == {
repository: $repository,
revision: $revision,
ref: $ref,
cohort: $cohort
}
and .run == {id: $runId, attempt: $runAttempt}
and .release == (if $release == "" then null else $release end)
and (has("aliases") | not)' \
"$contract_dir/contract.json" >/dev/null
- name: Upload validated managed image candidate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-candidate-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.agent }}-${{ matrix.artifact_platform }}
path: ${{ runner.temp }}/managed-image-candidate/contract.json
if-no-files-found: error
retention-days: 1
promote:
name: Promote complete multi-platform managed image cohort
needs: build-and-validate
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- name: Download all validated managed image candidates
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: managed-image-candidate-${{ github.run_id }}-${{ github.run_attempt }}-*
path: ${{ runner.temp }}/managed-image-candidates
merge-multiple: false
# This is the all-agent/all-architecture publication barrier. It fails
# closed before registry authentication or any alias operation.
- name: Validate complete managed image candidate set
id: candidates
shell: bash
env:
CANDIDATE_ROOT: ${{ runner.temp }}/managed-image-candidates
run: |
set -euo pipefail
if [[ ! "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "ERROR: source revision must be a full 40-character SHA." >&2
exit 1
fi
expected_cohort="ghrun-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
if [[ ! "$expected_cohort" =~ ^ghrun-[1-9][0-9]{0,19}-[1-9][0-9]{0,9}$ ]]; then
echo "ERROR: publication cohort is invalid: $expected_cohort" >&2
exit 1
fi
expected_release=""
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
expected_release="${GITHUB_REF#refs/tags/}"
if [[ ! "$expected_release" =~ ^v[0-9]+([.][0-9]+){1,3}([-.][0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then
echo "ERROR: release tag is not a supported version identity: $expected_release" >&2
exit 1
fi
elif [ "$GITHUB_REF" != "refs/heads/main" ]; then
echo "ERROR: managed images may only be promoted from main or a v* release tag." >&2
exit 1
fi
artifact_prefix="managed-image-candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
expected_artifacts=(
"${artifact_prefix}-openclaw-linux-amd64"
"${artifact_prefix}-openclaw-linux-arm64"
"${artifact_prefix}-hermes-linux-amd64"
"${artifact_prefix}-hermes-linux-arm64"
"${artifact_prefix}-langchain-deepagents-code-linux-amd64"
"${artifact_prefix}-langchain-deepagents-code-linux-arm64"
)
expected_agents=(
openclaw
openclaw
hermes
hermes
langchain-deepagents-code
langchain-deepagents-code
)
expected_platforms=(
linux/amd64
linux/arm64
linux/amd64
linux/arm64
linux/amd64
linux/arm64
)
if [ ! -d "$CANDIDATE_ROOT" ] || [ -L "$CANDIDATE_ROOT" ]; then
echo "ERROR: managed image candidate root is missing or unsafe." >&2
exit 1
fi
actual_entry_count="$(
find "$CANDIDATE_ROOT" ! -path "$CANDIDATE_ROOT" -prune -print |
wc -l | tr -d '[:space:]'
)"
if [ "$actual_entry_count" != "${#expected_artifacts[@]}" ]; then
echo "ERROR: expected exactly six managed image candidate artifacts." >&2
exit 1
fi
candidate_files=()
for index in "${!expected_artifacts[@]}"; do
artifact="${expected_artifacts[$index]}"
expected_agent="${expected_agents[$index]}"
expected_platform="${expected_platforms[$index]}"
artifact_dir="$CANDIDATE_ROOT/$artifact"
contract="$artifact_dir/contract.json"
if [ ! -d "$artifact_dir" ] || [ -L "$artifact_dir" ] ||
[ ! -f "$contract" ] || [ -L "$contract" ]; then
echo "ERROR: required managed image candidate is missing or unsafe: $artifact" >&2
exit 1
fi
artifact_entry_count="$(
find "$artifact_dir" ! -path "$artifact_dir" -prune -print |
wc -l | tr -d '[:space:]'
)"
if [ "$artifact_entry_count" != "1" ] ||
! jq -e \
--arg agent "$expected_agent" \
--arg platform "$expected_platform" \
'.agent == $agent and .platform == $platform' \
"$contract" >/dev/null; then
echo "ERROR: managed image candidate artifact identity is invalid: $artifact" >&2
exit 1
fi
candidate_files+=("$contract")
done
candidate_set="$RUNNER_TEMP/managed-image-candidate-set.json"
jq -s 'sort_by(.agent, .platform)' "${candidate_files[@]}" > "$candidate_set"
if ! jq -e \
--arg ref "$GITHUB_REF" \
--arg release "$expected_release" \
--arg repository "$GITHUB_REPOSITORY" \
--arg revision "$GITHUB_SHA" \
--arg cohort "$expected_cohort" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
'
def sha256: type == "string" and test("^sha256:[0-9a-f]{64}$");
def positive_size:
type == "number" and . > 0 and floor == .;
def workload_descriptor($platform):
(keys | sort) == ["digest", "mediaType", "platform", "size"]
and .mediaType == "application/vnd.oci.image.manifest.v1+json"
and (.digest | sha256)
and (.size | positive_size)
and .platform == (
$platform | split("/") | {os: .[0], architecture: .[1]}
);
def attestation_manifest_descriptor($workloadDigest):
(keys | sort) == ["annotations", "digest", "mediaType", "platform", "size"]
and .mediaType == "application/vnd.oci.image.manifest.v1+json"
and (.digest | sha256)
and (.size | positive_size)
and .platform == {os: "unknown", architecture: "unknown"}
and (.annotations | keys | sort) == [
"vnd.docker.reference.digest",
"vnd.docker.reference.type"
]
and .annotations["vnd.docker.reference.digest"] == $workloadDigest
and .annotations["vnd.docker.reference.type"] == "attestation-manifest";
def statement_descriptor($predicateType):
(keys | sort) == ["annotations", "digest", "mediaType", "size"]
and .mediaType == "application/vnd.in-toto+json"
and (.digest | sha256)
and (.size | positive_size)
and .annotations == {"in-toto.io/predicate-type": $predicateType};
length == 6
and ([.[].agent] | group_by(.) | map({key: .[0], value: length}) | from_entries) == {
"hermes": 2,
"langchain-deepagents-code": 2,
"openclaw": 2
}
and (group_by(.agent) | all(.[];
([.[].platform] | sort) == ["linux/amd64", "linux/arm64"]
))
and all(.[];
(keys | sort) == [
"agent",
"baseReference",
"contractVersion",
"digest",
"image",
"phase",
"platform",
"publicationEvidence",
"reference",
"release",
"run",
"source"
]
and .contractVersion == 2
and .phase == "candidate"
and (.digest | test("^sha256:[0-9a-f]{64}$"))
and .reference == (.image + "@" + .digest)
and (.baseReference | test("@sha256:[0-9a-f]{64}$"))
and (.publicationEvidence | keys | sort) == [
"attestations",
"candidateDescriptor",
"workloadDescriptor"
]
and (.publicationEvidence.candidateDescriptor | keys | sort) == [
"digest",
"mediaType",
"size"
]
and .publicationEvidence.candidateDescriptor.mediaType ==
"application/vnd.oci.image.index.v1+json"
and .publicationEvidence.candidateDescriptor.digest == .digest
and (.publicationEvidence.candidateDescriptor.size | positive_size)
and (
.platform as $candidatePlatform
| .publicationEvidence.workloadDescriptor
| workload_descriptor($candidatePlatform)
)
and (.publicationEvidence.attestations | keys | sort) == [
"manifestDescriptor",
"slsa",
"spdx"
]
and (
.publicationEvidence.workloadDescriptor.digest as $workloadDigest
|
.publicationEvidence.attestations.manifestDescriptor
| attestation_manifest_descriptor($workloadDigest)
)
and (.publicationEvidence.attestations.slsa | keys | sort) == [
"descriptor",
"statement"
]
and (
.publicationEvidence.attestations.slsa.descriptor
| statement_descriptor("https://slsa.dev/provenance/v1")
)
and (
.agent as $candidateAgent
| .baseReference as $candidateBaseReference
| .platform as $candidatePlatform
|
.publicationEvidence.workloadDescriptor.digest as $workloadDigest
| .publicationEvidence.attestations.slsa.statement
| (keys | sort) == [
"bindings",
"buildType",
"builderId",
"predicateType",
"subject",
"type"
]
and .type == "https://in-toto.io/Statement/v1"
and .predicateType == "https://slsa.dev/provenance/v1"
and .buildType ==
"https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md"
and (.subject | keys | sort) == ["digest", "name"]
and (.subject.name | type) == "string"
and (.subject.name | length) > 0
and .subject.digest == $workloadDigest
and (.bindings | keys | sort) == [
"agent",
"baseReference",
"cohort",
"platform",
"revision",
"source"
]
and .bindings.agent == $candidateAgent
and .bindings.baseReference == $candidateBaseReference
and .bindings.cohort == $cohort
and .bindings.platform == $candidatePlatform
and .bindings.revision == $revision
and .bindings.source == ("https://github.com/" + $repository)
and .builderId ==
("https://github.com/" + $repository + "/actions/runs/" +
($runId | tostring) + "/attempts/" + ($runAttempt | tostring))
)
and (.publicationEvidence.attestations.spdx | keys | sort) == [
"descriptor",
"statement"
]
and (
.publicationEvidence.attestations.spdx.descriptor
| statement_descriptor("https://spdx.dev/Document")
)
and (
.publicationEvidence.workloadDescriptor.digest as $workloadDigest
| .publicationEvidence.attestations.spdx.statement
| (keys | sort) == ["predicateType", "subject", "type"]
and .type == "https://in-toto.io/Statement/v1"
and .predicateType == "https://spdx.dev/Document"
and (.subject | keys | sort) == ["digest", "name"]
and (.subject.name | type) == "string"
and (.subject.name | length) > 0
and .subject.digest == $workloadDigest
)
and .publicationEvidence.attestations.slsa.descriptor.digest !=
.publicationEvidence.attestations.spdx.descriptor.digest
and .source == {
repository: $repository,
revision: $revision,
ref: $ref,
cohort: $cohort
}
and .run == {id: $runId, attempt: $runAttempt}
and .release == (if $release == "" then null else $release end)
and (has("aliases") | not)
)
and ([.[].reference] | unique | length) == 6
and ([.[].baseReference] | unique | length) == 6
and ([.[].publicationEvidence.workloadDescriptor.digest] | unique | length) == 6
and ([.[].publicationEvidence.attestations.manifestDescriptor.digest]
| unique | length) == 6
and ([.[].publicationEvidence.attestations.slsa.descriptor.digest]
| unique | length) == 6
and ([.[].publicationEvidence.attestations.spdx.descriptor.digest]
| unique | length) == 6
and (
map({key: (.agent + "|" + .platform), value: .image}) | from_entries
) == {
"openclaw|linux/amd64": "ghcr.io/nvidia/nemoclaw/openclaw-sandbox",
"openclaw|linux/arm64": "ghcr.io/nvidia/nemoclaw/openclaw-sandbox",
"hermes|linux/amd64": "ghcr.io/nvidia/nemoclaw/hermes-sandbox",
"hermes|linux/arm64": "ghcr.io/nvidia/nemoclaw/hermes-sandbox",
"langchain-deepagents-code|linux/amd64":
"ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox",
"langchain-deepagents-code|linux/arm64":
"ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox"
}
' "$candidate_set" >/dev/null
then
echo "ERROR: complete managed image candidate set failed closed validation." >&2
exit 1
fi
printf 'candidate_set=%s\n' "$candidate_set" >> "$GITHUB_OUTPUT"
- name: Verify release tag before managed image promotion
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_REVISION: ${{ github.sha }}
with:
script: |
const releaseTag = process.env.RELEASE_TAG ?? '';
const releaseRevision = process.env.RELEASE_REVISION ?? '';
if (!/^v\d+\.\d+\.\d+$/.test(releaseTag)) {
throw new Error(`Refusing to verify non-semver tag: ${releaseTag}`);
}
if (!/^[0-9a-f]{40}$/.test(releaseRevision)) {
throw new Error(`Refusing release promotion with invalid revision: ${releaseRevision}`);
}
const { owner, repo } = context.repo;
const ref = await github.rest.git.getRef({ owner, repo, ref: `tags/${releaseTag}` });
if (ref.data.object.type !== 'tag') {
throw new Error(`Release tag ${releaseTag} must be annotated`);
}
const tagObjectSha = ref.data.object.sha;
let tagObject;
for (let attempt = 1; attempt <= 10; attempt += 1) {
({ data: tagObject } = await github.rest.git.getTag({
owner,
repo,
tag_sha: tagObjectSha,
}));
if (tagObject.verification?.verified === true) break;
if (attempt < 10) {
core.info(`Waiting for GitHub tag verification (${attempt}/10)`);
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
if (
tagObject.tag !== releaseTag ||
tagObject.object.type !== 'commit' ||
tagObject.object.sha !== releaseRevision ||
tagObject.verification?.verified !== true
) {
throw new Error(`Release tag ${releaseTag} is not a verified direct commit tag`);
}
- name: Checkout publication verifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx for promotion
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to GHCR for promotion
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Revalidate exact managed image publication evidence
shell: bash
env:
CANDIDATE_SET: ${{ steps.candidates.outputs.candidate_set }}
run: |
set -euo pipefail
revalidated_root="$RUNNER_TEMP/revalidated-managed-image-evidence"
install -d -m 0700 "$revalidated_root"
index=0
while IFS= read -r candidate; do
index=$((index + 1))
reference="$(jq -er '.reference' <<<"$candidate")"
digest="$(jq -er '.digest' <<<"$candidate")"
platform="$(jq -er '.platform' <<<"$candidate")"
agent="$(jq -er '.agent' <<<"$candidate")"
base_reference="$(jq -er '.baseReference' <<<"$candidate")"
repository="$(jq -er '.source.repository' <<<"$candidate")"
revision="$(jq -er '.source.revision' <<<"$candidate")"
cohort="$(jq -er '.source.cohort' <<<"$candidate")"
run_id="$(jq -er '.run.id | tostring' <<<"$candidate")"
run_attempt="$(jq -er '.run.attempt | tostring' <<<"$candidate")"
actual="$revalidated_root/$index.json"
expected="$revalidated_root/$index.expected.json"
scripts/checks/verify-managed-image-publication-evidence.sh \
--reference "$reference" \
--digest "$digest" \
--platform "$platform" \
--agent "$agent" \
--base-reference "$base_reference" \
--repository "$repository" \
--revision "$revision" \
--cohort "$cohort" \
--run-id "$run_id" \
--run-attempt "$run_attempt" \
--output "$actual"
jq -S '.publicationEvidence' <<<"$candidate" > "$expected"
if ! cmp -s "$expected" <(jq -S . "$actual"); then
echo "ERROR: registry publication evidence changed after candidate validation: $reference" >&2
exit 1
fi
done < <(jq -c '.[]' "$CANDIDATE_SET")
if [ "$index" -ne 6 ]; then
echo "ERROR: registry evidence revalidation did not cover all six candidates." >&2
exit 1
fi
- name: Stage validated multi-platform managed image cohort and contracts
shell: bash
env:
CANDIDATE_SET: ${{ steps.candidates.outputs.candidate_set }}
run: |
set -euo pipefail
contract_root="$RUNNER_TEMP/managed-image-contracts"
mkdir -p "$contract_root"
cohort="ghrun-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
if [[ ! "$cohort" =~ ^ghrun-[1-9][0-9]{0,19}-[1-9][0-9]{0,9}$ ]]; then
echo "ERROR: publication cohort is invalid: $cohort" >&2
exit 1
fi
manifests="$RUNNER_TEMP/managed-image-cohort-manifests.jsonl"
: > "$manifests"
for agent in openclaw hermes langchain-deepagents-code; do
agent_candidates="$RUNNER_TEMP/managed-image-${agent}-candidates.json"
jq -ce --arg agent "$agent" \
'[.[] | select(.agent == $agent)] | sort_by(.platform)' \
"$CANDIDATE_SET" > "$agent_candidates"
image="$(jq -er '.[0].image' "$agent_candidates")"
descriptor_root="$RUNNER_TEMP/managed-image-${agent}-descriptors"
install -d -m 0700 "$descriptor_root"
descriptor_args=()
candidate_index=0
while IFS= read -r candidate; do
candidate_index=$((candidate_index + 1))
workload_descriptor="$descriptor_root/$candidate_index-workload.json"
attestation_descriptor="$descriptor_root/$candidate_index-attestation.json"
jq -S '.publicationEvidence.workloadDescriptor' \
<<<"$candidate" > "$workload_descriptor"
jq -S '.publicationEvidence.attestations.manifestDescriptor' \
<<<"$candidate" > "$attestation_descriptor"
descriptor_args+=(
--file "$workload_descriptor"
--file "$attestation_descriptor"
)
done < <(jq -c '.[]' "$agent_candidates")
if [ "$candidate_index" -ne 2 ] || [ "${#descriptor_args[@]}" -ne 8 ]; then
echo "ERROR: expected exactly two platform candidates for $agent." >&2
exit 1
fi
cohort_alias="${image}:cohort-${cohort}"
cohort_metadata="$RUNNER_TEMP/managed-image-${agent}-cohort-metadata.json"
docker buildx imagetools create \
--tag "$cohort_alias" \
--metadata-file "$cohort_metadata" \
"${descriptor_args[@]}"
cohort_raw="$RUNNER_TEMP/managed-image-${agent}-cohort.raw"
docker buildx imagetools inspect "$cohort_alias" --raw > "$cohort_raw"
expected_descriptors="$descriptor_root/expected.json"
actual_descriptors="$descriptor_root/actual.json"
jq -S '[
.[]
| (
.publicationEvidence.workloadDescriptor,
.publicationEvidence.attestations.manifestDescriptor
)
] | sort_by(.digest)' "$agent_candidates" > "$expected_descriptors"
jq -S '.manifests | sort_by(.digest)' "$cohort_raw" > "$actual_descriptors"
if ! jq -e '
(keys | sort) == ["manifests", "mediaType", "schemaVersion"]
and .schemaVersion == 2
and .mediaType == "application/vnd.oci.image.index.v1+json"
and (.manifests | length) == 4
and ([
.manifests[]
| select(.platform.os == "linux")
| "linux/" + .platform.architecture
] | sort) == ["linux/amd64", "linux/arm64"]
and ([
.manifests[]
| select(
.platform == {os: "unknown", architecture: "unknown"}
and .annotations["vnd.docker.reference.type"] ==
"attestation-manifest"
)
] | length) == 2
' "$cohort_raw" >/dev/null ||
! cmp -s "$expected_descriptors" "$actual_descriptors"; then
echo "ERROR: staged cohort descriptors differ from validated candidates: $cohort_alias" >&2
exit 1
fi
cohort_descriptor="$RUNNER_TEMP/managed-image-${agent}-cohort-descriptor.json"
jq -ce '.["containerimage.descriptor"]' \
"$cohort_metadata" > "$cohort_descriptor"
cohort_digest="$(jq -er '.digest' "$cohort_descriptor")"
cohort_size="$(wc -c < "$cohort_raw" | tr -d '[:space:]')"
actual_cohort_digest="sha256:$(sha256sum "$cohort_raw" | awk '{print $1}')"
if ! jq -e \
--arg digest "$actual_cohort_digest" \
--argjson size "$cohort_size" \
'
(keys | sort) == ["digest", "mediaType", "size"]
and .mediaType == "application/vnd.oci.image.index.v1+json"
and .digest == $digest
and .size == $size
' "$cohort_descriptor" >/dev/null; then
echo "ERROR: staged cohort descriptor does not match its registry bytes: $cohort_alias" >&2
exit 1
fi
cohort_reference="${image}@${cohort_digest}"
exact_raw="$RUNNER_TEMP/managed-image-${agent}-cohort-exact.raw"
docker buildx imagetools inspect "$cohort_reference" --raw > "$exact_raw"
if ! cmp -s "$cohort_raw" "$exact_raw"; then
echo "ERROR: staged cohort alias bytes do not match their exact digest: $cohort_alias" >&2
exit 1
fi
jq -cn \
--arg agent "$agent" \
--arg alias "$cohort_alias" \
--arg digest "$cohort_digest" \
--arg image "$image" \
--arg reference "$cohort_reference" \
--slurpfile descriptor "$cohort_descriptor" \
--slurpfile platforms "$agent_candidates" \
'{
agent: $agent,
image: $image,
digest: $digest,
reference: $reference,
descriptor: $descriptor[0],
alias: $alias,
platforms: ($platforms[0] | map({
key: .platform,
value: {
digest,
reference,
baseReference,
publicationEvidence
}
}) | from_entries)
}' >> "$manifests"
done
cohort_manifests="$RUNNER_TEMP/managed-image-cohort-manifests.json"
jq -s 'sort_by(.agent)' "$manifests" > "$cohort_manifests"
if ! jq -e '
length == 3
and ([.[].agent] | sort) == [
"hermes",
"langchain-deepagents-code",
"openclaw"
]
and all(.[];
(.digest | test("^sha256:[0-9a-f]{64}$"))
and .reference == (.image + "@" + .digest)
and .descriptor == {
mediaType: "application/vnd.oci.image.index.v1+json",
digest: .digest,
size: .descriptor.size
}
and (.descriptor.size | type) == "number"
and .descriptor.size > 0
and (.platforms | keys | sort) == ["linux/amd64", "linux/arm64"]
and all(.platforms[];
(.digest | test("^sha256:[0-9a-f]{64}$"))
and (.baseReference | test("@sha256:[0-9a-f]{64}$"))
and .publicationEvidence.candidateDescriptor.digest == .digest
and .publicationEvidence.attestations.manifestDescriptor.annotations[
"vnd.docker.reference.digest"
] == .publicationEvidence.workloadDescriptor.digest
and .publicationEvidence.attestations.slsa.statement.subject.digest ==
.publicationEvidence.workloadDescriptor.digest
and .publicationEvidence.attestations.spdx.statement.subject.digest ==
.publicationEvidence.workloadDescriptor.digest
)
)
' "$cohort_manifests" >/dev/null; then
echo "ERROR: staged multi-platform cohort failed exact validation." >&2
exit 1
fi
# Prove that every staged agent alias is anonymously pullable on both
# supported architectures before the single consumer pointer moves.
anonymous_config="$(mktemp -d "$RUNNER_TEMP/anonymous-cohort-docker-XXXXXX")"
chmod 0700 "$anonymous_config"
while IFS= read -r manifest; do
cohort_reference="$(jq -r '.reference' <<<"$manifest")"
for platform in linux/amd64 linux/arm64; do
DOCKER_CONFIG="$anonymous_config" docker pull \
--platform "$platform" \
"$cohort_reference"
docker image rm "$cohort_reference" >/dev/null
done
done < <(jq -c '.[]' "$cohort_manifests")
# Record the intended OpenClaw root pointers in every exact contract.
# They are not moved until all v2 evidence artifacts are durable.
openclaw_manifest="$(jq -ce '.[] | select(.agent == "openclaw")' "$cohort_manifests")"
consumer_aliases=("$(jq -r '.image' <<<"$openclaw_manifest"):${GITHUB_SHA}")
release_tag="$(jq -r '.[0].release // empty' "$CANDIDATE_SET")"
if [ -n "$release_tag" ]; then
consumer_aliases+=("$(jq -r '.image' <<<"$openclaw_manifest"):${release_tag}")
fi
while IFS= read -r candidate; do
agent="$(jq -r '.agent' <<<"$candidate")"
platform="$(jq -r '.platform' <<<"$candidate")"
artifact_platform="${platform//\//-}"
cohort_manifest="$(jq -ce --arg agent "$agent" '.[] | select(.agent == $agent)' "$cohort_manifests")"
cohort_alias="$(jq -r '.alias' <<<"$cohort_manifest")"
staged_cohort="$(
jq -c '{alias, descriptor, reference}' <<<"$cohort_manifest"
)"
aliases=("$cohort_alias")
if [ "$agent" = "openclaw" ]; then
aliases+=("${consumer_aliases[@]}")
fi
aliases_json="$(printf '%s\n' "${aliases[@]}" | jq -R . | jq -s .)"
contract_dir="$contract_root/$agent/$artifact_platform"
mkdir -p "$contract_dir"
jq \
--argjson aliases "$aliases_json" \
--argjson stagedCohort "$staged_cohort" \
'{
contractVersion,
agent,
image,
digest,
reference,
baseReference,
platform,
publicationEvidence,
stagedCohort: $stagedCohort,
source: {
repository: .source.repository,
revision: .source.revision,
release,
cohort: .source.cohort
},
run,
aliases: $aliases
}' <<<"$candidate" > "$contract_dir/contract.json"
if ! jq -e \
--arg agent "$agent" \
--arg platform "$platform" \
--arg cohort "$cohort" \
--argjson aliases "$aliases_json" \
'(keys | sort) == [
"agent",
"aliases",
"baseReference",
"contractVersion",
"digest",
"image",
"platform",
"publicationEvidence",
"reference",
"run",
"source",
"stagedCohort"
]
and .contractVersion == 2
and .agent == $agent
and .platform == $platform
and (.digest | test("^sha256:[0-9a-f]{64}$"))
and .reference == (.image + "@" + .digest)
and (.baseReference | test("@sha256:[0-9a-f]{64}$"))
and .publicationEvidence.candidateDescriptor.digest == .digest
and .publicationEvidence.workloadDescriptor.platform == (
$platform | split("/") | {os: .[0], architecture: .[1]}
)
and .publicationEvidence.attestations.manifestDescriptor.annotations[
"vnd.docker.reference.digest"
] == .publicationEvidence.workloadDescriptor.digest
and .stagedCohort.alias == (.image + ":cohort-" + $cohort)
and .stagedCohort.reference == (.image + "@" + .stagedCohort.descriptor.digest)
and .stagedCohort.descriptor.mediaType ==
"application/vnd.oci.image.index.v1+json"
and (.stagedCohort.descriptor.digest | test("^sha256:[0-9a-f]{64}$"))
and (.stagedCohort.descriptor.size | type) == "number"
and .stagedCohort.descriptor.size > 0
and .source.revision == $ENV.GITHUB_SHA
and .source.cohort == $cohort
and .run == {
id: ($ENV.GITHUB_RUN_ID | tonumber),
attempt: ($ENV.GITHUB_RUN_ATTEMPT | tonumber)
}
and .aliases == $aliases
and (
(.image + ":cohort-" + $cohort) as $cohortAlias
| (.aliases | index($cohortAlias)) != null
)' "$contract_dir/contract.json" >/dev/null; then
echo "ERROR: final exact platform contract failed validation: $agent/$platform" >&2
exit 1
fi
done < <(jq -c '.[]' "$CANDIDATE_SET")
cohort_contract="$contract_root/cohort.json"
jq -n \
--arg repository "$GITHUB_REPOSITORY" \
--arg revision "$GITHUB_SHA" \
--arg release "$release_tag" \
--arg cohort "$cohort" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
--slurpfile agents "$cohort_manifests" \
'{
contractVersion: 2,
cohort: $cohort,
source: {
repository: $repository,
revision: $revision,
release: (if $release == "" then null else $release end)
},
run: {id: $runId, attempt: $runAttempt},
platforms: ["linux/amd64", "linux/arm64"],
agents: ($agents[0] | map({key: .agent, value: del(.agent)}) | from_entries)
}' > "$cohort_contract"
jq -e \
--arg cohort "$cohort" \
--arg revision "$GITHUB_SHA" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
'(keys | sort) == [
"agents",
"cohort",
"contractVersion",
"platforms",
"run",
"source"
]
and .contractVersion == 2
and .cohort == $cohort
and .source.revision == $revision
and .run == {id: $runId, attempt: $runAttempt}
and .platforms == ["linux/amd64", "linux/arm64"]
and (.agents | keys | sort) == [
"hermes",
"langchain-deepagents-code",
"openclaw"
]
and all(.agents[];
(.digest | test("^sha256:[0-9a-f]{64}$"))
and .reference == (.image + "@" + .digest)
and .descriptor == {
mediaType: "application/vnd.oci.image.index.v1+json",
digest: .digest,
size: .descriptor.size
}
and (.descriptor.size | type) == "number"
and .descriptor.size > 0
and .alias == (.image + ":cohort-" + $cohort)
and (.platforms | keys | sort) == ["linux/amd64", "linux/arm64"]
and all(.platforms[];
.publicationEvidence.candidateDescriptor.digest == .digest
and .publicationEvidence.attestations.manifestDescriptor.annotations[
"vnd.docker.reference.digest"
] == .publicationEvidence.workloadDescriptor.digest
)
)' "$cohort_contract" >/dev/null
- name: Upload complete managed image cohort contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-cohort-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/managed-image-contracts/cohort.json
if-no-files-found: error
retention-days: 90
- name: Upload OpenClaw amd64 managed image contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-${{ github.run_id }}-${{ github.run_attempt }}-openclaw-linux-amd64
path: ${{ runner.temp }}/managed-image-contracts/openclaw/linux-amd64/contract.json
if-no-files-found: error
retention-days: 90
- name: Upload OpenClaw arm64 managed image contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-${{ github.run_id }}-${{ github.run_attempt }}-openclaw-linux-arm64
path: ${{ runner.temp }}/managed-image-contracts/openclaw/linux-arm64/contract.json
if-no-files-found: error
retention-days: 90
- name: Upload Hermes amd64 managed image contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-${{ github.run_id }}-${{ github.run_attempt }}-hermes-linux-amd64
path: ${{ runner.temp }}/managed-image-contracts/hermes/linux-amd64/contract.json
if-no-files-found: error
retention-days: 90
- name: Upload Hermes arm64 managed image contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-${{ github.run_id }}-${{ github.run_attempt }}-hermes-linux-arm64
path: ${{ runner.temp }}/managed-image-contracts/hermes/linux-arm64/contract.json
if-no-files-found: error
retention-days: 90
- name: Upload Deep Agents Code amd64 managed image contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-${{ github.run_id }}-${{ github.run_attempt }}-langchain-deepagents-code-linux-amd64
path: ${{ runner.temp }}/managed-image-contracts/langchain-deepagents-code/linux-amd64/contract.json
if-no-files-found: error
retention-days: 90
- name: Upload Deep Agents Code arm64 managed image contract
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: managed-image-${{ github.run_id }}-${{ github.run_attempt }}-langchain-deepagents-code-linux-arm64
path: ${{ runner.temp }}/managed-image-contracts/langchain-deepagents-code/linux-arm64/contract.json
if-no-files-found: error
retention-days: 90
# Keep this final: an evidence-artifact upload failure must never leave a
# consumer root pointing at a cohort without its durable v2 contracts.
- name: Promote durable managed image cohort pointers
shell: bash
run: |
set -euo pipefail
cohort_contract="$RUNNER_TEMP/managed-image-contracts/cohort.json"
if [ ! -f "$cohort_contract" ] || [ -L "$cohort_contract" ]; then
echo "ERROR: durable managed image cohort contract is missing or unsafe." >&2
exit 1
fi
cohort="ghrun-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
if ! jq -e \
--arg cohort "$cohort" \
--arg repository "$GITHUB_REPOSITORY" \
--arg revision "$GITHUB_SHA" \
--argjson runAttempt "$GITHUB_RUN_ATTEMPT" \
--argjson runId "$GITHUB_RUN_ID" \
'
.contractVersion == 2
and .cohort == $cohort
and .source.repository == $repository
and .source.revision == $revision
and .run == {id: $runId, attempt: $runAttempt}
and .agents.openclaw.reference ==
(.agents.openclaw.image + "@" + .agents.openclaw.digest)
and .agents.openclaw.descriptor == {
mediaType: "application/vnd.oci.image.index.v1+json",
digest: .agents.openclaw.digest,
size: .agents.openclaw.descriptor.size
}
and (.agents.openclaw.descriptor.size | type) == "number"
and .agents.openclaw.descriptor.size > 0
' "$cohort_contract" >/dev/null; then
echo "ERROR: durable managed image cohort contract cannot authorize promotion." >&2
exit 1
fi
image="$(jq -er '.agents.openclaw.image' "$cohort_contract")"
exact_reference="$(jq -er '.agents.openclaw.reference' "$cohort_contract")"
expected_digest="$(jq -er '.agents.openclaw.digest' "$cohort_contract")"
expected_size="$(jq -er '.agents.openclaw.descriptor.size | tostring' "$cohort_contract")"
release_tag="$(jq -r '.source.release // empty' "$cohort_contract")"
if [[ ! "$image" =~ ^ghcr\.io/nvidia/nemoclaw/openclaw-sandbox$ ]] ||
[[ ! "$exact_reference" =~ ^ghcr\.io/nvidia/nemoclaw/openclaw-sandbox@sha256:[0-9a-f]{64}$ ]] ||
[[ ! "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "ERROR: durable managed image pointer identity is invalid." >&2
exit 1
fi
consumer_aliases=("${image}:${GITHUB_SHA}")
if [ -n "$release_tag" ]; then
if [[ ! "$release_tag" =~ ^v[0-9]+([.][0-9]+){1,3}([-.][0-9A-Za-z][0-9A-Za-z.-]*)?$ ]]; then
echo "ERROR: durable managed image release identity is invalid." >&2
exit 1
fi
consumer_aliases+=("${image}:${release_tag}")
fi
exact_raw="$RUNNER_TEMP/managed-image-openclaw-cohort-exact.raw"
docker buildx imagetools inspect "$exact_reference" --raw > "$exact_raw"
actual_digest="sha256:$(sha256sum "$exact_raw" | awk '{print $1}')"
actual_size="$(wc -c < "$exact_raw" | tr -d '[:space:]')"
if [ "$actual_digest" != "$expected_digest" ] ||
[ "$actual_size" != "$expected_size" ]; then
echo "ERROR: exact OpenClaw cohort bytes differ from the durable contract." >&2
exit 1
fi
consumer_tag_args=()
for alias in "${consumer_aliases[@]}"; do
consumer_tag_args+=(--tag "$alias")
done
docker buildx imagetools create "${consumer_tag_args[@]}" "$exact_reference"
for alias in "${consumer_aliases[@]}"; do
alias_raw="$RUNNER_TEMP/managed-image-openclaw-pointer-${alias##*:}.raw"
docker buildx imagetools inspect "$alias" --raw > "$alias_raw"
if ! cmp -s "$exact_raw" "$alias_raw"; then
echo "ERROR: OpenClaw cohort pointer is not exact: $alias" >&2
exit 1
fi
done