Skip to content
Open
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
5 changes: 5 additions & 0 deletions bin/lib/host-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Thin CJS shim — implementation lives in src/lib/host-support.ts
module.exports = require("../../dist/lib/host-support");
11 changes: 11 additions & 0 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const onboardSession = require("./onboard-session");
const policies = require("./policies");
const { ensureUsageNoticeConsent } = require("./usage-notice");
const { checkPortAvailable, ensureSwap, getMemoryInfo } = require("./preflight");
const { checkHostSupport } = require("./host-support");

// Typed modules (compiled from src/lib/*.ts → dist/lib/*.js)
const gatewayState = require("../../dist/lib/gateway-state");
Expand Down Expand Up @@ -1724,6 +1725,16 @@ function getNonInteractiveModel(providerKey) {
async function preflight() {
step(1, 7, "Preflight checks");

const hostSupport = checkHostSupport();
if (hostSupport.status === "ok") {
console.log(` ✓ ${hostSupport.message}`);
} else if (hostSupport.status === "warning") {
console.log(` ⚠ ${hostSupport.message}`);
} else {
console.error(` ${hostSupport.message}`);
process.exit(1);
}

// Docker
if (!isDockerRunning()) {
console.error(" Docker is not running. Please start Docker and try again.");
Expand Down
110 changes: 110 additions & 0 deletions src/lib/host-support.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
// Import through compiled dist output for consistent coverage attribution.
import {
checkHostSupport,
classifyLinuxHost,
parseOsRelease,
} from "../../dist/lib/host-support";

describe("parseOsRelease", () => {
it("parses quoted and unquoted values", () => {
const parsed = parseOsRelease([
'NAME="Ubuntu"',
"ID=ubuntu",
'VERSION_ID="24.04"',
].join("\n"));

expect(parsed).toEqual({ id: "ubuntu", versionId: "24.04" });
});

it("returns empty fields when ID/VERSION_ID are missing", () => {
const parsed = parseOsRelease("NAME=Ubuntu\nPRETTY_NAME=Ubuntu Linux");
expect(parsed).toEqual({ id: "", versionId: "" });
});
});

describe("classifyLinuxHost", () => {
it("marks Ubuntu 24.04 as supported", () => {
const result = classifyLinuxHost("ubuntu", "24.04");
expect(result.status).toBe("ok");
expect(result.code).toBe("SUPPORTED");
});

it("marks Ubuntu 22.04 as supported", () => {
const result = classifyLinuxHost("ubuntu", "22.04");
expect(result.status).toBe("ok");
expect(result.code).toBe("SUPPORTED");
});

it("marks Ubuntu 20.04 as near EOL warning", () => {
const result = classifyLinuxHost("ubuntu", "20.04");
expect(result.status).toBe("warning");
expect(result.code).toBe("NEAR_EOL");
});

it("marks Ubuntu 18.04 as EOL error", () => {
const result = classifyLinuxHost("ubuntu", "18.04");
expect(result.status).toBe("error");
expect(result.code).toBe("EOL");
});

it("marks unknown Linux distro as warning", () => {
const result = classifyLinuxHost("debian", "12");
expect(result.status).toBe("warning");
expect(result.code).toBe("UNSUPPORTED_OS");
});
});

describe("checkHostSupport", () => {
it("classifies unsupported platform as error", () => {
const result = checkHostSupport({ platform: "win32" });
expect(result.status).toBe("error");
expect(result.code).toBe("UNSUPPORTED_OS");
});

it("classifies linux from os-release data", () => {
const result = checkHostSupport({
platform: "linux",
readFileSyncImpl: () => 'ID=ubuntu\nVERSION_ID="24.04"\n',
});

expect(result.status).toBe("ok");
expect(result.code).toBe("SUPPORTED");
});

it("returns warning when /etc/os-release cannot be read", () => {
const result = checkHostSupport({
platform: "linux",
readFileSyncImpl: () => {
throw new Error("missing");
},
});

expect(result.status).toBe("warning");
expect(result.code).toBe("UNKNOWN_VERSION");
});

it("uses mocked macOS version detection", () => {
const result = checkHostSupport({
platform: "darwin",
getMacosVersionImpl: () => "14.5",
});

expect(result.status).toBe("warning");
expect(result.code).toBe("UNSUPPORTED_OS");
expect(result.message).toContain("macOS 14");
});

it("returns unknown-version warning when macOS version is unavailable", () => {
const result = checkHostSupport({
platform: "darwin",
getMacosVersionImpl: () => "",
});

expect(result.status).toBe("warning");
expect(result.code).toBe("UNKNOWN_VERSION");
});
});
222 changes: 222 additions & 0 deletions src/lib/host-support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";

// runner.js is CJS — use require so we don't pull it into the TS build.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { runCapture } = require("../../bin/lib/runner");

export type HostSupportStatus = "ok" | "warning" | "error";
export type HostSupportCode =
| "SUPPORTED"
| "NEAR_EOL"
| "EOL"
| "UNSUPPORTED_OS"
| "UNKNOWN_VERSION";

export type HostSupportResult = {
os: string;
version: string;
status: HostSupportStatus;
code: HostSupportCode;
message: string;
};

export interface OsReleaseInfo {
id: string;
versionId: string;
}

export interface CheckHostSupportOpts {
platform?: NodeJS.Platform;
osReleasePath?: string;
readFileSyncImpl?: (path: string, encoding: BufferEncoding) => string;
getMacosVersionImpl?: () => string;
}

function stripQuotes(value: string): string {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
return trimmed.slice(1, -1);
}
return trimmed;
}

export function parseOsRelease(content: string): OsReleaseInfo {
const lines = content.split("\n");
let id = "";
let versionId = "";

for (const line of lines) {
if (!line || line.startsWith("#")) continue;
const eq = line.indexOf("=");
if (eq <= 0) continue;

const key = line.slice(0, eq).trim();
const value = stripQuotes(line.slice(eq + 1));

if (key === "ID") id = value.toLowerCase();
if (key === "VERSION_ID") versionId = value;
}

return { id, versionId };
}

function parseUbuntuVersion(version: string): { major: number; minor: number } | null {
const match = String(version).trim().match(/^(\d+)\.(\d+)/);
if (!match) return null;

const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
if (isNaN(major) || isNaN(minor)) return null;

return { major, minor };
}

export function classifyLinuxHost(id: string, versionId: string): HostSupportResult {
if (!id) {
return {
os: "linux",
version: versionId || "unknown",
status: "warning",
code: "UNKNOWN_VERSION",
message: "Linux detected but distro/version could not be determined from /etc/os-release.",
};
}

if (id !== "ubuntu") {
return {
os: id,
version: versionId || "unknown",
status: "warning",
code: "UNSUPPORTED_OS",
message: `${id} ${versionId || "unknown"} detected: recognized Linux distro, but explicit host support policy is currently defined for Ubuntu only.`,
};
}

if (!versionId) {
return {
os: "ubuntu",
version: "unknown",
status: "warning",
code: "UNKNOWN_VERSION",
message: "Ubuntu detected but VERSION_ID is missing; support level could not be determined.",
};
}

const parsed = parseUbuntuVersion(versionId);
if (!parsed) {
return {
os: "ubuntu",
version: versionId,
status: "warning",
code: "UNKNOWN_VERSION",
message: `Ubuntu ${versionId} detected, but version format is unrecognized; support level could not be determined.`,
};
}

const { major, minor } = parsed;
const normalized = `${major}.${minor.toString().padStart(2, "0")}`;

if ((major === 24 && minor === 4) || (major === 22 && minor === 4)) {
return {
os: "ubuntu",
version: normalized,
status: "ok",
code: "SUPPORTED",
message: `Ubuntu ${normalized} detected: supported.`,
};
}

if (major === 20 && minor === 4) {
return {
os: "ubuntu",
version: normalized,
status: "warning",
code: "NEAR_EOL",
message: `Ubuntu ${normalized} detected: older host OS; upgrade is recommended.`,
};
}

if (major < 20 || (major === 18 && minor <= 4)) {
return {
os: "ubuntu",
version: normalized,
status: "error",
code: "EOL",
message: `Ubuntu ${normalized} detected: unsupported or end-of-life host OS.`,
};
}

return {
os: "ubuntu",
version: normalized,
status: "warning",
code: "UNSUPPORTED_OS",
message: `Ubuntu ${normalized} detected: recognized host OS, but this version is outside the current explicit support policy.`,
};
}

export function classifyMacosHost(version: string): HostSupportResult {
if (!version || version === "unknown") {
return {
os: "macos",
version: "unknown",
status: "warning",
code: "UNKNOWN_VERSION",
message: "macOS detected but product version could not be determined; explicit support policy is not yet fully defined.",
};
}

const major = version.split(".")[0] || version;
return {
os: "macos",
version,
status: "warning",
code: "UNSUPPORTED_OS",
message: `macOS ${major} detected: recognized host OS; explicit support policy not yet fully defined.`,
};
}

function defaultGetMacosVersion(): string {
const out = runCapture("sw_vers -productVersion", { ignoreError: true });
return String(out || "").trim();
}

export function checkHostSupport(opts: CheckHostSupportOpts = {}): HostSupportResult {
const platform = opts.platform || os.platform();
const osReleasePath = opts.osReleasePath || "/etc/os-release";
const readFileSyncImpl = opts.readFileSyncImpl || fs.readFileSync;
const getMacosVersionImpl = opts.getMacosVersionImpl || defaultGetMacosVersion;

if (platform === "linux") {
try {
const content = readFileSyncImpl(osReleasePath, "utf-8");
const { id, versionId } = parseOsRelease(content);
return classifyLinuxHost(id, versionId);
} catch {
return {
os: "linux",
version: "unknown",
status: "warning",
code: "UNKNOWN_VERSION",
message: "Linux detected but /etc/os-release is unavailable; support level could not be determined.",
};
}
}

if (platform === "darwin") {
const version = getMacosVersionImpl() || "unknown";
return classifyMacosHost(version);
}

return {
os: platform,
version: "unknown",
status: "error",
code: "UNSUPPORTED_OS",
message: `${platform} detected: unsupported host operating system for NemoClaw onboarding.`,
};
}
Loading