diff --git a/.github/workflows/base-image.yaml b/.github/workflows/base-image.yaml index acc18c16902..2c5b3f4867e 100644 --- a/.github/workflows/base-image.yaml +++ b/.github/workflows/base-image.yaml @@ -34,18 +34,20 @@ on: # with tools/e2e/base-image-publication.mts. - "Dockerfile" - "agents/**" + - "ci/pi-agent-qualification-v1-*.json" - "nemoclaw/**" - "nemoclaw-blueprint/**" - "scripts/**" - "test/e2e/live/managed-image-activation-e2e*.ts" + - "test/e2e/live/mcp-bridge*.ts" - "src/lib/actions/sandbox/mcp-bridge-*.ts" - "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json" + - "src/lib/actions/sandbox/rebuild-post-restore-phase.ts" + - "src/lib/agent/candidate-authority.ts" - "src/lib/core/json-types.ts" - "src/lib/core/ports.ts" - "src/lib/messaging/**" - - "src/lib/onboard/managed-bootstrap/envelope.ts" - - "src/lib/onboard/managed-startup/**" - - "src/lib/onboard/managed-workload/onboard-orchestration.ts" + - "src/lib/onboard/**" - "src/lib/security/credential-hash.ts" - "src/lib/state/paths.ts" - "src/lib/state/state-root.ts" diff --git a/.github/workflows/e2e-standard-profile.yaml b/.github/workflows/e2e-standard-profile.yaml index cf9b4b2fee3..e773dac0aa5 100644 --- a/.github/workflows/e2e-standard-profile.yaml +++ b/.github/workflows/e2e-standard-profile.yaml @@ -27,6 +27,9 @@ on: managed_image_receipt: required: true type: string + managed_image_catalog: + required: true + type: string workload_source: required: true type: string @@ -113,6 +116,7 @@ jobs: env: E2E_JOB: "1" E2E_MANAGED_IMAGE_REVISION: ${{ inputs.managed_image_revision }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ inputs.managed_image_catalog }} E2E_WORKLOAD_SOURCE: ${{ inputs.workload_source }} E2E_TARGET_ID: ${{ inputs.target_id }} NEMOCLAW_RUN_LIVE_E2E: "1" diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 4c3a21ab0eb..46ba60c0d30 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -163,7 +163,8 @@ jobs: dcode_base_ref: ${{ steps.validate_dcode_base.outputs.base_ref }} managed_image_receipt: ${{ steps.validate_managed_cohort.outputs.receipt }} managed_image_revision: ${{ steps.validate_managed_cohort.outputs.revision }} - workload_source: ${{ steps.select_pr_source.outputs.workload_source || 'managed-image' }} + managed_image_catalog: ${{ steps.select_pr_source.outputs.catalog }} + workload_source: managed-image permissions: actions: read contents: read @@ -218,7 +219,7 @@ jobs: node-version: 22 - id: select_pr_source - name: Select PR workload source + name: Resolve exact PR managed-image publication if: ${{ inputs.pr_number != '' }} env: BASE_SHA: ${{ inputs.base_sha }} @@ -229,12 +230,32 @@ jobs: shell: bash run: | set -euo pipefail - workload_source="$(node --experimental-strip-types --no-warnings tools/e2e/pr-managed-image-publication.mts select-source)" - case "$workload_source" in - managed-image|local-dockerfile) ;; - *) echo "::error::PR workload source is invalid" >&2; exit 1 ;; + catalog_path="${RUNNER_TEMP}/pr-managed-image-catalog.json" + rm -f -- "$catalog_path" + selection="$(node --experimental-strip-types --no-warnings tools/e2e/pr-managed-image-publication.mts "$catalog_path")" + case "$selection" in + base-cohort) + [[ ! -e "$catalog_path" && ! -L "$catalog_path" ]] || { + echo "::error::base-cohort selection produced a candidate catalog" >&2 + exit 1 + } + ;; + candidate-catalog) + [[ -f "$catalog_path" && ! -L "$catalog_path" && -s "$catalog_path" ]] || { + echo "::error::exact PR managed-image catalog is invalid" >&2 + exit 1 + } + catalog="$(jq -ce . "$catalog_path")" + (( ${#catalog} <= 65536 )) || { + echo "::error::exact PR managed-image catalog exceeds the output limit" >&2 + exit 1 + } + printf 'catalog=%s\n' "$catalog" >>"$GITHUB_OUTPUT" + echo "::notice::Jetson dispatch is unavailable because the exact PR managed-image catalog qualifies linux/amd64 only." + ;; + *) echo "::error::PR managed-image selection is invalid" >&2; exit 1 ;; esac - printf 'workload_source=%s\n' "$workload_source" >>"$GITHUB_OUTPUT" + printf 'selection=%s\n' "$selection" >>"$GITHUB_OUTPUT" - id: publication name: Select base and optional managed-image publication @@ -242,8 +263,8 @@ jobs: EXPECTED_SHA: ${{ steps.publication_mode.outputs.expected_sha }} GITHUB_TOKEN: ${{ github.token }} PUBLICATION_HISTORY_ALLOW_NON_HEAD: ${{ steps.publication_mode.outputs.allow_non_head }} - REQUIRE_MANAGED_IMAGE_PUBLICATION: ${{ steps.select_pr_source.outputs.workload_source == 'local-dockerfile' && '0' || '1' }} - SELECT_NEAREST_SUCCESSFUL_PUBLICATION: ${{ steps.select_pr_source.outputs.workload_source == 'local-dockerfile' && '0' || steps.publication_mode.outputs.select_nearest_successful }} + REQUIRE_MANAGED_IMAGE_PUBLICATION: ${{ steps.select_pr_source.outputs.selection == 'candidate-catalog' && '0' || '1' }} + SELECT_NEAREST_SUCCESSFUL_PUBLICATION: ${{ steps.select_pr_source.outputs.selection == 'candidate-catalog' && '0' || steps.publication_mode.outputs.select_nearest_successful }} shell: bash run: | set -euo pipefail @@ -273,7 +294,7 @@ jobs: - id: download_managed_cohort name: Download immutable managed-image cohort contract - if: ${{ inputs.pr_number == '' || steps.select_pr_source.outputs.workload_source == 'managed-image' }} + if: ${{ inputs.pr_number == '' || steps.select_pr_source.outputs.selection == 'base-cohort' }} env: GITHUB_TOKEN: ${{ github.token }} PUBLICATION_ARTIFACT_KIND: managed-image-cohort @@ -284,7 +305,7 @@ jobs: - id: validate_managed_cohort name: Validate immutable managed-image cohort contract - if: ${{ inputs.pr_number == '' || steps.select_pr_source.outputs.workload_source == 'managed-image' }} + if: ${{ inputs.pr_number == '' || steps.select_pr_source.outputs.selection == 'base-cohort' }} env: PUBLICATION_HEAD_SHA: ${{ steps.publication.outputs.head_sha }} PUBLICATION_RUN_ATTEMPT: ${{ steps.publication.outputs.run_attempt }} @@ -358,8 +379,8 @@ jobs: run: | set -euo pipefail - [[ "$WORKFLOW_EVENT" == "workflow_dispatch" && "$WORKFLOW_REF" == refs/heads/* ]] || { - echo "::error::Manual PR E2E must be dispatched from this repository branch" >&2 + [[ "$WORKFLOW_EVENT" == "workflow_dispatch" && "$WORKFLOW_REF" == "refs/heads/main" ]] || { + echo "::error::Manual PR E2E must be dispatched from trusted main" >&2 exit 1 } [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::pr_number must be a positive integer" >&2; exit 1; } @@ -725,7 +746,7 @@ jobs: if [[ "$WORKFLOW_REPOSITORY" == "NVIDIA/NemoClaw" && "$NVIDIA_OWNED" == "true" && "$EVENT_NAME" == "workflow_dispatch" && - "$REF" == refs/heads/* && + "$REF" == "refs/heads/main" && "$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$ && "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA" && @@ -2676,6 +2697,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF: ${{ needs.generate-matrix.outputs.workload_source == 'managed-image' && needs.base-image-publication.outputs.dcode_base_ref || '' }} NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js NEMOCLAW_RUN_LIVE_E2E: "1" @@ -2971,6 +2993,7 @@ jobs: cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} managed_image_revision: ${{ needs.base-image-publication.outputs.managed_image_revision }} managed_image_receipt: ${{ needs.base-image-publication.outputs.managed_image_receipt }} + managed_image_catalog: ${{ needs.base-image-publication.outputs.managed_image_catalog }} workload_source: ${{ needs.generate-matrix.outputs.workload_source }} credential_boundary: no provider credential target_id: ${{ matrix.target_id }} @@ -3013,6 +3036,7 @@ jobs: cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} managed_image_revision: ${{ needs.base-image-publication.outputs.managed_image_revision }} managed_image_receipt: ${{ needs.base-image-publication.outputs.managed_image_receipt }} + managed_image_catalog: ${{ needs.base-image-publication.outputs.managed_image_catalog }} workload_source: ${{ needs.generate-matrix.outputs.workload_source }} credential_boundary: NVIDIA API key target_id: ${{ matrix.target_id }} @@ -3056,6 +3080,7 @@ jobs: cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} managed_image_revision: ${{ needs.base-image-publication.outputs.managed_image_revision }} managed_image_receipt: ${{ needs.base-image-publication.outputs.managed_image_receipt }} + managed_image_catalog: ${{ needs.base-image-publication.outputs.managed_image_catalog }} workload_source: ${{ needs.generate-matrix.outputs.workload_source }} credential_boundary: NVIDIA inference API key target_id: ${{ matrix.target_id }} @@ -3099,6 +3124,7 @@ jobs: cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} managed_image_revision: ${{ needs.base-image-publication.outputs.managed_image_revision }} managed_image_receipt: ${{ needs.base-image-publication.outputs.managed_image_receipt }} + managed_image_catalog: ${{ needs.base-image-publication.outputs.managed_image_catalog }} workload_source: ${{ needs.generate-matrix.outputs.workload_source }} credential_boundary: GitHub read token target_id: ${{ matrix.target_id }} @@ -3142,6 +3168,7 @@ jobs: cli_artifact_provenance: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} managed_image_revision: ${{ needs.base-image-publication.outputs.managed_image_revision }} managed_image_receipt: ${{ needs.base-image-publication.outputs.managed_image_receipt }} + managed_image_catalog: ${{ needs.base-image-publication.outputs.managed_image_catalog }} workload_source: ${{ needs.generate-matrix.outputs.workload_source }} credential_boundary: Brave and NVIDIA inference API keys target_id: ${{ matrix.target_id }} @@ -3340,6 +3367,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "mcp-bridge" E2E_OBSERVABLE_OUTCOME: "Stable OpenShell MCP bridge reaches tools and inference" @@ -3675,6 +3703,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "openshell-credential-generation-window" E2E_AGENT_RUNTIME: "openclaw" @@ -3850,6 +3879,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "mcp-bridge-dev" E2E_OBSERVABLE_OUTCOME: "Development OpenShell MCP bridge reaches tools and inference" @@ -5021,6 +5051,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "hermes-e2e" E2E_AGENT_RUNTIME: "hermes" @@ -5131,6 +5162,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "hermes-gpu-startup" E2E_AGENT_RUNTIME: "hermes" @@ -5392,7 +5424,7 @@ jobs: jetson-nvmap-gpu: needs: [base-image-publication, generate-matrix] - if: ${{ always() && needs['base-image-publication'].result == 'success' && needs['generate-matrix'].result == 'success' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }} + if: ${{ always() && needs['base-image-publication'].result == 'success' && needs['base-image-publication'].outputs.managed_image_revision != '' && needs['generate-matrix'].result == 'success' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }} concurrency: group: jetson-nvmap-gpu-dispatch cancel-in-progress: false @@ -5438,6 +5470,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "cloud-onboard" E2E_AGENT_RUNTIME: "openclaw" @@ -5569,6 +5602,7 @@ jobs: E2E_MANAGED_IMAGE_REVISION: ${{ needs.base-image-publication.outputs.managed_image_revision }} E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.base-image-publication.outputs.managed_image_receipt }} E2E_WORKLOAD_SOURCE: ${{ needs.generate-matrix.outputs.workload_source }} + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: ${{ needs.base-image-publication.outputs.managed_image_catalog }} E2E_JOB: "1" E2E_TARGET_ID: "messaging-providers" E2E_AGENT_RUNTIME: "openclaw" diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 89f2f5efa59..9ddc6d65748 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -870,6 +870,8 @@ jobs: E2E_JOB: "1" E2E_TARGET_ID: managed-image-activation NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js + NEMOCLAW_E2E_EXPECTED_SHA: ${{ github.event.pull_request.head.sha }} + NEMOCLAW_E2E_SHARD: default NEMOCLAW_MANAGED_ACTIVATION_CATALOG: ${{ github.workspace }}/managed-pr-catalog.json NEMOCLAW_NON_INTERACTIVE: "1" NEMOCLAW_RUN_LIVE_E2E: "1" @@ -887,6 +889,15 @@ jobs: with: node-version: 22.19.0 + - name: Bind E2E correlation identity + shell: bash + run: | + set -euo pipefail + correlation_id="$(node --input-type=module -e \ + 'import { randomUUID } from "node:crypto"; console.log(randomUUID())')" + [[ "$correlation_id" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$ ]] + printf 'NEMOCLAW_E2E_CORRELATION_ID=%s\n' "$correlation_id" >> "$GITHUB_ENV" + - name: Download exact published all-agent contracts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/pr-self-hosted.yaml b/.github/workflows/pr-self-hosted.yaml index c7e2ff48b84..e4413dd45d4 100644 --- a/.github/workflows/pr-self-hosted.yaml +++ b/.github/workflows/pr-self-hosted.yaml @@ -114,7 +114,9 @@ jobs: env: EXPECTED_SHA: ${{ steps.changed.outputs.base_sha }} GITHUB_TOKEN: ${{ github.token }} + PUBLICATION_HISTORY_ALLOW_NON_HEAD: "1" REQUIRE_MANAGED_IMAGE_PUBLICATION: "1" + SELECT_NEAREST_SUCCESSFUL_PUBLICATION: "1" shell: bash run: | set -euo pipefail diff --git a/scripts/checks/run-managed-image-direct-e2e.ts b/scripts/checks/run-managed-image-direct-e2e.ts index 7678c857b11..21c1317adc8 100755 --- a/scripts/checks/run-managed-image-direct-e2e.ts +++ b/scripts/checks/run-managed-image-direct-e2e.ts @@ -281,7 +281,10 @@ function managedConfig(agent: ShippedManagedImageAgent): string { } function waitForAgentCommand(containerId: string): void { - const deadline = Date.now() + 120_000; + // The image-owned hold allows up to 600 seconds for managed startup + // completion. Keep this observer from abandoning a still-running container + // before that bounded product wait can finish. + const deadline = Date.now() + 600_000; while (Date.now() < deadline) { const ready = docker( [ diff --git a/src/lib/onboard/managed-workload/onboard-orchestration.ts b/src/lib/onboard/managed-workload/onboard-orchestration.ts index 0f0d5fde99c..c502126df10 100644 --- a/src/lib/onboard/managed-workload/onboard-orchestration.ts +++ b/src/lib/onboard/managed-workload/onboard-orchestration.ts @@ -236,7 +236,9 @@ export function createManagedWorkloadOnboardRuntime( : { ...discoveredRuntimeCapabilities, managedImageSelectionPolicy: "prefer-managed" as const, - managedImages: input.stockManagedRuntime ? discoveredRuntimeCapabilities.managedImages : null, + managedImages: input.stockManagedRuntime + ? discoveredRuntimeCapabilities.managedImages + : null, }; const runtimeProvider = resolveRuntimeProviderBundle( input.computePlan.driverName, @@ -273,6 +275,9 @@ export function createManagedWorkloadOnboardRuntime( customDockerfilePath: input.customDockerfilePath, runtime: runtimeCapabilities, version: getVersion({ rootDir: input.rootDir }), + ...(!input.tempManagedRuntimeCatalog && liveCatalog?.catalog + ? { catalog: liveCatalog.catalog } + : {}), catalogPath: input.tempManagedRuntimeCatalog ?? liveCatalog?.path ?? null, ...(liveCatalog ? { expectedCatalogRevision: liveCatalog.revision } : {}), ...(catalogRevision ? { catalogRevision } : {}), diff --git a/src/lib/onboard/sandbox-workload-preparation.test.ts b/src/lib/onboard/sandbox-workload-preparation.test.ts index b7932724ea1..190d177f2e8 100644 --- a/src/lib/onboard/sandbox-workload-preparation.test.ts +++ b/src/lib/onboard/sandbox-workload-preparation.test.ts @@ -28,6 +28,7 @@ import { installedManagedImageCatalogRevision, liveE2eManagedImageCatalog, prepareSandboxWorkloadSource, + readLiveE2eManagedImageCatalogContracts, SandboxWorkloadPreparationError, } from "./workload/preparation"; import { resolveSandboxWorkloadRuntimeCapabilities } from "./workload/runtime"; @@ -179,6 +180,14 @@ describe("sandbox workload preparation", () => { NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath, }), ).toEqual({ path: catalogPath, revision: REVISION }); + expect( + liveE2eManagedImageCatalog({ + GITHUB_ACTIONS: "true", + NEMOCLAW_RUN_LIVE_E2E: "1", + NEMOCLAW_E2E_EXPECTED_SHA: REVISION, + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: JSON.stringify(CATALOG), + }), + ).toEqual({ catalog: CATALOG, revision: REVISION }); expect( liveE2eManagedImageCatalog({ GITHUB_ACTIONS: "true", @@ -194,6 +203,13 @@ describe("sandbox workload preparation", () => { NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath, }), ).toBeNull(); + expect( + liveE2eManagedImageCatalog({ + GITHUB_ACTIONS: "true", + GITHUB_WORKSPACE: path.join(fixtureRoot, "empty-workspace"), + NEMOCLAW_RUN_LIVE_E2E: "1", + }), + ).toBeNull(); expect( liveE2eManagedImageCatalog({ GITHUB_ACTIONS: "true", @@ -219,6 +235,49 @@ describe("sandbox workload preparation", () => { NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath, }), ).toThrow("requires an exact candidate revision"); + expect(() => + liveE2eManagedImageCatalog({ + GITHUB_ACTIONS: "true", + NEMOCLAW_RUN_LIVE_E2E: "1", + NEMOCLAW_E2E_EXPECTED_SHA: REVISION, + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath, + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: JSON.stringify(CATALOG), + }), + ).toThrow("conflicting authorities"); + } finally { + fs.rmSync(fixtureRoot, { force: true, recursive: true }); + } + }); + + it("validates every contract in an inline live E2E catalog", () => { + const selected = liveE2eManagedImageCatalog({ + GITHUB_ACTIONS: "true", + NEMOCLAW_RUN_LIVE_E2E: "1", + NEMOCLAW_E2E_EXPECTED_SHA: REVISION, + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: JSON.stringify(CATALOG), + }); + + expect(selected).not.toBeNull(); + expect(readLiveE2eManagedImageCatalogContracts(selected!)).toEqual( + new Map(SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => [agent, contract(agent, index)])), + ); + }); + + it("reads a regular live E2E catalog without following a symbolic link", () => { + const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-live-e2e-catalog-")); + const catalogPath = path.join(fixtureRoot, "catalog.json"); + const symlinkPath = path.join(fixtureRoot, "catalog-link.json"); + fs.writeFileSync(catalogPath, JSON.stringify(CATALOG), { mode: 0o600 }); + fs.symlinkSync(catalogPath, symlinkPath); + try { + expect( + readLiveE2eManagedImageCatalogContracts({ path: catalogPath, revision: REVISION }), + ).toEqual( + new Map(SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => [agent, contract(agent, index)])), + ); + expect(() => + readLiveE2eManagedImageCatalogContracts({ path: symlinkPath, revision: REVISION }), + ).toThrow("must be a bounded regular file"); } finally { fs.rmSync(fixtureRoot, { force: true, recursive: true }); } @@ -364,6 +423,21 @@ describe("sandbox workload preparation", () => { } }); + it("loads an exact inline all-agent catalog without using the registry resolver", async () => { + const resolveCatalog = vi.fn(async () => CATALOG); + + const prepared = await prepareSandboxWorkloadSource( + { ...input("hermes"), catalog: CATALOG, expectedCatalogRevision: REVISION }, + { resolveCatalog }, + ); + + expect(resolveCatalog).not.toHaveBeenCalled(); + expect(prepared.source).toMatchObject({ + kind: "managed-image", + contract: { source: { revision: REVISION } }, + }); + }); + it("rejects a symlinked local managed-image catalog before selection (#7744)", async () => { const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-catalog-")); const catalogPath = path.join(fixtureRoot, "catalog.json"); @@ -673,7 +747,7 @@ describe("sandbox workload preparation", () => { { ...input("pi"), acceptedCandidateContract: contract("pi", 3) }, { resolveCatalog }, ), - ).rejects.toThrow("requires an exact managed image catalog file"); + ).rejects.toThrow("requires an exact managed image catalog"); expect(resolveCatalog).not.toHaveBeenCalled(); }); diff --git a/src/lib/onboard/workload/preparation.ts b/src/lib/onboard/workload/preparation.ts index 4e1f70ad908..8995c554d97 100644 --- a/src/lib/onboard/workload/preparation.ts +++ b/src/lib/onboard/workload/preparation.ts @@ -19,6 +19,7 @@ import { type ManagedImageContractCatalog, type ManagedImageContractV1, type ManagedImagePlatform, + type ShippedManagedImageAgent, parseManagedImageContractV1, SHIPPED_MANAGED_IMAGE_AGENTS, } from "../managed-image/contract"; @@ -47,6 +48,7 @@ export interface PrepareSandboxWorkloadSourceInput { readonly runtime: SandboxWorkloadRuntimeCapabilities; readonly version: string; readonly policy?: ManagedImageSelectionPolicy; + readonly catalog?: ManagedImageContractCatalog | null; readonly catalogPath?: string | null; readonly expectedCatalogRevision?: string | null; readonly catalogRevision?: string | null; @@ -111,9 +113,30 @@ export function installedManagedImageCatalogRevision( return identity.sourceRevision; } -export interface LiveE2eManagedImageCatalog { - readonly path: string; - readonly revision: string; +export type LiveE2eManagedImageCatalog = + | { + readonly catalog: ManagedImageContractCatalog; + readonly path?: never; + readonly revision: string; + } + | { readonly catalog?: never; readonly path: string; readonly revision: string }; + +function parseInlineManagedImageCatalog(value: string): ManagedImageContractCatalog { + const size = Buffer.byteLength(value, "utf8"); + if (size < 2 || size > 64 * 1024) { + throw new SandboxWorkloadPreparationError( + "the live E2E managed-image catalog must be bounded JSON", + ); + } + try { + const parsed: unknown = JSON.parse(value); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(); + return parsed as ManagedImageContractCatalog; + } catch { + throw new SandboxWorkloadPreparationError( + "the live E2E managed-image catalog must be bounded JSON", + ); + } } /** Select the trusted PR catalog only for an exact live E2E candidate. */ @@ -123,11 +146,26 @@ export function liveE2eManagedImageCatalog( if (environment.GITHUB_ACTIONS !== "true" || environment.NEMOCLAW_RUN_LIVE_E2E !== "1") { return null; } + const inlineCatalog = environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON?.trim(); const configuredPath = environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG?.trim(); const workspace = environment.GITHUB_WORKSPACE?.trim(); const catalogPath = configuredPath || (workspace ? path.join(workspace, "dist", "e2e-managed-image-catalog.json") : ""); + if (inlineCatalog && configuredPath) { + throw new SandboxWorkloadPreparationError( + "the live E2E managed-image catalog has conflicting authorities", + ); + } + if (inlineCatalog) { + const revision = environment.NEMOCLAW_E2E_EXPECTED_SHA?.trim() ?? ""; + if (!/^[0-9a-f]{40}$/u.test(revision)) { + throw new SandboxWorkloadPreparationError( + "the live E2E managed-image catalog requires an exact candidate revision", + ); + } + return { catalog: parseInlineManagedImageCatalog(inlineCatalog), revision }; + } if (!catalogPath) return null; try { fs.lstatSync(catalogPath); @@ -230,13 +268,19 @@ function unavailableResult( function requireCompleteManagedImageCatalog( catalog: ManagedImageContractCatalog, - expectedRelease: string, - expectedPlatform: ManagedImagePlatform, + expectedRelease: string | null, + expectedPlatform: ManagedImagePlatform | null, expectedRevision: string | null, -): { readonly release: string; readonly revision: string } { +): { + readonly contracts: ReadonlyMap; + readonly release: string; + readonly revision: string; +} { + const contracts = new Map(); let cohortRevision: string | null = null; let cohortRelease: string | null = null; let publicationCohort: string | null = null; + let cohortPlatform = expectedPlatform; for (const agent of SHIPPED_MANAGED_IMAGE_AGENTS) { const candidate = catalog[agent]; if (candidate === undefined) { @@ -245,8 +289,17 @@ function requireCompleteManagedImageCatalog( ); } try { - const contract = parseManagedImageContractV1(candidate, agent, expectedPlatform); - if (expectedRevision === null && contract.source.release !== expectedRelease) { + const contract = parseManagedImageContractV1( + candidate, + agent, + cohortPlatform ?? undefined, + ); + cohortPlatform ??= contract.platform; + if ( + expectedRevision === null && + expectedRelease !== null && + contract.source.release !== expectedRelease + ) { throw new SandboxWorkloadPreparationError( `managed image catalog contract for '${agent}' belongs to '${contract.source.release}', not '${expectedRelease}'`, ); @@ -269,6 +322,7 @@ function requireCompleteManagedImageCatalog( "managed image catalog does not identify one all-agent publication cohort", ); } + contracts.set(agent, contract); } catch (error) { if (error instanceof SandboxWorkloadPreparationError) throw error; throw new SandboxWorkloadPreparationError( @@ -282,7 +336,23 @@ function requireCompleteManagedImageCatalog( "managed image catalog source revision does not match the trusted catalog revision", ); } - return { release: cohortRelease!, revision: cohortRevision! }; + return { contracts, release: cohortRelease!, revision: cohortRevision! }; +} + +/** Read and validate every contract in one selected live E2E catalog. */ +export function readLiveE2eManagedImageCatalogContracts( + selected: LiveE2eManagedImageCatalog, +): ReadonlyMap { + const catalog = selected.catalog ?? readExactManagedImageCatalog(selected.path); + if ( + JSON.stringify(Object.keys(catalog).sort()) !== + JSON.stringify([...SHIPPED_MANAGED_IMAGE_AGENTS].sort()) + ) { + throw new SandboxWorkloadPreparationError( + "managed image catalog must contain only the shipped agent contracts", + ); + } + return requireCompleteManagedImageCatalog(catalog, null, null, selected.revision).contracts; } function requireCandidateManagedImageCatalog( @@ -360,9 +430,15 @@ export async function prepareSandboxWorkloadSource( }; } - if (candidateSelection && !input.catalogPath) { + if (input.catalog && input.catalogPath) { + throw new SandboxWorkloadPreparationError( + "managed image catalog has conflicting content authorities", + ); + } + + if (candidateSelection && !input.catalog && !input.catalogPath) { throw new SandboxWorkloadPreparationError( - `'${input.agentName}' is a release candidate and requires an exact managed image catalog file`, + `'${input.agentName}' is a release candidate and requires an exact managed image catalog`, ); } @@ -400,15 +476,18 @@ export async function prepareSandboxWorkloadSource( ); } try { - catalog = input.catalogPath - ? readExactManagedImageCatalog(input.catalogPath) - : await ( - dependencies.resolveCatalog ?? ((options) => resolveManagedImageCatalogFromGhcr(options)) - )({ - release, - platform, - ...(input.catalogRevision ? { revision: input.catalogRevision } : {}), - }); + catalog = input.catalog + ? input.catalog + : input.catalogPath + ? readExactManagedImageCatalog(input.catalogPath) + : await ( + dependencies.resolveCatalog ?? + ((options) => resolveManagedImageCatalogFromGhcr(options)) + )({ + release, + platform, + ...(input.catalogRevision ? { revision: input.catalogRevision } : {}), + }); } catch (error) { if (!(error instanceof ManagedImageCatalogUnavailableError)) { throw new SandboxWorkloadPreparationError( diff --git a/src/lib/onboard/workload/rebuild.ts b/src/lib/onboard/workload/rebuild.ts index 03a6e3a69f2..23235e1bc56 100644 --- a/src/lib/onboard/workload/rebuild.ts +++ b/src/lib/onboard/workload/rebuild.ts @@ -199,6 +199,7 @@ export async function prepareManagedWorkloadRebuildHandoff( policy: "require-managed", ...(liveCatalog ? { + ...(liveCatalog.catalog ? { catalog: liveCatalog.catalog } : {}), catalogPath: liveCatalog.path, expectedCatalogRevision: liveCatalog.revision, } diff --git a/test/e2e/README.md b/test/e2e/README.md index e09ad7acb3b..a555d919257 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -91,25 +91,30 @@ After the checks pass, the action restores root `dist/` and `nemoclaw/dist/share If the version command fails, the action stops before the live test runs. This boundary keeps candidate source separate from the trusted workflow implementation. -The `base-image-publication` job selects the nearest fully successful publication on the PR base -first-parent history. It downloads the complete cohort contract and Deep Agents Code base contract -by immutable artifact ID. It binds each artifact to the selected workflow run, attempt, revision, -artifact ID, and artifact digest. The cohort validator requires OpenClaw, Hermes, and LangChain Deep -Agents Code on `linux/amd64` and `linux/arm64` before it emits `managed_image_revision`. -`generate-matrix` and every stock-onboarding job depend on this job, so a missing, failed, -incomplete, or mixed publication starts no onboarding consumer. - -For a manual same-repository PR run, the trusted planner compares the immutable base and candidate -commit trees with the reviewed base-image and managed-image input paths. When those inputs are -unchanged, the run uses the applicable trusted managed-image publication from the PR base history. -When any input changed, the run selects `local-dockerfile` and builds the candidate Dockerfiles -locally instead of waiting for a candidate publication. - -The selected source is passed to every stock-onboarding consumer. Managed-image runs receive the -selected base revision and complete cohort receipt. Local-Dockerfile runs resolve the shipped agent -Dockerfile at the final process boundary and require the resulting durable receipt to identify that -source. The GitHub token remains available only to the trusted planner and is not included in the -candidate CLI artifact. +The `base-image-publication` job selects managed-image authority before any stock-onboarding consumer starts. + +For a manual same-repository PR run, the trusted planner compares immutable base and candidate commit trees against the reviewed image-input paths. +When those paths are unchanged, the job selects the nearest fully successful cohort publication from the PR base's first-parent history. +It downloads the complete cohort and Deep Agents Code base contracts by immutable artifact ID. +It binds each artifact to the selected workflow run, attempt, revision, artifact ID, and digest. +The cohort validator requires OpenClaw, Hermes, and LangChain Deep Agents Code on `linux/amd64` and `linux/arm64`. +Only then does it emit `managed_image_revision` and the complete cohort receipt. + +When a reviewed image input changed, the planner validates every returned managed-image PR workflow run for the candidate commit and selects the newest successful run by run ID. +An earlier failed run does not block a later successful run. +Every returned run must belong to the same open PR and an NVIDIA/NemoClaw source branch. +The planner downloads one `managed-pr-contract-*` artifact for each shipped agent. +It binds every artifact to the workflow run, attempt, candidate commit, artifact ID, and digest. +It requires every shipped agent once, one candidate revision, one release, and one cohort before it assembles the candidate catalog. +No matching successful run, invalid or duplicated run metadata, or incomplete, duplicated, mixed, or substituted artifact evidence stops before any stock-onboarding consumer starts. +Manual PR E2E does not fall back to local Dockerfile builds. + +Unchanged runs pass the selected base revision and complete cohort receipt to every stock-onboarding consumer. +Changed-input runs pass the authenticated candidate catalog separately to those consumers. +The candidate CLI artifact cannot contain the catalog. +The GitHub token remains available only to the trusted planner and is not included in the candidate CLI artifact. +The candidate catalog qualifies `linux/amd64` only, so a changed-input manual PR run does not dispatch the Jetson target. +The trusted publication job reports this exclusion before the Jetson job is skipped. The same-repository `Images / Build, Test, and Publish Managed Images` PR workflow also runs the OpenClaw managed-image MCP discovery and lifecycle scope in two independent matrix jobs. Each job diff --git a/test/e2e/docs/jetson-dispatch.md b/test/e2e/docs/jetson-dispatch.md index a7aa4bde1d7..56f13c3bb83 100644 --- a/test/e2e/docs/jetson-dispatch.md +++ b/test/e2e/docs/jetson-dispatch.md @@ -113,7 +113,10 @@ later Jetson jobs instead of canceling a running job. For a trusted main run, the publication gate exports the first-parent commit whose successful managed-image workflow covers the candidate. The controller sends that commit as `managedImageRevision`. A manual candidate run preserves -the existing exact-candidate selection. +the exact-candidate selection only when it has a qualified `linux/arm64` +managed-image revision. A changed-input PR candidate catalog qualifies +`linux/amd64` only. The trusted publication job reports that limitation, and +the Jetson job does not dispatch. The job grants only `contents: read` and `id-token: write`. The controller requests a short-lived GitHub OIDC token with audience diff --git a/test/e2e/fixtures/availability-env.ts b/test/e2e/fixtures/availability-env.ts index 2d11753fcdc..2e6f0724f6b 100644 --- a/test/e2e/fixtures/availability-env.ts +++ b/test/e2e/fixtures/availability-env.ts @@ -17,6 +17,7 @@ const AVAILABILITY_PROBE_EXTRA_ENV_KEYS = [ "XDG_RUNTIME_DIR", "NEMOCLAW_E2E_EXPECTED_SHA", "NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG", + "NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON", "NEMOCLAW_OLLAMA_PULL_TIMEOUT", "NEMOCLAW_EXPERIMENTAL_PROFILE", "NEMOCLAW_RUN_LIVE_E2E", diff --git a/test/e2e/fixtures/managed-image-receipt.ts b/test/e2e/fixtures/managed-image-receipt.ts index 2678c29afe5..0604a8b11ba 100644 --- a/test/e2e/fixtures/managed-image-receipt.ts +++ b/test/e2e/fixtures/managed-image-receipt.ts @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -10,14 +9,15 @@ import { isShippedManagedImageAgent, MANAGED_IMAGE_PLATFORMS, MANAGED_IMAGE_REPOSITORIES, - parseManagedImageContractV1, SHIPPED_MANAGED_IMAGE_AGENTS, type ManagedImageContractV1, - type ManagedImagePlatform, type ShippedManagedImageAgent, } from "../../../src/lib/onboard/managed-image/contract.ts"; import { readManagedWorkloadAuthority } from "../../../src/lib/onboard/workload/authority.ts"; -import { liveE2eManagedImageCatalog } from "../../../src/lib/onboard/workload/preparation.ts"; +import { + liveE2eManagedImageCatalog, + readLiveE2eManagedImageCatalogContracts, +} from "../../../src/lib/onboard/workload/preparation.ts"; import { readConfigFile } from "../../../src/lib/state/config-io.ts"; import { parseSandboxRegistryEntries } from "../../../src/lib/state/registry-normalization.ts"; import { cloneSandboxWorkloadReceipt } from "../../../src/lib/state/registry/workload.ts"; @@ -33,55 +33,10 @@ function readCandidateCatalog( throw new Error("stock onboarding requires a selected candidate managed-image catalog"); } - let descriptor: number | null = null; try { - descriptor = fs.openSync(selected.path, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)); - const metadata = fs.fstatSync(descriptor); - const pathMetadata = fs.lstatSync(selected.path); - if ( - pathMetadata.isSymbolicLink() || - !metadata.isFile() || - metadata.dev !== pathMetadata.dev || - metadata.ino !== pathMetadata.ino || - metadata.size < 2 || - metadata.size > 64 * 1024 - ) { - throw new Error(); - } - const parsed = JSON.parse(fs.readFileSync(descriptor, "utf8")) as unknown; - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(); - const catalog = parsed as Record; - if ( - JSON.stringify(Object.keys(catalog).sort()) !== - JSON.stringify([...SHIPPED_MANAGED_IMAGE_AGENTS].sort()) - ) { - throw new Error(); - } - - const contracts = new Map(); - let cohort: string | null = null; - let platform: ManagedImagePlatform | null = null; - let release: string | null = null; - for (const agent of SHIPPED_MANAGED_IMAGE_AGENTS) { - const contract = parseManagedImageContractV1(catalog[agent], agent); - cohort ??= contract.source.cohort; - platform ??= contract.platform; - release ??= contract.source.release; - if ( - contract.source.revision !== selected.revision || - contract.source.cohort !== cohort || - contract.platform !== platform || - contract.source.release !== release - ) { - throw new Error(); - } - contracts.set(agent, contract); - } - return contracts; + return readLiveE2eManagedImageCatalogContracts(selected); } catch { throw new Error("stock onboarding candidate managed-image catalog is invalid"); - } finally { - if (descriptor !== null) fs.closeSync(descriptor); } } @@ -221,9 +176,6 @@ export function assertStockManagedImageReceipt(options: { readonly sandboxName: string; }): StockManagedImageReceiptEvidence | null { const environment = options.environment ?? process.env; - const workloadSource = - environment.E2E_WORKLOAD_SOURCE?.trim() ?? process.env.E2E_WORKLOAD_SOURCE?.trim(); - if (workloadSource === "local-dockerfile") return null; const revision = selectedManagedImageRevision(environment); const home = environment.HOME?.trim() || os.homedir(); const registryPath = path.join( diff --git a/test/e2e/live/mcp-bridge-onboard-env.ts b/test/e2e/live/mcp-bridge-onboard-env.ts index 527f32609e1..31b0c66e685 100644 --- a/test/e2e/live/mcp-bridge-onboard-env.ts +++ b/test/e2e/live/mcp-bridge-onboard-env.ts @@ -17,6 +17,7 @@ const MCP_BRIDGE_QUALIFICATION_ENV_KEYS = [ "E2E_MANAGED_IMAGE_COHORT_RECEIPT", "NEMOCLAW_E2E_EXPECTED_SHA", "NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG", + "NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON", "NEMOCLAW_RUN_LIVE_E2E", "OPENSHELL_DOCKER_SUPERVISOR_IMAGE", ] as const; @@ -48,7 +49,9 @@ export function assertMcpBridgeManagedImageReceipt(options: { }): void { const environment = options.environment ?? process.env; const selectedRevision = environment.E2E_MANAGED_IMAGE_REVISION?.trim(); - const exactCandidateCatalog = environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG?.trim(); + const exactCandidateCatalog = + environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG?.trim() || + environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON?.trim(); if (!selectedRevision && !exactCandidateCatalog) return; const expectedRevision = selectedRevision ?? environment.NEMOCLAW_E2E_EXPECTED_SHA?.trim() ?? ""; diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index b8d2f334c80..cefad34d1e9 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -544,6 +544,7 @@ "live": "test/e2e/live/mcp-bridge.test.ts", "liveSources": [ "test/e2e/live/mcp-bridge-cleanup.ts", + "test/e2e/live/mcp-bridge-onboard-env.ts", "test/e2e/live/mcp-bridge-reliability.ts", "test/e2e/live/mcp-bridge-sandbox.ts", "test/e2e/live/openshell-allowed-ips-rebinding.ts", diff --git a/test/e2e/support/e2e-operations-workflow-boundary.test.ts b/test/e2e/support/e2e-operations-workflow-boundary.test.ts index 86e63de1d11..9ceb9f150f1 100644 --- a/test/e2e/support/e2e-operations-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-operations-workflow-boundary.test.ts @@ -330,116 +330,6 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; ); }); - it.each([ - { - caseName: "matching repository and requested SHAs", - checkoutRepository: "NVIDIA/NemoClaw", - nvidiaOwned: true, - workflowRepository: "NVIDIA/NemoClaw", - checkoutShaMatches: true, - workflowShaMatches: true, - expectedAllowed: true, - }, - { - caseName: "an NVIDIA-owned sibling repository", - checkoutRepository: "NVIDIA/NemoClaw-E2E", - nvidiaOwned: true, - workflowRepository: "NVIDIA/NemoClaw", - checkoutShaMatches: true, - workflowShaMatches: true, - expectedAllowed: true, - }, - { - caseName: "a checkout repository outside NVIDIA", - checkoutRepository: "contributor/NemoClaw", - nvidiaOwned: false, - workflowRepository: "NVIDIA/NemoClaw", - checkoutShaMatches: true, - workflowShaMatches: true, - expectedAllowed: false, - }, - { - caseName: "a workflow repository outside NVIDIA/NemoClaw", - checkoutRepository: "NVIDIA/NemoClaw", - nvidiaOwned: true, - workflowRepository: "contributor/NemoClaw", - checkoutShaMatches: true, - workflowShaMatches: true, - expectedAllowed: false, - }, - { - caseName: "checkout_sha differs from the checked-out commit", - checkoutRepository: "NVIDIA/NemoClaw", - nvidiaOwned: true, - workflowRepository: "NVIDIA/NemoClaw", - checkoutShaMatches: false, - workflowShaMatches: true, - expectedAllowed: false, - }, - { - caseName: "a requested workflow SHA that differs from the running workflow", - checkoutRepository: "NVIDIA/NemoClaw", - nvidiaOwned: true, - workflowRepository: "NVIDIA/NemoClaw", - checkoutShaMatches: true, - workflowShaMatches: false, - expectedAllowed: false, - }, - ])( - "sets E2E credential access to $expectedAllowed for $caseName (#9047)", - ({ - checkoutRepository, - nvidiaOwned, - workflowRepository, - checkoutShaMatches, - workflowShaMatches, - expectedAllowed, - }) => { - const workflow = readE2eOperationsWorkflow(); - const credentialAuthorization = workflow.jobs["generate-matrix"].steps!.find( - (step) => step.name === "Authorize E2E credentials", - )!; - const checkedOutSha = spawnSync("git", ["rev-parse", "HEAD"], { - encoding: "utf8", - }).stdout.trim(); - const checkoutSha = checkoutShaMatches ? checkedOutSha : "0".repeat(40); - const workflowSha = "c".repeat(40); - const expectedWorkflowSha = workflowShaMatches ? workflowSha : "d".repeat(40); - const directory = mkdtempSync(join(tmpdir(), "nemoclaw-e2e-credentials-")); - const output = join(directory, "output"); - - try { - writeFileSync(output, ""); - const result = spawnSync( - "bash", - ["--noprofile", "--norc", "-e", "-o", "pipefail", "-c", credentialAuthorization.run!], - { - encoding: "utf8", - env: { - ...process.env, - CHECKOUT_REPOSITORY: checkoutRepository, - CHECKOUT_SHA: checkoutSha, - EVENT_NAME: "workflow_dispatch", - EXPECTED_WORKFLOW_SHA: expectedWorkflowSha, - GITHUB_OUTPUT: output, - NVIDIA_OWNED: nvidiaOwned ? "true" : "false", - REF: "refs/heads/main", - WORKFLOW_REPOSITORY: workflowRepository, - WORKFLOW_SHA: workflowSha, - }, - }, - ); - - expect(result.status, result.stderr).toBe(0); - expect(readFileSync(output, "utf8")).toBe( - `allowed=${expectedAllowed ? "true" : "false"}\n`, - ); - } finally { - rmSync(directory, { force: true, recursive: true }); - } - }, - ); - it("keeps catalogue-owned GPU targets out of the handwritten workflow jobs", () => { const workflow = readE2eOperationsWorkflow(); workflow.jobs["llama-cpp-generic-gpu"] = { @@ -587,6 +477,7 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; "a", "b", "c", + "refs/heads/main", "::error::checkout_repository must be an owner/repository name\n", ], [ @@ -595,6 +486,7 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; "a", "d", "c", + "refs/heads/main", "::error::base_sha must match the PR base SHA\n", ], [ @@ -603,8 +495,18 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; "a", "b", "d", + "refs/heads/main", "::error::workflow_sha must match the trusted main workflow SHA\n", ], + [ + "a matching workflow SHA from a non-main workflow ref", + "NVIDIA/NemoClaw", + "a", + "b", + "c", + "refs/heads/pr-controlled-workflow", + "::error::Manual PR E2E must be dispatched from trusted main\n", + ], ] as const)( "rejects manual PR authentication for %s", ( @@ -613,6 +515,7 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; requestedHeadCharacter, requestedBaseCharacter, expectedWorkflowCharacter, + workflowRef, expectedStderr, ) => { const apiHeadSha = "a".repeat(40); @@ -645,7 +548,7 @@ const interpolatedNeeds = \${{ toJSON ( needs ) }}; JOBS: "", PR_NUMBER: "42", WORKFLOW_EVENT: "workflow_dispatch", - WORKFLOW_REF: "refs/heads/main", + WORKFLOW_REF: workflowRef, WORKFLOW_SHA: workflowSha, }, }, diff --git a/test/e2e/support/jetson-managed-revision-boundary.test.ts b/test/e2e/support/jetson-managed-revision-boundary.test.ts new file mode 100644 index 00000000000..3c4c7b77ac6 --- /dev/null +++ b/test/e2e/support/jetson-managed-revision-boundary.test.ts @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { UPLOAD_E2E_ARTIFACTS_ACTION } from "../../../tools/e2e/upload-e2e-artifacts-workflow-boundary.mts"; +import { validateJetsonDispatchBoundary } from "../../../tools/e2e/workflow-boundary.mts"; + +const REQUIRED_SELECTOR = + "${{ always() && needs['base-image-publication'].result == 'success' && needs['base-image-publication'].outputs.managed_image_revision != '' && needs['generate-matrix'].result == 'success' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }}"; + +function syntheticJetsonWorkflow(selector = REQUIRED_SELECTOR): unknown { + return { + on: { + workflow_dispatch: { + inputs: { + allow_jetson_dispatch: { + default: false, + description: + "Requires the operator-owned dispatch backend, JETSON_DISPATCH_URL, and test/e2e/docs/jetson-dispatch.md", + type: "boolean", + }, + }, + }, + }, + jobs: { + "base-image-publication": { + outputs: { + managed_image_revision: "${{ steps.validate_managed_cohort.outputs.revision }}", + }, + }, + "jetson-nvmap-gpu": { + concurrency: { group: "jetson-nvmap-gpu-dispatch", "cancel-in-progress": false }, + if: selector, + needs: ["base-image-publication", "generate-matrix"], + permissions: { contents: "read", "id-token": "write" }, + "runs-on": "ubuntu-latest", + steps: [ + { + name: "Check out trusted Jetson controller", + uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", + with: { + repository: "NVIDIA/NemoClaw", + ref: "${{ github.workflow_sha }}", + "persist-credentials": false, + }, + }, + { + name: "Set up Node for Jetson controller", + uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020", + with: { "node-version": 22 }, + }, + { + env: { + E2E_ARTIFACT_DIR: "${{ runner.temp }}/e2e-artifacts/live/jetson-nvmap-gpu", + JETSON_DISPATCH_CANDIDATE_SHA: "${{ inputs.checkout_sha || github.sha }}", + JETSON_DISPATCH_MANAGED_IMAGE_REVISION: + "${{ needs.base-image-publication.outputs.managed_image_revision }}", + JETSON_DISPATCH_URL: "${{ vars.JETSON_DISPATCH_URL }}", + }, + name: "Dispatch exact commit to Jetson through operator backend", + run: "node --experimental-strip-types --no-warnings tools/e2e/jetson-dispatch-client.mts", + }, + { + if: "always()", + name: "Upload Jetson nvmap GPU artifacts", + uses: UPLOAD_E2E_ARTIFACTS_ACTION, + with: { + name: "e2e-jetson-nvmap-gpu", + path: "${{ runner.temp }}/e2e-artifacts/live/jetson-nvmap-gpu/", + }, + }, + ], + "timeout-minutes": 60, + }, + }, + }; +} + +describe("Jetson managed-image revision boundary", () => { + it("rejects dispatch without an ARM-qualified managed-image revision", () => { + expect(validateJetsonDispatchBoundary(syntheticJetsonWorkflow())).toEqual([]); + + const selectorWithoutRevision = REQUIRED_SELECTOR.replace( + " && needs['base-image-publication'].outputs.managed_image_revision != ''", + "", + ); + expect( + validateJetsonDispatchBoundary(syntheticJetsonWorkflow(selectorWithoutRevision)), + ).toContain( + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections", + ); + }); +}); diff --git a/test/e2e/support/managed-image-receipt.test.ts b/test/e2e/support/managed-image-receipt.test.ts index 09e9c72a7ed..d945ab71f72 100644 --- a/test/e2e/support/managed-image-receipt.test.ts +++ b/test/e2e/support/managed-image-receipt.test.ts @@ -125,6 +125,15 @@ function candidateCatalogEnvironment(home: string): NodeJS.ProcessEnv { }; } +function candidateInlineCatalogEnvironment(home: string): NodeJS.ProcessEnv { + const environment = candidateCatalogEnvironment(home); + const catalogPath = environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG!; + const catalog = fs.readFileSync(catalogPath, "utf8").trim(); + delete environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG; + environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON = catalog; + return environment; +} + function writeRegistry(workload: Record): string { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-only-receipt-")); temporaryHomes.push(home); @@ -173,6 +182,18 @@ describe("stock E2E managed-image receipt assertion", () => { ).toMatchObject({ agent: "openclaw", sourceRevision: REVISION }); }); + it("accepts the durable receipt from the trusted inline candidate catalog", () => { + const home = writeRegistry(managedReceipt()); + + expect( + assertStockManagedImageReceipt({ + environment: candidateInlineCatalogEnvironment(home), + expectedAgent: "openclaw", + sandboxName: SANDBOX_NAME, + }), + ).toMatchObject({ agent: "openclaw", sourceRevision: REVISION }); + }); + it("records local Dockerfile evidence without a managed-image receipt", () => { const home = writeRegistry(managedReceipt()); @@ -302,7 +323,7 @@ describe("stock E2E managed-image receipt assertion", () => { ).toThrow("complete selected managed-image cohort receipt"); }); - it("rejects a stock legacy Dockerfile receipt", () => { + it("does not let a local-Dockerfile marker bypass candidate receipt validation", () => { const home = writeRegistry({ schemaVersion: 1, kind: "legacy-dockerfile", @@ -312,7 +333,10 @@ describe("stock E2E managed-image receipt assertion", () => { expect(() => assertStockManagedImageReceipt({ - environment: { E2E_MANAGED_IMAGE_REVISION: REVISION, HOME: home }, + environment: { + ...candidateCatalogEnvironment(home), + E2E_WORKLOAD_SOURCE: "local-dockerfile", + }, sandboxName: SANDBOX_NAME, }), ).toThrow("must record a managed-image receipt"); diff --git a/test/e2e/support/manual-pr-credential-authorization.test.ts b/test/e2e/support/manual-pr-credential-authorization.test.ts new file mode 100644 index 00000000000..e29009fdf08 --- /dev/null +++ b/test/e2e/support/manual-pr-credential-authorization.test.ts @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { readE2eOperationsWorkflow } from "../../../tools/e2e/operations-workflow-boundary.mts"; + +describe("manual PR E2E credential authorization", () => { + it.each([ + { + caseName: "matching repository and requested SHAs", + checkoutRepository: "NVIDIA/NemoClaw", + nvidiaOwned: true, + workflowRepository: "NVIDIA/NemoClaw", + workflowRef: "refs/heads/main", + checkoutShaMatches: true, + workflowShaMatches: true, + expectedAllowed: true, + }, + { + caseName: "an NVIDIA-owned sibling repository", + checkoutRepository: "NVIDIA/NemoClaw-E2E", + nvidiaOwned: true, + workflowRepository: "NVIDIA/NemoClaw", + workflowRef: "refs/heads/main", + checkoutShaMatches: true, + workflowShaMatches: true, + expectedAllowed: true, + }, + { + caseName: "a checkout repository outside NVIDIA", + checkoutRepository: "contributor/NemoClaw", + nvidiaOwned: false, + workflowRepository: "NVIDIA/NemoClaw", + workflowRef: "refs/heads/main", + checkoutShaMatches: true, + workflowShaMatches: true, + expectedAllowed: false, + }, + { + caseName: "a workflow repository outside NVIDIA/NemoClaw", + checkoutRepository: "NVIDIA/NemoClaw", + nvidiaOwned: true, + workflowRepository: "contributor/NemoClaw", + workflowRef: "refs/heads/main", + checkoutShaMatches: true, + workflowShaMatches: true, + expectedAllowed: false, + }, + { + caseName: "checkout_sha differs from the checked-out commit", + checkoutRepository: "NVIDIA/NemoClaw", + nvidiaOwned: true, + workflowRepository: "NVIDIA/NemoClaw", + workflowRef: "refs/heads/main", + checkoutShaMatches: false, + workflowShaMatches: true, + expectedAllowed: false, + }, + { + caseName: "a requested workflow SHA that differs from the running workflow", + checkoutRepository: "NVIDIA/NemoClaw", + nvidiaOwned: true, + workflowRepository: "NVIDIA/NemoClaw", + workflowRef: "refs/heads/main", + checkoutShaMatches: true, + workflowShaMatches: false, + expectedAllowed: false, + }, + { + caseName: "a non-main workflow ref with otherwise matching identities", + checkoutRepository: "NVIDIA/NemoClaw", + nvidiaOwned: true, + workflowRepository: "NVIDIA/NemoClaw", + workflowRef: "refs/heads/pr-controlled-workflow", + checkoutShaMatches: true, + workflowShaMatches: true, + expectedAllowed: false, + }, + ])( + "sets E2E credential access to $expectedAllowed for $caseName (#9047)", + ({ + checkoutRepository, + nvidiaOwned, + workflowRepository, + workflowRef, + checkoutShaMatches, + workflowShaMatches, + expectedAllowed, + }) => { + const workflow = readE2eOperationsWorkflow(); + const credentialAuthorization = workflow.jobs["generate-matrix"].steps!.find( + (step) => step.name === "Authorize E2E credentials", + )!; + const checkedOutSha = spawnSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", + }).stdout.trim(); + const checkoutSha = checkoutShaMatches ? checkedOutSha : "0".repeat(40); + const workflowSha = "c".repeat(40); + const expectedWorkflowSha = workflowShaMatches ? workflowSha : "d".repeat(40); + const directory = mkdtempSync(join(tmpdir(), "nemoclaw-e2e-credentials-")); + const output = join(directory, "output"); + + try { + writeFileSync(output, ""); + const result = spawnSync( + "bash", + ["--noprofile", "--norc", "-e", "-o", "pipefail", "-c", credentialAuthorization.run!], + { + encoding: "utf8", + env: { + ...process.env, + CHECKOUT_REPOSITORY: checkoutRepository, + CHECKOUT_SHA: checkoutSha, + EVENT_NAME: "workflow_dispatch", + EXPECTED_WORKFLOW_SHA: expectedWorkflowSha, + GITHUB_OUTPUT: output, + NVIDIA_OWNED: nvidiaOwned ? "true" : "false", + REF: workflowRef, + WORKFLOW_REPOSITORY: workflowRepository, + WORKFLOW_SHA: workflowSha, + }, + }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(readFileSync(output, "utf8")).toBe( + `allowed=${expectedAllowed ? "true" : "false"}\n`, + ); + } finally { + rmSync(directory, { force: true, recursive: true }); + } + }, + ); +}); diff --git a/test/e2e/support/mcp-bridge-onboard-env.test.ts b/test/e2e/support/mcp-bridge-onboard-env.test.ts index 04d17737eea..0a26db4c0cd 100644 --- a/test/e2e/support/mcp-bridge-onboard-env.test.ts +++ b/test/e2e/support/mcp-bridge-onboard-env.test.ts @@ -71,6 +71,33 @@ function selectedWorkload( }; } +function candidateCatalog(): Record { + return Object.fromEntries( + SHIPPED_MANAGED_IMAGE_AGENTS.map((agent) => { + const reference = selectedReferences[agent][PLATFORM]; + return [ + agent, + { + contractVersion: 1, + agent, + platform: PLATFORM, + image: MANAGED_IMAGE_REPOSITORIES[agent], + digest: reference.slice(reference.indexOf("@") + 1), + reference, + source: { + repository: "NVIDIA/NemoClaw", + revision: SELECTED_REVISION, + release: "v0.0.114", + cohort: SELECTED_COHORT, + }, + startupProfileContractVersion: 1, + capabilityContractVersion: 1, + }, + ]; + }), + ); +} + describe("MCP bridge onboarding environment", () => { it("restores exact-main OpenShell overrides after child environment sanitization", () => { const env = buildMcpBridgeExactMainEnv({ @@ -119,6 +146,32 @@ describe("MCP bridge onboarding environment", () => { expect(env.UNRELATED_PARENT_VALUE).toBeUndefined(); }); + it("retains inline candidate authority and validates its exact MCP workload", () => { + const inlineCatalog = JSON.stringify(candidateCatalog()); + const environment = buildMcpBridgeExactMainEnv({ + baseEnv: { + GITHUB_ACTIONS: "true", + HOME: "/tmp/home", + PATH: "/usr/bin", + NEMOCLAW_E2E_EXPECTED_SHA: SELECTED_REVISION, + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: inlineCatalog, + NEMOCLAW_RUN_LIVE_E2E: "1", + }, + }); + + expect(environment.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON).toBe(inlineCatalog); + expect(() => + assertMcpBridgeManagedImageReceipt({ + environment, + expectedAgent: "langchain-deepagents-code", + workload: { + ...selectedWorkload("langchain-deepagents-code"), + release: "v0.0.114", + }, + }), + ).not.toThrow(); + }); + it("rejects a Dockerfile workload in managed-image MCP qualification", () => { expect(() => assertMcpBridgeManagedImageReceipt({ diff --git a/test/e2e/support/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index 2a6188ed8c6..a3a88b706ba 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -1,16 +1,39 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + import { afterEach, describe, expect, it, vi } from "vitest"; +import { + MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, + MANAGED_IMAGE_CONTRACT_VERSION, + MANAGED_IMAGE_REPOSITORIES, + MANAGED_IMAGE_SOURCE_REPOSITORY, + MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, + SHIPPED_MANAGED_IMAGE_AGENTS, + type ManagedImageAgent, + type ManagedImageContractV1, +} from "../../../src/lib/onboard/managed-image/contract"; import { githubRequest } from "../../../tools/e2e/base-image-publication.mts"; -import { resolvePrManagedImageSource } from "../../../tools/e2e/pr-managed-image-publication.mts"; +import { + assembleManagedImageCatalog, + resolvePrManagedImageCatalog, + selectManagedImagePublicationRun, + writeManagedImageCatalog, +} from "../../../tools/e2e/pr-managed-image-publication.mts"; +import { artifactZip } from "../../helpers/artifact-zip"; const BASE_SHA = "b".repeat(40); const CANDIDATE_SHA = "a".repeat(40); const BASE_TREE_SHA = "1".repeat(40); const CANDIDATE_TREE_SHA = "2".repeat(40); -const PR_NUMBER = 10_263; +const PR_NUMBER = 10_595; +const RUN_ID = 33_460_364_260; +const WORKFLOW_ID = 12_345; const CANONICAL_REPOSITORY = "NVIDIA/NemoClaw"; const WORKFLOW_SOURCE = `on: push: @@ -18,21 +41,88 @@ const WORKFLOW_SOURCE = `on: paths: - ".github/workflows/base-image.yaml" - "Dockerfile.base" + - "src/lib/onboard/**" workflow_dispatch: jobs: {} `; +const temporaryDirectories: string[] = []; + +function contractForCohort( + agent: ManagedImageAgent, + index: number, + cohort: ManagedImageContractV1["source"]["cohort"], +): ManagedImageContractV1 { + const image = MANAGED_IMAGE_REPOSITORIES[agent]; + const digest = `sha256:${String(index + 1).repeat(64)}` as const; + return { + contractVersion: MANAGED_IMAGE_CONTRACT_VERSION, + agent, + platform: "linux/amd64", + image, + digest, + reference: `${image}@${digest}`, + source: { + repository: MANAGED_IMAGE_SOURCE_REPOSITORY, + revision: CANDIDATE_SHA, + release: "v0.0.110", + cohort, + }, + startupProfileContractVersion: MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, + capabilityContractVersion: MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, + }; +} + +function contract(agent: ManagedImageAgent, index: number): ManagedImageContractV1 { + return contractForCohort(agent, index, `ghrun-${RUN_ID}-1`); +} -function treeEntry(path: string, sha: string) { - return { mode: "100644", path, sha, type: "blob" }; +function treeEntry(entryPath: string, sha: string) { + return { mode: "100644", path: entryPath, sha, type: "blob" }; } -function requestFor(candidateRepository: string, imageChanged: boolean) { +function workflowRunRecord(overrides: Record = {}) { + return { + id: RUN_ID, + run_attempt: 1, + workflow_id: WORKFLOW_ID, + name: "Images / Build, Test, and Publish Managed Images", + path: ".github/workflows/managed-images.yaml", + event: "pull_request", + head_sha: CANDIDATE_SHA, + status: "completed", + conclusion: "success", + repository: { full_name: CANONICAL_REPOSITORY }, + head_repository: { full_name: CANONICAL_REPOSITORY }, + pull_requests: [{ number: PR_NUMBER }], + ...overrides, + }; +} + +function workflowRun(overrides: Record = {}) { + return { total_count: 1, workflow_runs: [workflowRunRecord(overrides)] }; +} + +function candidateRequest(options: { + readonly candidateRepository?: string; + readonly changedPath?: string; + readonly imageChanged: boolean; + readonly run?: unknown; + readonly runPages?: readonly unknown[]; + readonly artifactHeadSha?: string; + readonly artifactRunAttempt?: number; + readonly artifactRunId?: number; + readonly missingAgent?: ManagedImageAgent; +}) { + const candidateRepository = options.candidateRepository ?? CANONICAL_REPOSITORY; + const changedPath = options.changedPath ?? "Dockerfile.base"; + const artifactRunAttempt = options.artifactRunAttempt ?? 1; + const artifactRunId = options.artifactRunId ?? RUN_ID; const baseEntries = [ - treeEntry("Dockerfile.base", "3".repeat(40)), + treeEntry(changedPath, "3".repeat(40)), treeEntry("docs/guide.mdx", "4".repeat(40)), ]; const candidateEntries = [ - treeEntry("Dockerfile.base", (imageChanged ? "5" : "3").repeat(40)), + treeEntry(changedPath, (options.imageChanged ? "5" : "3").repeat(40)), treeEntry("docs/guide.mdx", "4".repeat(40)), ]; const responses = new Map([ @@ -60,49 +150,379 @@ function requestFor(candidateRepository: string, imageChanged: boolean) { `/repos/${candidateRepository}/git/trees/${CANDIDATE_TREE_SHA}?recursive=1`, { sha: CANDIDATE_TREE_SHA, tree: candidateEntries, truncated: false }, ], + [ + `/repos/${CANONICAL_REPOSITORY}/actions/workflows/managed-images.yaml`, + { + id: WORKFLOW_ID, + name: "Images / Build, Test, and Publish Managed Images", + path: ".github/workflows/managed-images.yaml", + state: "active", + }, + ], ]); + const runsPath = `/repos/${CANONICAL_REPOSITORY}/actions/workflows/managed-images.yaml/runs?event=pull_request&head_sha=${CANDIDATE_SHA}&per_page=100`; + const runPages = options.runPages ?? [options.run ?? workflowRun()]; + for (const [index, page] of runPages.entries()) { + responses.set(`${runsPath}&page=${index + 1}`, page); + } + for (const [index, agent] of SHIPPED_MANAGED_IMAGE_AGENTS.entries()) { + const name = `managed-pr-contract-${artifactRunId}-${artifactRunAttempt}-${agent}`; + const archive = artifactZip([ + { name: "contract.json", contents: `${JSON.stringify(contract(agent, index))}\n` }, + ]); + const id = index + 100; + responses.set( + `/repos/${CANONICAL_REPOSITORY}/actions/runs/${artifactRunId}/artifacts?name=${encodeURIComponent(name)}&per_page=100`, + options.missingAgent === agent + ? { total_count: 0, artifacts: [] } + : { + total_count: 1, + artifacts: [ + { + archive_download_url: `https://api.github.com/repos/${CANONICAL_REPOSITORY}/actions/artifacts/${id}/zip`, + digest: `sha256:${createHash("sha256").update(archive).digest("hex")}`, + expired: false, + id, + name, + size_in_bytes: archive.length, + workflow_run: { + head_sha: options.artifactHeadSha ?? CANDIDATE_SHA, + id: artifactRunId, + }, + }, + ], + }, + ); + } return async (requestPath: string): Promise => responses.get(requestPath) ?? Promise.reject(new Error(`unexpected request ${requestPath}`)); } -function selectorInput(candidateRepository: string) { +function resolverInput(candidateRepository = CANONICAL_REPOSITORY) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pr-catalog-test-")); + temporaryDirectories.push(directory); return { baseSha: BASE_SHA, candidateRepository, candidateSha: CANDIDATE_SHA, + outputPath: path.join(directory, "catalog.json"), prNumber: PR_NUMBER, token: "test-token", workflowSource: WORKFLOW_SOURCE, }; } +function downloadContract( + identity: { readonly name: string }, + cohort: ManagedImageContractV1["source"]["cohort"] = `ghrun-${RUN_ID}-1`, +): Promise { + const index = SHIPPED_MANAGED_IMAGE_AGENTS.findIndex((agent) => identity.name.endsWith(agent)); + expect(index, "artifact identity must name one shipped agent").toBeGreaterThanOrEqual(0); + const agent = SHIPPED_MANAGED_IMAGE_AGENTS[index]!; + return Promise.resolve( + artifactZip([ + { + name: "contract.json", + contents: `${JSON.stringify(contractForCohort(agent, index, cohort))}\n`, + }, + ]), + ); +} + afterEach(() => { vi.unstubAllGlobals(); + for (const directory of temporaryDirectories.splice(0)) { + fs.rmSync(directory, { force: true, recursive: true }); + } }); -describe("PR managed-image source selection", () => { - it("keeps source selection bound to commit A during A-to-B-to-A PR drift", async () => { +describe("exact PR managed-image publication", () => { + it("keeps unchanged PRs on the authenticated base-history cohort", async () => { + const input = resolverInput(); + const download = vi.fn(downloadContract); + await expect( - resolvePrManagedImageSource( - selectorInput(CANONICAL_REPOSITORY), - requestFor(CANONICAL_REPOSITORY, true), + resolvePrManagedImageCatalog(input, candidateRequest({ imageChanged: false }), download), + ).resolves.toBe("base-cohort"); + expect(download).not.toHaveBeenCalled(); + expect(fs.existsSync(input.outputPath)).toBe(false); + }); + + it("writes one exact candidate catalog after an immutable image-input change", async () => { + const input = resolverInput(); + + await expect( + resolvePrManagedImageCatalog( + input, + candidateRequest({ imageChanged: true }), + downloadContract, + ), + ).resolves.toBe("candidate-catalog"); + expect(JSON.parse(fs.readFileSync(input.outputPath, "utf8"))).toEqual( + Object.fromEntries( + SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => [agent, contract(agent, index)]), ), - ).resolves.toBe("local-dockerfile"); + ); + expect(fs.statSync(input.outputPath).mode & 0o777).toBe(0o600); }); - it("reads a validated external candidate repository through the default request policy", async () => { - const candidateRepository = "external-contributor/NemoClaw"; - const request = requestFor(candidateRepository, false); - vi.stubGlobal("fetch", async (input: string) => { - const url = new URL(input); - return new Response(JSON.stringify(await request(`${url.pathname}${url.search}`)), { - status: 200, - }); + it("selects a candidate catalog when only managed-image onboarding runtime changes", async () => { + const input = resolverInput(); + + await expect( + resolvePrManagedImageCatalog( + input, + candidateRequest({ + changedPath: "src/lib/onboard/workload/preparation.ts", + imageChanged: true, + }), + downloadContract, + ), + ).resolves.toBe("candidate-catalog"); + expect(fs.existsSync(input.outputPath)).toBe(true); + }); + + it("uses one serialization contract for assembled and resolved candidate catalogs", async () => { + const input = resolverInput(); + await resolvePrManagedImageCatalog( + input, + candidateRequest({ imageChanged: true }), + downloadContract, + ); + + const assemblyRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pr-catalog-assembly-")); + temporaryDirectories.push(assemblyRoot); + const contractPaths = SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => { + const contractPath = path.join(assemblyRoot, `${agent}.json`); + fs.writeFileSync(contractPath, JSON.stringify(contract(agent, index)), "utf8"); + return contractPath; }); + const assembledPath = path.join(assemblyRoot, "assembled", "catalog.json"); + writeManagedImageCatalog(contractPaths, CANDIDATE_SHA, assembledPath, `ghrun-${RUN_ID}-1`); + + expect(fs.readFileSync(assembledPath)).toEqual(fs.readFileSync(input.outputPath)); + expect(fs.statSync(assembledPath).mode & 0o777).toBe(0o600); + expect(fs.statSync(input.outputPath).mode & 0o777).toBe(0o600); + }); + + it("rejects an image-changing fork before any artifact download", async () => { + const candidateRepository = "external-contributor/NemoClaw"; + const download = vi.fn(downloadContract); + + await expect( + resolvePrManagedImageCatalog( + resolverInput(candidateRepository), + candidateRequest({ candidateRepository, imageChanged: true }), + download, + ), + ).rejects.toThrow("requires a branch in NVIDIA/NemoClaw"); + expect(download).not.toHaveBeenCalled(); + }); + + it("rejects a failed exact-candidate Images run before artifact download", async () => { + const download = vi.fn(downloadContract); + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + candidateRequest({ imageChanged: true, run: workflowRun({ conclusion: "failure" }) }), + download, + ), + ).rejects.toThrow("must complete successfully before live E2E"); + expect(download).not.toHaveBeenCalled(); + }); + + it("uses the newest successful Images run after an earlier failure", async () => { + const laterRunId = RUN_ID + 10; + const request = vi.fn( + candidateRequest({ + artifactRunAttempt: 2, + artifactRunId: laterRunId, + imageChanged: true, + run: { + total_count: 2, + workflow_runs: [ + workflowRunRecord({ conclusion: "failure" }), + workflowRunRecord({ id: laterRunId, run_attempt: 2 }), + ], + }, + }), + ); + + await expect( + resolvePrManagedImageCatalog(resolverInput(), request, (identity) => + downloadContract(identity, `ghrun-${laterRunId}-2`), + ), + ).resolves.toBe("candidate-catalog"); + expect(request).toHaveBeenCalledWith( + expect.stringContaining(`/actions/runs/${laterRunId}/artifacts`), + ); + expect(request).not.toHaveBeenCalledWith( + expect.stringContaining(`/actions/runs/${RUN_ID}/artifacts`), + ); + }); + + it("selects a successful exact-candidate Images run beyond the first API page", async () => { + const laterRunId = RUN_ID + 200; + const failedRuns = Array.from({ length: 100 }, (_, index) => + workflowRunRecord({ conclusion: "failure", id: RUN_ID + index }), + ); + const request = vi.fn( + candidateRequest({ + artifactRunId: laterRunId, + imageChanged: true, + runPages: [ + { total_count: 101, workflow_runs: failedRuns }, + { + total_count: 101, + workflow_runs: [workflowRunRecord({ id: laterRunId })], + }, + ], + }), + ); + + await expect( + resolvePrManagedImageCatalog(resolverInput(), request, (identity) => + downloadContract(identity, `ghrun-${laterRunId}-1`), + ), + ).resolves.toBe("candidate-catalog"); + expect(request).toHaveBeenCalledWith(expect.stringContaining("per_page=100&page=2")); + expect(request).toHaveBeenCalledWith( + expect.stringContaining(`/actions/runs/${laterRunId}/artifacts`), + ); + }); - await expect(resolvePrManagedImageSource(selectorInput(candidateRepository))).resolves.toBe( - "managed-image", + it("fails closed when the exact-candidate Images run count keeps changing", async () => { + const firstPage = Array.from({ length: 100 }, (_, index) => + workflowRunRecord({ conclusion: "failure", id: RUN_ID + index }), ); + const pages = Array.from({ length: 3 }).flatMap(() => [ + { total_count: 101, workflow_runs: firstPage }, + { + total_count: 102, + workflow_runs: [workflowRunRecord({ id: RUN_ID + 200 })], + }, + ]); + const fallback = candidateRequest({ imageChanged: true }); + const runsPath = `/repos/${CANONICAL_REPOSITORY}/actions/workflows/managed-images.yaml/runs?event=pull_request&head_sha=${CANDIDATE_SHA}&per_page=100`; + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + (requestPath) => + requestPath.startsWith(runsPath) ? Promise.resolve(pages.shift()) : fallback(requestPath), + downloadContract, + ), + ).rejects.toThrow("workflow run total_count changed during 3 pagination attempts"); + }); + + it("rejects candidate contracts from another workflow run cohort", async () => { + const laterRunId = RUN_ID + 10; + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + candidateRequest({ + artifactRunAttempt: 2, + artifactRunId: laterRunId, + imageChanged: true, + run: workflowRun({ id: laterRunId, run_attempt: 2 }), + }), + downloadContract, + ), + ).rejects.toThrow("do not match the selected workflow run cohort"); + }); + + it("rejects missing or ambiguous exact-candidate Images runs", async () => { + const download = vi.fn(downloadContract); + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + candidateRequest({ imageChanged: true, run: { total_count: 0, workflow_runs: [] } }), + download, + ), + ).rejects.toThrow("missing or ambiguous"); + expect(download).not.toHaveBeenCalled(); + }); + + it("rejects a substituted artifact producer before content download", async () => { + const download = vi.fn(downloadContract); + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + candidateRequest({ artifactHeadSha: "f".repeat(40), imageChanged: true }), + download, + ), + ).rejects.toThrow("artifact producer head does not match"); + expect(download).not.toHaveBeenCalled(); + }); + + it("rejects an incomplete exact-candidate artifact set", async () => { + const download = vi.fn(downloadContract); + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + candidateRequest({ imageChanged: true, missingAgent: "hermes" }), + download, + ), + ).rejects.toThrow("exact artifact identity is missing or ambiguous"); + expect(download).toHaveBeenCalledTimes(1); + }); + + it.each([ + [ + "duplicate paths", + [treeEntry("Dockerfile.base", "5".repeat(40)), treeEntry("Dockerfile.base", "6".repeat(40))], + "contains duplicate paths", + ], + [ + "invalid type and mode", + [{ mode: "040000", path: "Dockerfile.base", sha: "5".repeat(40), type: "blob" }], + "entry mode is invalid", + ], + ])("rejects candidate commit trees with %s", async (_label, tree, message) => { + const request = candidateRequest({ imageChanged: true }); + const candidateTreePath = `/repos/${CANONICAL_REPOSITORY}/git/trees/${CANDIDATE_TREE_SHA}?recursive=1`; + + await expect( + resolvePrManagedImageCatalog( + resolverInput(), + async (requestPath) => + requestPath === candidateTreePath + ? { sha: CANDIDATE_TREE_SHA, tree, truncated: false } + : request(requestPath), + downloadContract, + ), + ).rejects.toThrow(message); + }); + + it("rejects a workflow run for another pull request", () => { + expect(() => + selectManagedImagePublicationRun(workflowRun({ pull_requests: [{ number: 10_693 }] }), { + headSha: CANDIDATE_SHA, + prNumber: PR_NUMBER, + workflowId: WORKFLOW_ID, + }), + ).toThrow("does not match the PR number"); + }); + + it("rejects mixed candidate revisions in an all-agent catalog", () => { + const contracts = SHIPPED_MANAGED_IMAGE_AGENTS.map(contract); + const substituted = { + ...contracts[0], + source: { ...contracts[0]!.source, revision: "f".repeat(40) }, + }; + + expect(() => + assembleManagedImageCatalog( + [substituted, ...contracts.slice(1)], + CANDIDATE_SHA, + `ghrun-${RUN_ID}-1`, + ), + ).toThrow("do not match the candidate commit"); }); it("rejects a GitHub request outside the canonical and candidate repositories", async () => { diff --git a/test/e2e/support/pr-self-hosted-llama-selector.test.ts b/test/e2e/support/pr-self-hosted-llama-selector.test.ts index 39c1126e473..ae36c3a17eb 100644 --- a/test/e2e/support/pr-self-hosted-llama-selector.test.ts +++ b/test/e2e/support/pr-self-hosted-llama-selector.test.ts @@ -158,7 +158,9 @@ describe("generic NVIDIA GPU PR selection", () => { env: { EXPECTED_SHA: "${{ steps.changed.outputs.base_sha }}", GITHUB_TOKEN: "${{ github.token }}", + PUBLICATION_HISTORY_ALLOW_NON_HEAD: "1", REQUIRE_MANAGED_IMAGE_PUBLICATION: "1", + SELECT_NEAREST_SUCCESSFUL_PUBLICATION: "1", }, if: "${{ steps.changed.outputs.selected == 'true' }}", }); diff --git a/tools/e2e/base-image-publication.mts b/tools/e2e/base-image-publication.mts index cbc11ff3635..c468368d879 100644 --- a/tools/e2e/base-image-publication.mts +++ b/tools/e2e/base-image-publication.mts @@ -42,6 +42,7 @@ const REVIEWED_PATH_GLOBS = new Map([ /^[.]github\/actions\/publish-base-image-manifest\/.+$/u, ], ["agents/**", /^agents\/.+$/u], + ["ci/pi-agent-qualification-v1-*.json", /^ci\/pi-agent-qualification-v1-[^/]*[.]json$/u], ["nemoclaw/**", /^nemoclaw\/.+$/u], ["nemoclaw-blueprint/**", /^nemoclaw-blueprint\/.+$/u], ["scripts/**", /^scripts\/.+$/u], @@ -49,6 +50,7 @@ const REVIEWED_PATH_GLOBS = new Map([ "test/e2e/live/managed-image-activation-e2e*.ts", /^test\/e2e\/live\/managed-image-activation-e2e[^/]*[.]ts$/u, ], + ["test/e2e/live/mcp-bridge*.ts", /^test\/e2e\/live\/mcp-bridge[^/]*[.]ts$/u], [ "src/lib/actions/sandbox/mcp-bridge-*.ts", /^src\/lib\/actions\/sandbox\/mcp-bridge-[^/]*[.]ts$/u, @@ -58,6 +60,7 @@ const REVIEWED_PATH_GLOBS = new Map([ /^src\/lib\/actions\/sandbox\/openshell-child-visible-credentials[.]v[^/]*[.]json$/u, ], ["src/lib/messaging/**", /^src\/lib\/messaging\/.+$/u], + ["src/lib/onboard/**", /^src\/lib\/onboard\/.+$/u], [ "src/lib/onboard/managed-bootstrap/envelope.ts", /^src\/lib\/onboard\/managed-bootstrap\/envelope[.]ts$/u, diff --git a/tools/e2e/mcp-dev-workflow-boundary-digests.mts b/tools/e2e/mcp-dev-workflow-boundary-digests.mts index c8ffe4734eb..ed9672a7c6c 100644 --- a/tools/e2e/mcp-dev-workflow-boundary-digests.mts +++ b/tools/e2e/mcp-dev-workflow-boundary-digests.mts @@ -6,7 +6,7 @@ import { createHash } from "node:crypto"; export const MCP_DEV_WORKFLOW_EXECUTION_CONTEXT_SHA256 = "052c49d5e8688266dbf38fa911733132d33e4470a29a61deb6e7a11067737559"; export const MCP_DEV_JOB_EXECUTION_CONTEXT_SHA256 = - "63fdbb0b1d775e0f06cb31adc36d992b79bf75866a51038e3faa87849f2545e4"; + "aeabd0776df3f02174594194272ef4ad6870b3b91928be7f90f96c55c4095cf4"; export const MCP_DEV_TRUSTED_NODE_SETUP_CONTENT_SHA256 = "504821ad93c57971d0281ef1130ed6008fadd331bd56acb1a6b5e6a3358f3e49"; export const MCP_DEV_TRUSTED_PREFIX_CONTENT_SHA256 = diff --git a/tools/e2e/mcp-workflow-boundary.mts b/tools/e2e/mcp-workflow-boundary.mts index e5775bebc62..008d34ab36d 100644 --- a/tools/e2e/mcp-workflow-boundary.mts +++ b/tools/e2e/mcp-workflow-boundary.mts @@ -883,6 +883,8 @@ function validateCredentialWindowJob( E2E_MANAGED_IMAGE_COHORT_RECEIPT: "${{ needs.base-image-publication.outputs.managed_image_receipt }}", E2E_WORKLOAD_SOURCE: "${{ needs.generate-matrix.outputs.workload_source }}", + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: + "${{ needs.base-image-publication.outputs.managed_image_catalog }}", E2E_JOB: "1", E2E_TARGET_ID: CREDENTIAL_WINDOW_JOB, E2E_AGENT_RUNTIME: "openclaw", diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 5a6d21ecf0b..41061ae76b0 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -35,7 +35,37 @@ const E2E_ARTIFACT_ACTION = "NVIDIA/NemoClaw/.github/actions/upload-e2e-artifact const COLD_ONBOARD_PERFORMANCE_EVIDENCE_PATH = "e2e-artifacts/live/${{ matrix.id }}/onboard-progress-budget.json"; const MANAGED_SOURCE_CONDITION = - "${{ inputs.pr_number == '' || steps.select_pr_source.outputs.workload_source == 'managed-image' }}"; + "${{ inputs.pr_number == '' || steps.select_pr_source.outputs.selection == 'base-cohort' }}"; +const PR_MANAGED_IMAGE_RESOLVER_SCRIPT = + [ + "set -euo pipefail", + 'catalog_path="${RUNNER_TEMP}/pr-managed-image-catalog.json"', + 'rm -f -- "$catalog_path"', + 'selection="$(node --experimental-strip-types --no-warnings tools/e2e/pr-managed-image-publication.mts "$catalog_path")"', + 'case "$selection" in', + " base-cohort)", + ' [[ ! -e "$catalog_path" && ! -L "$catalog_path" ]] || {', + ' echo "::error::base-cohort selection produced a candidate catalog" >&2', + " exit 1", + " }", + " ;;", + " candidate-catalog)", + ' [[ -f "$catalog_path" && ! -L "$catalog_path" && -s "$catalog_path" ]] || {', + ' echo "::error::exact PR managed-image catalog is invalid" >&2', + " exit 1", + " }", + ' catalog="$(jq -ce . "$catalog_path")"', + " (( ${#catalog} <= 65536 )) || {", + ' echo "::error::exact PR managed-image catalog exceeds the output limit" >&2', + " exit 1", + " }", + ' printf \'catalog=%s\\n\' "$catalog" >>"$GITHUB_OUTPUT"', + ' echo "::notice::Jetson dispatch is unavailable because the exact PR managed-image catalog qualifies linux/amd64 only."', + " ;;", + ' *) echo "::error::PR managed-image selection is invalid" >&2; exit 1 ;;', + "esac", + 'printf \'selection=%s\\n\' "$selection" >>"$GITHUB_OUTPUT"', + ].join("\n") + "\n"; const PUBLICATION_CLASSIFIER_SCRIPT = [ "set -euo pipefail", @@ -328,9 +358,7 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow packageCli.env?.MANAGED_IMAGE_CATALOG_SHA256 !== undefined || packageSource.includes("pr-managed-image-catalog.json") ) { - errors.push( - "Manual PR CLI packaging must not accept obsolete managed-image catalog authority", - ); + errors.push("Manual PR CLI packaging must not accept obsolete managed-image catalog authority"); } const authentication = authenticationIndex >= 0 ? steps[authenticationIndex] : {}; @@ -363,7 +391,7 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow } for (const fragment of [ '"$WORKFLOW_EVENT" == "workflow_dispatch"', - '"$WORKFLOW_REF" == refs/heads/*', + '"$WORKFLOW_REF" == "refs/heads/main"', '"$PR_NUMBER" =~ ^[1-9][0-9]*$', '"$CHECKOUT_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$', '"$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$', @@ -474,7 +502,7 @@ function validateManualPrDispatch(errors: string[], workflow: OperationsWorkflow '"$WORKFLOW_REPOSITORY" == "NVIDIA/NemoClaw"', '"$NVIDIA_OWNED" == "true"', '"$EVENT_NAME" == "workflow_dispatch"', - '"$REF" == refs/heads/*', + '"$REF" == "refs/heads/main"', '"$CHECKOUT_SHA" =~ ^[a-f0-9]{40}$', '"$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$', '"$EXPECTED_WORKFLOW_SHA" == "$WORKFLOW_SHA"', @@ -638,7 +666,8 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): dcode_base_ref: "${{ steps.validate_dcode_base.outputs.base_ref }}", managed_image_receipt: "${{ steps.validate_managed_cohort.outputs.receipt }}", managed_image_revision: "${{ steps.validate_managed_cohort.outputs.revision }}", - workload_source: "${{ steps.select_pr_source.outputs.workload_source || 'managed-image' }}", + managed_image_catalog: "${{ steps.select_pr_source.outputs.catalog }}", + workload_source: "managed-image", }, permissions: { actions: "read", @@ -678,7 +707,7 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): }, { id: "select_pr_source", - name: "Select PR workload source", + name: "Resolve exact PR managed-image publication", if: "${{ inputs.pr_number != '' }}", env: { BASE_SHA: "${{ inputs.base_sha }}", @@ -688,16 +717,7 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): PR_NUMBER: "${{ inputs.pr_number }}", }, shell: "bash", - run: [ - "set -euo pipefail", - 'workload_source="$(node --experimental-strip-types --no-warnings tools/e2e/pr-managed-image-publication.mts select-source)"', - 'case "$workload_source" in', - " managed-image|local-dockerfile) ;;", - ' *) echo "::error::PR workload source is invalid" >&2; exit 1 ;;', - "esac", - 'printf \'workload_source=%s\\n\' "$workload_source" >>"$GITHUB_OUTPUT"', - "", - ].join("\n"), + run: PR_MANAGED_IMAGE_RESOLVER_SCRIPT, }, { id: "publication", @@ -708,9 +728,9 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): PUBLICATION_HISTORY_ALLOW_NON_HEAD: "${{ steps.publication_mode.outputs.allow_non_head }}", REQUIRE_MANAGED_IMAGE_PUBLICATION: - "${{ steps.select_pr_source.outputs.workload_source == 'local-dockerfile' && '0' || '1' }}", + "${{ steps.select_pr_source.outputs.selection == 'candidate-catalog' && '0' || '1' }}", SELECT_NEAREST_SUCCESSFUL_PUBLICATION: - "${{ steps.select_pr_source.outputs.workload_source == 'local-dockerfile' && '0' || steps.publication_mode.outputs.select_nearest_successful }}", + "${{ steps.select_pr_source.outputs.selection == 'candidate-catalog' && '0' || steps.publication_mode.outputs.select_nearest_successful }}", }, shell: "bash", run: [ @@ -799,12 +819,41 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): ) { errors.push("cloud-onboard must use the selected managed-image revision"); } + if ( + cloudOnboard.env?.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON !== + "${{ needs.base-image-publication.outputs.managed_image_catalog }}" + ) { + errors.push("cloud-onboard must use the exact PR managed-image catalog"); + } if ( live.env?.E2E_MANAGED_IMAGE_REVISION !== "${{ needs.base-image-publication.outputs.managed_image_revision }}" ) { errors.push("live stock onboarding must use the selected managed-image revision"); } + if ( + live.env?.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON !== + "${{ needs.base-image-publication.outputs.managed_image_catalog }}" + ) { + errors.push("live stock onboarding must use the exact PR managed-image catalog"); + } + for (const jobName of [ + "mcp-bridge", + "openshell-credential-generation-window", + "mcp-bridge-dev", + "hermes-e2e", + "hermes-gpu-startup", + "cloud-onboard", + "messaging-providers", + ]) { + const consumer = workflow.jobs[jobName] ?? {}; + if ( + consumer.env?.NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON !== + "${{ needs.base-image-publication.outputs.managed_image_catalog }}" + ) { + errors.push(`${jobName} must use the exact PR managed-image catalog`); + } + } for (const jobName of [ "catalogue-standard", "catalogue-nvidia-api", @@ -822,10 +871,16 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): ) { errors.push(`${jobName} must use the selected managed-image revision`); } + if ( + catalogue.with?.managed_image_catalog !== + "${{ needs.base-image-publication.outputs.managed_image_catalog }}" + ) { + errors.push(`${jobName} must use the exact PR managed-image catalog`); + } } if ( live.env?.NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF !== - "${{ needs.generate-matrix.outputs.workload_source == 'managed-image' && needs.base-image-publication.outputs.dcode_base_ref || '' }}" + "${{ needs.generate-matrix.outputs.workload_source == 'managed-image' && needs.base-image-publication.outputs.dcode_base_ref || '' }}" ) { errors.push("live DCode must use the selected immutable base reference"); } @@ -1442,15 +1497,19 @@ function validateUnifiedAdvisorBoundary(errors: string[], advisorPath: string): errors.push("Unified advisor must not auto-dispatch workflows"); } const specialistEnv = advisor.jobs?.["review-specialists"]?.env ?? {}; - const expectedBaseRef = "${{ github.event_name == 'pull_request_target' && 'target/base' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'target/base' || inputs.base_ref) }}"; - const expectedHeadRef = "${{ github.event_name == 'pull_request_target' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}"; + const expectedBaseRef = + "${{ github.event_name == 'pull_request_target' && 'target/base' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'target/base' || inputs.base_ref) }}"; + const expectedHeadRef = + "${{ github.event_name == 'pull_request_target' && 'HEAD' || (github.event_name == 'workflow_dispatch' && inputs.target_repo != '' && inputs.target_pr != '' && 'HEAD' || inputs.head_ref) }}"; if (specialistEnv.BASE_REF !== expectedBaseRef || specialistEnv.HEAD_REF !== expectedHeadRef) { errors.push("Unified advisor specialists must retain target refs through execution"); } const discoverySteps = advisor.jobs?.["discover-specialists"]?.steps ?? []; const contextUpload = discoverySteps.find((step) => step.name === "Upload GitHub review context"); const specialistSteps = advisor.jobs?.["review-specialists"]?.steps ?? []; - const contextDownload = specialistSteps.find((step) => step.name === "Download GitHub review context"); + const contextDownload = specialistSteps.find( + (step) => step.name === "Download GitHub review context", + ); const specialistUpload = specialistSteps.find((step) => step.name === "Upload specialist review"); const contextArtifactName = "pr-review-advisor-context-${{ github.run_id }}"; if ( @@ -1460,7 +1519,9 @@ function validateUnifiedAdvisorBoundary(errors: string[], advisorPath: string): ) { errors.push("Unified advisor context artifact must survive failed-job and full reruns"); } - if (specialistUpload?.with?.name !== "${{ matrix.advisor.artifact_name }}-${{ github.run_attempt }}") { + if ( + specialistUpload?.with?.name !== "${{ matrix.advisor.artifact_name }}-${{ github.run_attempt }}" + ) { errors.push("Unified advisor specialist artifacts must be unique per rerun attempt"); } } diff --git a/tools/e2e/pr-managed-image-publication.mts b/tools/e2e/pr-managed-image-publication.mts index 4b8d32b0851..85492b1ecc7 100644 --- a/tools/e2e/pr-managed-image-publication.mts +++ b/tools/e2e/pr-managed-image-publication.mts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -9,23 +10,45 @@ import { parseManagedImageContractV1, SHIPPED_MANAGED_IMAGE_AGENTS, type ManagedImageContractCatalog, + type ManagedImageContractV1, } from "../../src/lib/onboard/managed-image/contract.ts"; import { baseImageInputsChanged, + collectPaginated, githubRequest, parseBaseImagePushPaths, } from "./base-image-publication.mts"; +import { + bindNamedExactArtifact, + downloadBoundArtifact, + materializeContractArchive, + type BoundArtifactIdentity, +} from "./exact-artifact-download.mts"; const REPOSITORY = "NVIDIA/NemoClaw"; const BASE_IMAGE_WORKFLOW_PATH = ".github/workflows/base-image.yaml"; +const MANAGED_IMAGE_WORKFLOW_FILE = "managed-images.yaml"; +const MANAGED_IMAGE_WORKFLOW_NAME = "Images / Build, Test, and Publish Managed Images"; +const MANAGED_IMAGE_WORKFLOW_PATH = ".github/workflows/managed-images.yaml"; const MAX_COMMIT_TREE_ENTRIES = 100_000; const SHA_PATTERN = /^[0-9a-f]{40}$/u; const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u; -const TREE_ENTRY_TYPES = new Set(["blob", "commit", "tree"]); +const TREE_ENTRY_MODES = new Map([ + ["blob", new Set(["100644", "100755", "120000"])], + ["commit", new Set(["160000"])], + ["tree", new Set(["040000"])], +]); type JsonRecord = Record; +type ManagedImageCohort = ManagedImageContractV1["source"]["cohort"]; + +export type PrManagedImageSelection = "base-cohort" | "candidate-catalog"; -export type PrManagedImageSource = "local-dockerfile" | "managed-image"; +export interface ManagedImagePublicationRun { + readonly attempt: number; + readonly headSha: string; + readonly id: number; +} function record(value: unknown, label: string): JsonRecord { if (!value || typeof value !== "object" || Array.isArray(value)) { @@ -45,9 +68,10 @@ function exactString(value: unknown, expected: string, label: string): void { if (value !== expected) throw new Error(`${label} must be ${expected}`); } -function assembleManagedImageCatalog( +export function assembleManagedImageCatalog( values: readonly unknown[], candidateSha: string, + expectedCohort: ManagedImageCohort, ): ManagedImageContractCatalog { if (!SHA_PATTERN.test(candidateSha)) throw new Error("candidate SHA is invalid"); if (values.length !== SHIPPED_MANAGED_IMAGE_AGENTS.length) { @@ -74,20 +98,33 @@ function assembleManagedImageCatalog( if (releases.size !== 1 || cohorts.size !== 1) { throw new Error("exact PR managed-image contracts do not form one publication cohort"); } + if (!cohorts.has(expectedCohort)) { + throw new Error( + "exact PR managed-image contracts do not match the selected workflow run cohort", + ); + } return Object.fromEntries( SHIPPED_MANAGED_IMAGE_AGENTS.map((agent) => [agent, byAgent.get(agent)!]), ); } -function writeManagedImageCatalog( +export function writeManagedImageCatalog( contractPaths: readonly string[], candidateSha: string, outputPath: string, + expectedCohort: ManagedImageCohort, ): void { const contracts = contractPaths.map( (contractPath) => JSON.parse(fs.readFileSync(contractPath, "utf8")) as unknown, ); - const catalog = assembleManagedImageCatalog(contracts, candidateSha); + const catalog = assembleManagedImageCatalog(contracts, candidateSha, expectedCohort); + writeValidatedManagedImageCatalog(catalog, outputPath); +} + +function writeValidatedManagedImageCatalog( + catalog: ManagedImageContractCatalog, + outputPath: string, +): void { fs.mkdirSync(path.dirname(path.resolve(outputPath)), { mode: 0o700, recursive: true }); fs.writeFileSync(outputPath, `${JSON.stringify(catalog)}\n`, { encoding: "utf8", @@ -128,20 +165,32 @@ async function readCommitTree( } const entries = new Map(); + const paths = new Set(); for (const value of payload.tree) { const entry = record(value, `${label} tree entry`); - if (typeof entry.path !== "string" || entry.path.length === 0) { + if ( + typeof entry.path !== "string" || + entry.path.length === 0 || + entry.path.length > 4_096 || + /[\0\r\n]/u.test(entry.path) || + entry.path.startsWith("/") || + entry.path.includes("//") || + entry.path.split("/").some((segment) => segment === "" || segment === "." || segment === "..") + ) { throw new Error(`${label} tree entry path is invalid`); } - if (typeof entry.type !== "string" || !TREE_ENTRY_TYPES.has(entry.type)) { + if (paths.has(entry.path)) throw new Error(`${label} commit tree contains duplicate paths`); + paths.add(entry.path); + const validModes = + typeof entry.type === "string" ? TREE_ENTRY_MODES.get(entry.type) : undefined; + if (!validModes) { throw new Error(`${label} tree entry type is invalid`); } - if (typeof entry.mode !== "string" || !/^[0-7]{6}$/u.test(entry.mode)) { + if (typeof entry.mode !== "string" || !validModes.has(entry.mode)) { throw new Error(`${label} tree entry mode is invalid`); } const entrySha = sha(entry.sha, `${label} tree entry SHA`); if (entry.type === "tree") continue; - if (entries.has(entry.path)) throw new Error(`${label} commit tree contains duplicate paths`); entries.set(entry.path, `${entry.mode}:${entry.type}:${entrySha}`); } return entries; @@ -203,12 +252,89 @@ function validatePr( ); } -/** Select the managed-image or local-Dockerfile source for a validated PR. */ -export async function resolvePrManagedImageSource( +function validateWorkflow(payload: unknown): number { + const workflow = record(payload, "managed-image workflow"); + const id = positiveInteger(workflow.id, "managed-image workflow id"); + exactString(workflow.name, MANAGED_IMAGE_WORKFLOW_NAME, "managed-image workflow name"); + exactString(workflow.path, MANAGED_IMAGE_WORKFLOW_PATH, "managed-image workflow path"); + exactString(workflow.state, "active", "managed-image workflow state"); + return id; +} + +/** Select one successful exact-candidate managed-image workflow run. */ +export function selectManagedImagePublicationRun( + payload: unknown, + expected: { readonly headSha: string; readonly prNumber: number; readonly workflowId: number }, +): ManagedImagePublicationRun { + if (!SHA_PATTERN.test(expected.headSha)) throw new Error("candidate SHA is invalid"); + positiveInteger(expected.prNumber, "PR number"); + positiveInteger(expected.workflowId, "managed-image workflow id"); + const response = record(payload, "managed-image workflow runs"); + if (!Array.isArray(response.workflow_runs)) { + throw new Error("exact managed-image workflow run listing is invalid"); + } + if (response.total_count !== response.workflow_runs.length) { + throw new Error("exact managed-image workflow run listing is incomplete"); + } + if (response.workflow_runs.length === 0) { + throw new Error("exact managed-image workflow run is missing or ambiguous"); + } + const successfulRuns: ManagedImagePublicationRun[] = []; + const runIds = new Set(); + for (const rawRun of response.workflow_runs) { + const run = record(rawRun, "managed-image workflow run"); + const id = positiveInteger(run.id, "managed-image workflow run id"); + const attempt = positiveInteger(run.run_attempt, "managed-image workflow run attempt"); + if (runIds.has(id)) { + throw new Error("exact managed-image workflow run listing contains duplicate runs"); + } + runIds.add(id); + if (run.workflow_id !== expected.workflowId) { + throw new Error("managed-image workflow run does not match the trusted workflow"); + } + exactString(run.name, MANAGED_IMAGE_WORKFLOW_NAME, "managed-image workflow run name"); + exactString(run.path, MANAGED_IMAGE_WORKFLOW_PATH, "managed-image workflow run path"); + exactString(run.event, "pull_request", "managed-image workflow run event"); + exactString(run.head_sha, expected.headSha, "managed-image workflow run commit"); + exactString( + record(run.repository, "managed-image workflow repository").full_name, + REPOSITORY, + "managed-image workflow repository", + ); + exactString( + record(run.head_repository, "managed-image workflow source repository").full_name, + REPOSITORY, + "managed-image workflow source repository", + ); + if ( + !Array.isArray(run.pull_requests) || + run.pull_requests.length !== 1 || + record(run.pull_requests[0], "managed-image workflow pull request").number !== + expected.prNumber + ) { + throw new Error("managed-image workflow run does not match the PR number"); + } + if (run.status === "completed" && run.conclusion === "success") { + successfulRuns.push({ attempt, headSha: expected.headSha, id }); + } + } + if (successfulRuns.length === 0) { + throw new Error( + `managed-image workflow for candidate ${expected.headSha} must complete successfully before live E2E`, + ); + } + const selectedRun = successfulRuns.sort((left, right) => right.id - left.id)[0]; + if (!selectedRun) throw new Error("successful managed-image workflow run is missing"); + return selectedRun; +} + +/** Resolve one exact PR candidate catalog before candidate code executes. */ +export async function resolvePrManagedImageCatalog( input: { readonly baseSha: string; readonly candidateRepository: string; readonly candidateSha: string; + readonly outputPath: string; readonly prNumber: number; readonly token: string; readonly workflowSource: string; @@ -217,7 +343,9 @@ export async function resolvePrManagedImageSource( githubRequest(apiPath, input.token, { additionalRepository: input.candidateRepository, }), -): Promise { + downloadArtifact: (identity: BoundArtifactIdentity) => Promise = (identity) => + downloadBoundArtifact(identity, input.token), +): Promise { if (!SHA_PATTERN.test(input.baseSha) || !SHA_PATTERN.test(input.candidateSha)) { throw new Error("PR base and candidate SHAs are required"); } @@ -232,7 +360,52 @@ export async function resolvePrManagedImageSource( validatePr(await request(`/repos/${REPOSITORY}/pulls/${input.prNumber}`), input); const changedFiles = await readChangedFiles(input, request); const patterns = parseBaseImagePushPaths(input.workflowSource); - return baseImageInputsChanged(changedFiles, patterns) ? "local-dockerfile" : "managed-image"; + if (!baseImageInputsChanged(changedFiles, patterns)) return "base-cohort"; + if (input.candidateRepository !== REPOSITORY) { + throw new Error("exact PR managed-image publication requires a branch in NVIDIA/NemoClaw"); + } + + const workflowId = validateWorkflow( + await request(`/repos/${REPOSITORY}/actions/workflows/${MANAGED_IMAGE_WORKFLOW_FILE}`), + ); + const runsPath = `/repos/${REPOSITORY}/actions/workflows/${MANAGED_IMAGE_WORKFLOW_FILE}/runs?event=pull_request&head_sha=${input.candidateSha}&per_page=100`; + const run = selectManagedImagePublicationRun( + await collectPaginated(request, runsPath, "workflow_runs"), + { headSha: input.candidateSha, prNumber: input.prNumber, workflowId }, + ); + + const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pr-managed-catalog-")); + try { + const contracts: ManagedImageContractV1[] = []; + for (const agent of SHIPPED_MANAGED_IMAGE_AGENTS) { + const name = `managed-pr-contract-${run.id}-${run.attempt}-${agent}`; + const metadata = await request( + `/repos/${REPOSITORY}/actions/runs/${run.id}/artifacts?name=${encodeURIComponent(name)}&per_page=100`, + ); + const identity = bindNamedExactArtifact( + metadata, + { headSha: run.headSha, runAttempt: run.attempt, runId: run.id }, + name, + ); + const archive = await downloadArtifact(identity); + const contractPath = materializeContractArchive( + archive, + path.join(temporaryDirectory, agent), + ); + contracts.push( + JSON.parse(fs.readFileSync(contractPath, "utf8")) as unknown as ManagedImageContractV1, + ); + } + const catalog = assembleManagedImageCatalog( + contracts, + input.candidateSha, + `ghrun-${run.id}-${run.attempt}` as const, + ); + writeValidatedManagedImageCatalog(catalog, input.outputPath); + return "candidate-catalog"; + } finally { + fs.rmSync(temporaryDirectory, { force: true, recursive: true }); + } } function requiredInteger(value: string | undefined, label: string): number { @@ -245,20 +418,28 @@ export async function main(argv = process.argv.slice(2), env = process.env): Pro if (argv.length < 4) { throw new Error("expected candidate SHA, output path, and managed-image contract paths"); } - writeManagedImageCatalog(argv.slice(3), argv[1], argv[2]); + const runId = requiredInteger(env.GITHUB_RUN_ID, "GITHUB_RUN_ID"); + const runAttempt = requiredInteger(env.GITHUB_RUN_ATTEMPT, "GITHUB_RUN_ATTEMPT"); + writeManagedImageCatalog( + argv.slice(3), + argv[1], + argv[2], + `ghrun-${runId}-${runAttempt}` as const, + ); console.log("pr-managed-image-catalog outcome=assembled"); return; } - if (argv.length !== 1 || argv[0] !== "select-source") throw new Error("expected select-source"); - const source = await resolvePrManagedImageSource({ + if (argv.length !== 1) throw new Error("expected one managed-image catalog output path"); + const selection = await resolvePrManagedImageCatalog({ baseSha: env.BASE_SHA ?? "", candidateRepository: env.CANDIDATE_REPOSITORY ?? "", candidateSha: env.CANDIDATE_SHA ?? "", + outputPath: argv[0], prNumber: requiredInteger(env.PR_NUMBER, "PR_NUMBER"), token: env.GITHUB_TOKEN ?? "", workflowSource: fs.readFileSync(BASE_IMAGE_WORKFLOW_PATH, "utf8"), }); - process.stdout.write(`${source}\n`); + process.stdout.write(`${selection}\n`); } if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { diff --git a/tools/e2e/standard-profile-workflow-boundary.mts b/tools/e2e/standard-profile-workflow-boundary.mts index e21d9ec04f2..d5fbfa1edf9 100644 --- a/tools/e2e/standard-profile-workflow-boundary.mts +++ b/tools/e2e/standard-profile-workflow-boundary.mts @@ -79,12 +79,7 @@ const PROFILE_JOBS = { job: "catalogue-brave-nvidia-inference", matrix: "catalogue_brave_nvidia_inference_matrix", credentialBoundary: "Brave and NVIDIA inference API keys", - secrets: [ - "BRAVE_API_KEY", - "DOCKERHUB_TOKEN", - "DOCKERHUB_USERNAME", - "NVIDIA_INFERENCE_API_KEY", - ], + secrets: ["BRAVE_API_KEY", "DOCKERHUB_TOKEN", "DOCKERHUB_USERNAME", "NVIDIA_INFERENCE_API_KEY"], githubToken: false, maxParallel: 2, }, @@ -163,6 +158,7 @@ function validateProfileCallers(errors: string[], workflow: WorkflowRecord): voi risk_signal_correlation_id: "${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.correlation_id || '' }}", cli_artifact_provenance: "${{ needs.generate-matrix.outputs.cli_artifact_provenance }}", + managed_image_catalog: "${{ needs.base-image-publication.outputs.managed_image_catalog }}", managed_image_revision: "${{ needs.base-image-publication.outputs.managed_image_revision }}", managed_image_receipt: "${{ needs.base-image-publication.outputs.managed_image_receipt }}", workload_source: "${{ needs.generate-matrix.outputs.workload_source }}", @@ -213,6 +209,7 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi risk_signal_expected_sha: "string", risk_signal_correlation_id: "string", cli_artifact_provenance: "string", + managed_image_catalog: "string", managed_image_revision: "string", managed_image_receipt: "string", workload_source: "string", @@ -283,6 +280,7 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi const jobEnv = record(runJob.env); const expectedJobEnv = { E2E_JOB: "1", + NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG_JSON: "${{ inputs.managed_image_catalog }}", E2E_MANAGED_IMAGE_REVISION: "${{ inputs.managed_image_revision }}", E2E_TARGET_ID: "${{ inputs.target_id }}", E2E_MANAGED_IMAGE_COHORT_RECEIPT: "${{ inputs.managed_image_receipt }}", @@ -466,11 +464,10 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi cloudflared.shell !== EXECUTION_PLAN_SHELL || !isDeepStrictEqual(record(cloudflared.env), { CLOUDFLARED_VERSION: "2026.6.1", - CLOUDFLARED_DEB_SHA256: - "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526", + CLOUDFLARED_DEB_SHA256: "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526", }) || !cloudflaredRun.includes( - 'https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb', + "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb", ) || !cloudflaredRun.includes("sha256sum -c -") || !cloudflaredRun.includes('dpkg-deb -f "${cloudflared_deb}" Package') || @@ -573,8 +570,7 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi "${{ inputs.trusted_main && secrets.NVIDIA_INFERENCE_API_KEY || '' }}" || executeEnv.COMPATIBLE_API_KEY !== "${{ inputs.compatible_api_key && inputs.trusted_main && secrets.NVIDIA_INFERENCE_API_KEY || '' }}" || - executeEnv.BRAVE_API_KEY !== - "${{ inputs.trusted_main && secrets.BRAVE_API_KEY || '' }}" || + executeEnv.BRAVE_API_KEY !== "${{ inputs.trusted_main && secrets.BRAVE_API_KEY || '' }}" || executeEnv.GITHUB_TOKEN !== "${{ inputs.github_token && inputs.trusted_main && github.token || '' }}" ) { diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 4638cb6abe0..c9f250ea2fa 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -1767,7 +1767,7 @@ function validateJetsonControllerBoundary(errors: string[], jobs: WorkflowRecord errors.push("jetson-nvmap-gpu job must depend on managed publication and generate-matrix"); } const trustedPushOrManualSelector = - "${{ always() && needs['base-image-publication'].result == 'success' && needs['generate-matrix'].result == 'success' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }}"; + "${{ always() && needs['base-image-publication'].result == 'success' && needs['base-image-publication'].outputs.managed_image_revision != '' && needs['generate-matrix'].result == 'success' && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.allow_jetson_dispatch && (inputs.checkout_repository == '' || inputs.checkout_repository == github.repository) && ((inputs.jobs == '' && inputs.targets == '') || contains(fromJSON(needs.generate-matrix.outputs.selected_jobs), 'jetson-nvmap-gpu')))) }}"; if (job.if !== trustedPushOrManualSelector) { errors.push( "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections",