diff --git a/.github/workflows/e2e-standard-profile.yaml b/.github/workflows/e2e-standard-profile.yaml index 0c97aa0593a..cf9b4b2fee3 100644 --- a/.github/workflows/e2e-standard-profile.yaml +++ b/.github/workflows/e2e-standard-profile.yaml @@ -21,15 +21,15 @@ on: cli_artifact_provenance: required: true type: string - managed_image_catalog: - required: true - type: string managed_image_revision: required: true type: string managed_image_receipt: required: true type: string + workload_source: + required: true + type: string credential_boundary: required: true type: string @@ -113,6 +113,7 @@ jobs: env: E2E_JOB: "1" E2E_MANAGED_IMAGE_REVISION: ${{ inputs.managed_image_revision }} + E2E_WORKLOAD_SOURCE: ${{ inputs.workload_source }} E2E_TARGET_ID: ${{ inputs.target_id }} NEMOCLAW_RUN_LIVE_E2E: "1" NEMOCLAW_E2E_EXPECTED_SHA: ${{ inputs.candidate_sha }} @@ -400,45 +401,6 @@ jobs: with: provenance-json: ${{ inputs.cli_artifact_provenance }} - - name: Materialize temporary managed-image catalog - if: ${{ inputs.managed_image_catalog != '' }} - shell: /bin/bash --noprofile --norc -e -o pipefail {0} - env: - CANDIDATE_SHA: ${{ inputs.candidate_sha }} - MANAGED_IMAGE_CATALOG: ${{ inputs.managed_image_catalog }} - RESTORE_CLI: ${{ inputs.restore_cli && 'true' || 'false' }} - run: | - set -euo pipefail - catalog_path="${RUNNER_TEMP}/e2e-managed-image-catalog.json" - jq -e --arg revision "$CANDIDATE_SHA" ' - type == "object" and length > 0 and - all(.[]; - .source.revision == $revision and - (.source.release | type == "string" and length > 0) and - (.source.cohort | type == "string" and length > 0) - ) and - ([.[].source.release] | unique | length) == 1 and - ([.[].source.cohort] | unique | length) == 1 - ' <<<"$MANAGED_IMAGE_CATALOG" >/dev/null || { - echo "::error::managed-image catalog source identity does not match the candidate" >&2 - exit 1 - } - if [[ "$RESTORE_CLI" == "true" ]]; then - candidate_release="v$(jq -r '.nemoclawVersion' dist/build-identity.json)" - jq -e --arg release "$candidate_release" ' - all(.[]; .source.release == $release) - ' <<<"$MANAGED_IMAGE_CATALOG" >/dev/null || { - echo "::error::managed-image catalog release does not match the restored CLI" >&2 - exit 1 - } - fi - jq -c . <<<"$MANAGED_IMAGE_CATALOG" >"$catalog_path" - [[ -s "$catalog_path" && ! -L "$catalog_path" ]] || { - echo "::error::temporary managed-image catalog is invalid" >&2 - exit 1 - } - printf 'NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG=%s\n' "$catalog_path" >>"$GITHUB_ENV" - - name: Install reviewed cloudflared if: ${{ inputs.cloudflared }} shell: /bin/bash --noprofile --norc -e -o pipefail {0} diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index fb51b28309e..4d13174838c 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -161,15 +161,13 @@ jobs: outputs: dcode_base_contract: ${{ steps.validate_dcode_base.outputs.contract }} dcode_base_ref: ${{ steps.validate_dcode_base.outputs.base_ref }} - managed_image_artifact_provenance: ${{ steps.download_managed_cohort.outputs.provenance }} - managed_image_cohort: ${{ steps.validate_managed_cohort.outputs.cohort }} managed_image_receipt: ${{ steps.validate_managed_cohort.outputs.receipt }} managed_image_revision: ${{ steps.validate_managed_cohort.outputs.revision }} - managed_image_run_attempt: ${{ steps.validate_managed_cohort.outputs.run_attempt }} - managed_image_run_id: ${{ steps.validate_managed_cohort.outputs.run_id }} + workload_source: ${{ steps.select_pr_source.outputs.workload_source || 'managed-image' }} permissions: actions: read contents: read + pull-requests: read steps: - id: publication_mode name: Classify base-image publication requirement @@ -219,14 +217,33 @@ jobs: with: node-version: 22 + - id: select_pr_source + name: Select PR workload source + if: ${{ inputs.pr_number != '' }} + env: + BASE_SHA: ${{ inputs.base_sha }} + CANDIDATE_REPOSITORY: ${{ inputs.checkout_repository }} + CANDIDATE_SHA: ${{ inputs.checkout_sha }} + GITHUB_TOKEN: ${{ github.token }} + 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" + - id: publication - name: Select complete base and managed-image publication + name: Select base and optional managed-image publication env: 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: "1" - SELECT_NEAREST_SUCCESSFUL_PUBLICATION: ${{ steps.publication_mode.outputs.select_nearest_successful }} + 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 }} shell: bash run: | set -euo pipefail @@ -256,6 +273,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' }} env: GITHUB_TOKEN: ${{ github.token }} PUBLICATION_ARTIFACT_KIND: managed-image-cohort @@ -266,6 +284,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' }} env: PUBLICATION_HEAD_SHA: ${{ steps.publication.outputs.head_sha }} PUBLICATION_RUN_ATTEMPT: ${{ steps.publication.outputs.run_attempt }} @@ -282,6 +301,7 @@ jobs: pull-requests: read outputs: cli_artifact_provenance: ${{ steps.record_cli_artifact.outputs.provenance }} + workload_source: ${{ needs.base-image-publication.outputs.workload_source }} e2e_credentials_allowed: ${{ steps.e2e_credentials.outputs.allowed }} managed_image_catalog: ${{ steps.resolve_pr_managed_image_catalog.outputs.catalog }} matrix: ${{ steps.matrix.outputs.matrix }} @@ -2821,9 +2841,10 @@ jobs: include: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} env: E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} - NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF: ${{ needs.base-image-publication.outputs.dcode_base_ref }} + 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_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" NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1" @@ -2879,7 +2900,7 @@ jobs: build-cli: "false" - name: Record immutable Deep Agents Code base evidence - if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }} + if: ${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' && needs.generate-matrix.outputs.workload_source == 'managed-image' }} env: BASE_CONTRACT: ${{ needs.base-image-publication.outputs.dcode_base_contract }} CANDIDATE_SHA: ${{ inputs.checkout_sha || github.sha }} @@ -3112,9 +3133,9 @@ jobs: risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} 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.generate-matrix.outputs.managed_image_catalog }} - managed_image_revision: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - managed_image_receipt: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} credential_boundary: no provider credential target_id: ${{ matrix.target_id }} catalogue_id: ${{ matrix.id }} @@ -3154,9 +3175,9 @@ jobs: risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} 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.generate-matrix.outputs.managed_image_catalog }} - managed_image_revision: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - managed_image_receipt: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} credential_boundary: NVIDIA API key target_id: ${{ matrix.target_id }} catalogue_id: ${{ matrix.id }} @@ -3197,9 +3218,9 @@ jobs: risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} 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.generate-matrix.outputs.managed_image_catalog }} - managed_image_revision: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - managed_image_receipt: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} credential_boundary: NVIDIA inference API key target_id: ${{ matrix.target_id }} catalogue_id: ${{ matrix.id }} @@ -3240,9 +3261,9 @@ jobs: risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} 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.generate-matrix.outputs.managed_image_catalog }} - managed_image_revision: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - managed_image_receipt: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} credential_boundary: GitHub read token target_id: ${{ matrix.target_id }} catalogue_id: ${{ matrix.id }} @@ -3283,9 +3304,9 @@ jobs: risk_signal_expected_sha: ${{ github.event_name == 'workflow_dispatch' && inputs.checkout_sha != '' && inputs.checkout_sha || '' }} 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.generate-matrix.outputs.managed_image_catalog }} - managed_image_revision: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - managed_image_receipt: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} credential_boundary: Brave and NVIDIA inference API keys target_id: ${{ matrix.target_id }} catalogue_id: ${{ matrix.id }} @@ -3479,8 +3500,9 @@ jobs: agent_runtime: langchain-deepagents-code coverage_variant: deepagents env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "mcp-bridge" E2E_OBSERVABLE_OUTCOME: "Stable OpenShell MCP bridge reaches tools and inference" @@ -3813,8 +3835,9 @@ jobs: # MCP lifecycle without sharing destructive sandbox state. timeout-minutes: 90 env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "openshell-credential-generation-window" E2E_AGENT_RUNTIME: "openclaw" @@ -3987,8 +4010,9 @@ jobs: agent_runtime: langchain-deepagents-code coverage_variant: deepagents env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "mcp-bridge-dev" E2E_OBSERVABLE_OUTCOME: "Development OpenShell MCP bridge reaches tools and inference" @@ -5157,8 +5181,9 @@ jobs: runs-on: ${{ fromJSON(needs.generate-matrix.outputs.runner_routing)['hermes-e2e'] }} timeout-minutes: 85 env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "hermes-e2e" E2E_AGENT_RUNTIME: "hermes" @@ -5266,8 +5291,9 @@ jobs: observable_outcome: "Compatibility-only GPU startup reaches the stable Ready route" coverage_variant: compatibility-only env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "hermes-gpu-startup" E2E_AGENT_RUNTIME: "hermes" @@ -5572,8 +5598,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 70 env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "cloud-onboard" E2E_AGENT_RUNTIME: "openclaw" @@ -5698,8 +5725,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 env: - E2E_MANAGED_IMAGE_REVISION: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }} - E2E_MANAGED_IMAGE_COHORT_RECEIPT: ${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }} + 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 }} E2E_JOB: "1" E2E_TARGET_ID: "messaging-providers" E2E_AGENT_RUNTIME: "openclaw" diff --git a/src/lib/onboard/managed-workload/onboard-orchestration.test.ts b/src/lib/onboard/managed-workload/onboard-orchestration.test.ts index fbf44f710e1..7cec7bcd60e 100644 --- a/src/lib/onboard/managed-workload/onboard-orchestration.test.ts +++ b/src/lib/onboard/managed-workload/onboard-orchestration.test.ts @@ -184,23 +184,15 @@ describe("managed workload onboard orchestration", () => { ).toBe(false); }); - it("keeps stock managed images required during providerless interceptor creation (#9833)", () => { - expect( - shouldActivateStockManagedRuntime({ - portableLifecycle: false, - hermesPortableLifecycle: false, - agentName: "openclaw", - }), - ).toBe(true); - }); - - it("rejects an unavailable catalog for stock managed-image onboarding", async () => { + it("uses the Dockerfile when the stock managed-image catalog is unavailable", async () => { const { runtime } = createFreshOnboardingRuntime( {}, { stockManagedRuntime: true, unavailableCatalog: true }, ); - await expect(runtime.ensurePreparedWorkload()).rejects.toThrow("registry offline"); + await expect(runtime.ensurePreparedWorkload()).resolves.toMatchObject({ + source: { kind: "legacy-dockerfile" }, + }); }); it("rejects an unavailable catalog for explicit temporary managed-image onboarding", async () => { diff --git a/src/lib/onboard/managed-workload/onboard-orchestration.ts b/src/lib/onboard/managed-workload/onboard-orchestration.ts index 25ba9ca4995..0f0d5fde99c 100644 --- a/src/lib/onboard/managed-workload/onboard-orchestration.ts +++ b/src/lib/onboard/managed-workload/onboard-orchestration.ts @@ -231,14 +231,13 @@ export function createManagedWorkloadOnboardRuntime( input.tempManagedRuntime || input.tempManagedRuntimeCatalog !== null || input.managedWorkloadRebuild !== null; - const runtimeCapabilities = - strictManagedRuntime || input.stockManagedRuntime - ? discoveredRuntimeCapabilities - : { - ...discoveredRuntimeCapabilities, - managedImageSelectionPolicy: "prefer-managed" as const, - managedImages: null, - }; + const runtimeCapabilities = strictManagedRuntime + ? discoveredRuntimeCapabilities + : { + ...discoveredRuntimeCapabilities, + managedImageSelectionPolicy: "prefer-managed" as const, + managedImages: input.stockManagedRuntime ? discoveredRuntimeCapabilities.managedImages : null, + }; const runtimeProvider = resolveRuntimeProviderBundle( input.computePlan.driverName, CURRENT_RUNTIME_PROVIDER_BUNDLES, diff --git a/test/e2e/README.md b/test/e2e/README.md index e9731f8e034..01b5aac2984 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -99,20 +99,17 @@ Agents Code on `linux/amd64` and `linux/arm64` before it emits `managed_image_re `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 same-repository PR that changes a managed-image workflow path, the trusted planner also -requires one successful `Images / Build, Test, and Publish Managed Images` run for the candidate -commit. Before candidate checkout, the planner downloads the three nonexpired contract artifacts by -immutable artifact ID. -It verifies each artifact digest, producer run, attempt, and candidate commit. The planner rejects a -missing, incomplete, or mixed all-agent publication before E2E jobs start. - -The planner adds the all-agent catalog to `dist/` after the candidate CLI build completes. -Each live E2E consumer verifies that the catalog source revision matches `checkout_sha`. A PR that -does not change a managed-image workflow path receives the selected base publication through -`E2E_MANAGED_IMAGE_REVISION` and the complete cohort receipt. Every stock-onboarding test asserts -its durable `managed-image` receipt against either the exact candidate catalog or the selected base -cohort before later probes. The GitHub token is available only to the trusted planner job and is not -included in the candidate CLI artifact. +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 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 41a43aebd1f..2678c29afe5 100644 --- a/test/e2e/fixtures/managed-image-receipt.ts +++ b/test/e2e/fixtures/managed-image-receipt.ts @@ -219,8 +219,11 @@ export function assertStockManagedImageReceipt(options: { readonly environment?: NodeJS.ProcessEnv; readonly expectedAgent?: string; readonly sandboxName: string; -}): StockManagedImageReceiptEvidence { +}): 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/fixtures/phases/onboarding.ts b/test/e2e/fixtures/phases/onboarding.ts index f50bd9165d5..de996524d44 100644 --- a/test/e2e/fixtures/phases/onboarding.ts +++ b/test/e2e/fixtures/phases/onboarding.ts @@ -256,11 +256,16 @@ export class OnboardingPhaseFixture { ); } const sandboxName = sandboxNameFromOptions(environment.onboarding, options); - const baseImageReference = requireDcodeBaseImageReference( - options.dcodeBaseImageReference === undefined - ? process.env - : { [DCODE_BASE_IMAGE_ENV]: options.dcodeBaseImageReference }, - ); + const localDockerfile = + options.dcodeBaseImageReference === undefined && + process.env.E2E_WORKLOAD_SOURCE === "local-dockerfile"; + const baseImageReference = localDockerfile + ? undefined + : requireDcodeBaseImageReference( + options.dcodeBaseImageReference === undefined + ? process.env + : { [DCODE_BASE_IMAGE_ENV]: options.dcodeBaseImageReference }, + ); const apiKey = this.secrets.required("NVIDIA_INFERENCE_API_KEY"); this.registerSandboxCleanup(sandboxName); const result = await this.host.nemoclaw([...ONBOARD_ARGS, "--observability"], { @@ -282,7 +287,7 @@ export class OnboardingPhaseFixture { NEMOCLAW_PREFERRED_API: process.env.NEMOCLAW_PREFERRED_API || "openai-completions", NVIDIA_INFERENCE_API_KEY: apiKey, [HOSTED_INFERENCE_CREDENTIAL_ENV]: apiKey, - [DCODE_BASE_IMAGE_ENV]: baseImageReference, + ...(baseImageReference ? { [DCODE_BASE_IMAGE_ENV]: baseImageReference } : {}), }), redactionValues: [apiKey], timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS, diff --git a/test/e2e/fixtures/shell-probe.ts b/test/e2e/fixtures/shell-probe.ts index f477318919e..a2bf837aa50 100644 --- a/test/e2e/fixtures/shell-probe.ts +++ b/test/e2e/fixtures/shell-probe.ts @@ -2,6 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import type { ArtifactSink } from "./artifacts.ts"; +import { loadAgent } from "../../../src/lib/agent/defs.ts"; +import { + CANDIDATE_AGENT_FEATURE_ENV, + CANDIDATE_QUALIFICATION_RECEIPT_ENV, +} from "../../../src/lib/agent/candidate.ts"; +import { CUA_FEATURE_ENV } from "../../../src/lib/cua/feature.ts"; import { type ChildProcessProgress, spawnObservedChild } from "./observed-child-process.ts"; import { superviseChild } from "./shell/supervisor.ts"; import type { TrustedShellCommand } from "./shell/trusted-command.ts"; @@ -41,6 +47,26 @@ export interface ShellProbeOutputEvent { export type { TrustedShellCommand, TrustedShellCommandInput } from "./shell/trusted-command.ts"; export { trustedShellCommand } from "./shell/trusted-command.ts"; +export function resolveLiveE2eWorkloadSourceEnv(input: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const targetId = input.E2E_TARGET_ID ?? process.env.E2E_TARGET_ID; + const source = input.E2E_WORKLOAD_SOURCE ?? process.env.E2E_WORKLOAD_SOURCE; + if (!targetId || source !== "local-dockerfile" || input.NEMOCLAW_FROM_DOCKERFILE) return input; + const agentName = input.NEMOCLAW_AGENT ?? process.env.NEMOCLAW_AGENT ?? "openclaw"; + const agent = loadAgent(agentName, { + [CANDIDATE_AGENT_FEATURE_ENV]: + input[CANDIDATE_AGENT_FEATURE_ENV] ?? process.env[CANDIDATE_AGENT_FEATURE_ENV], + [CANDIDATE_QUALIFICATION_RECEIPT_ENV]: + input[CANDIDATE_QUALIFICATION_RECEIPT_ENV] ?? + process.env[CANDIDATE_QUALIFICATION_RECEIPT_ENV], + [CUA_FEATURE_ENV]: input[CUA_FEATURE_ENV] ?? process.env[CUA_FEATURE_ENV], + }); + const dockerfilePath = agent.dockerfilePath ?? agent.legacyPaths?.dockerfile; + if (!dockerfilePath) { + throw new Error(`Agent '${agent.name}' has no Dockerfile for local E2E workload source.`); + } + return { ...input, NEMOCLAW_FROM_DOCKERFILE: dockerfilePath }; +} + export interface ShellProbeResult { command: string[]; /** Wall-clock command duration, persisted for CI bottleneck analysis. */ @@ -216,10 +242,10 @@ export class ShellProbe { const startedAtMs = Date.now(); const commandOutputObserver = options.onOutput === this.progress.onOutput ? undefined : options.onOutput; - const commandEnv: NodeJS.ProcessEnv = { + const commandEnv = resolveLiveE2eWorkloadSourceEnv({ ...(process.env.PATH === undefined ? {} : { PATH: process.env.PATH }), ...(options.env ?? {}), - }; + }); const child = spawnObservedChild(command, args, { activityLabel: `command: ${activityName}`, progress: this.progress, diff --git a/test/e2e/live/bedrock-runtime-compatible-anthropic-raw-command.ts b/test/e2e/live/bedrock-runtime-compatible-anthropic-raw-command.ts index a98e1a76221..2201191b593 100644 --- a/test/e2e/live/bedrock-runtime-compatible-anthropic-raw-command.ts +++ b/test/e2e/live/bedrock-runtime-compatible-anthropic-raw-command.ts @@ -6,6 +6,7 @@ import { spawnObservedChild } from "../fixtures/observed-child-process.ts"; import { REPO_ROOT } from "../fixtures/paths.ts"; import type { TestProgress, TestProgressCapability } from "../fixtures/progress.ts"; import { redactString } from "../fixtures/redaction.ts"; +import { resolveLiveE2eWorkloadSourceEnv } from "../fixtures/shell-probe.ts"; import { projectRawOutputForArtifact, type RawArtifactOutputMode, @@ -95,7 +96,7 @@ export async function runRawCommand( spawn: { cwd: options.cwd ?? REPO_ROOT, detached: true, - env: { ...(options.env ?? {}) }, + env: resolveLiveE2eWorkloadSourceEnv({ ...(options.env ?? {}) }), stdio: ["ignore", "pipe", "pipe"], }, }); diff --git a/test/e2e/live/dashboard-connect-handoff.ts b/test/e2e/live/dashboard-connect-handoff.ts index 23d07f60b1e..3055fff0d56 100644 --- a/test/e2e/live/dashboard-connect-handoff.ts +++ b/test/e2e/live/dashboard-connect-handoff.ts @@ -9,6 +9,7 @@ import { spawnObservedChild, } from "../fixtures/observed-child-process.ts"; import { REPO_ROOT } from "../fixtures/paths.ts"; +import { resolveLiveE2eWorkloadSourceEnv } from "../fixtures/shell-probe.ts"; import { dashboardRemoteBindConnectStarted } from "./dashboard-remote-bind-env.ts"; const CONNECT_CAPTURE_LIMIT_BYTES = 1024 * 1024; @@ -87,7 +88,7 @@ export async function runDashboardConnectUntilForwardHandoff( spawn: { cwd: REPO_ROOT, detached: true, - env: { ...options.env }, + env: resolveLiveE2eWorkloadSourceEnv({ ...options.env }), stdio: ["ignore", "pipe", "pipe"], }, }); diff --git a/test/e2e/live/full-e2e-workload-evidence.ts b/test/e2e/live/full-e2e-workload-evidence.ts index 56df7bb6250..401a971b612 100644 --- a/test/e2e/live/full-e2e-workload-evidence.ts +++ b/test/e2e/live/full-e2e-workload-evidence.ts @@ -8,6 +8,9 @@ export function readFullE2eColdWorkloadEvidence( usedBuildKitPrebuild: boolean, environment: NodeJS.ProcessEnv = process.env, ) { + if (environment.E2E_WORKLOAD_SOURCE === "local-dockerfile") { + return { kind: "legacy-dockerfile" } as const; + } if (usedBuildKitPrebuild) { throw new Error("managed-image cold onboarding must not use a local BuildKit prebuild"); } @@ -16,6 +19,7 @@ export function readFullE2eColdWorkloadEvidence( expectedAgent: "openclaw", sandboxName, }); + if (!receipt) throw new Error("managed-image cold onboarding receipt is missing"); return { kind: "managed-image", reference: receipt.reference, diff --git a/test/e2e/mock-parity.json b/test/e2e/mock-parity.json index f9692183e7b..549583ff712 100644 --- a/test/e2e/mock-parity.json +++ b/test/e2e/mock-parity.json @@ -145,7 +145,8 @@ "live": "test/e2e/live/full-e2e.test.ts", "liveSources": [ "test/e2e/live/agent-turn-latency-helpers.ts", - "test/e2e/live/full-e2e-inference-probe.ts" + "test/e2e/live/full-e2e-inference-probe.ts", + "test/e2e/live/full-e2e-workload-evidence.ts" ], "fast": [ "test/e2e/support/hosted-inference.test.ts", @@ -154,6 +155,7 @@ "test/e2e/support/openclaw-agent-output.test.ts", "test/e2e/support/e2e-cleanup-resources.test.ts", "test/e2e/support/e2e-clients.test.ts", + "test/e2e/support/managed-image-receipt.test.ts", "test/helpers/e2e-answer-assertions.test.ts" ] }, @@ -258,7 +260,8 @@ }, { "live": "test/e2e/live/dashboard-remote-bind.test.ts", - "liveOnlyReason": "Requires a real OpenShell dashboard forward on a remote Brev host to prove all-interface bind behavior after background connect." + "liveSources": ["test/e2e/live/dashboard-connect-handoff.ts"], + "fast": ["test/e2e/support/dashboard-connect-handoff.test.ts"] }, { "live": "test/e2e/live/sandbox-rlimits-connect.test.ts", @@ -331,6 +334,7 @@ }, { "live": "test/e2e/live/bedrock-runtime-compatible-anthropic.test.ts", + "liveSources": ["test/e2e/live/bedrock-runtime-compatible-anthropic-raw-command.ts"], "fast": [ "test/e2e/support/bedrock-runtime-compatible-anthropic-progress.test.ts", "test/e2e/support/e2e-cleanup-resources.test.ts", diff --git a/test/e2e/support/bedrock-runtime-compatible-anthropic-progress.test.ts b/test/e2e/support/bedrock-runtime-compatible-anthropic-progress.test.ts index 228d6faad16..2544621a856 100644 --- a/test/e2e/support/bedrock-runtime-compatible-anthropic-progress.test.ts +++ b/test/e2e/support/bedrock-runtime-compatible-anthropic-progress.test.ts @@ -54,8 +54,10 @@ afterEach(async () => { }); describe("Bedrock raw-command progress", () => { - it("reports timestamp-only output activity without forwarding child payloads", async () => { + it("applies the local Dockerfile and reports timestamp-only output activity", async () => { const secret = "opaque-bedrock-progress-secret"; + const expectedDockerfile = path.resolve("Dockerfile"); + const expectedOutput = `${secret}:${expectedDockerfile}`; const artifacts = await artifactSink("bedrock-progress-output"); const observation = progressProbe(); const { progress } = observation; @@ -64,19 +66,25 @@ describe("Bedrock raw-command progress", () => { process.execPath, [ "-e", - "process.stdout.write(process.env.BEDROCK_TEST_SECRET); process.stderr.write('stderr-ready')", + "process.stdout.write(`${process.env.BEDROCK_TEST_SECRET}:${process.env.NEMOCLAW_FROM_DOCKERFILE}`); process.stderr.write('stderr-ready')", ], { artifactName: "bedrock-progress-output", artifacts, - env: { ...process.env, BEDROCK_TEST_SECRET: secret }, + env: { + ...process.env, + BEDROCK_TEST_SECRET: secret, + E2E_TARGET_ID: "bedrock-runtime-compatible-anthropic", + E2E_WORKLOAD_SOURCE: "local-dockerfile", + NEMOCLAW_AGENT: "openclaw", + }, progress, redactionValues: [secret], }, ); expect(result.exitCode).toBe(0); - expect(result.stdout).toBe(secret); + expect(result.stdout).toBe(expectedOutput); observation.timers[0]?.(); expect(observation.lines.at(-1)).toContain("no active command"); expect(observation.lines).toEqual( @@ -95,7 +103,7 @@ describe("Bedrock raw-command progress", () => { path.join(artifacts.rootDir, "raw-shell/bedrock-progress-output.stdout.txt"), "utf8", ), - ).resolves.toBe("[REDACTED]"); + ).resolves.toBe(`[REDACTED]:${expectedDockerfile}`); }); it("emits an immediate content-free timeout event and closes command activity", async () => { diff --git a/test/e2e/support/dashboard-connect-handoff.test.ts b/test/e2e/support/dashboard-connect-handoff.test.ts index f259301ec46..b6673db0d8c 100644 --- a/test/e2e/support/dashboard-connect-handoff.test.ts +++ b/test/e2e/support/dashboard-connect-handoff.test.ts @@ -37,15 +37,25 @@ async function stopFixtureProcess(pid: number): Promise { expect(processExists(pid)).toBe(false); } -test("accepts a normally completed connect when the forward is already healthy", async ({ +test("accepts a normally completed connect with the local Dockerfile workload", async ({ artifacts, progress, }) => { const result = await runDashboardConnectUntilForwardHandoff({ artifacts, - command: [process.execPath, "-e", "process.exit(0)"], + command: [ + process.execPath, + "-e", + "process.exit(process.env.NEMOCLAW_FROM_DOCKERFILE === process.argv[1] ? 0 : 1)", + path.resolve("Dockerfile"), + ], dashboardPort: DASHBOARD_PORT, - env: process.env, + env: { + ...process.env, + E2E_TARGET_ID: "dashboard-remote-bind", + E2E_WORKLOAD_SOURCE: "local-dockerfile", + NEMOCLAW_AGENT: "openclaw", + }, progress, sandboxName: SANDBOX_NAME, timeoutMs: 2_000, diff --git a/test/e2e/support/managed-image-receipt.test.ts b/test/e2e/support/managed-image-receipt.test.ts index 5cc0e85ccfd..09e9c72a7ed 100644 --- a/test/e2e/support/managed-image-receipt.test.ts +++ b/test/e2e/support/managed-image-receipt.test.ts @@ -173,12 +173,15 @@ describe("stock E2E managed-image receipt assertion", () => { ).toMatchObject({ agent: "openclaw", sourceRevision: REVISION }); }); - it("uses the trusted candidate catalog for full E2E workload evidence", () => { + it("records local Dockerfile evidence without a managed-image receipt", () => { const home = writeRegistry(managedReceipt()); expect( - readFullE2eColdWorkloadEvidence(SANDBOX_NAME, false, candidateCatalogEnvironment(home)), - ).toMatchObject({ kind: "managed-image", sourceRevision: REVISION }); + readFullE2eColdWorkloadEvidence(SANDBOX_NAME, false, { + ...candidateCatalogEnvironment(home), + E2E_WORKLOAD_SOURCE: "local-dockerfile", + }), + ).toMatchObject({ kind: "legacy-dockerfile" }); }); it("rejects a candidate catalog whose source revision differs from the exact candidate revision", () => { diff --git a/test/e2e/support/pr-dcode-base-publication.test.ts b/test/e2e/support/pr-dcode-base-publication.test.ts deleted file mode 100644 index f6c185b9665..00000000000 --- a/test/e2e/support/pr-dcode-base-publication.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { createHash } from "node:crypto"; - -import { 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.ts"; -import { - buildDockerInspectionEnvironment, - readDcodeBaseResolution, - resolvePrDcodeBasePublication, -} from "../../../tools/e2e/pr-dcode-base-publication.mts"; - -const CANDIDATE_SHA = "a".repeat(40); -const BASE_SOURCE_SHA = "b".repeat(40); -const COHORT = "ghrun-32841372913-1"; -const BASE_IMAGE = "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base"; -const BASE_DIGEST = `sha256:${"c".repeat(64)}`; -const BASE_REFERENCE = `${BASE_IMAGE}@${BASE_DIGEST}`; -const MANAGED_DIGEST = `sha256:${"d".repeat(64)}`; -const DCODE_IMAGE = MANAGED_IMAGE_REPOSITORIES["langchain-deepagents-code"]; -const DCODE_REFERENCE = `${DCODE_IMAGE}@${MANAGED_DIGEST}`; - -function contract(agent: ManagedImageAgent, index: number): ManagedImageContractV1 { - const image = MANAGED_IMAGE_REPOSITORIES[agent]; - const digest: ManagedImageContractV1["digest"] = - agent === "langchain-deepagents-code" - ? (MANAGED_DIGEST as ManagedImageContractV1["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.113", - cohort: COHORT, - }, - startupProfileContractVersion: MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, - capabilityContractVersion: MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, - }; -} - -function catalog(): string { - return JSON.stringify( - Object.fromEntries( - SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => [agent, contract(agent, index)]), - ), - ); -} - -function baseResolution(overrides: Record = {}): Record { - const value = { - schema: 1, - key: "", - imageName: BASE_IMAGE, - ref: BASE_REFERENCE, - digest: BASE_DIGEST, - source: "override", - sourceRevision: BASE_SOURCE_SHA, - imageId: `sha256:${"e".repeat(64)}`, - os: "linux", - architecture: "amd64", - glibcVersion: "2.39", - requireOpenshellSandboxAbi: true, - minGlibcVersion: "2.39", - ...overrides, - }; - value.key = createHash("sha256") - .update(JSON.stringify({ ...value, key: "" })) - .digest("hex"); - return value; -} - -function inspection(resolution = baseResolution()): unknown { - const encoded = Buffer.from(JSON.stringify(resolution)).toString("base64url"); - return [ - { - Architecture: "amd64", - Os: "linux", - RepoDigests: [DCODE_REFERENCE], - Config: { - Labels: { - "com.nvidia.nemoclaw.base-resolution": encoded, - "com.nvidia.nemoclaw.base-resolution-key": resolution.key, - "io.nvidia.nemoclaw.agent": "langchain-deepagents-code", - "io.nvidia.nemoclaw.managed-image.cohort": COHORT, - "io.nvidia.nemoclaw.managed-image.contract": "1", - "io.nvidia.nemoclaw.managed-image.platform": "linux/amd64", - "org.opencontainers.image.revision": CANDIDATE_SHA, - "org.opencontainers.image.version": "v0.0.113", - }, - }, - }, - ]; -} - -const publication = { - id: 32832002443, - attempt: 1, - workflowId: 194842001, - headSha: BASE_SOURCE_SHA, - status: "completed", - conclusion: "success", - url: "https://github.com/NVIDIA/NemoClaw/actions/runs/32832002443", -}; - -describe("exact PR Deep Agents Code base publication pairing", () => { - it("keeps controller credentials out of Docker inspection subprocesses", () => { - expect( - buildDockerInspectionEnvironment({ - DOCKER_HOST: "unix:///var/run/docker.sock", - GITHUB_TOKEN: "github-secret", - LC_ALL: "C.UTF-8", - NVIDIA_API_KEY: "nvidia-secret", - PATH: "/usr/bin:/bin", - }), - ).toEqual({ - DOCKER_HOST: "unix:///var/run/docker.sock", - LC_ALL: "C.UTF-8", - PATH: "/usr/bin:/bin", - }); - }); - - it("selects the trusted publication for the base source revision recorded by the exact PR managed image", async () => { - const inspectManagedImage = vi.fn(() => inspection()); - const resolvePublication = vi.fn(async () => publication); - - await expect( - resolvePrDcodeBasePublication( - { candidateSha: CANDIDATE_SHA, catalog: catalog(), token: "test-token" }, - { inspectManagedImage, resolvePublication }, - ), - ).resolves.toEqual({ baseReference: BASE_REFERENCE, run: publication }); - expect(inspectManagedImage).toHaveBeenCalledWith(DCODE_REFERENCE); - expect(resolvePublication).toHaveBeenCalledWith(BASE_SOURCE_SHA); - }); - - it("accepts the fixed base-resolution key for the producer field order", () => { - const expectedResolutionHash = - "083e317cc6f3dd2a14a4f2c7a52c3cbd40864b35c8182df2a6546e3b6f4236a1"; - const resolution = { - schema: 1, - key: expectedResolutionHash, - imageName: BASE_IMAGE, - ref: BASE_REFERENCE, - digest: BASE_DIGEST, - source: "override", - sourceRevision: BASE_SOURCE_SHA, - imageId: `sha256:${"e".repeat(64)}`, - os: "linux", - architecture: "amd64", - glibcVersion: "2.39", - requireOpenshellSandboxAbi: true, - minGlibcVersion: "2.39", - }; - - expect( - readDcodeBaseResolution( - inspection(resolution), - contract("langchain-deepagents-code", 2), - CANDIDATE_SHA, - ), - ).toEqual({ reference: BASE_REFERENCE, sourceRevision: BASE_SOURCE_SHA }); - }); - - it("rejects a base-resolution key that does not bind the recorded fields", () => { - const resolution = baseResolution(); - resolution.key = "f".repeat(64); - - expect(() => - readDcodeBaseResolution( - inspection(resolution), - contract("langchain-deepagents-code", 2), - CANDIDATE_SHA, - ), - ).toThrow("base resolution key is invalid"); - }); - - it("rejects a trusted publication from a different base source revision", async () => { - await expect( - resolvePrDcodeBasePublication( - { candidateSha: CANDIDATE_SHA, catalog: catalog(), token: "test-token" }, - { - inspectManagedImage: () => inspection(), - resolvePublication: async () => ({ ...publication, headSha: "f".repeat(40) }), - }, - ), - ).rejects.toThrow("does not match the managed image binding"); - }); - - it("rejects an image inspection that is not bound to the exact managed-image reference", () => { - const value = inspection() as Array>; - value[0]!.RepoDigests = [`${DCODE_IMAGE}@sha256:${"f".repeat(64)}`]; - - expect(() => - readDcodeBaseResolution(value, contract("langchain-deepagents-code", 2), CANDIDATE_SHA), - ).toThrow("does not match its exact reference"); - }); -}); diff --git a/test/e2e/support/pr-managed-image-publication.test.ts b/test/e2e/support/pr-managed-image-publication.test.ts index 7c8b27d3f9e..2a6188ed8c6 100644 --- a/test/e2e/support/pr-managed-image-publication.test.ts +++ b/test/e2e/support/pr-managed-image-publication.test.ts @@ -1,468 +1,119 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { createHash } from "node:crypto"; -import { spawnSync } from "node:child_process"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; -import { describe, expect, it } from "vitest"; -import YAML from "yaml"; - -import { - MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, - MANAGED_IMAGE_CONTRACT_VERSION, - MANAGED_IMAGE_REPOSITORIES, - MANAGED_IMAGE_SOURCE_REPOSITORY, - MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, - type ManagedImageAgent, - type ManagedImageContractV1, - SHIPPED_MANAGED_IMAGE_AGENTS, -} from "../../../src/lib/onboard/managed-image/contract"; -import { - assembleManagedImageCatalog, - main, - managedImagePublicationRequired, - parseManagedImagePullRequestPaths, - resolvePrManagedImageCatalog, - selectManagedImagePublicationRun, -} from "../../../tools/e2e/pr-managed-image-publication.mts"; -import { - readE2eOperationsWorkflow, - validateE2eOperationsWorkflow, -} from "../../../tools/e2e/operations-workflow-boundary.mts"; +import { githubRequest } from "../../../tools/e2e/base-image-publication.mts"; +import { resolvePrManagedImageSource } from "../../../tools/e2e/pr-managed-image-publication.mts"; const BASE_SHA = "b".repeat(40); const CANDIDATE_SHA = "a".repeat(40); -const BASE_TREE_SHA = "c".repeat(40); -const CANDIDATE_TREE_SHA = "d".repeat(40); -const PR_NUMBER = 8746; -const WORKFLOW_ID = 12345; -const MANAGED_IMAGE_WORKFLOW = fs.readFileSync(".github/workflows/managed-images.yaml", "utf8"); - -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-32144654845-1", - }, - startupProfileContractVersion: MANAGED_IMAGE_STARTUP_PROFILE_CONTRACT_VERSION, - capabilityContractVersion: MANAGED_IMAGE_CAPABILITY_CONTRACT_VERSION, - }; +const BASE_TREE_SHA = "1".repeat(40); +const CANDIDATE_TREE_SHA = "2".repeat(40); +const PR_NUMBER = 10_263; +const CANONICAL_REPOSITORY = "NVIDIA/NemoClaw"; +const WORKFLOW_SOURCE = `on: + push: + branches: [main] + paths: + - ".github/workflows/base-image.yaml" + - "Dockerfile.base" + workflow_dispatch: +jobs: {} +`; + +function treeEntry(path: string, sha: string) { + return { mode: "100644", path, sha, type: "blob" }; } -function run(overrides: Record = {}): unknown { - return { - total_count: 1, - workflow_runs: [ +function requestFor(candidateRepository: string, imageChanged: boolean) { + 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("docs/guide.mdx", "4".repeat(40)), + ]; + const responses = new Map([ + [ + `/repos/${CANONICAL_REPOSITORY}/pulls/${PR_NUMBER}`, { - id: 32144654845, - 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: "NVIDIA/NemoClaw" }, - head_repository: { full_name: "NVIDIA/NemoClaw" }, - pull_requests: [{ number: PR_NUMBER }], - ...overrides, + state: "open", + base: { sha: BASE_SHA, repo: { full_name: CANONICAL_REPOSITORY } }, + head: { sha: CANDIDATE_SHA, repo: { full_name: candidateRepository } }, }, ], - }; -} - -function exactCommitRequest(changedPath: string, onUnexpected: (apiPath: string) => unknown) { - const baseBlob = "1".repeat(40); - const candidateBlob = "2".repeat(40); - return async (apiPath: string): Promise => { - switch (apiPath) { - case `/repos/NVIDIA/NemoClaw/pulls/${PR_NUMBER}`: - return { - state: "open", - base: { sha: BASE_SHA, repo: { full_name: "NVIDIA/NemoClaw" } }, - head: { sha: CANDIDATE_SHA, repo: { full_name: "NVIDIA/NemoClaw" } }, - }; - case `/repos/NVIDIA/NemoClaw/git/commits/${BASE_SHA}`: - return { sha: BASE_SHA, tree: { sha: BASE_TREE_SHA } }; - case `/repos/NVIDIA/NemoClaw/git/commits/${CANDIDATE_SHA}`: - return { sha: CANDIDATE_SHA, tree: { sha: CANDIDATE_TREE_SHA } }; - case `/repos/NVIDIA/NemoClaw/git/trees/${BASE_TREE_SHA}?recursive=1`: - return { - sha: BASE_TREE_SHA, - truncated: false, - tree: [{ path: changedPath, mode: "100644", type: "blob", sha: baseBlob }], - }; - case `/repos/NVIDIA/NemoClaw/git/trees/${CANDIDATE_TREE_SHA}?recursive=1`: - return { - sha: CANDIDATE_TREE_SHA, - truncated: false, - tree: [{ path: changedPath, mode: "100644", type: "blob", sha: candidateBlob }], - }; - default: - return onUnexpected(apiPath); - } - }; -} - -describe("exact PR managed-image publication (#8746, #9464)", () => { - it("derives applicability from the trusted managed-image workflow", () => { - const patterns = parseManagedImagePullRequestPaths( - fs.readFileSync(".github/workflows/managed-images.yaml", "utf8"), - ); - - expect( - managedImagePublicationRequired(["src/lib/onboard/workload/preparation.ts"], patterns), - ).toBe(true); - expect( - managedImagePublicationRequired(["tools/mcp-tool-discovery-runtime/server.mts"], patterns), - ).toBe(true); - expect( - managedImagePublicationRequired( - ["src/lib/actions/sandbox/mcp-bridge-adapter-openclaw.ts"], - patterns, - ), - ).toBe(true); - expect(managedImagePublicationRequired(["docs/My Guide.md"], patterns)).toBe(false); - expect(() => - managedImagePublicationRequired(["src/lib/onboard/file.ts\nother"], patterns), - ).toThrow("changed-file path is invalid"); - }); - - it("rejects an unreviewed path-filter glob", () => { - expect(() => - parseManagedImagePullRequestPaths(` -on: - pull_request: - paths: - - ".github/workflows/managed-images.yaml" - - "src/**/nested/**" -`), - ).toThrow("unsupported glob"); - }); - - it("classifies immutable commit trees without reading the mutable PR file listing", async () => { - const requests: string[] = []; - const request = exactCommitRequest("docs/upgrade.md", (apiPath) => { - requests.push(apiPath); - throw new Error(`unexpected request: ${apiPath}`); - }); - - await expect( - resolvePrManagedImageCatalog( - { - baseSha: BASE_SHA, - candidateRepository: "NVIDIA/NemoClaw", - candidateSha: CANDIDATE_SHA, - outputPath: path.join(os.tmpdir(), "unused-pr-managed-image-catalog.json"), - prNumber: PR_NUMBER, - token: "test-token", - workflowSource: MANAGED_IMAGE_WORKFLOW, - }, - async (apiPath) => { - requests.push(apiPath); - return request(apiPath); - }, - ), - ).resolves.toBe("not-required"); - expect(requests).toContain(`/repos/NVIDIA/NemoClaw/git/commits/${BASE_SHA}`); - expect(requests).toContain(`/repos/NVIDIA/NemoClaw/git/commits/${CANDIDATE_SHA}`); - expect(requests.some((apiPath) => apiPath.includes(`/pulls/${PR_NUMBER}/files`))).toBe(false); - }); - - it("requires exact publication after an immutable managed-image input change", async () => { - const request = exactCommitRequest("agents/hermes/plugin/__init__.py", (apiPath) => { - throw new Error(`publication lookup reached: ${apiPath}`); - }); - - await expect( - resolvePrManagedImageCatalog( - { - baseSha: BASE_SHA, - candidateRepository: "NVIDIA/NemoClaw", - candidateSha: CANDIDATE_SHA, - outputPath: path.join(os.tmpdir(), "unused-pr-managed-image-catalog.json"), - prNumber: PR_NUMBER, - token: "test-token", - workflowSource: MANAGED_IMAGE_WORKFLOW, - }, - request, - ), - ).rejects.toThrow("publication lookup reached: /repos/NVIDIA/NemoClaw/actions/workflows"); - }); - - it("rejects a truncated immutable commit tree", async () => { - const request = exactCommitRequest("docs/upgrade.md", (apiPath) => { - throw new Error(`unexpected request: ${apiPath}`); - }); - const truncatedTreePath = `/repos/NVIDIA/NemoClaw/git/trees/${BASE_TREE_SHA}?recursive=1`; - const substitutedResponses = new Map([ - [ - truncatedTreePath, - { - sha: BASE_TREE_SHA, - truncated: true, - tree: [ - { - path: "docs/upgrade.md", - mode: "100644", - type: "blob", - sha: "1".repeat(40), - }, - ], - }, - ], - ]); - await expect( - resolvePrManagedImageCatalog( - { - baseSha: BASE_SHA, - candidateRepository: "NVIDIA/NemoClaw", - candidateSha: CANDIDATE_SHA, - outputPath: path.join(os.tmpdir(), "unused-pr-managed-image-catalog.json"), - prNumber: PR_NUMBER, - token: "test-token", - workflowSource: MANAGED_IMAGE_WORKFLOW, - }, - async (apiPath) => substitutedResponses.get(apiPath) ?? request(apiPath), - ), - ).rejects.toThrow("PR base commit tree is truncated"); - }); - - it.each([ [ - "duplicate directories", - [ - { path: "agents", mode: "040000", type: "tree", sha: "1".repeat(40) }, - { path: "agents", mode: "040000", type: "tree", sha: "2".repeat(40) }, - ], + `/repos/${CANONICAL_REPOSITORY}/git/commits/${BASE_SHA}`, + { sha: BASE_SHA, tree: { sha: BASE_TREE_SHA } }, ], [ - "directory and file collisions", - [ - { path: "agents", mode: "040000", type: "tree", sha: "1".repeat(40) }, - { path: "agents", mode: "100644", type: "blob", sha: "2".repeat(40) }, - ], + `/repos/${CANONICAL_REPOSITORY}/git/trees/${BASE_TREE_SHA}?recursive=1`, + { sha: BASE_TREE_SHA, tree: baseEntries, truncated: false }, ], - ])("rejects %s in an immutable commit tree", async (_description, tree) => { - const request = exactCommitRequest("docs/upgrade.md", (apiPath) => { - throw new Error(`unexpected request: ${apiPath}`); - }); - const baseTreePath = `/repos/NVIDIA/NemoClaw/git/trees/${BASE_TREE_SHA}?recursive=1`; + [ + `/repos/${candidateRepository}/git/commits/${CANDIDATE_SHA}`, + { sha: CANDIDATE_SHA, tree: { sha: CANDIDATE_TREE_SHA } }, + ], + [ + `/repos/${candidateRepository}/git/trees/${CANDIDATE_TREE_SHA}?recursive=1`, + { sha: CANDIDATE_TREE_SHA, tree: candidateEntries, truncated: false }, + ], + ]); + return async (requestPath: string): Promise => + responses.get(requestPath) ?? Promise.reject(new Error(`unexpected request ${requestPath}`)); +} - await expect( - resolvePrManagedImageCatalog( - { - baseSha: BASE_SHA, - candidateRepository: "NVIDIA/NemoClaw", - candidateSha: CANDIDATE_SHA, - outputPath: path.join(os.tmpdir(), "unused-pr-managed-image-catalog.json"), - prNumber: PR_NUMBER, - token: "test-token", - workflowSource: MANAGED_IMAGE_WORKFLOW, - }, - async (apiPath) => - apiPath === baseTreePath - ? { sha: BASE_TREE_SHA, truncated: false, tree } - : request(apiPath), - ), - ).rejects.toThrow("PR base commit tree contains duplicate paths"); - }); +function selectorInput(candidateRepository: string) { + return { + baseSha: BASE_SHA, + candidateRepository, + candidateSha: CANDIDATE_SHA, + prNumber: PR_NUMBER, + token: "test-token", + workflowSource: WORKFLOW_SOURCE, + }; +} - it.each([ - ["blob", "040000"], - ["commit", "100644"], - ["tree", "100755"], - ])("rejects an immutable %s entry with Git mode %s", async (type, mode) => { - const request = exactCommitRequest("docs/upgrade.md", (apiPath) => { - throw new Error(`unexpected request: ${apiPath}`); - }); - const baseTreePath = `/repos/NVIDIA/NemoClaw/git/trees/${BASE_TREE_SHA}?recursive=1`; +afterEach(() => { + vi.unstubAllGlobals(); +}); +describe("PR managed-image source selection", () => { + it("keeps source selection bound to commit A during A-to-B-to-A PR drift", async () => { await expect( - resolvePrManagedImageCatalog( - { - baseSha: BASE_SHA, - candidateRepository: "NVIDIA/NemoClaw", - candidateSha: CANDIDATE_SHA, - outputPath: path.join(os.tmpdir(), "unused-pr-managed-image-catalog.json"), - prNumber: PR_NUMBER, - token: "test-token", - workflowSource: MANAGED_IMAGE_WORKFLOW, - }, - async (apiPath) => - apiPath === baseTreePath - ? { - sha: BASE_TREE_SHA, - truncated: false, - tree: [{ path: "agents", mode, type, sha: "1".repeat(40) }], - } - : request(apiPath), + resolvePrManagedImageSource( + selectorInput(CANONICAL_REPOSITORY), + requestFor(CANONICAL_REPOSITORY, true), ), - ).rejects.toThrow("PR base tree entry mode is invalid"); + ).resolves.toBe("local-dockerfile"); }); - it("rejects a manual PR catalog without a trusted pre-checkout producer", () => { - const workflow = readE2eOperationsWorkflow(); - delete workflow.jobs["generate-matrix"].outputs?.managed_image_catalog; + it("reads a validated external candidate repository through the default request policy", async () => { + const candidateRepository = "external-contributor/NemoClaw"; + const request = requestFor(candidateRepository, false); + vi.stubGlobal("fetch", async (input: string) => { + const url = new URL(input); + return new Response(JSON.stringify(await request(`${url.pathname}${url.search}`)), { + status: 200, + }); + }); - expect(validateE2eOperationsWorkflow(workflow)).toContain( - "Manual PR managed-image catalog must be authenticated before candidate checkout", + await expect(resolvePrManagedImageSource(selectorInput(candidateRepository))).resolves.toBe( + "managed-image", ); }); - it("rejects a candidate mutation of the authenticated managed-image catalog", () => { - const workflow = YAML.parse(fs.readFileSync(".github/workflows/e2e.yaml", "utf8")); - const packageCli = workflow.jobs["generate-matrix"].steps.find( - (step: Record) => step.name === "Package exact-commit CLI", - ); - const stagingMatch = packageCli.run.match( - /# BEGIN exact managed-image catalog staging\n([\s\S]*?)# END exact managed-image catalog staging/u, - ); - const stagingSource = stagingMatch?.[1] ?? ""; - - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pr-catalog-stage-test-")); - try { - fs.mkdirSync(path.join(directory, "dist"), { mode: 0o700 }); - const trustedCatalog = JSON.stringify( - Object.fromEntries( - SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => [agent, contract(agent, index)]), - ), - ); - const trustedDigest = createHash("sha256") - .update(`${trustedCatalog}\n`, "utf8") - .digest("hex"); - fs.writeFileSync( - path.join(directory, "pr-managed-image-catalog.json"), - '{"candidateMutation":true}\n', - { mode: 0o600 }, - ); - const scriptPath = path.join(directory, "stage-managed-image-catalog.sh"); - fs.writeFileSync(scriptPath, `set -euo pipefail\n${stagingSource}`, { mode: 0o700 }); - - const result = spawnSync("/bin/bash", ["--noprofile", "--norc", scriptPath], { - cwd: directory, - encoding: "utf8", - env: { - MANAGED_IMAGE_CATALOG: trustedCatalog, - MANAGED_IMAGE_CATALOG_SHA256: trustedDigest, - PATH: "/usr/bin:/bin:/usr/sbin:/sbin", - RUNNER_TEMP: directory, + it("rejects a GitHub request outside the canonical and candidate repositories", async () => { + await expect( + githubRequest(`/repos/other-owner/other-repository/git/commits/${CANDIDATE_SHA}`, "token", { + additionalRepository: "external-contributor/NemoClaw", + attempts: 1, + fetchImpl: async () => { + throw new Error("must not fetch"); }, - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - "trusted PR managed-image catalog changed after authentication", - ); - expect(fs.existsSync(path.join(directory, "dist/e2e-managed-image-catalog.json"))).toBe( - false, - ); - } finally { - fs.rmSync(directory, { force: true, recursive: true }); - } - }); - - it("selects one successful workflow run for the candidate commit", () => { - expect( - selectManagedImagePublicationRun(run(), { - headSha: CANDIDATE_SHA, - prNumber: PR_NUMBER, - workflowId: WORKFLOW_ID, - }), - ).toEqual({ id: 32144654845, attempt: 1, headSha: CANDIDATE_SHA }); - }); - - it.each([ - ["pending", { status: "in_progress", conclusion: null }, "must complete successfully"], - ["failed", { conclusion: "failure" }, "must complete successfully"], - ["different commit", { head_sha: "b".repeat(40) }, "commit must be"], - ["different PR", { pull_requests: [{ number: 9464 }] }, "PR number"], - ])("rejects a %s publication run", (_label, overrides, message) => { - expect(() => - selectManagedImagePublicationRun(run(overrides), { - headSha: CANDIDATE_SHA, - prNumber: PR_NUMBER, - workflowId: WORKFLOW_ID, }), - ).toThrow(message); - }); - - it("assembles one exact all-agent catalog", () => { - const contracts = SHIPPED_MANAGED_IMAGE_AGENTS.map(contract); - - expect(assembleManagedImageCatalog(contracts, CANDIDATE_SHA)).toEqual( - Object.fromEntries(contracts.map((value) => [value.agent, value])), - ); - }); - - it("writes a validated catalog through the shared assembly command", async () => { - const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-pr-catalog-test-")); - try { - const contracts = SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => { - const contractPath = path.join(directory, `${agent}.json`); - fs.writeFileSync(contractPath, JSON.stringify(contract(agent, index))); - return contractPath; - }); - const outputPath = path.join(directory, "catalog.json"); - - await main(["assemble", CANDIDATE_SHA, outputPath, ...contracts], {}); - - expect(JSON.parse(fs.readFileSync(outputPath, "utf8"))).toEqual( - Object.fromEntries( - SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => [agent, contract(agent, index)]), - ), - ); - expect(fs.statSync(outputPath).mode & 0o777).toBe(0o600); - } finally { - fs.rmSync(directory, { force: true, recursive: true }); - } - }); - - it.each([ - [ - "candidate revision", - SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => - index === 0 - ? { - ...contract(agent, index), - source: { ...contract(agent, index).source, revision: "b".repeat(40) }, - } - : contract(agent, index), - ), - "candidate commit", - ], - [ - "publication cohort", - SHIPPED_MANAGED_IMAGE_AGENTS.map((agent, index) => - index === 0 - ? { - ...contract(agent, index), - source: { ...contract(agent, index).source, cohort: "ghrun-32144654845-2" }, - } - : contract(agent, index), - ), - "publication cohort", - ], - [ - "agent set", - [contract("openclaw", 0), contract("openclaw", 0), contract("hermes", 1)], - "every shipped agent", - ], - ])("rejects mixed %s authority", (_label, contracts, message) => { - expect(() => assembleManagedImageCatalog(contracts, CANDIDATE_SHA)).toThrow(message); + ).rejects.toThrow("GitHub API path must stay within an allowed repository"); }); }); diff --git a/tools/e2e/base-image-publication.mts b/tools/e2e/base-image-publication.mts index 8d0f6935969..1a71d44c74f 100644 --- a/tools/e2e/base-image-publication.mts +++ b/tools/e2e/base-image-publication.mts @@ -20,6 +20,7 @@ const RUN_URL_ROOT = `https://github.com/${REPOSITORY}/actions/runs`; const WORKFLOW_URL = `https://github.com/${REPOSITORY}/blob/${MAIN_BRANCH}/${WORKFLOW_PATH}`; const PAGE_SIZE = 100; const MAX_API_PAGES = 10; +const MAX_CHANGED_PATHS = 6_000; const PAGINATION_ATTEMPTS = 3; const REQUEST_ATTEMPTS = 3; const REQUEST_TIMEOUT_MS = 20_000; @@ -134,6 +135,7 @@ export function writePublicationRunOutputs(path: string, run: PublicationRun): v } export interface GithubRequestOptions { + additionalRepository?: string; fetchImpl?: (input: string, init: RequestInit) => Promise; sleep?: (milliseconds: number) => Promise; now?: () => number; @@ -206,6 +208,49 @@ function parseQuotedPath(raw: string, lineNumber: number): string { return value; } +/** Match one validated base-image workflow path without duplicating its glob semantics. */ +export function matchesBaseImagePushPath(pattern: string, changedPath: string): boolean { + if ( + changedPath.length === 0 || + changedPath.length > 4_096 || + /[\0\r\n]/u.test(changedPath) || + changedPath.startsWith("/") || + changedPath.includes("//") || + changedPath.split("/").some((segment) => segment === "" || segment === "." || segment === "..") + ) { + throw new Error("base-image changed path is invalid"); + } + const matcher = REVIEWED_PATH_GLOBS.get(pattern); + if (matcher) return matcher.test(changedPath); + if ( + !SAFE_PATH_PATTERN.test(pattern) || + pattern.startsWith("/") || + pattern.startsWith("-") || + pattern.startsWith(":") || + pattern.includes("//") || + pattern.split("/").some((segment) => segment === "" || segment === "." || segment === "..") + ) { + throw new Error("base-image push pattern is not reviewed"); + } + return pattern === changedPath; +} + +/** Determine whether reviewed base-image inputs contain a changed repository path. */ +export function baseImageInputsChanged( + changedFiles: readonly string[], + reviewedPaths: readonly string[], +): boolean { + if (changedFiles.length > MAX_CHANGED_PATHS) { + throw new Error(`PR changed-path count exceeds ${MAX_CHANGED_PATHS}`); + } + for (const changedFile of changedFiles) { + if (reviewedPaths.some((reviewedPath) => matchesBaseImagePushPath(reviewedPath, changedFile))) { + return true; + } + } + return false; +} + /** * Read the controlled path list without requiring a dependency install in the * preflight job. Deliberately reject YAML features such as flow lists, aliases, @@ -737,8 +782,21 @@ export async function githubRequest( token: string, options: GithubRequestOptions = {}, ): Promise { - if (!path.startsWith(`/repos/${REPOSITORY}/`) || path.includes("\r") || path.includes("\n")) { - throw new Error("GitHub API path must stay within the canonical NemoClaw repository"); + const additionalRepository = options.additionalRepository; + if ( + additionalRepository !== undefined && + (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(additionalRepository) || + additionalRepository.split("/").some((segment) => segment === "." || segment === "..")) + ) { + throw new Error("additional GitHub API repository is invalid"); + } + const allowedRepositories = [REPOSITORY, ...(additionalRepository ? [additionalRepository] : [])]; + if ( + path.includes("\r") || + path.includes("\n") || + !allowedRepositories.some((repository) => path.startsWith(`/repos/${repository}/`)) + ) { + throw new Error("GitHub API path must stay within an allowed repository"); } const fetchImpl = options.fetchImpl ?? fetch; const sleep = diff --git a/tools/e2e/mcp-dev-workflow-boundary-digests.mts b/tools/e2e/mcp-dev-workflow-boundary-digests.mts index 1705c2853f6..c8ffe4734eb 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 = - "6b37ff9bfe69b299c76d517c0f165dd22d25fe2520c80bc9edc2af4fe4f65c43"; + "63fdbb0b1d775e0f06cb31adc36d992b79bf75866a51038e3faa87849f2545e4"; export const MCP_DEV_TRUSTED_NODE_SETUP_CONTENT_SHA256 = "504821ad93c57971d0281ef1130ed6008fadd331bd56acb1a6b5e6a3358f3e49"; export const MCP_DEV_TRUSTED_PREFIX_CONTENT_SHA256 = diff --git a/tools/e2e/mcp-workflow-boundary.mts b/tools/e2e/mcp-workflow-boundary.mts index d0dcc679d6c..e5775bebc62 100644 --- a/tools/e2e/mcp-workflow-boundary.mts +++ b/tools/e2e/mcp-workflow-boundary.mts @@ -172,13 +172,13 @@ function validateJobIdentity( requireEqual( errors, env.E2E_MANAGED_IMAGE_REVISION, - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}", + "${{ needs.base-image-publication.outputs.managed_image_revision }}", `${jobName} must receive the selected managed-image cohort revision`, ); requireEqual( errors, env.E2E_MANAGED_IMAGE_COHORT_RECEIPT, - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }}", + "${{ needs.base-image-publication.outputs.managed_image_receipt }}", `${jobName} must receive the complete selected managed-image cohort receipt`, ); requireEqual( @@ -879,9 +879,10 @@ function validateCredentialWindowJob( const env = asRecord(job.env); const expectedEnv = { E2E_MANAGED_IMAGE_REVISION: - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}", + "${{ needs.base-image-publication.outputs.managed_image_revision }}", E2E_MANAGED_IMAGE_COHORT_RECEIPT: - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }}", + "${{ needs.base-image-publication.outputs.managed_image_receipt }}", + E2E_WORKLOAD_SOURCE: "${{ needs.generate-matrix.outputs.workload_source }}", E2E_JOB: "1", E2E_TARGET_ID: CREDENTIAL_WINDOW_JOB, E2E_AGENT_RUNTIME: "openclaw", diff --git a/tools/e2e/operations-workflow-boundary.mts b/tools/e2e/operations-workflow-boundary.mts index 5a7bf57ae7f..07d1b7c3384 100644 --- a/tools/e2e/operations-workflow-boundary.mts +++ b/tools/e2e/operations-workflow-boundary.mts @@ -34,6 +34,8 @@ const LIVE_VITEST_HELPER = "tools/e2e/live-vitest-invocation.mts run --test-path const E2E_ARTIFACT_ACTION = "NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@"; 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' }}"; const PUBLICATION_CLASSIFIER_SCRIPT = [ "set -euo pipefail", @@ -660,16 +662,14 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): outputs: { dcode_base_contract: "${{ steps.validate_dcode_base.outputs.contract }}", dcode_base_ref: "${{ steps.validate_dcode_base.outputs.base_ref }}", - managed_image_artifact_provenance: "${{ steps.download_managed_cohort.outputs.provenance }}", - managed_image_cohort: "${{ steps.validate_managed_cohort.outputs.cohort }}", managed_image_receipt: "${{ steps.validate_managed_cohort.outputs.receipt }}", managed_image_revision: "${{ steps.validate_managed_cohort.outputs.revision }}", - managed_image_run_attempt: "${{ steps.validate_managed_cohort.outputs.run_attempt }}", - managed_image_run_id: "${{ steps.validate_managed_cohort.outputs.run_id }}", + workload_source: "${{ steps.select_pr_source.outputs.workload_source || 'managed-image' }}", }, permissions: { actions: "read", contents: "read", + "pull-requests": "read", }, steps: [ { @@ -702,17 +702,41 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): "node-version": 22, }, }, + { + id: "select_pr_source", + name: "Select PR workload source", + if: "${{ inputs.pr_number != '' }}", + env: { + BASE_SHA: "${{ inputs.base_sha }}", + CANDIDATE_REPOSITORY: "${{ inputs.checkout_repository }}", + CANDIDATE_SHA: "${{ inputs.checkout_sha }}", + GITHUB_TOKEN: "${{ github.token }}", + 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"), + }, { id: "publication", - name: "Select complete base and managed-image publication", + name: "Select base and optional managed-image publication", env: { 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: "1", + REQUIRE_MANAGED_IMAGE_PUBLICATION: + "${{ steps.select_pr_source.outputs.workload_source == 'local-dockerfile' && '0' || '1' }}", SELECT_NEAREST_SUCCESSFUL_PUBLICATION: - "${{ steps.publication_mode.outputs.select_nearest_successful }}", + "${{ steps.select_pr_source.outputs.workload_source == 'local-dockerfile' && '0' || steps.publication_mode.outputs.select_nearest_successful }}", }, shell: "bash", run: [ @@ -750,6 +774,7 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): { id: "download_managed_cohort", name: "Download immutable managed-image cohort contract", + if: MANAGED_SOURCE_CONDITION, env: { GITHUB_TOKEN: "${{ github.token }}", PUBLICATION_ARTIFACT_KIND: "managed-image-cohort", @@ -762,6 +787,7 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): { id: "validate_managed_cohort", name: "Validate immutable managed-image cohort contract", + if: MANAGED_SOURCE_CONDITION, env: { PUBLICATION_HEAD_SHA: "${{ steps.publication.outputs.head_sha }}", PUBLICATION_RUN_ATTEMPT: "${{ steps.publication.outputs.run_attempt }}", @@ -795,19 +821,15 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): } if ( cloudOnboard.env?.E2E_MANAGED_IMAGE_REVISION !== - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}" + "${{ needs.base-image-publication.outputs.managed_image_revision }}" ) { - errors.push( - "cloud-onboard must use the selected managed-image revision when no exact PR catalog is present", - ); + errors.push("cloud-onboard must use the selected managed-image revision"); } if ( live.env?.E2E_MANAGED_IMAGE_REVISION !== - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}" + "${{ needs.base-image-publication.outputs.managed_image_revision }}" ) { - errors.push( - "live stock onboarding must use the selected managed-image revision when no exact PR catalog is present", - ); + errors.push("live stock onboarding must use the selected managed-image revision"); } for (const jobName of [ "catalogue-standard", @@ -822,22 +844,14 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): } if ( catalogue.with?.managed_image_revision !== - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}" - ) { - errors.push( - `${jobName} must use the selected managed-image revision when no exact PR catalog is present`, - ); - } - if ( - catalogue.with?.managed_image_catalog !== - "${{ needs.generate-matrix.outputs.managed_image_catalog }}" + "${{ needs.base-image-publication.outputs.managed_image_revision }}" ) { - errors.push(`${jobName} must pass the selected exact PR managed-image catalog`); + errors.push(`${jobName} must use the selected managed-image revision`); } } if ( live.env?.NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF !== - "${{ 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"); } @@ -849,7 +863,8 @@ export function validateBaseImagePublicationGate(workflow: OperationsWorkflow): .filter(Boolean); const liveSteps = live.steps ?? []; if ( - evidence.if !== "${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' }}" || + evidence.if !== + "${{ matrix.id == 'ubuntu-repo-cloud-langchain-deepagents-code' && needs.generate-matrix.outputs.workload_source == 'managed-image' }}" || evidence.env?.BASE_CONTRACT !== "${{ needs.base-image-publication.outputs.dcode_base_contract }}" || !String(evidence.run ?? "").includes("dcode-base-image.json") || @@ -894,9 +909,9 @@ const STOCK_ONBOARDING_CATALOGUE_JOBS = [ ] as const; const MANAGED_IMAGE_REVISION_EXPRESSION = - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}"; + "${{ needs.base-image-publication.outputs.managed_image_revision }}"; const MANAGED_IMAGE_RECEIPT_EXPRESSION = - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }}"; + "${{ needs.base-image-publication.outputs.managed_image_receipt }}"; /** Require publication success and one exact cohort revision for every stock onboarding job. */ export function validateStockOnboardingPublicationBoundary(workflow: OperationsWorkflow): string[] { diff --git a/tools/e2e/pr-dcode-base-publication.mts b/tools/e2e/pr-dcode-base-publication.mts deleted file mode 100644 index 145f2d7b3c3..00000000000 --- a/tools/e2e/pr-dcode-base-publication.mts +++ /dev/null @@ -1,367 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { createHash } from "node:crypto"; -import { execFileSync } from "node:child_process"; -import { appendFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import path from "node:path"; - -import { - MANAGED_IMAGE_REPOSITORIES, - parseManagedImageContractV1, - SHIPPED_MANAGED_IMAGE_AGENTS, - type ManagedImageContractCatalog, - type ManagedImageContractV1, -} from "../../src/lib/onboard/managed-image/contract.ts"; -import { - githubRequest, - type PublicationRun, - waitForBaseImagePublication, - writePublicationRunOutputs, -} from "./base-image-publication.mts"; - -const REPOSITORY = "NVIDIA/NemoClaw"; -const AGENT = "langchain-deepagents-code"; -const BASE_IMAGE = "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base"; -const BASE_RESOLUTION_KEY_LABEL = "com.nvidia.nemoclaw.base-resolution-key"; -const BASE_RESOLUTION_LABEL = "com.nvidia.nemoclaw.base-resolution"; -const SHA_PATTERN = /^[0-9a-f]{40}$/u; -const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u; -const KEY_PATTERN = /^[0-9a-f]{64}$/u; -const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u; -const MAX_CATALOG_BYTES = 64 * 1024; -const MAX_RESOLUTION_BYTES = 8 * 1024; -const DOCKER_ENV_NAMES = new Set([ - "DOCKER_HOST", - "HOME", - "HTTPS_PROXY", - "HTTP_PROXY", - "LANG", - "NODE_EXTRA_CA_CERTS", - "NO_PROXY", - "PATH", - "SSL_CERT_DIR", - "SSL_CERT_FILE", - "TEMP", - "TMP", - "TMPDIR", - "https_proxy", - "http_proxy", - "no_proxy", -]); -const DOCKER_ENV_PREFIXES = ["LC_", "XDG_"] as const; - -type JsonRecord = Record; - -export interface DcodeBaseResolution { - readonly reference: string; - readonly sourceRevision: string; -} - -export interface PrDcodeBasePublication { - readonly baseReference: string; - readonly run: PublicationRun; -} - -export interface PrDcodeBasePublicationDependencies { - readonly inspectManagedImage?: (reference: string) => unknown; - readonly resolvePublication?: (sourceRevision: string) => Promise; -} - -function record(value: unknown, label: string): JsonRecord { - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error(`${label} must be a JSON object`); - } - return value as JsonRecord; -} - -function exactKeys(value: JsonRecord, expected: readonly string[], label: string): void { - if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...expected].sort())) { - throw new Error(`${label} has unexpected fields`); - } -} - -function parseCatalog(raw: string, candidateSha: string): ManagedImageContractV1 { - if (!SHA_PATTERN.test(candidateSha)) throw new Error("candidate SHA is invalid"); - if (!raw || Buffer.byteLength(raw) > MAX_CATALOG_BYTES) { - throw new Error("exact PR managed-image catalog is missing or oversized"); - } - let parsed: unknown; - try { - parsed = JSON.parse(raw) as unknown; - } catch { - throw new Error("exact PR managed-image catalog is invalid JSON"); - } - const catalog = record(parsed, "exact PR managed-image catalog"); - exactKeys(catalog, SHIPPED_MANAGED_IMAGE_AGENTS, "exact PR managed-image catalog"); - const contracts = SHIPPED_MANAGED_IMAGE_AGENTS.map((agent) => - parseManagedImageContractV1(catalog[agent], agent, "linux/amd64"), - ); - if ( - new Set(contracts.map((contract) => contract.source.revision)).size !== 1 || - contracts[0]?.source.revision !== candidateSha - ) { - throw new Error("exact PR managed-image contracts do not match the candidate commit"); - } - if ( - new Set(contracts.map((contract) => contract.source.release)).size !== 1 || - new Set(contracts.map((contract) => contract.source.cohort)).size !== 1 - ) { - throw new Error("exact PR managed-image contracts do not form one publication cohort"); - } - const assembled = Object.fromEntries( - contracts.map((contract) => [contract.agent, contract]), - ) as ManagedImageContractCatalog; - return parseManagedImageContractV1(assembled[AGENT], AGENT, "linux/amd64"); -} - -/** Limit controller data exposed to the Docker image inspection subprocess. */ -export function buildDockerInspectionEnvironment( - environment: NodeJS.ProcessEnv, -): NodeJS.ProcessEnv { - return Object.fromEntries( - Object.entries(environment).filter( - ([name, value]) => - value !== undefined && - (DOCKER_ENV_NAMES.has(name) || - DOCKER_ENV_PREFIXES.some((prefix) => name.startsWith(prefix))), - ), - ); -} - -function defaultInspectManagedImage(reference: string): unknown { - const env = buildDockerInspectionEnvironment(process.env); - execFileSync("docker", ["pull", "--platform", "linux/amd64", reference], { - encoding: "utf8", - env, - killSignal: "SIGKILL", - maxBuffer: 4 * 1024 * 1024, - stdio: ["ignore", "ignore", "pipe"], - timeout: 600_000, - }); - return JSON.parse( - execFileSync("docker", ["image", "inspect", reference], { - encoding: "utf8", - env, - killSignal: "SIGKILL", - maxBuffer: 4 * 1024 * 1024, - stdio: ["ignore", "pipe", "pipe"], - timeout: 30_000, - }), - ) as unknown; -} - -function parseResolutionLabel(raw: string): JsonRecord { - if ( - !BASE64URL_PATTERN.test(raw) || - raw.length > MAX_RESOLUTION_BYTES || - Buffer.from(raw, "base64url").toString("base64url") !== raw - ) { - throw new Error("Deep Agents Code managed image base resolution label is invalid"); - } - try { - return record( - JSON.parse(Buffer.from(raw, "base64url").toString("utf8")) as unknown, - "Deep Agents Code managed image base resolution", - ); - } catch (error) { - if (error instanceof Error && error.message.includes("must be a JSON object")) throw error; - throw new Error("Deep Agents Code managed image base resolution label is invalid JSON"); - } -} - -/** Bind the exact PR managed image to the base reference recorded by its trusted producer. */ -export function readDcodeBaseResolution( - inspection: unknown, - contract: ManagedImageContractV1, - candidateSha: string, -): DcodeBaseResolution { - if ( - contract.agent !== AGENT || - contract.image !== MANAGED_IMAGE_REPOSITORIES[AGENT] || - contract.platform !== "linux/amd64" || - contract.source.revision !== candidateSha - ) { - throw new Error("Deep Agents Code managed-image contract does not match the candidate"); - } - if (!Array.isArray(inspection) || inspection.length !== 1) { - throw new Error("Deep Agents Code managed image inspection must contain one image"); - } - const image = record(inspection[0], "Deep Agents Code managed image inspection"); - if ( - image.Os !== "linux" || - image.Architecture !== "amd64" || - !Array.isArray(image.RepoDigests) || - !image.RepoDigests.includes(contract.reference) - ) { - throw new Error("Deep Agents Code managed image inspection does not match its exact reference"); - } - const labels = record( - record(image.Config, "Deep Agents Code managed image configuration").Labels, - "Deep Agents Code managed image labels", - ); - const rawResolution = labels[BASE_RESOLUTION_LABEL]; - const resolutionKey = labels[BASE_RESOLUTION_KEY_LABEL]; - if ( - labels["io.nvidia.nemoclaw.agent"] !== AGENT || - labels["io.nvidia.nemoclaw.managed-image.contract"] !== "1" || - labels["io.nvidia.nemoclaw.managed-image.platform"] !== "linux/amd64" || - labels["io.nvidia.nemoclaw.managed-image.cohort"] !== contract.source.cohort || - labels["org.opencontainers.image.revision"] !== candidateSha || - labels["org.opencontainers.image.version"] !== contract.source.release || - typeof rawResolution !== "string" || - typeof resolutionKey !== "string" - ) { - throw new Error("Deep Agents Code managed image labels do not match its exact contract"); - } - - const resolution = parseResolutionLabel(rawResolution); - exactKeys( - resolution, - [ - "architecture", - "digest", - "glibcVersion", - "imageId", - "imageName", - "key", - "minGlibcVersion", - "os", - "ref", - "requireOpenshellSandboxAbi", - "schema", - "source", - "sourceRevision", - ], - "Deep Agents Code managed image base resolution", - ); - const sourceRevision = resolution.sourceRevision; - const digest = resolution.digest; - const reference = resolution.ref; - if ( - resolution.schema !== 1 || - resolution.imageName !== BASE_IMAGE || - typeof digest !== "string" || - !DIGEST_PATTERN.test(digest) || - reference !== `${BASE_IMAGE}@${digest}` || - resolution.source !== "override" || - typeof sourceRevision !== "string" || - !SHA_PATTERN.test(sourceRevision) || - resolution.os !== "linux" || - resolution.architecture !== "amd64" || - typeof resolution.imageId !== "string" || - !DIGEST_PATTERN.test(resolution.imageId) || - typeof resolution.glibcVersion !== "string" || - !/^[0-9]+[.][0-9]+$/u.test(resolution.glibcVersion) || - resolution.minGlibcVersion !== "2.39" || - resolution.requireOpenshellSandboxAbi !== true || - typeof resolution.key !== "string" || - !KEY_PATTERN.test(resolution.key) || - resolution.key !== resolutionKey - ) { - throw new Error("Deep Agents Code managed image base resolution is invalid"); - } - const canonical = { - schema: 1, - key: "", - imageName: BASE_IMAGE, - ref: reference, - digest, - source: "override", - sourceRevision, - imageId: resolution.imageId, - os: "linux", - architecture: "amd64", - glibcVersion: resolution.glibcVersion, - requireOpenshellSandboxAbi: true, - minGlibcVersion: "2.39", - }; - const expectedKey = createHash("sha256").update(JSON.stringify(canonical)).digest("hex"); - if (resolution.key !== expectedKey) { - throw new Error("Deep Agents Code managed image base resolution key is invalid"); - } - return { reference, sourceRevision }; -} - -async function defaultResolvePublication( - sourceRevision: string, - token: string, -): Promise { - return waitForBaseImagePublication({ - history: { - expectedSha: sourceRevision, - relevantSha: sourceRevision, - relevantDistance: 0, - distanceBySha: new Map([[sourceRevision, 0]]), - }, - request: (apiPath) => githubRequest(apiPath, token), - requireWorkflowSuccess: true, - waitMs: 60_000, - pollMs: 5_000, - }); -} - -/** Resolve one trusted base publication for the source revision recorded by the exact PR managed image. */ -export async function resolvePrDcodeBasePublication( - input: { - readonly candidateSha: string; - readonly catalog: string; - readonly token: string; - }, - dependencies: PrDcodeBasePublicationDependencies = {}, -): Promise { - const contract = parseCatalog(input.catalog, input.candidateSha); - const inspection = (dependencies.inspectManagedImage ?? defaultInspectManagedImage)( - contract.reference, - ); - const resolution = readDcodeBaseResolution(inspection, contract, input.candidateSha); - const run = await ( - dependencies.resolvePublication ?? - ((sourceRevision) => defaultResolvePublication(sourceRevision, input.token)) - )(resolution.sourceRevision); - if (run.headSha !== resolution.sourceRevision) { - throw new Error("trusted base-image publication does not match the managed image binding"); - } - return { baseReference: resolution.reference, run }; -} - -export async function main(argv = process.argv.slice(2), env = process.env): Promise { - if (argv.length !== 0) throw new Error("expected no arguments"); - const candidateSha = env.CANDIDATE_SHA ?? ""; - const workflowSha = env.GITHUB_SHA ?? ""; - if ( - env.GITHUB_ACTIONS !== "true" || - env.GITHUB_EVENT_NAME !== "workflow_dispatch" || - env.GITHUB_REPOSITORY !== REPOSITORY || - env.CANDIDATE_REPOSITORY !== REPOSITORY || - !SHA_PATTERN.test(candidateSha) || - !SHA_PATTERN.test(workflowSha) || - candidateSha === workflowSha - ) { - throw new Error( - "Deep Agents Code base publication selection requires a trusted manual PR run for NVIDIA/NemoClaw", - ); - } - const token = env.GITHUB_TOKEN ?? ""; - if (!token || token.includes("\r") || token.includes("\n")) { - throw new Error("GITHUB_TOKEN must be a non-empty single-line value"); - } - const result = await resolvePrDcodeBasePublication({ - candidateSha, - catalog: env.MANAGED_IMAGE_CATALOG ?? "", - token, - }); - const outputPath = env.GITHUB_OUTPUT ?? ""; - writePublicationRunOutputs(outputPath, result.run); - appendFileSync(outputPath, `base_ref=${result.baseReference}\n`, "utf8"); -} - -if (path.resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { - main().catch((error: unknown) => { - console.error( - error instanceof Error ? error.message : "unknown PR Deep Agents Code base publication error", - ); - process.exitCode = 1; - }); -} diff --git a/tools/e2e/pr-managed-image-publication.mts b/tools/e2e/pr-managed-image-publication.mts index a187981050a..4b8d32b0851 100644 --- a/tools/e2e/pr-managed-image-publication.mts +++ b/tools/e2e/pr-managed-image-publication.mts @@ -2,46 +2,30 @@ // 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"; -import YAML from "yaml"; - import { parseManagedImageContractV1, SHIPPED_MANAGED_IMAGE_AGENTS, type ManagedImageContractCatalog, - type ManagedImageContractV1, } from "../../src/lib/onboard/managed-image/contract.ts"; -import { githubRequest } from "./base-image-publication.mts"; import { - bindNamedExactArtifact, - downloadBoundArtifact, - materializeContractArchive, -} from "./exact-artifact-download.mts"; + baseImageInputsChanged, + githubRequest, + parseBaseImagePushPaths, +} from "./base-image-publication.mts"; const REPOSITORY = "NVIDIA/NemoClaw"; -const WORKFLOW_PATH = ".github/workflows/managed-images.yaml"; -const WORKFLOW_FILE = "managed-images.yaml"; -const WORKFLOW_NAME = "Images / Build, Test, and Publish Managed Images"; -const MAX_CHANGED_FILES = 3_000; +const BASE_IMAGE_WORKFLOW_PATH = ".github/workflows/base-image.yaml"; const MAX_COMMIT_TREE_ENTRIES = 100_000; const SHA_PATTERN = /^[0-9a-f]{40}$/u; -const SAFE_PATH_PATTERN = /^[A-Za-z0-9._/*-]+$/u; -const TREE_ENTRY_MODES = new Map([ - ["blob", new Set(["100644", "100755", "120000"])], - ["commit", new Set(["160000"])], - ["tree", new Set(["040000"])], -]); +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u; +const TREE_ENTRY_TYPES = new Set(["blob", "commit", "tree"]); type JsonRecord = Record; -export interface ManagedImagePublicationRun { - readonly id: number; - readonly attempt: number; - readonly headSha: string; -} +export type PrManagedImageSource = "local-dockerfile" | "managed-image"; function record(value: unknown, label: string): JsonRecord { if (!value || typeof value !== "object" || Array.isArray(value)) { @@ -61,138 +45,7 @@ function exactString(value: unknown, expected: string, label: string): void { if (value !== expected) throw new Error(`${label} must be ${expected}`); } -function sha(value: unknown, label: string): string { - if (typeof value !== "string" || !SHA_PATTERN.test(value)) { - throw new Error(`${label} must be a lowercase 40-character SHA`); - } - return value; -} - -function compileManagedImagePath(pattern: string): RegExp { - if ( - !SAFE_PATH_PATTERN.test(pattern) || - pattern.startsWith("/") || - pattern.includes("//") || - pattern.split("/").some((segment) => segment === "" || segment === "." || segment === "..") - ) { - throw new Error(`managed-image PR path '${pattern}' is invalid`); - } - const stars = [...pattern.matchAll(/\*/gu)].map((match) => match.index); - if (stars.length === 0) { - return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}$`, "u"); - } - if (pattern.endsWith("/**") && stars.length === 2) { - const prefix = pattern.slice(0, -3).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); - return new RegExp(`^${prefix}/.+$`, "u"); - } - if (stars.length === 1) { - const escaped = pattern.replace(/[.+?^${}()|[\]\\]/gu, "\\$&").replaceAll("*", "[^/]*"); - return new RegExp(`^${escaped}$`, "u"); - } - throw new Error(`managed-image PR path '${pattern}' uses an unsupported glob`); -} - -/** Read the managed-image workflow path filter from the trusted workflow source. */ -export function parseManagedImagePullRequestPaths(source: string): string[] { - const workflow = record(YAML.parse(source), "managed-image workflow"); - const triggers = record(workflow.on, "managed-image workflow on block"); - const pullRequest = record(triggers.pull_request, "managed-image pull_request trigger"); - if (!Array.isArray(pullRequest.paths) || pullRequest.paths.length === 0) { - throw new Error("managed-image pull_request trigger must declare paths"); - } - const paths = pullRequest.paths.map((value) => { - if (typeof value !== "string" || value.length === 0 || value.trim() !== value) { - throw new Error("managed-image PR paths must be non-empty strings"); - } - compileManagedImagePath(value); - return value; - }); - if (new Set(paths).size !== paths.length) { - throw new Error("managed-image PR paths must be unique"); - } - if (!paths.includes(WORKFLOW_PATH)) { - throw new Error(`managed-image PR paths must include ${WORKFLOW_PATH}`); - } - return paths; -} - -/** Determine whether changed PR files require exact managed-image publication. */ -export function managedImagePublicationRequired( - changedFiles: readonly string[], - patterns: readonly string[], -): boolean { - if (changedFiles.length > MAX_CHANGED_FILES * 2) { - throw new Error(`PR changed-path count exceeds ${MAX_CHANGED_FILES * 2}`); - } - const matchers = patterns.map(compileManagedImagePath); - for (const file of changedFiles) { - if ( - file.length === 0 || - file.length > 4_096 || - /[\0\r\n]/u.test(file) || - file.startsWith("/") || - file.includes("//") || - file.split("/").some((segment) => segment === "" || segment === "." || segment === "..") - ) { - throw new Error("PR changed-file path is invalid"); - } - if (matchers.some((matcher) => matcher.test(file))) return true; - } - return false; -} - -/** Select the unique successful managed-image workflow run for one PR commit. */ -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, WORKFLOW_NAME, "managed-image workflow run name"); - exactString(run.path, 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 { id, attempt, headSha: expected.headSha }; -} - -/** Assemble one all-agent catalog and reject mixed publication authority. */ -export function assembleManagedImageCatalog( +function assembleManagedImageCatalog( values: readonly unknown[], candidateSha: string, ): ManagedImageContractCatalog { @@ -226,8 +79,7 @@ export function assembleManagedImageCatalog( ); } -/** Write one validated exact-candidate catalog from local contract paths. */ -export function writeManagedImageCatalog( +function writeManagedImageCatalog( contractPaths: readonly string[], candidateSha: string, outputPath: string, @@ -244,28 +96,27 @@ export function writeManagedImageCatalog( }); } -function validateWorkflow(payload: unknown): number { - const workflow = record(payload, "managed-image workflow"); - const id = positiveInteger(workflow.id, "managed-image workflow id"); - exactString(workflow.name, WORKFLOW_NAME, "managed-image workflow name"); - exactString(workflow.path, WORKFLOW_PATH, "managed-image workflow path"); - exactString(workflow.state, "active", "managed-image workflow state"); - return id; +function sha(value: unknown, label: string): string { + if (typeof value !== "string" || !SHA_PATTERN.test(value)) { + throw new Error(`${label} must be a lowercase 40-character SHA`); + } + return value; } async function readCommitTree( + repository: string, revision: string, label: string, request: (path: string) => Promise, ): Promise> { const commit = record( - await request(`/repos/${REPOSITORY}/git/commits/${revision}`), + await request(`/repos/${repository}/git/commits/${revision}`), `${label} commit`, ); exactString(commit.sha, revision, `${label} commit SHA`); const treeSha = sha(record(commit.tree, `${label} commit tree`).sha, `${label} tree SHA`); const payload = record( - await request(`/repos/${REPOSITORY}/git/trees/${treeSha}?recursive=1`), + await request(`/repos/${repository}/git/trees/${treeSha}?recursive=1`), `${label} tree`, ); exactString(payload.sha, treeSha, `${label} tree SHA`); @@ -277,35 +128,40 @@ 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) { throw new Error(`${label} tree entry path is invalid`); } - 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) { + if (typeof entry.type !== "string" || !TREE_ENTRY_TYPES.has(entry.type)) { throw new Error(`${label} tree entry type is invalid`); } - if (typeof entry.mode !== "string" || !validModes.has(entry.mode)) { + if (typeof entry.mode !== "string" || !/^[0-7]{6}$/u.test(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; } async function readChangedFiles( - input: { readonly baseSha: string; readonly candidateSha: string }, + input: { + readonly baseSha: string; + readonly candidateRepository: string; + readonly candidateSha: string; + }, request: (path: string) => Promise, ): Promise { - const baseTree = await readCommitTree(input.baseSha, "PR base", request); - const candidateTree = await readCommitTree(input.candidateSha, "PR candidate", request); + const baseTree = await readCommitTree(REPOSITORY, input.baseSha, "PR base", request); + const candidateTree = await readCommitTree( + input.candidateRepository, + input.candidateSha, + "PR candidate", + request, + ); const changedFiles: string[] = []; for (const changedPath of new Set([...baseTree.keys(), ...candidateTree.keys()])) { if (baseTree.get(changedPath) === candidateTree.get(changedPath)) continue; @@ -316,7 +172,11 @@ async function readChangedFiles( function validatePr( payload: unknown, - expected: { readonly baseSha: string; readonly candidateSha: string; readonly prNumber: number }, + expected: { + readonly baseSha: string; + readonly candidateRepository: string; + readonly candidateSha: string; + }, ): void { const pull = record(payload, "pull request"); exactString(pull.state, "open", "pull request state"); @@ -338,81 +198,41 @@ function validatePr( exactString( record(record(pull.head, "pull request source").repo, "pull request source repository") .full_name, - REPOSITORY, + expected.candidateRepository, "pull request source repository", ); } -/** Resolve and download the exact all-agent catalog before candidate code executes. */ -export async function resolvePrManagedImageCatalog( +/** Select the managed-image or local-Dockerfile source for a validated PR. */ +export async function resolvePrManagedImageSource( input: { readonly baseSha: string; readonly candidateRepository: string; readonly candidateSha: string; - readonly outputPath: string; readonly prNumber: number; readonly token: string; readonly workflowSource: string; }, - request: (path: string) => Promise = (apiPath) => githubRequest(apiPath, input.token), -): Promise<"not-required" | "written"> { - if (input.candidateRepository !== REPOSITORY) return "not-required"; + request: (apiPath: string) => Promise = (apiPath) => + githubRequest(apiPath, input.token, { + additionalRepository: input.candidateRepository, + }), +): Promise { if (!SHA_PATTERN.test(input.baseSha) || !SHA_PATTERN.test(input.candidateSha)) { throw new Error("PR base and candidate SHAs are required"); } + if ( + !REPOSITORY_PATTERN.test(input.candidateRepository) || + input.candidateRepository.split("/").some((segment) => segment === "." || segment === "..") + ) { + throw new Error("candidate repository is invalid"); + } positiveInteger(input.prNumber, "PR number"); if (!input.token) throw new Error("GITHUB_TOKEN is required"); validatePr(await request(`/repos/${REPOSITORY}/pulls/${input.prNumber}`), input); const changedFiles = await readChangedFiles(input, request); - const patterns = parseManagedImagePullRequestPaths(input.workflowSource); - if (!managedImagePublicationRequired(changedFiles, patterns)) return "not-required"; - - const workflowId = validateWorkflow( - await request(`/repos/${REPOSITORY}/actions/workflows/${WORKFLOW_FILE}`), - ); - const runs = await request( - `/repos/${REPOSITORY}/actions/workflows/${WORKFLOW_FILE}/runs?event=pull_request&head_sha=${input.candidateSha}&per_page=100`, - ); - const run = selectManagedImagePublicationRun(runs, { - headSha: input.candidateSha, - prNumber: input.prNumber, - workflowId, - }); - - const tempDirectory = 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 downloadBoundArtifact(identity, input.token); - const contractPath = materializeContractArchive(archive, path.join(tempDirectory, 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 "written"; - } finally { - fs.rmSync(tempDirectory, { force: true, recursive: true }); - } + const patterns = parseBaseImagePushPaths(input.workflowSource); + return baseImageInputsChanged(changedFiles, patterns) ? "local-dockerfile" : "managed-image"; } function requiredInteger(value: string | undefined, label: string): number { @@ -429,19 +249,16 @@ 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) throw new Error("expected one managed-image catalog output path"); - const candidateSha = env.CANDIDATE_SHA ?? ""; - if (!candidateSha) return; - const result = await resolvePrManagedImageCatalog({ + if (argv.length !== 1 || argv[0] !== "select-source") throw new Error("expected select-source"); + const source = await resolvePrManagedImageSource({ baseSha: env.BASE_SHA ?? "", candidateRepository: env.CANDIDATE_REPOSITORY ?? "", - candidateSha, - outputPath: argv[0], + candidateSha: env.CANDIDATE_SHA ?? "", prNumber: requiredInteger(env.PR_NUMBER, "PR_NUMBER"), token: env.GITHUB_TOKEN ?? "", - workflowSource: fs.readFileSync(WORKFLOW_PATH, "utf8"), + workflowSource: fs.readFileSync(BASE_IMAGE_WORKFLOW_PATH, "utf8"), }); - console.log(`pr-managed-image-catalog outcome=${result}`); + process.stdout.write(`${source}\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 8958e291b4b..e21d9ec04f2 100644 --- a/tools/e2e/standard-profile-workflow-boundary.mts +++ b/tools/e2e/standard-profile-workflow-boundary.mts @@ -163,11 +163,9 @@ 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.generate-matrix.outputs.managed_image_catalog }}", - managed_image_revision: - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_revision || '' }}", - managed_image_receipt: - "${{ needs.generate-matrix.outputs.managed_image_catalog == '' && needs.base-image-publication.outputs.managed_image_receipt || '' }}", + 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 }}", credential_boundary: contract.credentialBoundary, catalogue_id: "${{ matrix.id }}", target_id: "${{ matrix.target_id }}", @@ -215,9 +213,9 @@ 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", credential_boundary: "string", catalogue_id: "string", target_id: "string", @@ -288,6 +286,7 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi E2E_MANAGED_IMAGE_REVISION: "${{ inputs.managed_image_revision }}", E2E_TARGET_ID: "${{ inputs.target_id }}", E2E_MANAGED_IMAGE_COHORT_RECEIPT: "${{ inputs.managed_image_receipt }}", + E2E_WORKLOAD_SOURCE: "${{ inputs.workload_source }}", NEMOCLAW_RUN_LIVE_E2E: "1", NEMOCLAW_E2E_EXPECTED_SHA: "${{ inputs.candidate_sha }}", NEMOCLAW_E2E_CORRELATION_ID: "${{ inputs.risk_signal_correlation_id }}", @@ -310,7 +309,6 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi "Install target host dependencies", "Prepare E2E workspace", "Restore exact-commit CLI artifact", - "Materialize temporary managed-image catalog", "Install reviewed cloudflared", "Add swap for Hermes image rebuild", "Initialize runner comparison telemetry", @@ -461,38 +459,6 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi ) { errors.push("standard E2E profile must restore the planned exact-commit CLI artifact"); } - const managedCatalog = requireStep( - errors, - workflowSteps, - "Materialize temporary managed-image catalog", - ); - const managedCatalogRun = String(managedCatalog?.run ?? ""); - if ( - managedCatalog?.if !== "${{ inputs.managed_image_catalog != '' }}" || - managedCatalog.shell !== EXECUTION_PLAN_SHELL || - !isDeepStrictEqual(record(managedCatalog.env), { - CANDIDATE_SHA: "${{ inputs.candidate_sha }}", - MANAGED_IMAGE_CATALOG: "${{ inputs.managed_image_catalog }}", - RESTORE_CLI: "${{ inputs.restore_cli && 'true' || 'false' }}", - }) || - !managedCatalogRun.includes(".source.revision == $revision") || - !managedCatalogRun.includes("[.[].source.release] | unique | length") || - !managedCatalogRun.includes("[.[].source.cohort] | unique | length") || - !managedCatalogRun.includes('[[ "$RESTORE_CLI" == "true" ]]') || - !managedCatalogRun.includes(".source.release == $release") || - !managedCatalogRun.includes( - "managed-image catalog source identity does not match the candidate", - ) || - !managedCatalogRun.includes("managed-image catalog release does not match the restored CLI") || - !managedCatalogRun.includes("NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG") || - managedCatalogRun.includes("NEMOCLAW_E2E_EXACT_RELEASE") || - managedCatalogRun.includes(".source.release = $release") || - workflowSteps.indexOf(managedCatalog ?? {}) !== workflowSteps.indexOf(restore ?? {}) + 1 - ) { - errors.push( - "standard E2E profile must materialize only the exact-candidate managed-image catalog", - ); - } const cloudflared = requireStep(errors, workflowSteps, "Install reviewed cloudflared"); const cloudflaredRun = String(cloudflared?.run ?? ""); if ( @@ -510,7 +476,7 @@ function validateProfileWorkflow(errors: string[], profile: WorkflowRecord): voi !cloudflaredRun.includes('dpkg-deb -f "${cloudflared_deb}" Package') || !cloudflaredRun.includes('"${architecture}" != "amd64"') || cloudflaredRun.includes("command -v cloudflared") || - workflowSteps.indexOf(cloudflared ?? {}) !== workflowSteps.indexOf(managedCatalog ?? {}) + 1 + workflowSteps.indexOf(cloudflared ?? {}) !== workflowSteps.indexOf(restore ?? {}) + 1 ) { errors.push("standard E2E profile must install only the reviewed cloudflared package"); }