Skip to content
Merged
30 changes: 11 additions & 19 deletions .github/workflows/managed-images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ jobs:
contents: read
env:
CANDIDATE_SHA: ${{ github.event.pull_request.head.sha }}
STAGING_PRODUCER_SHA: cd0cb2b89965d57cd3d7d97a30d4bdab26781b61
STAGING_QA_SOURCE_SHA: d097a22145859102c0495b0310de264b7a27624f
STAGING_QA_RECORDED_INDEX_DIGEST: sha256:ceaa94a895ba5cb3f231074b97f9910df3ce9d375802cb1d21c777691e0feb43
# Retains the reviewed discovery-permission repair and the current
# managed-image security inventory. The original staging source pinned
# Expat 2.8.2, which cannot satisfy the candidate's 2.8.3 contract.
STAGING_QA_SOURCE_SHA: 250d4abd2d602f864659c06d170699347b5d38bc
STAGING_QA_BASE_IMAGE: nemoclaw-deepagents-code-base:staging-31396519688
STAGING_QA_FINAL_IMAGE: nemoclaw-managed-pr/langchain-deepagents-code-staging-qa
steps:
Expand All @@ -71,9 +72,8 @@ jobs:
path: candidate
persist-credentials: false

# Run 31396519688 built this source locally and never published it. Keep
# the recorded local-only index digest as provenance. Rebuild the base from
# the exact source with Dockerfile.base and its repository build context.
# Rebuild a security-compatible base from the exact source that retains
# the reviewed discovery-permission repair.
- name: Checkout exact staging QA base source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -86,7 +86,7 @@ jobs:
with:
node-version: 22.19.0

- name: Validate staging QA source and recorded digest format
- name: Validate staging QA source
shell: bash
run: |
set -euo pipefail
Expand All @@ -96,19 +96,11 @@ jobs:
echo "ERROR: staging QA base source does not match the recorded NemoClaw commit." >&2
exit 1
fi
if [[ ! "$STAGING_QA_RECORDED_INDEX_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: staging QA base provenance has an invalid recorded index digest." >&2
exit 1
fi
printf '### Staging run 31396519688 Deep Agents Code QA base\n\n'
printf 'Producer commit: %s\n\n' "$STAGING_PRODUCER_SHA"
printf 'NemoClaw source commit: %s\n\n' "$STAGING_QA_SOURCE_SHA"
printf 'Recorded local-only index digest: %s\n' "$STAGING_QA_RECORDED_INDEX_DIGEST"
printf '### Staging QA Deep Agents Code base\n\n'
printf 'NemoClaw source commit: %s\n' "$STAGING_QA_SOURCE_SHA"
{
printf '### Staging run 31396519688 Deep Agents Code QA base\n\n'
printf 'Producer commit: %s\n\n' "$STAGING_PRODUCER_SHA"
printf 'NemoClaw source commit: %s\n\n' "$STAGING_QA_SOURCE_SHA"
printf 'Recorded local-only index digest: %s\n' "$STAGING_QA_RECORDED_INDEX_DIGEST"
printf '### Staging QA Deep Agents Code base\n\n'
printf 'NemoClaw source commit: %s\n' "$STAGING_QA_SOURCE_SHA"
} >> "$GITHUB_STEP_SUMMARY"

- name: Reproduce staging discovery permission drift
Expand Down
10 changes: 0 additions & 10 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2578,16 +2578,6 @@ async function createSandboxWithBaseImageResolution(
process.removeListener("exit", cleanupBuildCtx);
}

if (manageDashboard) {
console.log(" Waiting for NemoClaw dashboard to become ready...");
sandboxReadinessTracing.waitForDashboardReadyWithTrace({
sandboxName,
port: effectiveDashboardPort,
runCaptureOpenshell,
sleep: sleepSeconds,
});
}

if (effectiveSandboxGpuConfig.sandboxGpuEnabled) {
await dockerGpuLocalInference.verifyGpuSandboxLocalInferenceAndCommitAfterReady(
effectiveSandboxGpuConfig,
Expand Down
32 changes: 31 additions & 1 deletion src/lib/onboard/provider-selection-failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function report(overrides: Partial<Parameters<typeof reportProviderSelectionFail

reportProviderSelectionFailure({
reason: { kind: "requested-provider-unavailable", providerKey: "missing" },
availableProviderKeys: ["build"],
isWindowsHostOllama: false,
rejectWindowsHostOllama: (providerKey, windowsHostSelected) => {
rejected.push({ providerKey, windowsHostSelected });
Expand Down Expand Up @@ -85,14 +86,43 @@ describe("reportProviderSelectionFailure", () => {
]);
});

it("reports unavailable requested providers", () => {
it("tells unavailable requested providers how to recover", () => {
const { errors, rejected } = report({
reason: { kind: "requested-provider-unavailable", providerKey: "missing-provider" },
});

assert.deepEqual(rejected, []);
assert.deepEqual(errors, [
" Requested provider 'missing-provider' is not available in this environment.",
" Re-run without NEMOCLAW_PROVIDER to choose an available provider, or install and start the requested provider before retrying.",
]);
});

it("routes a missing vLLM server to the available managed runtime", () => {
const { errors, rejected } = report({
reason: { kind: "requested-provider-unavailable", providerKey: "vllm" },
availableProviderKeys: ["build", "install-vllm"],
});

assert.deepEqual(rejected, []);
assert.deepEqual(errors, [
" Requested provider 'vllm' is not available in this environment.",
" NEMOCLAW_PROVIDER=vllm requires an already-running local vLLM server, but none was detected.",
" Re-run with NEMOCLAW_PROVIDER=install-vllm to install and start the managed vLLM runtime.",
]);
});

it("does not recommend managed vLLM when the host has no managed runtime option", () => {
const { errors, rejected } = report({
reason: { kind: "requested-provider-unavailable", providerKey: "vllm" },
availableProviderKeys: ["build"],
});

assert.deepEqual(rejected, []);
assert.deepEqual(errors, [
" Requested provider 'vllm' is not available in this environment.",
" NEMOCLAW_PROVIDER=vllm requires an already-running local vLLM server, but none was detected.",
" Start a compatible local vLLM server and retry, or choose another provider.",
]);
});
});
15 changes: 15 additions & 0 deletions src/lib/onboard/provider-selection-failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ProviderSelectionFailureReason } from "./provider-selection";

export interface ReportProviderSelectionFailureInput {
reason: ProviderSelectionFailureReason;
availableProviderKeys: readonly string[];
isWindowsHostOllama: boolean;
rejectWindowsHostOllama(providerKey: string, windowsHostSelected: boolean): boolean;
writeError(message: string): void;
Expand Down Expand Up @@ -44,6 +45,20 @@ export function reportProviderSelectionFailure(input: ReportProviderSelectionFai
input.writeError(
` Requested provider '${input.reason.providerKey}' is not available in this environment.`,
);
if (input.reason.providerKey === "vllm") {
input.writeError(
" NEMOCLAW_PROVIDER=vllm requires an already-running local vLLM server, but none was detected.",
);
input.writeError(
input.availableProviderKeys.includes("install-vllm")
? " Re-run with NEMOCLAW_PROVIDER=install-vllm to install and start the managed vLLM runtime."
: " Start a compatible local vLLM server and retry, or choose another provider.",
);
break;
}
input.writeError(
" Re-run without NEMOCLAW_PROVIDER to choose an available provider, or install and start the requested provider before retrying.",
);
break;
}
}
1 change: 1 addition & 0 deletions src/lib/onboard/setup-nim-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ export function createSetupNim(
if (providerSelection.kind === "failure") {
reportProviderSelectionFailure({
reason: providerSelection.reason,
availableProviderKeys: options.map((option) => option.key),
isWindowsHostOllama,
rejectWindowsHostOllama,
writeError: deps.error,
Expand Down
26 changes: 26 additions & 0 deletions src/lib/verify-deployment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,32 @@ describe("verifyDeployment", () => {
expect(sleepCalls).toEqual([10, 10]);
});

it("keeps polling through a cold OpenClaw gateway startup before verification (#8901)", async () => {
let elapsedMs = 0;
const deps = makeDeps({
executeSandboxCommand: (_name: string, script: string) => ({
status: 0,
stdout: script.includes("openclaw --version")
? "2026.5.27"
: script.includes("inference.local")
? "200"
: elapsedMs >= 60_000
? "200"
: "000",
stderr: "",
}),
});

const result = await verifyDeployment("my-sandbox", chain, deps, {
sleep: async (ms: number) => {
elapsedMs += ms;
},
});

expect(result.healthy).toBe(true);
expect(elapsedMs).toBe(60_000);
});

it("retries the dashboard probe and recovers when the port forward comes up late (#3563)", async () => {
let dashboardCalls = 0;
const deps = makeDeps({
Expand Down
6 changes: 4 additions & 2 deletions src/lib/verify-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface VerifyDeploymentOptions {
* returns from createSandbox before the gateway process or the host port
* forward have finished coming up. Each entry below adds one extra attempt
* after the initial try, scheduled at the given delay from the previous
* attempt. The defaults give roughly a 25 s budget per probe before the
* attempt. The defaults give a 90 s budget per probe before the
* wizard surfaces a ✗ marker.
* Tests pass `[]` to disable retry.
*/
Expand All @@ -131,7 +131,9 @@ export interface VerifyDeploymentOptions {
diagnoseCustomOpenClawRuntime?: boolean;
}

const DEFAULT_RETRY_DELAYS_MS: readonly number[] = [1000, 2000, 5000, 7000, 10000];
const DEFAULT_RETRY_DELAYS_MS: readonly number[] = [
1000, 2000, 5000, 7000, 10000, 15000, 20000, 30000,
];
// OpenClaw cron stops its provider preflight after 2.5 seconds. Require a
// response within 2 seconds so onboarding leaves time for client overhead.
const INFERENCE_ROUTE_REACHABILITY_MAX_SECONDS = 2;
Expand Down
7 changes: 3 additions & 4 deletions test/managed-image-publication-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,12 +653,11 @@ describe("complete managed-image publication workflow", () => {
expect(qaBuilder.permissions).toEqual({ contents: "read" });
expect(qaBuilder.env).toMatchObject({
CANDIDATE_SHA: "${{ github.event.pull_request.head.sha }}",
STAGING_PRODUCER_SHA: "cd0cb2b89965d57cd3d7d97a30d4bdab26781b61",
STAGING_QA_SOURCE_SHA: "d097a22145859102c0495b0310de264b7a27624f",
STAGING_QA_RECORDED_INDEX_DIGEST:
"sha256:ceaa94a895ba5cb3f231074b97f9910df3ce9d375802cb1d21c777691e0feb43",
STAGING_QA_SOURCE_SHA: "250d4abd2d602f864659c06d170699347b5d38bc",
Comment thread
cjagwani marked this conversation as resolved.
STAGING_QA_BASE_IMAGE: "nemoclaw-deepagents-code-base:staging-31396519688",
});
expect(qaBuilder.env).not.toHaveProperty("STAGING_PRODUCER_SHA");
expect(qaBuilder.env).not.toHaveProperty("STAGING_QA_RECORDED_INDEX_DIGEST");
expect(JSON.stringify(qaBuilder)).not.toContain(":latest");
expect(prCheckout.with).toMatchObject({
ref: "${{ github.event.pull_request.head.sha }}",
Expand Down
2 changes: 2 additions & 0 deletions test/onboard-selection-windows-provider-rejection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ describe("onboard Windows-host Ollama provider rejection", () => {
const errors: string[] = [];
reportProviderSelectionFailure({
reason: failedResolution.reason,
availableProviderKeys: options.map((option) => option.key),
isWindowsHostOllama: false,
rejectWindowsHostOllama: () => {
setup();
Expand Down Expand Up @@ -143,6 +144,7 @@ describe("onboard Windows-host Ollama provider rejection", () => {
const errors: string[] = [];
reportProviderSelectionFailure({
reason: failedResolution.reason,
availableProviderKeys: options.map((option) => option.key),
isWindowsHostOllama: false,
rejectWindowsHostOllama: () => {
install();
Expand Down
Loading