From 300660d1a7b9b7b50c044a152301d2fde0fdd53d Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 31 Aug 2026 21:10:37 -0700 Subject: [PATCH 01/11] fix(e2e): restore exact PR managed images Signed-off-by: Prekshi Vyas --- .github/workflows/e2e-standard-profile.yaml | 4 + .github/workflows/e2e.yaml | 55 +++- .../managed-workload/onboard-orchestration.ts | 7 +- .../sandbox-workload-preparation.test.ts | 34 +- src/lib/onboard/workload/preparation.ts | 82 +++-- src/lib/onboard/workload/rebuild.ts | 1 + test/e2e/README.md | 40 +-- test/e2e/fixtures/managed-image-receipt.ts | 34 +- .../e2e/support/managed-image-receipt.test.ts | 21 ++ .../pr-managed-image-publication.test.ts | 300 ++++++++++++++++-- ...pr-managed-image-workflow-boundary.test.ts | 17 +- .../e2e/mcp-dev-workflow-boundary-digests.mts | 2 +- tools/e2e/operations-workflow-boundary.mts | 106 +++++-- tools/e2e/pr-managed-image-publication.mts | 173 +++++++++- .../standard-profile-workflow-boundary.mts | 18 +- 15 files changed, 752 insertions(+), 142 deletions(-) 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 ec5873260bc..6989c064255 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,31 @@ 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 "::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 +262,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 +293,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 +304,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 }} @@ -2783,6 +2803,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" @@ -3078,6 +3099,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 }} @@ -3120,6 +3142,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 }} @@ -3163,6 +3186,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 }} @@ -3206,6 +3230,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 }} @@ -3249,6 +3274,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 }} @@ -3446,6 +3472,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" @@ -3781,6 +3808,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" @@ -3956,6 +3984,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" @@ -5127,6 +5156,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" @@ -5237,6 +5267,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" @@ -5544,6 +5575,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" @@ -5675,6 +5707,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/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..69d4a015fbc 100644 --- a/src/lib/onboard/sandbox-workload-preparation.test.ts +++ b/src/lib/onboard/sandbox-workload-preparation.test.ts @@ -179,6 +179,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", @@ -219,6 +227,15 @@ 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 }); } @@ -364,6 +381,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 +705,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..908cc42bd36 100644 --- a/src/lib/onboard/workload/preparation.ts +++ b/src/lib/onboard/workload/preparation.ts @@ -47,6 +47,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 +112,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,27 +145,36 @@ 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 (!catalogPath) return null; - try { - fs.lstatSync(catalogPath); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + if (inlineCatalog && configuredPath) { throw new SandboxWorkloadPreparationError( - "the live E2E managed-image catalog path could not be inspected", - { cause: error }, + "the live E2E managed-image catalog has conflicting authorities", ); } + if (!inlineCatalog && !catalogPath) return null; 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", ); } + if (inlineCatalog) { + return { catalog: parseInlineManagedImageCatalog(inlineCatalog), revision }; + } + try { + fs.lstatSync(catalogPath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw new SandboxWorkloadPreparationError( + "the live E2E managed-image catalog path could not be inspected", + { cause: error }, + ); + } return { path: catalogPath, revision }; } @@ -360,9 +391,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 +437,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 01b5aac2984..4eca7b521b9 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -91,25 +91,27 @@ 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 requires one successful managed-image PR workflow run for the candidate commit. +The 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. +Missing, ambiguous, failed, incomplete, mixed, or substituted 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 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/fixtures/managed-image-receipt.ts b/test/e2e/fixtures/managed-image-receipt.ts index 2678c29afe5..0eb6e8c7859 100644 --- a/test/e2e/fixtures/managed-image-receipt.ts +++ b/test/e2e/fixtures/managed-image-receipt.ts @@ -35,20 +35,28 @@ function readCandidateCatalog( 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(); + let parsed: unknown; + if (selected.catalog) { + parsed = selected.catalog; + } else { + 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(); + } + parsed = JSON.parse(fs.readFileSync(descriptor, "utf8")) as unknown; } - 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 ( diff --git a/test/e2e/support/managed-image-receipt.test.ts b/test/e2e/support/managed-image-receipt.test.ts index 09e9c72a7ed..673c5a4841f 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()); diff --git a/test/e2e/support/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index 2a6188ed8c6..80effa445b7 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -1,16 +1,38 @@ // 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, +} 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: @@ -21,18 +43,70 @@ const WORKFLOW_SOURCE = `on: workflow_dispatch: jobs: {} `; +const temporaryDirectories: string[] = []; + +function contract(agent: ManagedImageAgent, index: number): 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: `ghrun-${RUN_ID}-1`, + }, + startupProfileContractVersion: MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, + capabilityContractVersion: MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, + }; +} -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 workflowRun(overrides: Record = {}) { + return { + total_count: 1, + workflow_runs: [ + { + 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 candidateRequest(options: { + readonly candidateRepository?: string; + readonly imageChanged: boolean; + readonly run?: unknown; + readonly artifactHeadSha?: string; + readonly missingAgent?: ManagedImageAgent; +}) { + const candidateRepository = options.candidateRepository ?? CANONICAL_REPOSITORY; const baseEntries = [ treeEntry("Dockerfile.base", "3".repeat(40)), treeEntry("docs/guide.mdx", "4".repeat(40)), ]; const candidateEntries = [ - treeEntry("Dockerfile.base", (imageChanged ? "5" : "3").repeat(40)), + treeEntry("Dockerfile.base", (options.imageChanged ? "5" : "3").repeat(40)), treeEntry("docs/guide.mdx", "4".repeat(40)), ]; const responses = new Map([ @@ -60,49 +134,225 @@ 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", + }, + ], + [ + `/repos/${CANONICAL_REPOSITORY}/actions/workflows/managed-images.yaml/runs?event=pull_request&head_sha=${CANDIDATE_SHA}&per_page=100`, + options.run ?? workflowRun(), + ], ]); + for (const [index, agent] of SHIPPED_MANAGED_IMAGE_AGENTS.entries()) { + const name = `managed-pr-contract-${RUN_ID}-1-${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/${RUN_ID}/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: RUN_ID }, + }, + ], + }, + ); + } 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 }): 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(contract(agent, index))}\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( + 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( - resolvePrManagedImageSource( - selectorInput(CANONICAL_REPOSITORY), - requestFor(CANONICAL_REPOSITORY, true), + resolvePrManagedImageCatalog( + input, + candidateRequest({ imageChanged: true }), + downloadContract, ), - ).resolves.toBe("local-dockerfile"); + ).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)]), + ), + ); + expect(fs.statSync(input.outputPath).mode & 0o777).toBe(0o600); }); - it("reads a validated external candidate repository through the default request policy", async () => { + it("rejects an image-changing fork before any artifact download", 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, - }); - }); - - await expect(resolvePrManagedImageSource(selectorInput(candidateRepository))).resolves.toBe( - "managed-image", - ); + 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("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), + ).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-managed-image-workflow-boundary.test.ts b/test/e2e/support/pr-managed-image-workflow-boundary.test.ts index 3d435fa913f..f1c8718adf4 100644 --- a/test/e2e/support/pr-managed-image-workflow-boundary.test.ts +++ b/test/e2e/support/pr-managed-image-workflow-boundary.test.ts @@ -9,6 +9,21 @@ import { } from "../../../tools/e2e/operations-workflow-boundary.mts"; describe("manual PR managed-image workflow boundary", () => { + it("keeps exact PR candidate catalogs outside the candidate CLI artifact", () => { + const workflow = readE2eOperationsWorkflow(); + + expect(workflow.jobs["base-image-publication"].outputs?.managed_image_catalog).toBe( + "${{ steps.select_pr_source.outputs.catalog }}", + ); + expect(workflow.jobs["generate-matrix"].outputs?.managed_image_catalog).toBeUndefined(); + expect(validateE2eOperationsWorkflow(workflow)).not.toEqual( + expect.arrayContaining([ + "Manual PR E2E must not resolve an exact candidate managed-image catalog", + "Manual PR CLI packaging must not accept obsolete managed-image catalog authority", + ]), + ); + }); + it.each([ ["workflow", (workflow: ReturnType) => workflow], [ @@ -39,7 +54,7 @@ describe("manual PR managed-image workflow boundary", () => { ); }); - it("rejects restoration of the obsolete manual PR catalog resolver", () => { + it("rejects moving the trusted catalog resolver into candidate CLI packaging", () => { const workflow = readE2eOperationsWorkflow(); const matrixJob = workflow.jobs["generate-matrix"]; matrixJob.outputs!.managed_image_catalog = 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/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 5a6d21ecf0b..7a6e390669f 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -35,7 +35,36 @@ 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 "::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 +357,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] : {}; @@ -638,7 +665,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 +706,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 +716,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 +727,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 +818,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 +870,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 +1496,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 +1518,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..a4c3600198a 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,43 @@ import { parseManagedImageContractV1, SHIPPED_MANAGED_IMAGE_AGENTS, type ManagedImageContractCatalog, + type ManagedImageContractV1, } from "../../src/lib/onboard/managed-image/contract.ts"; import { baseImageInputsChanged, 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; -export type PrManagedImageSource = "local-dockerfile" | "managed-image"; +export type PrManagedImageSelection = "base-cohort" | "candidate-catalog"; + +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,7 +66,7 @@ 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, ): ManagedImageContractCatalog { @@ -79,7 +100,7 @@ function assembleManagedImageCatalog( ); } -function writeManagedImageCatalog( +export function writeManagedImageCatalog( contractPaths: readonly string[], candidateSha: string, outputPath: string, @@ -128,20 +149,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 +236,72 @@ 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 (response.total_count !== 1 || !Array.isArray(response.workflow_runs)) { + throw new Error("exact managed-image workflow run is missing or ambiguous"); + } + if (response.workflow_runs.length !== 1) { + throw new Error("exact managed-image workflow run listing is incomplete"); + } + const run = record(response.workflow_runs[0], "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 (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") { + throw new Error( + `managed-image workflow for candidate ${expected.headSha} must complete successfully before live E2E`, + ); + } + return { attempt, headSha: expected.headSha, id }; +} + +/** 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 +310,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 +327,54 @@ 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 run = selectManagedImagePublicationRun( + await request( + `/repos/${REPOSITORY}/actions/workflows/${MANAGED_IMAGE_WORKFLOW_FILE}/runs?event=pull_request&head_sha=${input.candidateSha}&per_page=100`, + ), + { 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); + fs.mkdirSync(path.dirname(path.resolve(input.outputPath)), { mode: 0o700, recursive: true }); + fs.writeFileSync(input.outputPath, `${JSON.stringify(catalog)}\n`, { + encoding: "utf8", + flag: "wx", + mode: 0o600, + }); + return "candidate-catalog"; + } finally { + fs.rmSync(temporaryDirectory, { force: true, recursive: true }); + } } function requiredInteger(value: string | undefined, label: string): number { @@ -249,16 +391,17 @@ export async function main(argv = process.argv.slice(2), env = process.env): Pro 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 || '' }}" ) { From c3d229642bbe8b42044bd9a2dd346a7339b736bc Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 31 Aug 2026 22:13:10 -0700 Subject: [PATCH 02/11] fix(e2e): bind candidate workflow authorities Signed-off-by: Prekshi Vyas --- .github/workflows/managed-images.yaml | 1 + .../managed/managed-image-publication-workflow.test.ts | 3 +++ tools/e2e/mcp-workflow-boundary.mts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 89f2f5efa59..62a93fe8958 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -870,6 +870,7 @@ 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_MANAGED_ACTIVATION_CATALOG: ${{ github.workspace }}/managed-pr-catalog.json NEMOCLAW_NON_INTERACTIVE: "1" NEMOCLAW_RUN_LIVE_E2E: "1" diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 491b729b81d..585ac888331 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -617,6 +617,9 @@ describe("complete managed-image publication workflow", () => { ); expect(activation.permissions).toEqual({ contents: "read" }); expect(activation.env?.CANDIDATE_SHA).toBe("${{ github.event.pull_request.head.sha }}"); + expect(activation.env?.NEMOCLAW_E2E_EXPECTED_SHA).toBe( + "${{ github.event.pull_request.head.sha }}", + ); expect(activation.env?.NEMOCLAW_MANAGED_ACTIVATION_CATALOG).toBe( "${{ github.workspace }}/managed-pr-catalog.json", ); 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", From 90df99d5982274e0155a4f97f68207d188690784 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 31 Aug 2026 22:21:27 -0700 Subject: [PATCH 03/11] fix(e2e): preserve candidate catalog evidence Signed-off-by: Prekshi Vyas --- .../sandbox-workload-preparation.test.ts | 7 +++ src/lib/onboard/workload/preparation.ts | 20 ++++--- test/e2e/fixtures/availability-env.ts | 1 + test/e2e/fixtures/managed-image-receipt.ts | 33 ++++-------- test/e2e/live/mcp-bridge-onboard-env.ts | 5 +- .../e2e/support/managed-image-receipt.test.ts | 7 ++- .../support/mcp-bridge-onboard-env.test.ts | 53 +++++++++++++++++++ .../pr-managed-image-publication.test.ts | 24 +++++++++ tools/e2e/pr-managed-image-publication.mts | 14 ++--- 9 files changed, 125 insertions(+), 39 deletions(-) diff --git a/src/lib/onboard/sandbox-workload-preparation.test.ts b/src/lib/onboard/sandbox-workload-preparation.test.ts index 69d4a015fbc..b162ddbfc41 100644 --- a/src/lib/onboard/sandbox-workload-preparation.test.ts +++ b/src/lib/onboard/sandbox-workload-preparation.test.ts @@ -202,6 +202,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", diff --git a/src/lib/onboard/workload/preparation.ts b/src/lib/onboard/workload/preparation.ts index 908cc42bd36..25722082ffd 100644 --- a/src/lib/onboard/workload/preparation.ts +++ b/src/lib/onboard/workload/preparation.ts @@ -156,16 +156,16 @@ export function liveE2eManagedImageCatalog( "the live E2E managed-image catalog has conflicting authorities", ); } - if (!inlineCatalog && !catalogPath) return null; - 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", - ); - } 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); } catch (error) { @@ -175,6 +175,12 @@ export function liveE2eManagedImageCatalog( { cause: error }, ); } + 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 { path: catalogPath, revision }; } 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 0eb6e8c7859..14185a52de7 100644 --- a/test/e2e/fixtures/managed-image-receipt.ts +++ b/test/e2e/fixtures/managed-image-receipt.ts @@ -1,10 +1,13 @@ // 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"; +import { + openRegularFileNoFollow, + type OpenRegularFile, +} from "../../../src/lib/adapters/fs/regular-file.ts"; import { DEFAULT_GATEWAY_PORT } from "../../../src/lib/core/ports.ts"; import { isShippedManagedImageAgent, @@ -33,29 +36,16 @@ function readCandidateCatalog( throw new Error("stock onboarding requires a selected candidate managed-image catalog"); } - let descriptor: number | null = null; + let candidateCatalog: OpenRegularFile | null = null; try { let parsed: unknown; if (selected.catalog) { parsed = selected.catalog; } else { - 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(); - } - parsed = JSON.parse(fs.readFileSync(descriptor, "utf8")) as unknown; + candidateCatalog = openRegularFileNoFollow(selected.path); + const metadata = candidateCatalog.stat(); + if (metadata.size < 2 || metadata.size > 64 * 1024) throw new Error(); + parsed = JSON.parse(candidateCatalog.readBytes(64 * 1024).toString("utf8")) as unknown; } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(); const catalog = parsed as Record; @@ -89,7 +79,7 @@ function readCandidateCatalog( } catch { throw new Error("stock onboarding candidate managed-image catalog is invalid"); } finally { - if (descriptor !== null) fs.closeSync(descriptor); + candidateCatalog?.close(); } } @@ -229,9 +219,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/support/managed-image-receipt.test.ts b/test/e2e/support/managed-image-receipt.test.ts index 673c5a4841f..d945ab71f72 100644 --- a/test/e2e/support/managed-image-receipt.test.ts +++ b/test/e2e/support/managed-image-receipt.test.ts @@ -323,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", @@ -333,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/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 80effa445b7..0c4c39de883 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -23,6 +23,7 @@ import { assembleManagedImageCatalog, resolvePrManagedImageCatalog, selectManagedImagePublicationRun, + writeManagedImageCatalog, } from "../../../tools/e2e/pr-managed-image-publication.mts"; import { artifactZip } from "../../helpers/artifact-zip"; @@ -240,6 +241,29 @@ describe("exact PR managed-image publication", () => { expect(fs.statSync(input.outputPath).mode & 0o777).toBe(0o600); }); + 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); + + 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); diff --git a/tools/e2e/pr-managed-image-publication.mts b/tools/e2e/pr-managed-image-publication.mts index a4c3600198a..b7f69bc5b8c 100644 --- a/tools/e2e/pr-managed-image-publication.mts +++ b/tools/e2e/pr-managed-image-publication.mts @@ -109,6 +109,13 @@ export function writeManagedImageCatalog( (contractPath) => JSON.parse(fs.readFileSync(contractPath, "utf8")) as unknown, ); const catalog = assembleManagedImageCatalog(contracts, candidateSha); + 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", @@ -365,12 +372,7 @@ export async function resolvePrManagedImageCatalog( ); } const catalog = assembleManagedImageCatalog(contracts, input.candidateSha); - fs.mkdirSync(path.dirname(path.resolve(input.outputPath)), { mode: 0o700, recursive: true }); - fs.writeFileSync(input.outputPath, `${JSON.stringify(catalog)}\n`, { - encoding: "utf8", - flag: "wx", - mode: 0o600, - }); + writeValidatedManagedImageCatalog(catalog, input.outputPath); return "candidate-catalog"; } finally { fs.rmSync(temporaryDirectory, { force: true, recursive: true }); From 11b97d5a28f35b7dfafec03139adeb59ee330f09 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 31 Aug 2026 23:42:43 -0700 Subject: [PATCH 04/11] fix(e2e): repair managed-image PR qualification Signed-off-by: Prekshi Vyas --- .github/workflows/managed-images.yaml | 10 +++ .../sandbox-workload-preparation.test.ts | 35 +++++++++ src/lib/onboard/workload/preparation.ts | 45 +++++++++-- test/e2e/README.md | 7 +- test/e2e/fixtures/managed-image-receipt.ts | 53 ++----------- test/e2e/mock-parity.json | 1 + .../pr-managed-image-publication.test.ts | 78 +++++++++++++------ ...managed-image-publication-workflow.test.ts | 2 + tools/e2e/pr-managed-image-publication.mts | 77 +++++++++++------- 9 files changed, 199 insertions(+), 109 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 62a93fe8958..9ddc6d65748 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -871,6 +871,7 @@ jobs: 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" @@ -888,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/src/lib/onboard/sandbox-workload-preparation.test.ts b/src/lib/onboard/sandbox-workload-preparation.test.ts index b162ddbfc41..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"; @@ -248,6 +249,40 @@ describe("sandbox workload preparation", () => { } }); + 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 }); + } + }); + it.each(SHIPPED_MANAGED_IMAGE_AGENTS)( "resolves the complete release catalog and exact %s image (#7744)", async (agent) => { diff --git a/src/lib/onboard/workload/preparation.ts b/src/lib/onboard/workload/preparation.ts index 25722082ffd..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"; @@ -267,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) { @@ -282,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}'`, ); @@ -306,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( @@ -319,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( diff --git a/test/e2e/README.md b/test/e2e/README.md index f563f05fab8..4f475ab2f65 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -100,12 +100,13 @@ It binds each artifact to the selected workflow run, attempt, revision, artifact 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 requires one successful managed-image PR workflow run for the candidate commit. -The run must belong to the same open PR and an NVIDIA/NemoClaw source branch. +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. -Missing, ambiguous, failed, incomplete, mixed, or substituted evidence stops before any stock-onboarding consumer starts. +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. diff --git a/test/e2e/fixtures/managed-image-receipt.ts b/test/e2e/fixtures/managed-image-receipt.ts index 14185a52de7..0604a8b11ba 100644 --- a/test/e2e/fixtures/managed-image-receipt.ts +++ b/test/e2e/fixtures/managed-image-receipt.ts @@ -4,23 +4,20 @@ import os from "node:os"; import path from "node:path"; -import { - openRegularFileNoFollow, - type OpenRegularFile, -} from "../../../src/lib/adapters/fs/regular-file.ts"; import { DEFAULT_GATEWAY_PORT } from "../../../src/lib/core/ports.ts"; 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"; @@ -36,50 +33,10 @@ function readCandidateCatalog( throw new Error("stock onboarding requires a selected candidate managed-image catalog"); } - let candidateCatalog: OpenRegularFile | null = null; try { - let parsed: unknown; - if (selected.catalog) { - parsed = selected.catalog; - } else { - candidateCatalog = openRegularFileNoFollow(selected.path); - const metadata = candidateCatalog.stat(); - if (metadata.size < 2 || metadata.size > 64 * 1024) throw new Error(); - parsed = JSON.parse(candidateCatalog.readBytes(64 * 1024).toString("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 { - candidateCatalog?.close(); } } diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index dab86ffd3a4..3080b3b6347 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -537,6 +537,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/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index 0c4c39de883..b72ec9eeecf 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -71,37 +71,40 @@ function treeEntry(entryPath: string, sha: string) { return { mode: "100644", path: entryPath, sha, type: "blob" }; } -function workflowRun(overrides: Record = {}) { +function workflowRunRecord(overrides: Record = {}) { return { - total_count: 1, - workflow_runs: [ - { - 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, - }, - ], + 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 imageChanged: boolean; readonly run?: unknown; readonly artifactHeadSha?: string; + readonly artifactRunAttempt?: number; + readonly artifactRunId?: number; readonly missingAgent?: ManagedImageAgent; }) { const candidateRepository = options.candidateRepository ?? CANONICAL_REPOSITORY; + const artifactRunAttempt = options.artifactRunAttempt ?? 1; + const artifactRunId = options.artifactRunId ?? RUN_ID; const baseEntries = [ treeEntry("Dockerfile.base", "3".repeat(40)), treeEntry("docs/guide.mdx", "4".repeat(40)), @@ -150,13 +153,13 @@ function candidateRequest(options: { ], ]); for (const [index, agent] of SHIPPED_MANAGED_IMAGE_AGENTS.entries()) { - const name = `managed-pr-contract-${RUN_ID}-1-${agent}`; + 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/${RUN_ID}/artifacts?name=${encodeURIComponent(name)}&per_page=100`, + `/repos/${CANONICAL_REPOSITORY}/actions/runs/${artifactRunId}/artifacts?name=${encodeURIComponent(name)}&per_page=100`, options.missingAgent === agent ? { total_count: 0, artifacts: [] } : { @@ -169,7 +172,10 @@ function candidateRequest(options: { id, name, size_in_bytes: archive.length, - workflow_run: { head_sha: options.artifactHeadSha ?? CANDIDATE_SHA, id: RUN_ID }, + workflow_run: { + head_sha: options.artifactHeadSha ?? CANDIDATE_SHA, + id: artifactRunId, + }, }, ], }, @@ -291,6 +297,34 @@ describe("exact PR managed-image publication", () => { 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, downloadContract), + ).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("rejects missing or ambiguous exact-candidate Images runs", async () => { const download = vi.fn(downloadContract); diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 585ac888331..60987ca253e 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -620,6 +620,7 @@ describe("complete managed-image publication workflow", () => { expect(activation.env?.NEMOCLAW_E2E_EXPECTED_SHA).toBe( "${{ github.event.pull_request.head.sha }}", ); + expect(activation.env?.NEMOCLAW_E2E_SHARD).toBe("default"); expect(activation.env?.NEMOCLAW_MANAGED_ACTIVATION_CATALOG).toBe( "${{ github.workspace }}/managed-pr-catalog.json", ); @@ -628,6 +629,7 @@ describe("complete managed-image publication workflow", () => { expect(step(activation, "Checkout exact PR head").with?.ref).toBe( "${{ github.event.pull_request.head.sha }}", ); + expect(step(activation, "Bind E2E correlation identity").run).toContain("randomUUID()"); expect(step(activation, "Assemble exact all-agent activation catalog").run).toMatch( /npm ci --ignore-scripts[\s\S]*pr-managed-image-publication\.mts assemble[\s\S]*"\$CANDIDATE_SHA"[\s\S]*"\$\{contracts\[@\]\}"/u, ); diff --git a/tools/e2e/pr-managed-image-publication.mts b/tools/e2e/pr-managed-image-publication.mts index b7f69bc5b8c..8d53486d833 100644 --- a/tools/e2e/pr-managed-image-publication.mts +++ b/tools/e2e/pr-managed-image-publication.mts @@ -261,45 +261,62 @@ export function selectManagedImagePublicationRun( positiveInteger(expected.prNumber, "PR number"); positiveInteger(expected.workflowId, "managed-image workflow id"); const response = record(payload, "managed-image workflow runs"); - if (response.total_count !== 1 || !Array.isArray(response.workflow_runs)) { - throw new Error("exact managed-image workflow run is missing or ambiguous"); + if (!Array.isArray(response.workflow_runs)) { + throw new Error("exact managed-image workflow run listing is invalid"); } - if (response.workflow_runs.length !== 1) { + if (response.total_count !== response.workflow_runs.length) { throw new Error("exact managed-image workflow run listing is incomplete"); } - const run = record(response.workflow_runs[0], "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 (run.workflow_id !== expected.workflowId) { - throw new Error("managed-image workflow run does not match the trusted workflow"); + if (response.workflow_runs.length === 0) { + throw new Error("exact managed-image workflow run is missing or ambiguous"); } - 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"); + 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 (run.status !== "completed" || run.conclusion !== "success") { + if (successfulRuns.length === 0) { throw new Error( `managed-image workflow for candidate ${expected.headSha} must complete successfully before live E2E`, ); } - return { attempt, headSha: expected.headSha, id }; + 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. */ From ae289f514d2745f091542f35f621b8cb33f90454 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Tue, 1 Sep 2026 00:45:37 -0700 Subject: [PATCH 05/11] fix(e2e): select successful PR GPU base cohort Signed-off-by: Prekshi Vyas --- .github/workflows/pr-self-hosted.yaml | 4 +++- test/e2e/support/pr-self-hosted-llama-selector.test.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-self-hosted.yaml b/.github/workflows/pr-self-hosted.yaml index c7e2ff48b84..218799c60f9 100644 --- a/.github/workflows/pr-self-hosted.yaml +++ b/.github/workflows/pr-self-hosted.yaml @@ -114,13 +114,15 @@ 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 export GITHUB_REF=refs/heads/main export GITHUB_SHA="$EXPECTED_SHA" - node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 3000 --poll-seconds 30 + node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 300 --poll-seconds 30 llama-cpp-generic-gpu: name: llama.cpp on generic NVIDIA GPU 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..bbda155bfdc 100644 --- a/test/e2e/support/pr-self-hosted-llama-selector.test.ts +++ b/test/e2e/support/pr-self-hosted-llama-selector.test.ts @@ -158,14 +158,16 @@ 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' }}", }); expect(publication?.run).toContain("export GITHUB_REF=refs/heads/main"); expect(publication?.run).toContain('export GITHUB_SHA="$EXPECTED_SHA"'); expect(publication?.run).toContain( - "node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 3000 --poll-seconds 30", + "node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 300 --poll-seconds 30", ); expect(value.jobs["llama-cpp-generic-gpu"]?.env?.E2E_MANAGED_IMAGE_REVISION).toBe( From 98bcb600abfa6b95d90d77b9235d22d60ae89247 Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 1 Sep 2026 15:53:21 +0700 Subject: [PATCH 06/11] fix(e2e): bind candidate publication evidence Signed-off-by: San Dang --- .github/workflows/e2e.yaml | 3 +- .github/workflows/pr-self-hosted.yaml | 2 +- test/e2e/README.md | 2 + test/e2e/docs/jetson-dispatch.md | 5 +- .../support/jetson-workflow-boundary.test.ts | 20 ++++++- .../pr-managed-image-publication.test.ts | 56 ++++++++++++++++--- .../pr-self-hosted-llama-selector.test.ts | 2 +- ...managed-image-publication-workflow.test.ts | 14 ++++- tools/e2e/operations-workflow-boundary.mts | 1 + tools/e2e/pr-managed-image-publication.mts | 25 ++++++++- tools/e2e/workflow-boundary.mts | 4 +- 11 files changed, 114 insertions(+), 20 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index de5b36ec8bf..b3a8067da10 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -251,6 +251,7 @@ jobs: 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 @@ -5423,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 diff --git a/.github/workflows/pr-self-hosted.yaml b/.github/workflows/pr-self-hosted.yaml index 218799c60f9..e4413dd45d4 100644 --- a/.github/workflows/pr-self-hosted.yaml +++ b/.github/workflows/pr-self-hosted.yaml @@ -122,7 +122,7 @@ jobs: set -euo pipefail export GITHUB_REF=refs/heads/main export GITHUB_SHA="$EXPECTED_SHA" - node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 300 --poll-seconds 30 + node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 3000 --poll-seconds 30 llama-cpp-generic-gpu: name: llama.cpp on generic NVIDIA GPU diff --git a/test/e2e/README.md b/test/e2e/README.md index 4f475ab2f65..a555d919257 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -113,6 +113,8 @@ Unchanged runs pass the selected base revision and complete cohort receipt to ev 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/support/jetson-workflow-boundary.test.ts b/test/e2e/support/jetson-workflow-boundary.test.ts index b5536471b7e..aa2588e1607 100644 --- a/test/e2e/support/jetson-workflow-boundary.test.ts +++ b/test/e2e/support/jetson-workflow-boundary.test.ts @@ -45,6 +45,22 @@ describe("Jetson nvmap GPU E2E workflow boundary", () => { ); }); + it("rejects Jetson dispatch without an ARM-qualified managed-image revision", () => { + const errors = validateWorkflowMutation((workflow) => { + const job = (workflow.jobs as Record)["jetson-nvmap-gpu"] as { + if?: string; + }; + job.if = job.if?.replace( + " && needs['base-image-publication'].outputs.managed_image_revision != ''", + "", + ); + }); + + expect(errors).toContain( + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", + ); + }); + it("keeps manual Jetson dispatch disabled by default (#8142)", () => { const inputErrors = validateWorkflowMutation((workflow) => { const triggers = (workflow.on ?? workflow[true as unknown as string]) as { @@ -76,7 +92,7 @@ describe("Jetson nvmap GPU E2E workflow boundary", () => { }); expect(errors).toContain( - "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections", + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", ); }); @@ -89,7 +105,7 @@ describe("Jetson nvmap GPU E2E workflow boundary", () => { }); expect(errors).toContain( - "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections", + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", ); }); diff --git a/test/e2e/support/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index b72ec9eeecf..54d2b4305ed 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -46,7 +46,11 @@ jobs: {} `; const temporaryDirectories: string[] = []; -function contract(agent: ManagedImageAgent, index: number): ManagedImageContractV1 { +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 { @@ -60,13 +64,17 @@ function contract(agent: ManagedImageAgent, index: number): ManagedImageContract repository: MANAGED_IMAGE_SOURCE_REPOSITORY, revision: CANDIDATE_SHA, release: "v0.0.110", - cohort: `ghrun-${RUN_ID}-1`, + 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(entryPath: string, sha: string) { return { mode: "100644", path: entryPath, sha, type: "blob" }; } @@ -199,13 +207,19 @@ function resolverInput(candidateRepository = CANONICAL_REPOSITORY) { }; } -function downloadContract(identity: { readonly name: string }): Promise { +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(contract(agent, index))}\n` }, + { + name: "contract.json", + contents: `${JSON.stringify(contractForCohort(agent, index, cohort))}\n`, + }, ]), ); } @@ -263,7 +277,12 @@ describe("exact PR managed-image publication", () => { return contractPath; }); const assembledPath = path.join(assemblyRoot, "assembled", "catalog.json"); - writeManagedImageCatalog(contractPaths, CANDIDATE_SHA, assembledPath); + 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); @@ -315,7 +334,9 @@ describe("exact PR managed-image publication", () => { ); await expect( - resolvePrManagedImageCatalog(resolverInput(), request, downloadContract), + resolvePrManagedImageCatalog(resolverInput(), request, (identity) => + downloadContract(identity, `ghrun-${laterRunId}-2`), + ), ).resolves.toBe("candidate-catalog"); expect(request).toHaveBeenCalledWith( expect.stringContaining(`/actions/runs/${laterRunId}/artifacts`), @@ -325,6 +346,23 @@ describe("exact PR managed-image publication", () => { ); }); + 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); @@ -409,7 +447,11 @@ describe("exact PR managed-image publication", () => { }; expect(() => - assembleManagedImageCatalog([substituted, ...contracts.slice(1)], CANDIDATE_SHA), + assembleManagedImageCatalog( + [substituted, ...contracts.slice(1)], + CANDIDATE_SHA, + `ghrun-${RUN_ID}-1`, + ), ).toThrow("do not match the candidate commit"); }); 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 bbda155bfdc..ae36c3a17eb 100644 --- a/test/e2e/support/pr-self-hosted-llama-selector.test.ts +++ b/test/e2e/support/pr-self-hosted-llama-selector.test.ts @@ -167,7 +167,7 @@ describe("generic NVIDIA GPU PR selection", () => { expect(publication?.run).toContain("export GITHUB_REF=refs/heads/main"); expect(publication?.run).toContain('export GITHUB_SHA="$EXPECTED_SHA"'); expect(publication?.run).toContain( - "node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 300 --poll-seconds 30", + "node --experimental-strip-types --no-warnings tools/e2e/base-image-publication.mts --wait-seconds 3000 --poll-seconds 30", ); expect(value.jobs["llama-cpp-generic-gpu"]?.env?.E2E_MANAGED_IMAGE_REVISION).toBe( diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 60987ca253e..28f62dd614f 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -84,6 +84,16 @@ function managedPrOpenClawMcpDiscovery(workflow: Workflow): Job { return required(workflow.jobs?.["pr-openclaw-mcp-discovery"], "missing exact PR MCP gate"); } +function expectCorrelationIdentity(run: string | undefined): void { + const source = required(run, "managed-image workflow is missing its correlation binding"); + expect(source).toContain("randomUUID()"); + expect(source).toContain( + '[[ "$correlation_id" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$ ]]', + ); + expect(source).toContain("printf 'NEMOCLAW_E2E_CORRELATION_ID=%s\\n'"); + expect(source).toContain('>> "$GITHUB_ENV"'); +} + describe("complete managed-image publication workflow", () => { it("rejects managed package paths redirected outside node_modules", () => { const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-plugin-")); @@ -629,7 +639,7 @@ describe("complete managed-image publication workflow", () => { expect(step(activation, "Checkout exact PR head").with?.ref).toBe( "${{ github.event.pull_request.head.sha }}", ); - expect(step(activation, "Bind E2E correlation identity").run).toContain("randomUUID()"); + expectCorrelationIdentity(step(activation, "Bind E2E correlation identity").run); expect(step(activation, "Assemble exact all-agent activation catalog").run).toMatch( /npm ci --ignore-scripts[\s\S]*pr-managed-image-publication\.mts assemble[\s\S]*"\$CANDIDATE_SHA"[\s\S]*"\$\{contracts\[@\]\}"/u, ); @@ -681,7 +691,7 @@ describe("complete managed-image publication workflow", () => { expect(step(discovery, "Checkout exact PR head").with?.ref).toBe( "${{ github.event.pull_request.head.sha }}", ); - expect(step(discovery, "Bind E2E correlation identity").run).toContain("randomUUID()"); + expectCorrelationIdentity(step(discovery, "Bind E2E correlation identity").run); const assemble = step(discovery, "Assemble exact all-agent MCP catalog").run ?? ""; expect(assemble).toMatch( /npm ci --ignore-scripts[\s\S]*pr-managed-image-publication\.mts assemble[\s\S]*"\$CANDIDATE_SHA"[\s\S]*"\$\{contracts\[@\]\}"/u, diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 7a6e390669f..d7456af1691 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -60,6 +60,7 @@ const PR_MANAGED_IMAGE_RESOLVER_SCRIPT = " 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", diff --git a/tools/e2e/pr-managed-image-publication.mts b/tools/e2e/pr-managed-image-publication.mts index 8d53486d833..4fdd660c75a 100644 --- a/tools/e2e/pr-managed-image-publication.mts +++ b/tools/e2e/pr-managed-image-publication.mts @@ -39,6 +39,7 @@ const TREE_ENTRY_MODES = new Map([ ]); type JsonRecord = Record; +type ManagedImageCohort = ManagedImageContractV1["source"]["cohort"]; export type PrManagedImageSelection = "base-cohort" | "candidate-catalog"; @@ -69,6 +70,7 @@ function exactString(value: unknown, expected: string, label: string): void { 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) { @@ -95,6 +97,11 @@ export 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)!]), ); @@ -104,11 +111,12 @@ 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); } @@ -388,7 +396,11 @@ export async function resolvePrManagedImageCatalog( JSON.parse(fs.readFileSync(contractPath, "utf8")) as unknown as ManagedImageContractV1, ); } - const catalog = assembleManagedImageCatalog(contracts, input.candidateSha); + const catalog = assembleManagedImageCatalog( + contracts, + input.candidateSha, + `ghrun-${run.id}-${run.attempt}` as const, + ); writeValidatedManagedImageCatalog(catalog, input.outputPath); return "candidate-catalog"; } finally { @@ -406,7 +418,14 @@ 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; } diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 4638cb6abe0..56bae7211a2 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -1767,10 +1767,10 @@ 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", + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", ); } if (job["runs-on"] !== "ubuntu-latest") { From 5516a59963b8b8c3a5b10a0492c56efe5803bc5e Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 1 Sep 2026 16:10:57 +0700 Subject: [PATCH 07/11] fix(e2e): cover managed image inputs Signed-off-by: San Dang --- .github/workflows/base-image.yaml | 8 +++-- .../pr-managed-image-publication.test.ts | 30 ++++++++++++++----- ...pr-managed-image-workflow-boundary.test.ts | 17 +---------- tools/e2e/base-image-publication.mts | 3 ++ 4 files changed, 31 insertions(+), 27 deletions(-) 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/test/e2e/support/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index 54d2b4305ed..ee220d6326b 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -41,6 +41,7 @@ const WORKFLOW_SOURCE = `on: paths: - ".github/workflows/base-image.yaml" - "Dockerfile.base" + - "src/lib/onboard/**" workflow_dispatch: jobs: {} `; @@ -103,6 +104,7 @@ function workflowRun(overrides: Record = {}) { function candidateRequest(options: { readonly candidateRepository?: string; + readonly changedPath?: string; readonly imageChanged: boolean; readonly run?: unknown; readonly artifactHeadSha?: string; @@ -111,14 +113,15 @@ function candidateRequest(options: { 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", (options.imageChanged ? "5" : "3").repeat(40)), + treeEntry(changedPath, (options.imageChanged ? "5" : "3").repeat(40)), treeEntry("docs/guide.mdx", "4".repeat(40)), ]; const responses = new Map([ @@ -261,6 +264,22 @@ describe("exact PR managed-image publication", () => { expect(fs.statSync(input.outputPath).mode & 0o777).toBe(0o600); }); + 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( @@ -277,12 +296,7 @@ describe("exact PR managed-image publication", () => { return contractPath; }); const assembledPath = path.join(assemblyRoot, "assembled", "catalog.json"); - writeManagedImageCatalog( - contractPaths, - CANDIDATE_SHA, - assembledPath, - `ghrun-${RUN_ID}-1`, - ); + 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); diff --git a/test/e2e/support/pr-managed-image-workflow-boundary.test.ts b/test/e2e/support/pr-managed-image-workflow-boundary.test.ts index f1c8718adf4..3d435fa913f 100644 --- a/test/e2e/support/pr-managed-image-workflow-boundary.test.ts +++ b/test/e2e/support/pr-managed-image-workflow-boundary.test.ts @@ -9,21 +9,6 @@ import { } from "../../../tools/e2e/operations-workflow-boundary.mts"; describe("manual PR managed-image workflow boundary", () => { - it("keeps exact PR candidate catalogs outside the candidate CLI artifact", () => { - const workflow = readE2eOperationsWorkflow(); - - expect(workflow.jobs["base-image-publication"].outputs?.managed_image_catalog).toBe( - "${{ steps.select_pr_source.outputs.catalog }}", - ); - expect(workflow.jobs["generate-matrix"].outputs?.managed_image_catalog).toBeUndefined(); - expect(validateE2eOperationsWorkflow(workflow)).not.toEqual( - expect.arrayContaining([ - "Manual PR E2E must not resolve an exact candidate managed-image catalog", - "Manual PR CLI packaging must not accept obsolete managed-image catalog authority", - ]), - ); - }); - it.each([ ["workflow", (workflow: ReturnType) => workflow], [ @@ -54,7 +39,7 @@ describe("manual PR managed-image workflow boundary", () => { ); }); - it("rejects moving the trusted catalog resolver into candidate CLI packaging", () => { + it("rejects restoration of the obsolete manual PR catalog resolver", () => { const workflow = readE2eOperationsWorkflow(); const matrixJob = workflow.jobs["generate-matrix"]; matrixJob.outputs!.managed_image_catalog = 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, From 2f6f5d5dc161f190b26842ab9026bad19ca0636d Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 1 Sep 2026 16:24:54 +0700 Subject: [PATCH 08/11] test(e2e): replace workflow shape assertions Signed-off-by: San Dang --- .github/workflows/managed-images.yaml | 8 +- .../support/e2e-correlation-identity.test.ts | 46 +++++++++ .../jetson-managed-revision-boundary.test.ts | 94 +++++++++++++++++++ .../support/jetson-workflow-boundary.test.ts | 20 +--- ...managed-image-publication-workflow.test.ts | 17 +--- tools/e2e/bind-correlation-identity.mts | 38 ++++++++ tools/e2e/workflow-boundary.mts | 2 +- 7 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 test/e2e/support/e2e-correlation-identity.test.ts create mode 100644 test/e2e/support/jetson-managed-revision-boundary.test.ts create mode 100644 tools/e2e/bind-correlation-identity.mts diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 9ddc6d65748..99dbc74e4a6 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -890,13 +890,7 @@ jobs: 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" + run: node --experimental-strip-types --no-warnings tools/e2e/bind-correlation-identity.mts - name: Download exact published all-agent contracts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/test/e2e/support/e2e-correlation-identity.test.ts b/test/e2e/support/e2e-correlation-identity.test.ts new file mode 100644 index 00000000000..4d282d78716 --- /dev/null +++ b/test/e2e/support/e2e-correlation-identity.test.ts @@ -0,0 +1,46 @@ +// 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"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { bindE2eCorrelationIdentity } from "../../../tools/e2e/bind-correlation-identity.mts"; + +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + fs.rmSync(directory, { force: true, recursive: true }); + } +}); + +describe("E2E correlation identity", () => { + it("exports one generated lowercase UUIDv4 through the GitHub environment file", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-e2e-correlation-")); + temporaryDirectories.push(directory); + const outputPath = path.join(directory, "github-env"); + const correlationId = "01234567-89ab-4def-8abc-0123456789ab"; + + expect(bindE2eCorrelationIdentity(outputPath, () => correlationId)).toBe(correlationId); + expect(fs.readFileSync(outputPath, "utf8")).toBe( + `NEMOCLAW_E2E_CORRELATION_ID=${correlationId}\n`, + ); + }); + + it.each(["not-a-uuid", "01234567-89ab-3def-8abc-0123456789ab"])( + "rejects invalid generated identity %s", + (correlationId) => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-e2e-correlation-")); + temporaryDirectories.push(directory); + const outputPath = path.join(directory, "github-env"); + + expect(() => bindE2eCorrelationIdentity(outputPath, () => correlationId)).toThrow( + "must be a lowercase UUIDv4", + ); + expect(fs.existsSync(outputPath)).toBe(false); + }, + ); +}); 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/jetson-workflow-boundary.test.ts b/test/e2e/support/jetson-workflow-boundary.test.ts index aa2588e1607..b5536471b7e 100644 --- a/test/e2e/support/jetson-workflow-boundary.test.ts +++ b/test/e2e/support/jetson-workflow-boundary.test.ts @@ -45,22 +45,6 @@ describe("Jetson nvmap GPU E2E workflow boundary", () => { ); }); - it("rejects Jetson dispatch without an ARM-qualified managed-image revision", () => { - const errors = validateWorkflowMutation((workflow) => { - const job = (workflow.jobs as Record)["jetson-nvmap-gpu"] as { - if?: string; - }; - job.if = job.if?.replace( - " && needs['base-image-publication'].outputs.managed_image_revision != ''", - "", - ); - }); - - expect(errors).toContain( - "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", - ); - }); - it("keeps manual Jetson dispatch disabled by default (#8142)", () => { const inputErrors = validateWorkflowMutation((workflow) => { const triggers = (workflow.on ?? workflow[true as unknown as string]) as { @@ -92,7 +76,7 @@ describe("Jetson nvmap GPU E2E workflow boundary", () => { }); expect(errors).toContain( - "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections", ); }); @@ -105,7 +89,7 @@ describe("Jetson nvmap GPU E2E workflow boundary", () => { }); expect(errors).toContain( - "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in plus an ARM-qualified managed-image revision for manual selections", + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections", ); }); diff --git a/test/inference/managed/managed-image-publication-workflow.test.ts b/test/inference/managed/managed-image-publication-workflow.test.ts index 28f62dd614f..491b729b81d 100644 --- a/test/inference/managed/managed-image-publication-workflow.test.ts +++ b/test/inference/managed/managed-image-publication-workflow.test.ts @@ -84,16 +84,6 @@ function managedPrOpenClawMcpDiscovery(workflow: Workflow): Job { return required(workflow.jobs?.["pr-openclaw-mcp-discovery"], "missing exact PR MCP gate"); } -function expectCorrelationIdentity(run: string | undefined): void { - const source = required(run, "managed-image workflow is missing its correlation binding"); - expect(source).toContain("randomUUID()"); - expect(source).toContain( - '[[ "$correlation_id" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$ ]]', - ); - expect(source).toContain("printf 'NEMOCLAW_E2E_CORRELATION_ID=%s\\n'"); - expect(source).toContain('>> "$GITHUB_ENV"'); -} - describe("complete managed-image publication workflow", () => { it("rejects managed package paths redirected outside node_modules", () => { const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-plugin-")); @@ -627,10 +617,6 @@ describe("complete managed-image publication workflow", () => { ); expect(activation.permissions).toEqual({ contents: "read" }); expect(activation.env?.CANDIDATE_SHA).toBe("${{ github.event.pull_request.head.sha }}"); - expect(activation.env?.NEMOCLAW_E2E_EXPECTED_SHA).toBe( - "${{ github.event.pull_request.head.sha }}", - ); - expect(activation.env?.NEMOCLAW_E2E_SHARD).toBe("default"); expect(activation.env?.NEMOCLAW_MANAGED_ACTIVATION_CATALOG).toBe( "${{ github.workspace }}/managed-pr-catalog.json", ); @@ -639,7 +625,6 @@ describe("complete managed-image publication workflow", () => { expect(step(activation, "Checkout exact PR head").with?.ref).toBe( "${{ github.event.pull_request.head.sha }}", ); - expectCorrelationIdentity(step(activation, "Bind E2E correlation identity").run); expect(step(activation, "Assemble exact all-agent activation catalog").run).toMatch( /npm ci --ignore-scripts[\s\S]*pr-managed-image-publication\.mts assemble[\s\S]*"\$CANDIDATE_SHA"[\s\S]*"\$\{contracts\[@\]\}"/u, ); @@ -691,7 +676,7 @@ describe("complete managed-image publication workflow", () => { expect(step(discovery, "Checkout exact PR head").with?.ref).toBe( "${{ github.event.pull_request.head.sha }}", ); - expectCorrelationIdentity(step(discovery, "Bind E2E correlation identity").run); + expect(step(discovery, "Bind E2E correlation identity").run).toContain("randomUUID()"); const assemble = step(discovery, "Assemble exact all-agent MCP catalog").run ?? ""; expect(assemble).toMatch( /npm ci --ignore-scripts[\s\S]*pr-managed-image-publication\.mts assemble[\s\S]*"\$CANDIDATE_SHA"[\s\S]*"\$\{contracts\[@\]\}"/u, diff --git a/tools/e2e/bind-correlation-identity.mts b/tools/e2e/bind-correlation-identity.mts new file mode 100644 index 00000000000..f474a418cd5 --- /dev/null +++ b/tools/e2e/bind-correlation-identity.mts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { randomUUID } from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const UUID_V4_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u; + +export function bindE2eCorrelationIdentity( + outputPath: string, + generate: () => string = randomUUID, +): string { + if (!outputPath || /[\r\n]/u.test(outputPath)) { + throw new Error("GITHUB_ENV must be a non-empty single-line path"); + } + const correlationId = generate(); + if (!UUID_V4_PATTERN.test(correlationId)) { + throw new Error("generated E2E correlation identity must be a lowercase UUIDv4"); + } + fs.appendFileSync(outputPath, `NEMOCLAW_E2E_CORRELATION_ID=${correlationId}\n`, "utf8"); + return correlationId; +} + +export function main(environment: NodeJS.ProcessEnv = process.env): void { + bindE2eCorrelationIdentity(environment.GITHUB_ENV ?? ""); + console.log("e2e-correlation-identity outcome=bound"); +} + +if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (error) { + console.error(error instanceof Error ? error.message : "unknown E2E correlation error"); + process.exitCode = 1; + } +} diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 56bae7211a2..c9f250ea2fa 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -1770,7 +1770,7 @@ function validateJetsonControllerBoundary(errors: string[], jobs: WorkflowRecord "${{ 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 plus an ARM-qualified managed-image revision for manual selections", + "jetson-nvmap-gpu job must run on trusted main pushes and require opt-in for same-repository manual selections", ); } if (job["runs-on"] !== "ubuntu-latest") { From ecafcefc7651975f406bf81d7f1edb21e104d229 Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 1 Sep 2026 16:42:16 +0700 Subject: [PATCH 09/11] fix(e2e): harden candidate evidence selection Signed-off-by: San Dang --- .github/workflows/e2e.yaml | 6 +- .github/workflows/managed-images.yaml | 8 +- .../support/e2e-correlation-identity.test.ts | 46 ------ .../e2e-operations-workflow-boundary.test.ts | 125 ++-------------- ...manual-pr-credential-authorization.test.ts | 140 ++++++++++++++++++ .../pr-managed-image-publication.test.ts | 64 +++++++- tools/e2e/bind-correlation-identity.mts | 38 ----- tools/e2e/operations-workflow-boundary.mts | 4 +- tools/e2e/pr-managed-image-publication.mts | 6 +- 9 files changed, 229 insertions(+), 208 deletions(-) delete mode 100644 test/e2e/support/e2e-correlation-identity.test.ts create mode 100644 test/e2e/support/manual-pr-credential-authorization.test.ts delete mode 100644 tools/e2e/bind-correlation-identity.mts diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index b3a8067da10..46ba60c0d30 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -379,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; } @@ -746,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" && diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 99dbc74e4a6..9ddc6d65748 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -890,7 +890,13 @@ jobs: node-version: 22.19.0 - name: Bind E2E correlation identity - run: node --experimental-strip-types --no-warnings tools/e2e/bind-correlation-identity.mts + 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 diff --git a/test/e2e/support/e2e-correlation-identity.test.ts b/test/e2e/support/e2e-correlation-identity.test.ts deleted file mode 100644 index 4d282d78716..00000000000 --- a/test/e2e/support/e2e-correlation-identity.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// 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"; - -import { afterEach, describe, expect, it } from "vitest"; - -import { bindE2eCorrelationIdentity } from "../../../tools/e2e/bind-correlation-identity.mts"; - -const temporaryDirectories: string[] = []; - -afterEach(() => { - for (const directory of temporaryDirectories.splice(0)) { - fs.rmSync(directory, { force: true, recursive: true }); - } -}); - -describe("E2E correlation identity", () => { - it("exports one generated lowercase UUIDv4 through the GitHub environment file", () => { - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-e2e-correlation-")); - temporaryDirectories.push(directory); - const outputPath = path.join(directory, "github-env"); - const correlationId = "01234567-89ab-4def-8abc-0123456789ab"; - - expect(bindE2eCorrelationIdentity(outputPath, () => correlationId)).toBe(correlationId); - expect(fs.readFileSync(outputPath, "utf8")).toBe( - `NEMOCLAW_E2E_CORRELATION_ID=${correlationId}\n`, - ); - }); - - it.each(["not-a-uuid", "01234567-89ab-3def-8abc-0123456789ab"])( - "rejects invalid generated identity %s", - (correlationId) => { - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-e2e-correlation-")); - temporaryDirectories.push(directory); - const outputPath = path.join(directory, "github-env"); - - expect(() => bindE2eCorrelationIdentity(outputPath, () => correlationId)).toThrow( - "must be a lowercase UUIDv4", - ); - expect(fs.existsSync(outputPath)).toBe(false); - }, - ); -}); 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/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/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index ee220d6326b..a3a88b706ba 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -107,6 +107,7 @@ function candidateRequest(options: { readonly changedPath?: string; readonly imageChanged: boolean; readonly run?: unknown; + readonly runPages?: readonly unknown[]; readonly artifactHeadSha?: string; readonly artifactRunAttempt?: number; readonly artifactRunId?: number; @@ -158,11 +159,12 @@ function candidateRequest(options: { state: "active", }, ], - [ - `/repos/${CANONICAL_REPOSITORY}/actions/workflows/managed-images.yaml/runs?event=pull_request&head_sha=${CANDIDATE_SHA}&per_page=100`, - options.run ?? workflowRun(), - ], ]); + 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([ @@ -360,6 +362,60 @@ describe("exact PR managed-image publication", () => { ); }); + 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`), + ); + }); + + 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; diff --git a/tools/e2e/bind-correlation-identity.mts b/tools/e2e/bind-correlation-identity.mts deleted file mode 100644 index f474a418cd5..00000000000 --- a/tools/e2e/bind-correlation-identity.mts +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { randomUUID } from "node:crypto"; -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const UUID_V4_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u; - -export function bindE2eCorrelationIdentity( - outputPath: string, - generate: () => string = randomUUID, -): string { - if (!outputPath || /[\r\n]/u.test(outputPath)) { - throw new Error("GITHUB_ENV must be a non-empty single-line path"); - } - const correlationId = generate(); - if (!UUID_V4_PATTERN.test(correlationId)) { - throw new Error("generated E2E correlation identity must be a lowercase UUIDv4"); - } - fs.appendFileSync(outputPath, `NEMOCLAW_E2E_CORRELATION_ID=${correlationId}\n`, "utf8"); - return correlationId; -} - -export function main(environment: NodeJS.ProcessEnv = process.env): void { - bindE2eCorrelationIdentity(environment.GITHUB_ENV ?? ""); - console.log("e2e-correlation-identity outcome=bound"); -} - -if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { - try { - main(); - } catch (error) { - console.error(error instanceof Error ? error.message : "unknown E2E correlation error"); - process.exitCode = 1; - } -} diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index d7456af1691..41061ae76b0 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -391,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}$', @@ -502,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"', diff --git a/tools/e2e/pr-managed-image-publication.mts b/tools/e2e/pr-managed-image-publication.mts index 4fdd660c75a..85492b1ecc7 100644 --- a/tools/e2e/pr-managed-image-publication.mts +++ b/tools/e2e/pr-managed-image-publication.mts @@ -14,6 +14,7 @@ import { } from "../../src/lib/onboard/managed-image/contract.ts"; import { baseImageInputsChanged, + collectPaginated, githubRequest, parseBaseImagePushPaths, } from "./base-image-publication.mts"; @@ -367,10 +368,9 @@ export async function resolvePrManagedImageCatalog( 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 request( - `/repos/${REPOSITORY}/actions/workflows/${MANAGED_IMAGE_WORKFLOW_FILE}/runs?event=pull_request&head_sha=${input.candidateSha}&per_page=100`, - ), + await collectPaginated(request, runsPath, "workflow_runs"), { headSha: input.candidateSha, prNumber: input.prNumber, workflowId }, ); From c6ae364ee2a5b0811571c63844e0190ff41b70eb Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 1 Sep 2026 17:05:17 +0700 Subject: [PATCH 10/11] test(e2e): align managed startup wait Signed-off-by: San Dang --- scripts/checks/run-managed-image-direct-e2e.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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( [ From 0ad5eab1ab3fc0808887f9aabe5ff104c39802dc Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 1 Sep 2026 17:14:08 +0700 Subject: [PATCH 11/11] chore: refresh PR head Signed-off-by: San Dang