Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/inference/set-up-openai-compatible-endpoint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ The portable profile handles the descriptor as follows:

| Descriptor state | Onboarding result |
|---|---|
| Absent | NemoClaw keeps the existing portable behavior. The local Podman `qwen3-vl:4b` model remains the active inference route. |
| Absent | Fresh Portable onboarding uses a nonempty `NEMOCLAW_MODEL`. An empty or absent value uses the local Podman `qwen3-vl:4b` model. A valid Portable inference descriptor remains authoritative when present. Portable resume ignores ambient model selectors and reuses the recorded provider and model authority. |
| Directory and file metadata meet the requirements above, the descriptor is valid, and the authenticated onboarding checks pass | NemoClaw makes the compatible endpoint model the active inference route. If the local Podman `qwen3-vl:4b` runner already exists, NemoClaw leaves it installed as standby. |
| File passes the metadata checks but contains malformed JSON, an invalid schema, an expired credential, or a rejected endpoint | NemoClaw deletes the descriptor and exits before it changes gateway, provider, sandbox, or onboarding state. |
| Descriptor is valid, but the endpoint, selected model, or configured route fails an authenticated onboarding check | NemoClaw deletes the descriptor and exits without reporting onboarding success. The compatible route may already be configured. NemoClaw does not activate an existing local runner automatically. |
Expand Down
79 changes: 78 additions & 1 deletion src/lib/onboard/portable-environment-scope.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,94 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

import {
createDefaultResumeProfileEnvironmentScope,
createPortableOnboardEnvironmentScope,
PORTABLE_RUNTIME_ENV_KEYS,
} from "./session-bootstrap";

const { getNonInteractiveModel } = require("./providers") as {
getNonInteractiveModel: (providerKey: string) => string | null;
};

const CLEARED_PORTABLE_RUNTIME_ENV_KEYS = PORTABLE_RUNTIME_ENV_KEYS.filter(
(key) => key !== "NEMOCLAW_EXPERIMENTAL_PROFILE",
);

describe("portable onboarding environment scope", () => {
it("preserves an explicit model during fresh portable onboarding (#9200)", () => {
const env: NodeJS.ProcessEnv = { NEMOCLAW_MODEL: "qwen3.6:35b" };
const scope = createPortableOnboardEnvironmentScope(env, null);

expect(env.NEMOCLAW_PROVIDER).toBe("ollama");
expect(env.NEMOCLAW_MODEL).toBe("qwen3.6:35b");

scope.restore();
expect(env).toEqual({ NEMOCLAW_MODEL: "qwen3.6:35b" });
});

it("uses qwen3-vl:4b when fresh portable model intent is absent (#9200)", () => {
const env: NodeJS.ProcessEnv = {};
const scope = createPortableOnboardEnvironmentScope(env, null);

expect(env.NEMOCLAW_PROVIDER).toBe("ollama");
expect(env.NEMOCLAW_MODEL).toBe("qwen3-vl:4b");

scope.restore();
expect(env).toEqual({});
});

it("uses qwen3-vl:4b and restores empty fresh portable model intent (#9200)", () => {
const env: NodeJS.ProcessEnv = { NEMOCLAW_MODEL: "" };
const scope = createPortableOnboardEnvironmentScope(env, null);

expect(env.NEMOCLAW_PROVIDER).toBe("ollama");
expect(env.NEMOCLAW_MODEL).toBe("qwen3-vl:4b");

scope.restore();
expect(env).toEqual({ NEMOCLAW_MODEL: "" });
});

it("uses the activation model instead of ambient fresh model intent (#9200)", () => {
const env: NodeJS.ProcessEnv = { NEMOCLAW_MODEL: "ambient/model" };
const scope = createPortableOnboardEnvironmentScope(env, {
schemaVersion: 1,
baseUrl: "https://inference.example.test/v1",
model: "activation/model",
expiresAt: "2026-08-13T20:00:00.000Z",
});

expect(env.NEMOCLAW_PROVIDER).toBe("custom");
expect(env.NEMOCLAW_MODEL).toBe("activation/model");

scope.restore();
expect(env).toEqual({ NEMOCLAW_MODEL: "ambient/model" });
});

it("rejects malformed fresh model intent with the provider validator (#9200)", () => {
vi.stubEnv("NEMOCLAW_MODEL", "model;unsupported");
const scope = createPortableOnboardEnvironmentScope(process.env, null);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new Error(`process.exit(${code ?? 0})`);
}) as never);

try {
expect(() => getNonInteractiveModel("ollama")).toThrow("process.exit(1)");
expect(exitSpy).toHaveBeenCalledWith(1);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("Invalid NEMOCLAW_MODEL for provider 'ollama'"),
);
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
scope.restore();
vi.unstubAllEnvs();
}
});

it("restores default checkpoint classification over hostile ambient portable intent", () => {
const env: NodeJS.ProcessEnv = { NEMOCLAW_EXPERIMENTAL_PROFILE: "portable" };
const scope = createDefaultResumeProfileEnvironmentScope(env);
Expand Down Expand Up @@ -102,6 +177,7 @@ describe("portable onboarding environment scope", () => {
const env: NodeJS.ProcessEnv = {
NEMOCLAW_PROVIDER: "ollama",
NEMOCLAW_MODEL: "hostile-model",
NEMOCLAW_PROVIDER_MODEL: "hostile-fallback-model",
NEMOCLAW_ENDPOINT_URL: "https://attacker.test/v1",
NEMOCLAW_PREFERRED_API: "openai-completions",
NEMOCLAW_POLICY_MODE: "custom",
Expand All @@ -121,6 +197,7 @@ describe("portable onboarding environment scope", () => {
for (const key of [
"NEMOCLAW_PROVIDER",
"NEMOCLAW_MODEL",
"NEMOCLAW_PROVIDER_MODEL",
"NEMOCLAW_ENDPOINT_URL",
"NEMOCLAW_PREFERRED_API",
"NEMOCLAW_POLICY_TIER",
Expand Down
4 changes: 3 additions & 1 deletion src/lib/onboard/session-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const PORTABLE_DEFAULT_ENV_KEYS = [
TOOL_DISCLOSURE_ENV,
"NEMOCLAW_PROVIDER",
"NEMOCLAW_MODEL",
"NEMOCLAW_PROVIDER_MODEL",
"NEMOCLAW_ENDPOINT_URL",
"NEMOCLAW_PREFERRED_API",
"NEMOCLAW_OLLAMA_NO_AUTOSTART",
Expand Down Expand Up @@ -218,9 +219,10 @@ export function createPortableOnboardEnvironmentScope(
env.NEMOCLAW_PREFERRED_API = "openai-completions";
}
if (!options.resume) {
const requestedModel = previous.get("NEMOCLAW_MODEL")?.value?.trim();
env[TOOL_DISCLOSURE_ENV] = "direct";
env.NEMOCLAW_PROVIDER = activation ? "custom" : "ollama";
env.NEMOCLAW_MODEL = activation?.model ?? "qwen3-vl:4b";
env.NEMOCLAW_MODEL = activation?.model ?? (requestedModel || "qwen3-vl:4b");
env.NEMOCLAW_POLICY_MODE = "custom";
env.NEMOCLAW_POLICY_PRESETS =
previous.get("NEMOCLAW_POLICY_PRESETS")?.value ?? "personal-open-internet";
Expand Down
Loading