Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ jobs:
generate-matrix:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: read
contents: read
pull-requests: read
outputs:
cli_artifact_provenance: ${{ steps.record_cli_artifact.outputs.provenance }}
e2e_credentials_allowed: ${{ steps.e2e_credentials.outputs.allowed }}
Expand Down Expand Up @@ -536,6 +540,16 @@ jobs:
fi
fi

- name: Resolve exact PR managed-image catalog
if: ${{ inputs.checkout_sha != '' && (inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '') }}
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 }}
run: node --experimental-strip-types --no-warnings tools/e2e/pr-managed-image-publication.mts "${RUNNER_TEMP}/pr-managed-image-catalog.json"

- name: Check out E2E candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: ${{ inputs.checkout_sha == '' || inputs.jobs != 'native-runtime-qualification-producer' || inputs.targets != '' }}
Expand Down Expand Up @@ -652,6 +666,14 @@ jobs:
' dist/build-identity.json >/dev/null ||
{ echo "::error::candidate CLI build identity does not match the candidate commit SHA"; exit 1; }

managed_catalog="${RUNNER_TEMP}/pr-managed-image-catalog.json"
rm -f -- dist/e2e-managed-image-catalog.json
if [[ -e "$managed_catalog" ]]; then
[[ -f "$managed_catalog" && ! -L "$managed_catalog" && -s "$managed_catalog" ]] ||
{ echo "::error::trusted PR managed-image catalog is not a nonempty regular file"; exit 1; }
install -m 0600 "$managed_catalog" dist/e2e-managed-image-catalog.json
fi

artifact_dir="${RUNNER_TEMP}/nemoclaw-cli-artifact"
install -d -m 0700 "$artifact_dir"
payload="$artifact_dir/nemoclaw-cli.tar"
Expand Down
157 changes: 136 additions & 21 deletions .github/workflows/managed-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -863,31 +863,18 @@ jobs:
shell: bash
run: |
set -euo pipefail
npm ci --ignore-scripts
mapfile -d '' contracts < <(
find "$RUNNER_TEMP/managed-pr-contracts" -type f -name contract.json -print0
)
[[ "${#contracts[@]}" -eq 3 ]] || {
echo "ERROR: exact all-agent activation requires three contracts" >&2
exit 1
}
jq -se \
--arg revision "$CANDIDATE_SHA" '
if (
length == 3 and
([.[].agent] | sort) == ["hermes", "langchain-deepagents-code", "openclaw"] and
([.[].source.revision] | unique) == [$revision] and
([.[].source.cohort] | unique | length) == 1 and
([.[].platform] | unique) == ["linux/amd64"]
)
then map({key: .agent, value: .}) | from_entries
else error("published PR contracts do not form one exact all-agent cohort")
end
' "${contracts[@]}" > "$NEMOCLAW_MANAGED_ACTIVATION_CATALOG"
node --experimental-strip-types --no-warnings \
tools/e2e/pr-managed-image-publication.mts assemble \
"$CANDIDATE_SHA" \
"$NEMOCLAW_MANAGED_ACTIVATION_CATALOG" \
"${contracts[@]}"

- name: Build exact candidate CLI
run: |
npm ci --ignore-scripts
npm run build:cli
run: npm run build:cli

- name: Install OpenShell CLI
shell: bash
Expand All @@ -904,7 +891,7 @@ jobs:
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
export OPENSHELL_BIN="$(command -v openshell)"
"$OPENSHELL_BIN" --version
npx tsx tools/e2e/live-vitest-invocation.mts run \
npx --no-install tsx tools/e2e/live-vitest-invocation.mts run \
--test-path test/e2e/live/managed-image-activation-e2e.test.ts

- name: Upload managed runtime activation evidence
Expand All @@ -916,6 +903,134 @@ jobs:
if-no-files-found: error
retention-days: 1

