Skip to content
Closed
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
64 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
cf1b255
fix(e2e): reconcile credential window state
sandl99 Aug 26, 2026
bcf3ba5
merge: update main
sandl99 Aug 26, 2026
664f47f
fix(e2e): restore credential window runtime
sandl99 Aug 26, 2026
5e6168f
merge: stack on #10263
sandl99 Aug 26, 2026
1effafb
merge(main): synchronize current base
sandl99 Aug 26, 2026
c9d95b5
merge: update #10263 stack
sandl99 Aug 26, 2026
cfc0492
fix(onboard): revalidate deferred provider effects
sandl99 Aug 26, 2026
9895bc7
fix(e2e): reconcile credential window state
sandl99 Aug 26, 2026
74927a5
fix(e2e): restore credential window runtime
sandl99 Aug 26, 2026
0213472
merge: detach credential window from #10263
sandl99 Aug 26, 2026
81977a2
fix(e2e): preserve CLI artifact dependency
sandl99 Aug 26, 2026
684d9b6
fix(onboard): revalidate deferred provider effects
sandl99 Aug 26, 2026
968c085
ci: restart checks after Actions outage
sandl99 Aug 26, 2026
6be2a78
test(onboard): remove incidental policy I/O
sandl99 Aug 26, 2026
3151e34
test(onboard): isolate portable policy planning
sandl99 Aug 26, 2026
9c62e22
ci: retry missing pull request dispatch
sandl99 Aug 26, 2026
3cda65f
merge: update branch from main
sandl99 Aug 26, 2026
29a523d
fix(e2e): align restored workflow identities
sandl99 Aug 26, 2026
2bad35c
ci: refresh mergeability and checks
sandl99 Aug 26, 2026
8d2aa7c
merge: reconcile PR head with main
sandl99 Aug 26, 2026
31d95a6
merge: update branch from main
sandl99 Aug 26, 2026
b04fa10
test(e2e): scope credential provider detach
sandl99 Aug 26, 2026
ad12ee8
merge: update branch from main
sandl99 Aug 26, 2026
2bf26ff
fix(e2e): use supported credential detach lifecycle
sandl99 Aug 26, 2026
b2e010c
merge: update branch from main
sandl99 Aug 26, 2026
7a52f7e
fix(e2e): reconcile MCP discovery transport failure
sandl99 Aug 26, 2026
4d56248
merge: update branch from main
sandl99 Aug 26, 2026
8d9508f
ci: trigger pull request checks
sandl99 Aug 26, 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
165 changes: 162 additions & 3 deletions test/credentials/openshell-credential-generation-window.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { EventEmitter } from "node:events";
import fs from "node:fs";
import vm from "node:vm";

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

Expand All @@ -18,7 +20,8 @@ import {
credentialWindowRequestId,
credentialWindowSecrets,
OPENSHELL_RETAINED_CREDENTIAL_GENERATIONS,
} from "../e2e/live/openshell-credential-generation-window.ts";
parseCredentialWindowRequestObservation,
} from "../e2e/live/mcp-bridge-credential-generation-window.ts";

