Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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`, or records both route fields as `null` with `deferredN1xManagedVllmAccepted: true`.
- 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 the receipt, so later rebuilds do not require the environment variable.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ 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",
Expand Down Expand Up @@ -116,9 +116,15 @@ 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
? {
deferredN1xManagedVllmAccepted: true,
}
: {}),
mcp,
} as never,
rebuildAgent: "openclaw",
Expand Down Expand Up @@ -212,6 +218,14 @@ 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 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
47 changes: 46 additions & 1 deletion src/lib/domain/sandbox/n1x-managed-vllm-rebuild.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("recorded N1x managed-vLLM rebuild eligibility", () => {
sandboxEntry,
rebuildSelection,
parseHostLocalInferenceReceipt,
vllmPort,
{ vllmPort },
);

it.each([
Expand All @@ -59,6 +59,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 +144,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
12 changes: 10 additions & 2 deletions src/lib/domain/sandbox/n1x-managed-vllm-rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface RecordedN1xManagedVllmRoute {
endpointSource?: string | null;
openshellDriver?: string | null;
hostLocalInferenceReceipt?: string | null;
deferredN1xManagedVllmAccepted?: unknown;
}

export interface N1xManagedVllmRebuildSelection {
Expand All @@ -43,16 +44,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
9 changes: 9 additions & 0 deletions src/lib/onboard/authoritative-rebuild-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ describe("authoritative rebuild sandbox flow options", () => {
authoritativeResumeConfig: false,
}),
).toEqual({ authoritativeResumeConfig: false });
expect(
authoritativeRebuildSandboxFlowOptions({
authoritativeResumeConfig: true,
allowDeferredN1xManagedVllm: true,
}),
).toEqual({
authoritativeResumeConfig: true,
deferredN1xManagedVllmPreviewIntent: true,
});
});
});

Expand Down
9 changes: 8 additions & 1 deletion src/lib/onboard/authoritative-rebuild-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ import type { OnboardOptions } from "./types";
export type AuthoritativeOnboardGatewayBinding = { name: string; port: number };

export function authoritativeRebuildSandboxFlowOptions(
opts: Pick<OnboardOptions, "authoritativeResumeConfig" | "rebuildPolicySourcePath">,
opts: Pick<
OnboardOptions,
"allowDeferredN1xManagedVllm" | "authoritativeResumeConfig" | "rebuildPolicySourcePath"
>,
): {
authoritativeResumeConfig: boolean;
deferredN1xManagedVllmPreviewIntent?: true;
rebuildPolicySourcePath?: string;
} {
if (opts.authoritativeResumeConfig !== true) return { authoritativeResumeConfig: false };
return {
authoritativeResumeConfig: true,
...(opts.allowDeferredN1xManagedVllm === true
? { deferredN1xManagedVllmPreviewIntent: true as const }
: {}),
...(opts.rebuildPolicySourcePath
? { rebuildPolicySourcePath: opts.rebuildPolicySourcePath }
: {}),
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
16 changes: 16 additions & 0 deletions src/lib/onboard/machine/core-flow-phases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,22 @@ describe("core onboard flow phases", () => {
});
});

it("carries explicit N1x managed preview intent into sandbox creation (#10959)", async () => {
const createSandbox = vi.fn(async () => "created-sandbox");
const { sandbox } = createPhases({
sandboxOptions: { env: { NEMOCLAW_PROVIDER: "install-vllm" } },
sandboxDeps: { createSandbox },
});

await sandbox.run(
context({ provider: "vllm-local", model: "nvidia/Qwen3.6-35B-A3B-NVFP4" }),
);

expect((createSandbox.mock.calls[0] as unknown[] | undefined)?.[15]).toMatchObject({
deferredN1xManagedVllmPreviewIntent: true,
});
});

it("keeps ordinary provider effects on the create-time path", async () => {
const events: string[] = [];
const stageSandboxCredentialProviders = vi.fn(async () => {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/onboard/machine/core-flow-phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface SandboxOnboardFlowPhaseOptions<
hermesPortableLifecycle?: boolean;
apfInterceptorRequested?: boolean;
authoritativeResumeConfig?: boolean;
deferredN1xManagedVllmPreviewIntent?: boolean;

recreateJournalTargetIntentFingerprint?: string | null;
resumeAgentChanged: boolean;
Expand Down Expand Up @@ -333,6 +334,9 @@ export function createSandboxOnboardFlowPhase<
hermesPortableLifecycle: options.hermesPortableLifecycle === true,
apfInterceptorRequested: options.apfInterceptorRequested === true,
authoritativeResumeConfig: options.authoritativeResumeConfig,
deferredN1xManagedVllmPreviewIntent:
options.deferredN1xManagedVllmPreviewIntent === true ||
String(options.env.NEMOCLAW_PROVIDER ?? "").trim() === "install-vllm",
deferSandboxEffectsUntilIdentityVerification: options.apfInterceptorRequested === true,

recreateJournalTargetIntentFingerprint: options.recreateJournalTargetIntentFingerprint,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/onboard/machine/handlers/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ export interface SandboxStateOptions<
apfInterceptorRequested?: boolean;
/** Internal rebuild mode: null web-search state is an authoritative disable, not a prompt. */
authoritativeResumeConfig?: boolean;
/** Explicit Deferred N1x managed-vLLM choice admitted by preflight. */
deferredN1xManagedVllmPreviewIntent?: boolean;
/** Internal rebuild tier that must govern create-time and resumed policy selection. */
/** Keep provider and credential effects behind the exact post-create identity gate. */
deferSandboxEffectsUntilIdentityVerification?: boolean;
Expand Down Expand Up @@ -1756,6 +1758,9 @@ class SandboxStateFlow<
...(this.options.endpointUrl ? { endpointUrl: this.options.endpointUrl } : {}),
...compatibleEndpointReasoningForCreateIntent(this.options.compatibleEndpointReasoning),
endpointSource: this.options.endpointSource ?? null,
...(this.options.deferredN1xManagedVllmPreviewIntent === true
? { deferredN1xManagedVllmPreviewIntent: true as const }
: {}),
...(state.session?.observabilityRequestedExplicitly === true
? { observabilityRequestedExplicitly: true as const }
: {}),
Expand Down
28 changes: 28 additions & 0 deletions src/lib/onboard/sandbox-registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,34 @@ function createdRegistryEntryInput(
}

describe("buildCreatedSandboxRegistryEntry", () => {
it("records only valid Deferred N1x preview acceptance (#10959)", () => {
const entry = buildCreatedSandboxRegistryEntry(
createdRegistryEntryInput({
inferenceSelection: {
provider: "vllm-local",
model: "nvidia/Qwen3.6-35B-A3B-NVFP4",
endpointUrl: null,
endpointSource: null,
credentialEnv: null,
preferredInferenceApi: "openai-completions",
compatibleEndpointReasoning: null,
compatibleEndpointReasoningEffort: null,
nimContainer: null,
},
deferredN1xManagedVllmPreviewIntent: true,
}),
);

expect(entry.deferredN1xManagedVllmAccepted).toBe(true);
expect(() =>
buildCreatedSandboxRegistryEntry(
createdRegistryEntryInput({
deferredN1xManagedVllmPreviewIntent: true,
}),
),
).toThrow("Deferred N1x preview acceptance failed closed validation");
});

it("records explicit OpenClaw identity for a managed workload receipt (#9356)", () => {
const workload = managedWorkloadReceipt("openclaw");
const entry = buildCreatedSandboxRegistryEntry(
Expand Down
Loading
Loading