Skip to content

Commit 6b26b72

Browse files
committed
fix(onboard): preserve derived N1x rebuild intent
Signed-off-by: San Dang <sdang@nvidia.com>
1 parent 323753b commit 6b26b72

15 files changed

Lines changed: 462 additions & 12 deletions

docs/inference/set-up-vllm.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,22 @@ Accepting the preview prompt sets explicit managed-vLLM intent for the narrow pe
371371
Declining the prompt or setting only `NEMOCLAW_NO_EXPRESS=1` stops the installer before onboarding; neither path opens interactive provider selection.
372372
Set `NEMOCLAW_PROVIDER=install-vllm` when you need to bypass the prompt with the same explicit preview intent.
373373

374+
On a host that still meets the N1x identity checks, `rebuild` reuses the recorded route as explicit preview intent only when all of these conditions hold:
375+
376+
- The sandbox registry records provider `vllm-local`.
377+
- The registry records model `nvidia/Qwen3.6-35B-A3B-NVFP4`.
378+
- If the registry records `endpointUrl` as `null`, NemoClaw derives `http://host.openshell.internal:8000/v1`; otherwise, `endpointUrl` must equal that URL.
379+
- The registry records `endpointSource` as `onboard` and `openshellDriver` as `docker`.
380+
- The replacement keeps the recorded provider and model, then re-derives the canonical local endpoint.
381+
- `hostLocalInferenceReceipt` is absent or `null`, or contains a canonical vLLM receipt for `host.openshell.internal:8000` and the same model.
382+
383+
NemoClaw v0.0.109 can omit `hostLocalInferenceReceipt` or record it as `null`.
384+
Matching sandboxes from that release remain eligible only when every condition above passes.
385+
You do not need to set `NEMOCLAW_PROVIDER` again for a matching rebuild.
386+
Any unmet condition stops readiness preflight before NemoClaw deletes the existing sandbox.
387+
A malformed, non-vLLM, or conflicting receipt also stops readiness preflight before deletion.
388+
This reuse waives only the pending physical-validation finding and does not change the Deferred status.
389+
374390
<Warning title="N1x Express Qualification Pending">
375391
Physical CUDA and CDI checks passed on one N1x host.
376392
The full NemoClaw Express E2E test is still pending, so the N1x platform status remains Deferred.

src/lib/actions/sandbox/rebuild-gpu-opt-out.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export type RebuildRecreateOnboardOpts = {
127127
rebuildRegistryInferenceRoute?: RebuildRouteHandoff;
128128
rebuildProviderReconfigure?: RebuildProviderReconfigureHandoff;
129129
providerRecoveryReceipt?: ProviderRecoveryReceipt;
130+
/** Recorded managed-vLLM intent admitted only by the N1x readiness exception. */
131+
allowDeferredN1xManagedVllm?: true;
130132
/** Target-scoped authority admitted by the authoritative rebuild preflight. */
131133
rebuildGatewayAuthority?: CheckpointGatewayAuthority;
132134
preparedImageRebuild?: PreparedImageRebuildHandoff;

src/lib/actions/sandbox/rebuild-managed-workload-context-window.test.ts renamed to src/lib/actions/sandbox/rebuild-preflight-target-phase-orchestration.test.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { describe, expect, it, vi } from "vitest";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
55

66
const mocks = vi.hoisted(() => ({
77
bail: vi.fn(),
8+
preflightAuthoritativeOnboardRuntime: vi.fn(async (..._args: unknown[]) => false),
89
prepareManagedWorkloadRebuildHandoff: vi.fn(),
910
prepareSandboxWorkloadSourceFromRebuildHandoff: vi.fn(),
1011
prepareRebuildTargetConfig: vi.fn(),
@@ -33,7 +34,7 @@ vi.mock("../../onboard/workload/runtime", () => ({
3334
vi.mock("./rebuild-target-preflight", async (importOriginal) => ({
3435
...(await importOriginal<typeof import("./rebuild-target-preflight")>()),
3536
hydrateMessagingConfigForRebuild: vi.fn(),
36-
preflightAuthoritativeOnboardRuntime: vi.fn(async () => false),
37+
preflightAuthoritativeOnboardRuntime: mocks.preflightAuthoritativeOnboardRuntime,
3738
prepareRebuildRecreateOptions: mocks.prepareRebuildRecreateOptions,
3839
prepareRebuildTargetConfig: mocks.prepareRebuildTargetConfig,
3940
stageRebuildHermesDashboardConfig: vi.fn(() => true),
@@ -50,7 +51,64 @@ vi.mock("./rebuild-messaging-conflict-preflight", () => ({
5051
import { managedRebuildProfileDependencies } from "./agents/managed-workload-rebuild-profile";
5152
import { prepareRebuildTargetPreflights } from "./rebuild-preflight-target-phase";
5253

53-
describe("managed workload rebuild context-window preflight", () => {
54+
describe("prepareRebuildTargetPreflights", () => {
55+
beforeEach(() => {
56+
vi.clearAllMocks();
57+
mocks.prepareManagedWorkloadRebuildHandoff.mockResolvedValue(null);
58+
mocks.preflightAuthoritativeOnboardRuntime.mockResolvedValue(false);
59+
});
60+
61+
async function prepareN1xTarget(endpointSource: "onboard" | "inference-set") {
62+
const resumeConfig = {
63+
provider: "vllm-local",
64+
model: "nvidia/Qwen3.6-35B-A3B-NVFP4",
65+
preferredInferenceApi: "openai-completions",
66+
pinEndpoint: true,
67+
endpointUrl: null,
68+
compatibleEndpointReasoning: null,
69+
compatibleEndpointReasoningEffort: null,
70+
registryInferenceRoute: null,
71+
};
72+
mocks.prepareRebuildTargetConfig.mockReturnValue({
73+
agentDefinition: {},
74+
resumeConfig,
75+
durableConfig: {
76+
toolDisclosure: "progressive",
77+
dcodeAutoApprovalMode: "disabled",
78+
webSearchConfig: null,
79+
},
80+
credentialEnv: null,
81+
fromDockerfile: false,
82+
hermesToolGateways: [],
83+
});
84+
mocks.prepareRebuildRecreateOptions.mockReturnValue({
85+
controlUiPort: 18_789,
86+
targetGatewayName: "nemoclaw",
87+
toolDisclosure: "progressive",
88+
dcodeAutoApprovalMode: "disabled",
89+
observabilityEnabled: false,
90+
});
91+
92+
await prepareRebuildTargetPreflights({
93+
sandboxName: "my-assistant",
94+
sandboxEntry: {
95+
name: "my-assistant",
96+
agent: "openclaw",
97+
gatewayName: "nemoclaw",
98+
openshellDriver: "docker",
99+
provider: resumeConfig.provider,
100+
model: resumeConfig.model,
101+
endpointUrl: "http://host.openshell.internal:8000/v1",
102+
endpointSource,
103+
} as never,
104+
rebuildAgent: "openclaw",
105+
autoYes: true,
106+
log: vi.fn(),
107+
bail: mocks.bail as never,
108+
});
109+
return mocks.preflightAuthoritativeOnboardRuntime.mock.calls[0]?.[2];
110+
}
111+
54112
it("resolves the Ollama context window through target preparation", async () => {
55113
const catalogHandoff = {
56114
agent: "openclaw",
@@ -123,4 +181,18 @@ describe("managed workload rebuild context-window preflight", () => {
123181
).resolves.toBeNull();
124182
expect(mocks.resolveContextWindowForModel).toHaveBeenCalledWith("ollama-local", "qwen3.5:9b");
125183
});
184+
185+
it("passes exact legacy N1x intent into authoritative readiness (#9292)", async () => {
186+
const readinessOptions = await prepareN1xTarget("onboard");
187+
188+
expect(readinessOptions).toEqual(
189+
expect.objectContaining({ allowDeferredN1xManagedVllm: true }),
190+
);
191+
});
192+
193+
it("withholds N1x intent for a mismatched endpoint source (#9292)", async () => {
194+
const readinessOptions = await prepareN1xTarget("inference-set");
195+
196+
expect(readinessOptions).not.toHaveProperty("allowDeferredN1xManagedVllm");
197+
});
126198
});

src/lib/actions/sandbox/rebuild-preflight-target-phase.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33

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

6+
import {
7+
hostLocalInferenceReceipt,
8+
serializedHostLocalInferenceReceipt,
9+
serializedLlamaCppHostLocalInferenceReceipt,
10+
} from "../../../../test/helpers/host-local-inference-receipt";
611
import type {
712
ProviderRecoveryReceipt,
813
RegistryInferenceRoute,
914
} from "../../onboard/rebuild-route-handoff";
15+
import { serializeHostLocalInferenceReceipt } from "../../onboard/runtime-provider/host-local-inference";
1016
import type { SandboxBaseImageResolutionMetadata } from "../../sandbox-base-image";
1117
import {
1218
pinRebuildTargetGatewayForReadiness,
1319
runRebuildGatewayRecoveryAfterReadiness,
1420
stageRebuildBaseImageResolutionHandoff,
21+
stageRecordedManagedVllmIntent,
1522
stageRegistryProviderRecoveryReceipt,
1623
} from "./rebuild-preflight-target-phase";
1724

@@ -121,6 +128,152 @@ describe("stageRegistryProviderRecoveryReceipt", () => {
121128
});
122129
});
123130

131+
describe("stageRecordedManagedVllmIntent", () => {
132+
const n1xExpressEntry = {
133+
provider: "vllm-local",
134+
model: "nvidia/Qwen3.6-35B-A3B-NVFP4",
135+
endpointUrl: null,
136+
endpointSource: "onboard" as const,
137+
openshellDriver: "docker",
138+
};
139+
const n1xExpressSelection = {
140+
provider: n1xExpressEntry.provider,
141+
model: n1xExpressEntry.model,
142+
pinEndpoint: true,
143+
endpointUrl: null,
144+
};
145+
146+
it.each([
147+
{ state: "derived endpoint and absent receipt", sandboxEntry: n1xExpressEntry },
148+
{
149+
state: "derived endpoint and null receipt",
150+
sandboxEntry: { ...n1xExpressEntry, hostLocalInferenceReceipt: null },
151+
},
152+
{
153+
state: "recorded canonical endpoint",
154+
sandboxEntry: {
155+
...n1xExpressEntry,
156+
endpointUrl: "http://host.openshell.internal:8000/v1",
157+
},
158+
},
159+
])(
160+
"carries the v0.0.109 N1x Express selection with a $state into rebuild readiness (#9292)",
161+
({ sandboxEntry }) => {
162+
const recreateOptions: { allowDeferredN1xManagedVllm?: true } = {};
163+
164+
stageRecordedManagedVllmIntent(recreateOptions, sandboxEntry, n1xExpressSelection);
165+
166+
expect(recreateOptions.allowDeferredN1xManagedVllm).toBe(true);
167+
},
168+
);
169+
170+
it("also accepts a canonical vLLM receipt on the exact N1x Express selection (#9292)", () => {
171+
const recreateOptions: { allowDeferredN1xManagedVllm?: true } = {};
172+
const genericReceipt = hostLocalInferenceReceipt("docker");
173+
const n1xReceipt = serializeHostLocalInferenceReceipt({
174+
...genericReceipt,
175+
endpoint: {
176+
...genericReceipt.endpoint,
177+
host: "host.openshell.internal",
178+
},
179+
inference: {
180+
protocol: "openai-chat-completions",
181+
model: n1xExpressEntry.model,
182+
toolCallingRequired: true,
183+
},
184+
});
185+
186+
stageRecordedManagedVllmIntent(
187+
recreateOptions,
188+
{ ...n1xExpressEntry, hostLocalInferenceReceipt: n1xReceipt },
189+
n1xExpressSelection,
190+
);
191+
192+
expect(recreateOptions.allowDeferredN1xManagedVllm).toBe(true);
193+
});
194+
195+
it.each([
196+
{
197+
caseName: "different recorded provider",
198+
sandboxEntry: { ...n1xExpressEntry, provider: "compatible-endpoint" },
199+
rebuildSelection: n1xExpressSelection,
200+
},
201+
{
202+
caseName: "different recorded model",
203+
sandboxEntry: { ...n1xExpressEntry, model: "meta-llama/Llama-3.1-8B-Instruct" },
204+
rebuildSelection: n1xExpressSelection,
205+
},
206+
{
207+
caseName: "different recorded endpoint",
208+
sandboxEntry: { ...n1xExpressEntry, endpointUrl: "http://host.openshell.internal:8001/v1" },
209+
rebuildSelection: n1xExpressSelection,
210+
},
211+
{
212+
caseName: "different endpoint source",
213+
sandboxEntry: { ...n1xExpressEntry, endpointSource: "inference-set" as const },
214+
rebuildSelection: n1xExpressSelection,
215+
},
216+
{
217+
caseName: "different OpenShell driver",
218+
sandboxEntry: { ...n1xExpressEntry, openshellDriver: "podman" },
219+
rebuildSelection: n1xExpressSelection,
220+
},
221+
{
222+
caseName: "malformed receipt",
223+
sandboxEntry: { ...n1xExpressEntry, hostLocalInferenceReceipt: "not-json" },
224+
rebuildSelection: n1xExpressSelection,
225+
},
226+
{
227+
caseName: "different host-local service",
228+
sandboxEntry: {
229+
...n1xExpressEntry,
230+
hostLocalInferenceReceipt: serializedLlamaCppHostLocalInferenceReceipt(),
231+
},
232+
rebuildSelection: n1xExpressSelection,
233+
},
234+
{
235+
caseName: "conflicting vLLM receipt",
236+
sandboxEntry: {
237+
...n1xExpressEntry,
238+
hostLocalInferenceReceipt: serializedHostLocalInferenceReceipt(),
239+
},
240+
rebuildSelection: n1xExpressSelection,
241+
},
242+
{
243+
caseName: "different rebuild provider",
244+
sandboxEntry: n1xExpressEntry,
245+
rebuildSelection: { ...n1xExpressSelection, provider: "compatible-endpoint" },
246+
},
247+
{
248+
caseName: "different rebuild model",
249+
sandboxEntry: n1xExpressEntry,
250+
rebuildSelection: { ...n1xExpressSelection, model: "meta-llama/Llama-3.1-8B-Instruct" },
251+
},
252+
{
253+
caseName: "unresolved rebuild endpoint",
254+
sandboxEntry: n1xExpressEntry,
255+
rebuildSelection: { ...n1xExpressSelection, pinEndpoint: false },
256+
},
257+
{
258+
caseName: "noncanonical rebuild endpoint",
259+
sandboxEntry: n1xExpressEntry,
260+
rebuildSelection: {
261+
...n1xExpressSelection,
262+
endpointUrl: "http://host.openshell.internal:8001/v1",
263+
},
264+
},
265+
])(
266+
"does not infer N1x Express intent from a $caseName (#9292)",
267+
({ sandboxEntry, rebuildSelection }) => {
268+
const recreateOptions: { allowDeferredN1xManagedVllm?: true } = {};
269+
270+
stageRecordedManagedVllmIntent(recreateOptions, sandboxEntry, rebuildSelection);
271+
272+
expect(recreateOptions).not.toHaveProperty("allowDeferredN1xManagedVllm");
273+
},
274+
);
275+
});
276+
124277
describe("stageRebuildBaseImageResolutionHandoff", () => {
125278
it("binds outer resolver provenance to its immutable local handoff (#7144)", () => {
126279
const imageId = `sha256:${"a".repeat(64)}`;

src/lib/actions/sandbox/rebuild-preflight-target-phase.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CLI_NAME } from "../../cli/branding";
66
import type { SandboxMessagingPlan } from "../../messaging";
77
import { isSandboxBaseImageRefreshRequested } from "../../onboard/base-image-resolution-flow";
88
import type { DcodeAutoApprovalMode } from "../../onboard/dcode-auto-approval";
9+
import { parseHostLocalInferenceReceipt } from "../../onboard/runtime-provider/host-local-inference";
910
import {
1011
createRebuildProviderReconfigureHandoff,
1112
mintProviderRecoveryReceipt,
@@ -83,6 +84,64 @@ export function stageRegistryProviderRecoveryReceipt(
8384
);
8485
}
8586

87+
const N1X_EXPRESS_PROVIDER = "vllm-local";
88+
const N1X_EXPRESS_MODEL = "nvidia/Qwen3.6-35B-A3B-NVFP4";
89+
const N1X_EXPRESS_ENDPOINT_URL = "http://host.openshell.internal:8000/v1";
90+
91+
/** Reuse only the exact N1x Express selection recorded by onboarding. */
92+
export function stageRecordedManagedVllmIntent(
93+
recreateOptions: Pick<RebuildRecreateOnboardOpts, "allowDeferredN1xManagedVllm">,
94+
sandboxEntry: Pick<
95+
RebuildSandboxEntry,
96+
| "provider"
97+
| "model"
98+
| "endpointUrl"
99+
| "endpointSource"
100+
| "openshellDriver"
101+
| "hostLocalInferenceReceipt"
102+
>,
103+
rebuildSelection: {
104+
provider: string;
105+
model: string;
106+
pinEndpoint: boolean;
107+
endpointUrl: string | null;
108+
},
109+
): void {
110+
const recordedEndpointUsesCanonicalLocalRoute =
111+
sandboxEntry.endpointUrl === null || sandboxEntry.endpointUrl === N1X_EXPRESS_ENDPOINT_URL;
112+
if (
113+
sandboxEntry.provider !== N1X_EXPRESS_PROVIDER ||
114+
sandboxEntry.model !== N1X_EXPRESS_MODEL ||
115+
!recordedEndpointUsesCanonicalLocalRoute ||
116+
sandboxEntry.endpointSource !== "onboard" ||
117+
sandboxEntry.openshellDriver !== "docker" ||
118+
rebuildSelection.provider !== sandboxEntry.provider ||
119+
rebuildSelection.model !== sandboxEntry.model ||
120+
rebuildSelection.pinEndpoint !== true ||
121+
rebuildSelection.endpointUrl !== null
122+
) {
123+
return;
124+
}
125+
const serialized = sandboxEntry.hostLocalInferenceReceipt;
126+
if (serialized === undefined || serialized === null) {
127+
recreateOptions.allowDeferredN1xManagedVllm = true;
128+
return;
129+
}
130+
try {
131+
const receipt = parseHostLocalInferenceReceipt(serialized);
132+
if (
133+
receipt.service === "vllm" &&
134+
receipt.endpoint.host === "host.openshell.internal" &&
135+
receipt.endpoint.port === 8000 &&
136+
receipt.inference?.model === N1X_EXPRESS_MODEL
137+
) {
138+
recreateOptions.allowDeferredN1xManagedVllm = true;
139+
}
140+
} catch {
141+
// Malformed durable state must not grant the narrow N1x readiness exception.
142+
}
143+
}
144+
86145
export interface RebuildPreparedTarget {
87146
targetConfig: RebuildTargetConfig;
88147
recreateOptions: RebuildRecreateOnboardOpts;
@@ -233,6 +292,7 @@ export async function prepareRebuildTargetPreflights(args: {
233292
recreateOptions.observabilityEnabled =
234293
requestedObservabilityEnabled ?? recreateOptions.observabilityEnabled;
235294
recreateOptions.observabilityRequestedExplicitly = requestedObservabilityEnabled !== undefined;
295+
stageRecordedManagedVllmIntent(recreateOptions, sandboxEntry, resumeConfig);
236296
if (
237297
!stageRebuildHermesDashboardConfig(
238298
rebuildAgent,

0 commit comments

Comments
 (0)