describe("OpenShell exact-main credential generation-window proof", () => {
it("crosses the complete upstream retention window with unique scannable values", () => {
Expand All @@ -43,8 +46,6 @@ describe("OpenShell exact-main credential generation-window proof", () => {
expect(script.indexOf(snapshot)).toBeLessThan(script.indexOf("while (Date.now() < deadline"));
expect(script).toContain('"^openshell:resolve:env:(v[0-9]{1,20})_" + config.envName + "$"');
expect(script).toContain('authorization: "Bearer " + credentialPlaceholder');
expect(script).toContain('response.statusCode === 200 ? "allowed" : "denied"');
expect(script).toContain('outbound.on("error", () => resolve("denied"))');
expect(script).toContain("outbound.setTimeout(30_000");
expect(script).toContain(JSON.stringify(CREDENTIAL_WINDOW_PATHS.acknowledgement));
expect(script).toContain(JSON.stringify(CREDENTIAL_WINDOW_STEPS.allowedBeforeExpiry));
Expand All @@ -57,6 +58,164 @@ describe("OpenShell exact-main credential generation-window proof", () => {
expect(script).not.toContain(MCP_BRIDGE_TEST_CREDENTIALS.generationWindow);
});

it("settles a request when the response closes before ending (#10155)", async () => {
const writes = new Map<string, string>();
let control: string = CREDENTIAL_WINDOW_STEPS.deniedAfterDetach;
let stdout = "";
let stderr = "";
const script = buildCredentialWindowChildScript({
mcpUrl: "https://credential-window.example.test/mcp",
maxRuntimeMs: 10_000,
});
const response = Object.assign(new EventEmitter(), {
readableEnded: false,
resume: () => response,
statusCode: 200,
});
const https = {
request: (_options: unknown, onResponse: (value: typeof response) => void) => {
const outbound = Object.assign(new EventEmitter(), {
destroy: () => outbound,
end: () => {
onResponse(response);
queueMicrotask(() => response.emit("close"));
return outbound;
},
setTimeout: () => outbound,
});
return outbound;
},
};
const fileSystem = {
existsSync: () => true,
readFileSync: () => control,
writeFileSync: (file: string, value: string) => {
writes.set(file, value);
control =
file === CREDENTIAL_WINDOW_PATHS.acknowledgement ? CREDENTIAL_WINDOW_STEPS.stop : control;
},
};
const builtins = new Map<string, unknown>([
["node:fs", fileSystem],
["node:https", https],
]);
const childProcess = {
env: {
[CREDENTIAL_WINDOW_ENV_NAME]: `openshell:resolve:env:v7_${CREDENTIAL_WINDOW_ENV_NAME}`,
},
exitCode: undefined as number | undefined,
stderr: {
write: (value: string) => {
stderr += value;
return true;
},
},
stdout: {
write: (value: string) => {
stdout += value;
return true;
},
},
};

await (vm.runInNewContext(script, {
Buffer,
URL,
process: childProcess,
queueMicrotask,
require: (id: string) =>
builtins.get(id) ??
(() => {
throw new Error(`unexpected child require: ${id}`);
})(),
setTimeout: (callback: () => void) => {
queueMicrotask(callback);
return 1;
},
}) as Promise<void>);

const observation = {
step: CREDENTIAL_WINDOW_STEPS.deniedAfterDetach,
outcome: "denied",
statusCode: null,
errorCode: "PREMATURE_CLOSE",
};
expect(stderr).toBe("");
expect(childProcess.exitCode).toBeUndefined();
expect(JSON.parse(writes.get(CREDENTIAL_WINDOW_PATHS.acknowledgement) ?? "")).toEqual(
observation,
);
expect(JSON.parse(stdout)).toEqual({ revision: "v7", outcomes: [observation] });
});

it("accepts bounded request diagnostics and rejects raw error text (#10155)", () => {
expect(
parseCredentialWindowRequestObservation(
JSON.stringify({
step: CREDENTIAL_WINDOW_STEPS.allowedBeforeExpiry,
outcome: "allowed",
statusCode: 200,
errorCode: null,
}),
),
).toEqual({
step: CREDENTIAL_WINDOW_STEPS.allowedBeforeExpiry,
outcome: "allowed",
statusCode: 200,
errorCode: null,
});
expect(
parseCredentialWindowRequestObservation(
JSON.stringify({
step: CREDENTIAL_WINDOW_STEPS.deniedAfterExpiry,
outcome: "denied",
statusCode: 503,
errorCode: null,
}),
),
).toEqual({
step: CREDENTIAL_WINDOW_STEPS.deniedAfterExpiry,
outcome: "denied",
statusCode: 503,
errorCode: null,
});
expect(
parseCredentialWindowRequestObservation(
JSON.stringify({
step: CREDENTIAL_WINDOW_STEPS.deniedAfterDetach,
outcome: "denied",
statusCode: null,
errorCode: "ECONNRESET",
}),
),
).toEqual({
step: CREDENTIAL_WINDOW_STEPS.deniedAfterDetach,
outcome: "denied",
statusCode: null,
errorCode: "ECONNRESET",
});
expect(
parseCredentialWindowRequestObservation(
JSON.stringify({
step: CREDENTIAL_WINDOW_STEPS.deniedAfterDetach,
outcome: "denied",
statusCode: null,
errorCode: "Error: fixture credential leaked",
}),
),
).toBeNull();
expect(
parseCredentialWindowRequestObservation(
JSON.stringify({
step: CREDENTIAL_WINDOW_STEPS.allowedBeforeExpiry,
outcome: "allowed",
statusCode: 401,
errorCode: null,
}),
),
).toBeNull();
});

it("builds explicit bounded expiry and attached-key-removal updates", () => {
expect(CREDENTIAL_WINDOW_EXPIRY_DELAY_MS).toBe(3 * 60_000);
expect(buildCredentialWindowProviderUpdateArgs("owned-provider", 123_456)).toEqual([
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
Loading
Loading