pr-openclaw-mcp-discovery:
name: PR exact OpenClaw trusted-private MCP discovery (pass ${{ matrix.pass }})
needs: pr-build-and-entrypoint
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: read
strategy:
fail-fast: false
matrix:
pass: [1, 2]
env:
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/pr-exact-mcp/pass-${{ matrix.pass }}
E2E_JOB: "1"
E2E_TARGET_ID: mcp-bridge
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_E2E_EXPECTED_SHA: ${{ github.event.pull_request.head.sha }}
NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: ${{ github.workspace }}/managed-pr-catalog.json
NEMOCLAW_E2E_REQUIRE_EXECUTED_TEST: "1"
NEMOCLAW_E2E_SHARD: openclaw
NEMOCLAW_MCP_BRIDGE_AGENT: openclaw
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1"
NEMOCLAW_RUN_LIVE_E2E: "1"
NEMOCLAW_SANDBOX_NAME: e2e-pr-exact-mcp-${{ matrix.pass }}
OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:b58be5e40c788977ffa0e8305a8cad9c656efdf1a3fe182582a00ca870bb0edb
steps:
- name: Checkout exact PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.19.0

- name: Bind E2E correlation identity
shell: bash
run: |
set -euo pipefail
correlation_id="$(node --input-type=module -e \
'import { randomUUID } from "node:crypto"; console.log(randomUUID())')"
[[ "$correlation_id" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$ ]]
printf 'NEMOCLAW_E2E_CORRELATION_ID=%s\n' "$correlation_id" >> "$GITHUB_ENV"

- name: Download exact published all-agent contracts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: managed-pr-contract-${{ github.run_id }}-${{ github.run_attempt }}-*
path: ${{ runner.temp }}/managed-pr-contracts
merge-multiple: false

- name: Assemble exact all-agent MCP catalog
shell: bash
run: |
set -euo pipefail
npm ci --ignore-scripts
mapfile -d '' contracts < <(
find "$RUNNER_TEMP/managed-pr-contracts" -type f -name contract.json -print0
)
node --experimental-strip-types --no-warnings \
tools/e2e/pr-managed-image-publication.mts assemble \
"$CANDIDATE_SHA" \
"$NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG" \
"${contracts[@]}"

- name: Build exact candidate CLI
run: npm run build:cli

- name: Install and verify cloudflared prerequisite
env:
CLOUDFLARED_VERSION: "2026.6.1"
CLOUDFLARED_DEB_SHA256: "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526"
run: |
set -euo pipefail
cloudflared_deb="${RUNNER_TEMP}/cloudflared-${CLOUDFLARED_VERSION}-linux-amd64.deb"
curl -fL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb" -o "${cloudflared_deb}"
printf '%s %s\n' "${CLOUDFLARED_DEB_SHA256}" "${cloudflared_deb}" | sha256sum -c -
package="$(dpkg-deb -f "${cloudflared_deb}" Package)"
version="$(dpkg-deb -f "${cloudflared_deb}" Version)"
architecture="$(dpkg-deb -f "${cloudflared_deb}" Architecture)"
if [[ "${package}" != "cloudflared" || "${version}" != "${CLOUDFLARED_VERSION}" || "${architecture}" != "amd64" ]]; then
printf 'Unexpected cloudflared package metadata: package=%s version=%s architecture=%s\n' "${package}" "${version}" "${architecture}" >&2
exit 1
fi
sudo dpkg -i "${cloudflared_deb}"
cloudflared --version | grep -F "cloudflared version ${CLOUDFLARED_VERSION}"

- name: Generate MCP test TLS
run: bash test/e2e/setup-mcp-test-tls.sh

- name: Install OpenShell CLI
env:
NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1"
run: bash scripts/install-openshell.sh

- name: Run exact OpenClaw trusted-private MCP discovery
shell: bash
run: |
set -euo pipefail
[[ "$(git rev-parse --verify HEAD)" == "$CANDIDATE_SHA" ]] || {
echo "ERROR: MCP discovery checkout does not match the exact PR head" >&2
exit 1
}
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
export OPENSHELL_BIN="$(command -v openshell)"
"$OPENSHELL_BIN" --version
npx --no-install tsx tools/e2e/live-vitest-invocation.mts run \
--test-path test/e2e/live/mcp-bridge.test.ts

- id: mcp_artifact_secret_scan
name: Scan MCP artifacts for fixture credentials
if: always()
run: npx --no-install tsx tools/e2e/assert-mcp-artifact-secrets-absent.mts "$E2E_ARTIFACT_DIR"

- name: Upload exact MCP discovery evidence
if: ${{ always() && steps.mcp_artifact_secret_scan.outcome == 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr-exact-openclaw-mcp-${{ github.run_id }}-${{ github.run_attempt }}-pass-${{ matrix.pass }}
path: e2e-artifacts/live/pr-exact-mcp/pass-${{ matrix.pass }}/
if-no-files-found: error
retention-days: 1

pi-candidate:
name: Build and validate the Pi candidate managed image (${{ matrix.arch }})
if: github.repository == 'NVIDIA/NemoClaw' && github.event_name == 'pull_request'
Expand Down
29 changes: 29 additions & 0 deletions src/lib/onboard/managed-workload/onboard-orchestration.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { describe, expect, it, vi } from "vitest";

import { createHermesStateVolumeDockerHarness } from "../__test-helpers__/hermes-state-volume";
Expand Down Expand Up @@ -104,6 +108,31 @@ describe("managed workload onboard orchestration", () => {
);
});

it("binds fresh onboarding to the exact PR catalog (#9464)", async () => {
const catalogRevision = "b".repeat(40);
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-live-e2e-catalog-"));
const catalogPath = path.join(fixtureRoot, "catalog.json");
fs.writeFileSync(catalogPath, "{}\n", { mode: 0o600 });
try {
const { prepared, runtime } = createFreshOnboardingRuntime({
GITHUB_ACTIONS: "true",
NEMOCLAW_RUN_LIVE_E2E: "1",
NEMOCLAW_E2E_EXPECTED_SHA: catalogRevision,
NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath,
});

await expect(runtime.ensurePreparedWorkload()).resolves.toBe(prepared);
expect(prepareSandboxWorkloadSource).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
catalogPath,
expectedCatalogRevision: catalogRevision,
}),
);
} finally {
fs.rmSync(fixtureRoot, { force: true, recursive: true });
}
});

