Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
16 changes: 13 additions & 3 deletions docs/inference/set-up-vllm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -486,13 +486,23 @@ On a host that still meets the N1x identity checks, `rebuild` reuses the recorde
- The sandbox registry records provider `vllm-local`.
- The registry records model `nvidia/Qwen3.6-35B-A3B-NVFP4`.
- If the registry records `endpointUrl` as `null`, NemoClaw derives `http://host.openshell.internal:${NEMOCLAW_VLLM_PORT:-8000}/v1`; otherwise, `endpointUrl` must equal that URL.
- The registry records `endpointSource` as `onboard` and `openshellDriver` as `docker`.
- The registry records `openshellDriver` as `docker`.
- The registry records `endpointSource` as `onboard`, records both route fields as `null` with `deferredN1xManagedVllmAccepted: true`, or the current process supplies the one-time `NEMOCLAW_PROVIDER=install-vllm` recovery decision below.
- The replacement keeps the recorded provider and model, then re-derives the canonical local endpoint.
- `hostLocalInferenceReceipt` is absent or `null`, or contains a canonical vLLM receipt for `host.openshell.internal:${NEMOCLAW_VLLM_PORT:-8000}` and the same model.

NemoClaw v0.0.109 can omit `hostLocalInferenceReceipt` or record it as `null`.
Matching sandboxes from that release remain eligible only when every condition above passes.
You do not need to set `NEMOCLAW_PROVIDER` again for a matching rebuild.
NemoClaw v0.0.119 can record both `endpointUrl` and `endpointSource` as `null` without durable preview acceptance.
For one rebuild of such a sandbox, repeat the explicit preview choice on the command that triggers the rebuild.

```bash
NEMOCLAW_PROVIDER=install-vllm \
$$nemoclaw my-assistant channels add slack
```

A successful rebuild records `deferredN1xManagedVllmAccepted: true`, so later rebuilds do not require the environment variable.
Matching sandboxes from either release remain eligible only when every other condition above passes.
Except for the one-time v0.0.119 recovery above, you do not need to set `NEMOCLAW_PROVIDER` again for a matching rebuild.
Any unmet condition stops readiness preflight before NemoClaw deletes the existing sandbox.
A malformed, non-vLLM, or conflicting receipt also stops readiness preflight before deletion.
This reuse waives only the pending physical-validation finding and does not change the Deferred status.
Expand Down
7 changes: 7 additions & 0 deletions docs/manage-sandboxes/add-channels-after-onboarding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ If you defer the rebuild, apply the change later:
$$nemoclaw my-assistant rebuild
```

For an affected v0.0.119 N1x sandbox, repeat the one-time recovery choice described in [Set Up vLLM](../../inference/local-inference/set-up-vllm#use-n1x-express):

```bash
NEMOCLAW_PROVIDER=install-vllm \
$$nemoclaw my-assistant rebuild
```

## Add a Channel Non-Interactively

Set required environment variables before running `channels add`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

const mocks = vi.hoisted(() => ({
bail: vi.fn(),
Expand Down Expand Up @@ -59,6 +59,8 @@ import type { RebuildRecreateOnboardOpts } from "./rebuild-gpu-opt-out";
import { prepareRebuildTargetPreflights } from "./rebuild-preflight-target-phase";

describe("prepareRebuildTargetPreflights", () => {
afterEach(() => vi.unstubAllEnvs());

beforeEach(() => {
vi.clearAllMocks();
mocks.getMcpPreparationRuntimeSelection.mockReturnValue({
Expand All @@ -71,11 +73,16 @@ describe("prepareRebuildTargetPreflights", () => {
});

async function prepareN1xTarget(
endpointSource: "onboard" | "inference-set",
endpointSource: "onboard" | "inference-set" | null,
mcp: { bridges: Record<string, { server: string }> } | null = null,
provider = "vllm-local",
model = "nvidia/Qwen3.6-35B-A3B-NVFP4",
nimContainer: string | null = null,
accepted = endpointSource === null,
entryOverrides: {
endpointUrl?: string | null;
hostLocalInferenceReceipt?: string | null;
} = {},
) {
const resumeConfig = {
provider,
Expand Down Expand Up @@ -116,10 +123,17 @@ describe("prepareRebuildTargetPreflights", () => {
openshellDriver: "docker",
provider: resumeConfig.provider,
model: resumeConfig.model,
endpointUrl: "http://host.openshell.internal:8000/v1",
endpointUrl:
endpointSource === null ? null : "http://host.openshell.internal:8000/v1",
endpointSource,
nimContainer,
...(endpointSource === null && accepted
? {
deferredN1xManagedVllmAccepted: true,
}
: {}),
mcp,
...entryOverrides,
} as never,
rebuildAgent: "openclaw",
autoYes: true,
Expand Down Expand Up @@ -212,6 +226,43 @@ describe("prepareRebuildTargetPreflights", () => {
);
});

it("passes normalized N1x Express intent into readiness (#10959)", async () => {
const readinessOptions = await prepareN1xTarget(null);

expect(readinessOptions).toEqual(
expect.objectContaining({ allowDeferredN1xManagedVllm: true }),
);
});

it("passes explicit v0.0.119 recovery intent into readiness (#10959)", async () => {
vi.stubEnv("NEMOCLAW_PROVIDER", "install-vllm");
const readinessOptions = await prepareN1xTarget(null, null, undefined, undefined, null, false);

expect(readinessOptions).toEqual(
expect.objectContaining({ allowDeferredN1xManagedVllm: true }),
);
});

it.each([
["a recorded endpoint", null, null, { endpointUrl: "http://host.openshell.internal:8000/v1" }],
["another endpoint source", "inference-set", null, {}],
["a NIM container", null, "nemoclaw-nim", {}],
["a malformed receipt", null, null, { hostLocalInferenceReceipt: "invalid" }],
] as const)("withholds explicit recovery for %s (#10959)", async (_case, source, nim, overrides) => {
vi.stubEnv("NEMOCLAW_PROVIDER", "install-vllm");
const readinessOptions = await prepareN1xTarget(
source,
null,
undefined,
undefined,
nim,
false,
overrides,
);

expect(readinessOptions).not.toHaveProperty("allowDeferredN1xManagedVllm");
});

it("passes recorded Ollama intent into authoritative readiness (#11041)", async () => {
const readinessOptions = await prepareN1xTarget("onboard", null, "ollama-local", "qwen3.5:9b");

Expand Down
25 changes: 25 additions & 0 deletions src/lib/actions/sandbox/rebuild-target-staging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { RebuildSandboxEntry } from "./rebuild-flow-helpers";
import {
hydrateMessagingConfigForRebuild,
prepareRebuildRecreateOptions,
stageRecordedDeferredN1xIntent,
} from "./rebuild-target-staging";

const SANDBOX_ENTRY = {
Expand Down Expand Up @@ -158,6 +159,30 @@ describe("prepareRebuildRecreateOptions", () => {
});
});

describe("stageRecordedDeferredN1xIntent", () => {
it("accepts one explicit preview retry for a normalized v0.0.119 route (#10959)", () => {
const recreateOptions: { allowDeferredN1xManagedVllm?: true } = {};
const route = {
provider: "vllm-local",
model: "nvidia/Qwen3.6-35B-A3B-NVFP4",
endpointUrl: null,
endpointSource: null,
openshellDriver: "docker",
hostLocalInferenceReceipt: null,
nimContainer: null,
};

stageRecordedDeferredN1xIntent(
recreateOptions,
route,
{ ...route, pinEndpoint: true },
{ NEMOCLAW_PROVIDER: "install-vllm" },
);

expect(recreateOptions).toEqual({ allowDeferredN1xManagedVllm: true });
});
});

describe("hydrateMessagingConfigForRebuild", () => {
it("hydrates registry config instead of conflicting session config", () => {
const registryPlan = messagingConfigPlan("0");
Expand Down
6 changes: 6 additions & 0 deletions src/lib/actions/sandbox/rebuild-target-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function stageRecordedDeferredN1xIntent(
| "endpointSource"
| "openshellDriver"
| "hostLocalInferenceReceipt"
| "deferredN1xManagedVllmAccepted"
| "nimContainer"
>,
rebuildSelection: {
Expand All @@ -117,6 +118,7 @@ export function stageRecordedDeferredN1xIntent(
pinEndpoint: boolean;
endpointUrl: string | null;
},
env: Readonly<Record<string, string | undefined>> = process.env,
): void {
const selectionMatchesRecord =
rebuildSelection.provider === sandboxEntry.provider &&
Expand All @@ -141,6 +143,10 @@ export function stageRecordedDeferredN1xIntent(
sandboxEntry,
rebuildSelection,
parseHostLocalInferenceReceipt,
{
explicitPreviewIntent:
String(env.NEMOCLAW_PROVIDER ?? "").trim() === "install-vllm",
},
);
if (recordedStandardProviderIsEligible || recordedManagedVllmIsEligible) {
recreateOptions.allowDeferredN1xManagedVllm = true;
Expand Down
74 changes: 72 additions & 2 deletions src/lib/domain/sandbox/n1x-managed-vllm-rebuild.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,32 @@ import {
parseHostLocalInferenceReceipt,
serializeHostLocalInferenceReceipt,
} from "../../onboard/runtime-provider/host-local-inference";
import { isRecordedN1xManagedVllmRebuildEligible } from "./n1x-managed-vllm-rebuild";
import {
isDeferredN1xManagedVllmAcceptanceRoute,
isRecordedN1xManagedVllmRebuildEligible,
} from "./n1x-managed-vllm-rebuild";

describe("Deferred N1x managed-vLLM acceptance route", () => {
const accepted = {
provider: "vllm-local",
model: "nvidia/Qwen3.6-35B-A3B-NVFP4",
endpointUrl: null,
endpointSource: null,
nimContainer: null,
openshellDriver: "docker",
};

it.each([
["the exact route", accepted, true],
["another model", { ...accepted, model: "other" }, false],
["a recorded endpoint", { ...accepted, endpointUrl: "http://localhost:8000/v1" }, false],
["another endpoint source", { ...accepted, endpointSource: "inference-set" }, false],
["a NIM container", { ...accepted, nimContainer: "nemoclaw-nim" }, false],
["another driver", { ...accepted, openshellDriver: "kubernetes" }, false],
])("classifies %s", (_case, route, expected) => {
expect(isDeferredN1xManagedVllmAcceptanceRoute(route)).toBe(expected);
});
});

describe("recorded N1x managed-vLLM rebuild eligibility", () => {
const n1xExpressEntry = {
Expand All @@ -39,7 +64,7 @@ describe("recorded N1x managed-vLLM rebuild eligibility", () => {
sandboxEntry,
rebuildSelection,
parseHostLocalInferenceReceipt,
vllmPort,
{ vllmPort },
);

it.each([
Expand All @@ -59,6 +84,27 @@ describe("recorded N1x managed-vLLM rebuild eligibility", () => {
expect(eligible(sandboxEntry)).toBe(true);
});

it("accepts normalized N1x Express intent with a derived endpoint (#10959)", () => {
expect(
eligible({
...n1xExpressEntry,
endpointSource: null,
deferredN1xManagedVllmAccepted: true,
}),
).toBe(true);
});

it("accepts an explicit preview retry for a normalized legacy record (#10959)", () => {
expect(
isRecordedN1xManagedVllmRebuildEligible(
{ ...n1xExpressEntry, endpointSource: null },
n1xExpressSelection,
parseHostLocalInferenceReceipt,
{ explicitPreviewIntent: true },
),
).toBe(true);
});

it("accepts a canonical vLLM receipt on the exact N1x Express selection (#9292)", () => {
const genericReceipt = hostLocalInferenceReceipt("docker");
const n1xReceipt = serializeHostLocalInferenceReceipt({
Expand Down Expand Up @@ -123,6 +169,30 @@ describe("recorded N1x managed-vLLM rebuild eligibility", () => {
sandboxEntry: { ...n1xExpressEntry, endpointSource: "inference-set" },
rebuildSelection: n1xExpressSelection,
},
{
caseName: "normalized source without preview provenance",
sandboxEntry: { ...n1xExpressEntry, endpointSource: null },
rebuildSelection: n1xExpressSelection,
},
{
caseName: "normalized source with a recorded endpoint",
sandboxEntry: {
...n1xExpressEntry,
endpointUrl: "http://host.openshell.internal:8000/v1",
endpointSource: null,
deferredN1xManagedVllmAccepted: true,
},
rebuildSelection: n1xExpressSelection,
},
{
caseName: "malformed preview acceptance",
sandboxEntry: {
...n1xExpressEntry,
endpointSource: null,
deferredN1xManagedVllmAccepted: "true",
},
rebuildSelection: n1xExpressSelection,
},
{
caseName: "different OpenShell driver",
sandboxEntry: { ...n1xExpressEntry, openshellDriver: "podman" },
Expand Down
34 changes: 32 additions & 2 deletions src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,36 @@ export function isN1xManagedVllmProviderModel(
return provider === N1X_EXPRESS_PROVIDER && model === N1X_EXPRESS_MODEL;
}

export interface DeferredN1xManagedVllmAcceptanceRoute {
provider?: string | null;
model?: string | null;
endpointUrl?: string | null;
endpointSource?: string | null;
nimContainer?: unknown;
openshellDriver?: string | null;
}

/** True only when durable Deferred N1x acceptance is valid for this route. */
export function isDeferredN1xManagedVllmAcceptanceRoute(
route: DeferredN1xManagedVllmAcceptanceRoute,
): boolean {
return (
isN1xManagedVllmProviderModel(route.provider, route.model) &&
route.endpointUrl === null &&
route.endpointSource === null &&
route.nimContainer == null &&
route.openshellDriver === "docker"
);
}

export interface RecordedN1xManagedVllmRoute {
provider?: string | null;
model?: string | null;
endpointUrl?: string | null;
endpointSource?: string | null;
openshellDriver?: string | null;
hostLocalInferenceReceipt?: string | null;
deferredN1xManagedVllmAccepted?: unknown;
}

export interface N1xManagedVllmRebuildSelection {
Expand All @@ -43,16 +66,23 @@ export function isRecordedN1xManagedVllmRebuildEligible(
sandboxEntry: RecordedN1xManagedVllmRoute,
rebuildSelection: N1xManagedVllmRebuildSelection,
parseReceipt: ParseN1xManagedVllmReceipt,
vllmPort = VLLM_PORT,
options: { vllmPort?: number; explicitPreviewIntent?: boolean } = {},
): boolean {
const vllmPort = options.vllmPort ?? VLLM_PORT;
if (!Number.isInteger(vllmPort) || vllmPort < 1024 || vllmPort > 65535) return false;
const canonicalEndpointUrl = `http://host.openshell.internal:${String(vllmPort)}/v1`;
const recordedEndpointUsesCanonicalLocalRoute =
sandboxEntry.endpointUrl === null || sandboxEntry.endpointUrl === canonicalEndpointUrl;
const recordedSourceIsEligible =
sandboxEntry.endpointSource === "onboard" ||
(sandboxEntry.endpointSource === null &&
sandboxEntry.endpointUrl === null &&
(sandboxEntry.deferredN1xManagedVllmAccepted === true ||
options.explicitPreviewIntent === true));
if (
!isN1xManagedVllmProviderModel(sandboxEntry.provider, sandboxEntry.model) ||
!recordedEndpointUsesCanonicalLocalRoute ||
sandboxEntry.endpointSource !== "onboard" ||
!recordedSourceIsEligible ||
sandboxEntry.openshellDriver !== "docker" ||
rebuildSelection.provider !== sandboxEntry.provider ||
rebuildSelection.model !== sandboxEntry.model ||
Expand Down
4 changes: 4 additions & 0 deletions src/lib/onboard/created-sandbox-finalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,7 @@ function assertVerifiedCreateBoundaryMatchesLifecycle(

type OnboardCreateIntent = {
readonly endpointSource?: RegistrationSeed["inferenceSelection"]["endpointSource"];
readonly deferredN1xManagedVllmPreviewIntent?: RegistrationSeed["deferredN1xManagedVllmPreviewIntent"];
readonly observabilityEnabled?: boolean;
} | null;
type OnboardResolvedCreateIntent = {
Expand Down Expand Up @@ -722,6 +723,9 @@ export function createOnboardCreatedSandboxCompletion(
preferredInferenceApi,
createIntent?.endpointSource ?? null,
),
...(createIntent?.deferredN1xManagedVllmPreviewIntent
? { deferredN1xManagedVllmPreviewIntent: true as const }
: {}),
runtimeFields,
agent,
agentVersionKnown: !fromDockerfile,
Expand Down
Loading
Loading