fix(portable): separate Podman activation readiness #2621
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |