Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
055f2bc
fix(e2e): build changed sandbox images locally
sandl99 Aug 25, 2026
723a32b
fix(e2e): complete local image qualification
sandl99 Aug 25, 2026
6a55a5f
refactor(e2e): share image input matching
sandl99 Aug 25, 2026
72e7af8
fix(e2e): route Pi to its candidate image
sandl99 Aug 25, 2026
f151510
fix(onboard): report pairing settlement failures
sandl99 Aug 25, 2026
c1e3570
fix(e2e): preserve local DCode base resolution
sandl99 Aug 25, 2026
f340c61
fix(onboard): consume pairing producer receipts
sandl99 Aug 25, 2026
c0d08cd
test(e2e): use valid vLLM storage GPU UUID
sandl99 Aug 25, 2026
5acda20
Merge remote-tracking branch 'origin/main' into fix/e2e-local-build-f…
sandl99 Aug 25, 2026
51d0933
fix(e2e): use private managed-image catalog writer
sandl99 Aug 25, 2026
b8c4387
test(e2e): remove duplicate managed-image cases
sandl99 Aug 25, 2026
a1b9a0a
test(e2e): cover renamed managed-image inputs
sandl99 Aug 25, 2026
88ab371
refactor(e2e): remove unrelated pairing changes
sandl99 Aug 25, 2026
4d2df93
test(e2e): cover PR revision guards
sandl99 Aug 25, 2026
d73cd20
fix(e2e): qualify local images on arm64
sandl99 Aug 25, 2026
80f0aba
test(e2e): set multiarch workload source
sandl99 Aug 25, 2026
95cd106
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 26, 2026
6b980cc
fix(onboard): restore local build fallback
sandl99 Aug 26, 2026
65ed831
fix(e2e): harden managed image selection
sandl99 Aug 26, 2026
39d60e4
fix(e2e): qualify local images on protected runtime
sandl99 Aug 26, 2026
b188ad6
fix(mcp): wait for final provider revision
sandl99 Aug 26, 2026
12fe0e8
fix(e2e): resolve local Dockerfile from agent
sandl99 Aug 26, 2026
087f361
fix(onboard): wait for canonical pairing before warmup
sandl99 Aug 26, 2026
b49077a
docs(e2e): qualify GitHub rate-limit retries
sandl99 Aug 26, 2026
82206e4
chore(mcp): leave provider revision fix to owning PR
sandl99 Aug 26, 2026
66fd3ed
chore(onboard): leave pairing settlement unchanged
sandl99 Aug 26, 2026
2513729
merge(main): synchronize current base
sandl99 Aug 26, 2026
b6f45b2
fix(e2e): preserve local base identity on GPU
sandl99 Aug 26, 2026
272b372
fix(e2e): keep workload selection acyclic
sandl99 Aug 26, 2026
f560f03
fix(ci): resolve managed image verification failures
sandl99 Aug 26, 2026
b425a07
Merge remote-tracking branch 'origin/main' into fix/e2e-local-build-f…
sandl99 Aug 26, 2026
6b57594
test(ci): remove duplicate advisor watch trigger
sandl99 Aug 26, 2026
993c194
chore(mcp): remove unrelated credential revision fix
sandl99 Aug 26, 2026
9259f23
chore(ci): leave advisor trigger fix to owner
sandl99 Aug 26, 2026
44026cd
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 26, 2026
649968b
fix(e2e): bind workload selection to commit trees
sandl99 Aug 26, 2026
1effafb
merge(main): synchronize current base
sandl99 Aug 26, 2026
7dee76e
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
8803497
merge: resolve conflicts with main
github-actions[bot] Aug 26, 2026
577fe69
Merge branch 'main' into fix/e2e-local-build-for-image-changes
prekshivyas Aug 26, 2026
4f2407a
merge(main): resolve PR #10263 conflicts
sandl99 Aug 28, 2026
4661092
Merge remote-tracking branch 'origin/main' into fix/e2e-local-build-f…
sandl99 Aug 28, 2026
e1b8531
refactor(e2e): remove unused PR image catalog path
sandl99 Aug 28, 2026
a2b575e
fix(e2e): retain managed image catalog assembler
sandl99 Aug 28, 2026
0847dd9
refactor(e2e): remove obsolete DCode publication path
sandl99 Aug 28, 2026
20a39f1
fix(e2e): retain trusted base for local builds
sandl99 Aug 28, 2026
fecee10
refactor(e2e): remove unused publication outputs
sandl99 Aug 28, 2026
cdf63ce
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 28, 2026
86bcb5b
Merge branch 'main' into fix/e2e-local-build-for-image-changes
sandl99 Aug 28, 2026
5994ff0
fix(e2e): bind image selection to commit trees
sandl99 Aug 29, 2026
f39859c
merge(main): synchronize current base
sandl99 Aug 29, 2026
6f7f097
test(e2e): map live helpers to existing coverage
sandl99 Aug 29, 2026
d9331e3
test(onboard): remove duplicate activation case
sandl99 Aug 29, 2026
363bbea
merge: resolve conflicts with main
github-actions[bot] Aug 30, 2026
f977478
merge: resolve conflicts with main
github-actions[bot] Aug 31, 2026
b247dee
Merge branch 'main' into fix/e2e-local-build-for-image-changes
cv Aug 31, 2026
53fc625
fix(e2e): restore workload source contract
cv Aug 31, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/base-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ on:
- ".github/workflows/managed-images.yaml"
- "test/e2e/live/managed-image-activation-e2e.test.ts"
- "test/e2e/live/managed-image-activation-e2e-helpers.ts"
- "test/e2e/fixtures/workload-source-env.ts"
- ".github/actions/ci-reviewed-npm-audit/**"
- ".github/actions/build-base-image-platform/**"
- ".github/actions/publish-base-image-manifest/**"
Expand All @@ -41,6 +42,8 @@ on:
- "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json"
- "src/lib/core/json-types.ts"
- "src/lib/core/ports.ts"
- "src/lib/hermes-managed-route.ts"
- "src/lib/inference/managed-dcode/identity.ts"
- "src/lib/messaging/**"
- "src/lib/onboard/managed-bootstrap/envelope.ts"
- "src/lib/onboard/managed-startup/**"
Expand Down
46 changes: 6 additions & 40 deletions .github/workflows/e2e-standard-profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ on:
cli_artifact_provenance:
required: true
type: string
managed_image_catalog:
managed_image_revision:
required: true
type: string
workload_source:
required: true
type: string
credential_boundary:
Expand Down Expand Up @@ -106,7 +109,9 @@ jobs:
timeout-minutes: ${{ inputs.timeout_minutes }}
env:
E2E_JOB: "1"
E2E_MANAGED_IMAGE_REVISION: ${{ inputs.managed_image_revision }}
E2E_TARGET_ID: ${{ inputs.target_id }}
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 }}
Expand Down Expand Up @@ -392,45 +397,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}
Expand Down
473 changes: 315 additions & 158 deletions .github/workflows/e2e.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .github/workflows/managed-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ jobs:
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 \
tools/e2e/pr-managed-image-source.mts assemble \
"$CANDIDATE_SHA" \
"$NEMOCLAW_MANAGED_ACTIVATION_CATALOG" \
"${contracts[@]}"
Expand Down Expand Up @@ -1147,7 +1147,7 @@ jobs:
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 \
tools/e2e/pr-managed-image-source.mts assemble \
"$CANDIDATE_SHA" \
"$NEMOCLAW_E2E_MANAGED_IMAGE_CATALOG" \
"${contracts[@]}"
Expand Down
7 changes: 4 additions & 3 deletions scripts/checks/build-protected-managed-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ esac
target_arch="${platform#linux/}"
npm_target_os="linux"
npm_target_libc="glibc"
[[ "$openclaw_base" =~ ^ghcr[.]io/nvidia/nemoclaw/sandbox-base@sha256:[a-f0-9]{64}$ ]] || usage
[[ "$hermes_base" =~ ^ghcr[.]io/nvidia/nemoclaw/hermes-sandbox-base@sha256:[a-f0-9]{64}$ ]] || usage
[[ "$dcode_base" =~ ^ghcr[.]io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base@sha256:[a-f0-9]{64}$ ]] || usage
base_prefix='(ghcr[.]io/nvidia/nemoclaw|localhost:5000/nemoclaw-managed-protected-base)'
[[ "$openclaw_base" =~ ^${base_prefix}/sandbox-base@sha256:[a-f0-9]{64}$ ]] || usage
[[ "$hermes_base" =~ ^${base_prefix}/hermes-sandbox-base@sha256:[a-f0-9]{64}$ ]] || usage
[[ "$dcode_base" =~ ^${base_prefix}/langchain-deepagents-code-sandbox-base@sha256:[a-f0-9]{64}$ ]] || usage
[[ "$source_root" == /* && "$source_root" != *$'\n'* && -d "$source_root" && ! -L "$source_root" ]] || usage
source_root="$(cd -- "$source_root" && pwd -P)"
seed_helper="$source_root/scripts/checks/materialize-locked-npm-cache-seed.mts"
Expand Down
12 changes: 10 additions & 2 deletions src/lib/onboard/managed-workload/onboard-orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,21 @@ describe("managed workload onboard orchestration", () => {
).toBe(false);
});

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",
dockerfilePath: "agents/openclaw/Dockerfile",
reason: "contract-unavailable",
},
release: null,
fallbackDiagnostic: expect.stringContaining("registry offline"),
});
});

it("rejects an unavailable catalog for explicit temporary managed-image onboarding", async () => {
Expand Down
17 changes: 9 additions & 8 deletions src/lib/onboard/managed-workload/onboard-orchestration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,15 @@ export function createManagedWorkloadOnboardRuntime(
input.computePlan,
);
const strictManagedRuntime = input.tempManagedRuntime || 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,
Expand Down
21 changes: 11 additions & 10 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,17 @@ After the checks pass, the action restores root `dist/` and `nemoclaw/dist/share
If the version command fails, the action stops before the live test runs.
This boundary keeps candidate source separate from the trusted workflow implementation.

For a same-repository PR that changes a managed-image workflow path, the trusted planner also
requires one successful `Images / 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 exact 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 keeps the released catalog behavior. The GitHub token
is available only to the trusted planner job and is not included in the candidate CLI artifact.
For a manual PR run, the trusted planner compares the immutable base and
candidate commit trees with the base-image and managed-image input paths. When
those inputs are unchanged, the run uses the applicable trusted managed-image
publication from `main`. When any input changed, the run builds the candidate
Dockerfiles locally instead of waiting for a candidate publication.

The native startup jobs build the local candidate bases for each platform. The
AMD64 producer passes their exact digests and build caches to the protected GPU
job. That job rebuilds with networking disabled and rejects any digest mismatch
before it runs Ollama, NIM, or vLLM qualification. The GitHub token is available
only to the trusted planner and is not included in the candidate CLI artifact.

The same-repository `Images / Managed Images` PR workflow also runs the OpenClaw managed-image MCP
discovery and lifecycle scope in two independent matrix jobs. Each job assembles one exact candidate
Expand Down
17 changes: 11 additions & 6 deletions test/e2e/fixtures/phases/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 useCandidateBase =
options.dcodeBaseImageReference === undefined &&
process.env.E2E_WORKLOAD_SOURCE === "local-dockerfile";
const baseImageReference = useCandidateBase
? 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"], {
Expand All @@ -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,
Expand Down
53 changes: 31 additions & 22 deletions test/e2e/fixtures/workload-source-env.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import path from "node:path";
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 { REPO_ROOT } from "./paths.ts";

const LEGACY_DOCKERFILE_BY_AGENT = {
openclaw: "Dockerfile",
hermes: "agents/hermes/Dockerfile",
"langchain-deepagents-code": "agents/langchain-deepagents-code/Dockerfile",
} as const;

type LegacyDockerfileAgent = keyof typeof LEGACY_DOCKERFILE_BY_AGENT;
function agentSelectionEnv(input: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return {
[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],
};
}

function legacyDockerfileAgent(env: NodeJS.ProcessEnv): LegacyDockerfileAgent {
const agent = env.NEMOCLAW_AGENT ?? "openclaw";
if (agent in LEGACY_DOCKERFILE_BY_AGENT) return agent as LegacyDockerfileAgent;
return "openclaw";
function localDockerfilePath(input: NodeJS.ProcessEnv): string {
const agentName = input.NEMOCLAW_AGENT ?? process.env.NEMOCLAW_AGENT ?? "openclaw";
const agent = loadAgent(agentName, agentSelectionEnv(input));
const dockerfilePath = agent.dockerfilePath ?? agent.legacyPaths?.dockerfile;
if (!dockerfilePath) {
throw new Error(`Agent '${agent.name}' has no Dockerfile for local E2E workload source.`);
}
return dockerfilePath;
}

/**
* Existing live E2E targets retain the product's default legacy-Dockerfile
* path. Targets that must select a source explicitly do so through the
* provider-neutral E2E_WORKLOAD_SOURCE contract.
* Live E2E targets select the trusted managed image or a Dockerfile from the
* candidate checkout through the E2E_WORKLOAD_SOURCE contract.
*
* This is applied at the final fixture spawn boundary so an agent selected by
* a test command receives its own Dockerfile rather than an OpenClaw default.
*/
export function resolveLiveE2eWorkloadSourceEnv(input: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const targetId = input.E2E_TARGET_ID;
const source = input.E2E_WORKLOAD_SOURCE;
if (!targetId || source !== "legacy-dockerfile") return input;
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") return input;
if (input.NEMOCLAW_FROM_DOCKERFILE) return input;
const agent = legacyDockerfileAgent(input);
const dockerfilePath = localDockerfilePath(input);
return {
...input,
NEMOCLAW_FROM_DOCKERFILE: path.join(REPO_ROOT, LEGACY_DOCKERFILE_BY_AGENT[agent]),
NEMOCLAW_FROM_DOCKERFILE: dockerfilePath,
};
}
Loading
Loading