it("omits the qualification catalog revision outside GitHub Actions (#9385)", async () => {
const { prepared, runtime } = createFreshOnboardingRuntime({
E2E_MANAGED_IMAGE_REVISION: "a".repeat(40),
Expand Down
8 changes: 7 additions & 1 deletion src/lib/onboard/managed-workload/onboard-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import { getSandboxReadyTimeoutSecs } from "../sandbox-gpu-create";
import type { SandboxGpuConfig } from "../sandbox-gpu-mode";
import {
liveE2eManagedImageCatalog,
liveE2eManagedImageRevision,
type PreparedSandboxWorkloadSource,
prepareSandboxWorkloadSource,
Expand Down Expand Up @@ -196,6 +197,10 @@ export function createManagedWorkloadOnboardRuntime(

const ensurePreparedWorkload = async (): Promise<PreparedSandboxWorkloadSource> => {
const catalogRevision = liveE2eManagedImageRevision(input.startupProfile.environment);
const liveCatalog = liveE2eManagedImageCatalog(input.startupProfile.environment);
if (catalogRevision && liveCatalog) {
throw new Error("live E2E managed-image revision and catalog authority conflict");
}
preparedWorkloadPromise ??= input.managedWorkloadRebuild
? Promise.resolve(
prepareSandboxWorkloadSourceFromRebuildHandoff(
Expand All @@ -210,7 +215,8 @@ export function createManagedWorkloadOnboardRuntime(
customDockerfilePath: input.customDockerfilePath,
runtime: runtimeCapabilities,
version: getVersion({ rootDir: input.rootDir }),
catalogPath: input.tempManagedRuntimeCatalog,
catalogPath: input.tempManagedRuntimeCatalog ?? liveCatalog?.path ?? null,
...(liveCatalog ? { expectedCatalogRevision: liveCatalog.revision } : {}),
...(catalogRevision ? { catalogRevision } : {}),
acceptedCandidateContract: isCandidateAgent(input.agentName)
? readCandidateQualificationReceipt(input.agentName)
Expand Down
79 changes: 79 additions & 0 deletions src/lib/onboard/sandbox-workload-preparation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "./managed-image/contract";
import { createRuntimeProviderBundleRegistry } from "./runtime-provider/registry";
import {
liveE2eManagedImageCatalog,
prepareSandboxWorkloadSource,
SandboxWorkloadPreparationError,
} from "./workload/preparation";
Expand Down Expand Up @@ -85,6 +86,67 @@ function input(agentName: string) {
}

describe("sandbox workload preparation", () => {
it("selects an exact embedded catalog only for live PR E2E (#9464)", () => {
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-live-e2e-catalog-"));
const catalogPath = path.join(fixtureRoot, "catalog.json");
const packagedCatalogPath = path.join(fixtureRoot, "dist", "e2e-managed-image-catalog.json");
fs.mkdirSync(path.dirname(packagedCatalogPath));
fs.writeFileSync(catalogPath, "{}\n", { mode: 0o600 });
fs.writeFileSync(packagedCatalogPath, "{}\n", { mode: 0o600 });
try {
expect(
liveE2eManagedImageCatalog({
GITHUB_ACTIONS: "true",
NEMOCLAW_RUN_LIVE_E2E: "1",
NEMOCLAW_E2E_EXPECTED_SHA: REVISION,
NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath,
}),
).toEqual({ path: catalogPath, revision: REVISION });
expect(
liveE2eManagedImageCatalog({
GITHUB_ACTIONS: "true",
GITHUB_WORKSPACE: fixtureRoot,
NEMOCLAW_RUN_LIVE_E2E: "1",
NEMOCLAW_E2E_EXPECTED_SHA: REVISION,
}),
).toEqual({ path: packagedCatalogPath, revision: REVISION });
expect(
liveE2eManagedImageCatalog({
NEMOCLAW_RUN_LIVE_E2E: "1",
NEMOCLAW_E2E_EXPECTED_SHA: REVISION,
NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath,
}),
).toBeNull();
expect(
liveE2eManagedImageCatalog({
GITHUB_ACTIONS: "true",
NEMOCLAW_RUN_LIVE_E2E: "1",
NEMOCLAW_E2E_EXPECTED_SHA: REVISION,
NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: path.join(fixtureRoot, "missing.json"),
}),
).toBeNull();
} finally {
fs.rmSync(fixtureRoot, { force: true, recursive: true });
}
});

it("rejects an embedded catalog without an exact candidate revision (#9464)", () => {
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-live-e2e-catalog-"));
const catalogPath = path.join(fixtureRoot, "catalog.json");
fs.writeFileSync(catalogPath, "{}\n", { mode: 0o600 });
try {
expect(() =>
liveE2eManagedImageCatalog({
GITHUB_ACTIONS: "true",
NEMOCLAW_RUN_LIVE_E2E: "1",
NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG: catalogPath,
}),
).toThrow("requires an exact candidate revision");
} finally {
fs.rmSync(fixtureRoot, { force: true, recursive: true });
}
});

it.each(
SHIPPED_MANAGED_IMAGE_AGENTS,
)("resolves the complete release catalog and exact %s image (#7744)", async (agent) => {
Expand Down Expand Up @@ -122,6 +184,23 @@ describe("sandbox workload preparation", () => {
});
});

it("rejects an exact catalog from another PR commit (#9464)", async () => {
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-catalog-"));
const catalogPath = path.join(fixtureRoot, "catalog.json");
fs.writeFileSync(catalogPath, JSON.stringify(CATALOG), { mode: 0o600 });
try {
await expect(
prepareSandboxWorkloadSource({
...input("openclaw"),
catalogPath,
expectedCatalogRevision: "b".repeat(40),
}),
).rejects.toThrow("does not match the live E2E candidate revision");
} finally {
fs.rmSync(fixtureRoot, { force: true, recursive: true });
}
});

it("loads an exact local all-agent catalog without using the registry resolver (#7744)", async () => {
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-managed-catalog-"));
const catalogPath = path.join(fixtureRoot, "catalog.json");
Expand Down
Loading
Loading