From cfa7cbb3ca0003fdbf755c644640d31ada8d2d07 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 1 Aug 2026 05:24:39 -0700 Subject: [PATCH 1/6] feat(runtime): qualify Podman inference GPUs Signed-off-by: Aaron Erickson --- src/lib/adapters/container-engine.ts | 1 + src/lib/adapters/podman/index.ts | 6 +- src/lib/onboard/runtime-provider/contract.ts | 1 + .../persisted-engine-authority.test.ts | 19 ++++++ .../persisted-engine-authority.ts | 1 + .../runtime-provider/podman-gpu.test.ts | 51 +++++++++++++++ .../onboard/runtime-provider/podman-gpu.ts | 63 +++++++++++++++++++ .../runtime-provider/podman-preflight.test.ts | 33 ++++++++++ .../runtime-provider/podman-preflight.ts | 30 +++++++++ src/lib/onboard/runtime-provider/registry.ts | 1 + test/runtime-provider-source-shape.test.ts | 1 + 11 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/lib/onboard/runtime-provider/podman-gpu.test.ts create mode 100644 src/lib/onboard/runtime-provider/podman-gpu.ts diff --git a/src/lib/adapters/container-engine.ts b/src/lib/adapters/container-engine.ts index 03419f53942..05d95ec4a93 100644 --- a/src/lib/adapters/container-engine.ts +++ b/src/lib/adapters/container-engine.ts @@ -8,6 +8,7 @@ import { buildSubprocessEnv } from "../subprocess-env"; export type ContainerEngineOperationScope = | "host-doctor" + | "host-local-inference" | "gateway-inspection" | "managed-bootstrap" | "sandbox-lifecycle" diff --git a/src/lib/adapters/podman/index.ts b/src/lib/adapters/podman/index.ts index eba8ee0e1ed..a6639b73be2 100644 --- a/src/lib/adapters/podman/index.ts +++ b/src/lib/adapters/podman/index.ts @@ -15,7 +15,11 @@ import { } from "./socket-authority"; export interface PodmanContainerEngineOptions { - readonly operation: "host-doctor" | "managed-bootstrap" | "sandbox-lifecycle"; + readonly operation: + | "host-doctor" + | "host-local-inference" + | "managed-bootstrap" + | "sandbox-lifecycle"; readonly socketAuthority: PodmanSocketAuthority; readonly executable?: string; readonly capture?: ContainerEngineCommandCapture; diff --git a/src/lib/onboard/runtime-provider/contract.ts b/src/lib/onboard/runtime-provider/contract.ts index 8938b6244ff..f63e429e739 100644 --- a/src/lib/onboard/runtime-provider/contract.ts +++ b/src/lib/onboard/runtime-provider/contract.ts @@ -29,6 +29,7 @@ export type RuntimeProviderMutationOperation = | "workload-cleanup"; export type RuntimeProviderContainerEngineOperation = | "host-doctor" + | "host-local-inference" | "gateway-inspection" | "sandbox-lifecycle" | "workload-cleanup"; diff --git a/src/lib/onboard/runtime-provider/persisted-engine-authority.test.ts b/src/lib/onboard/runtime-provider/persisted-engine-authority.test.ts index e32389b979d..ee79e6252c7 100644 --- a/src/lib/onboard/runtime-provider/persisted-engine-authority.test.ts +++ b/src/lib/onboard/runtime-provider/persisted-engine-authority.test.ts @@ -86,6 +86,25 @@ describe("persisted engine authority", () => { expect(fs.readFileSync(target, "utf8")).toBe(serializePersistedEngineAuthority(authority)); }); + it("persists a host-local inference engine independently of lifecycle authority", () => { + const store = createFilePersistedEngineAuthorityStore(temporaryRoot()); + const inference = createPersistedEngineAuthority( + "mxc", + engine("host-local-inference"), + BINDING_SHA256, + ); + const lifecycle = createPersistedEngineAuthority( + "mxc", + engine("sandbox-lifecycle"), + BINDING_SHA256, + ); + + expect(store.record(inference)).toEqual(inference); + expect(store.record(lifecycle)).toEqual(lifecycle); + expect(store.load("host-local-inference")).toEqual(inference); + expect(store.load("sandbox-lifecycle")).toEqual(lifecycle); + }); + it.each([ { label: "provider", diff --git a/src/lib/onboard/runtime-provider/persisted-engine-authority.ts b/src/lib/onboard/runtime-provider/persisted-engine-authority.ts index 5dbd9a97026..e4300d84d45 100644 --- a/src/lib/onboard/runtime-provider/persisted-engine-authority.ts +++ b/src/lib/onboard/runtime-provider/persisted-engine-authority.ts @@ -22,6 +22,7 @@ const AUTHORITY_ID = /^[a-z][a-z0-9-]{0,62}:[A-Za-z0-9._:-]{1,255}$/u; const SHA256 = /^[a-f0-9]{64}$/u; const OPERATIONS = new Set([ "host-doctor", + "host-local-inference", "gateway-inspection", "managed-bootstrap", "sandbox-lifecycle", diff --git a/src/lib/onboard/runtime-provider/podman-gpu.test.ts b/src/lib/onboard/runtime-provider/podman-gpu.test.ts new file mode 100644 index 00000000000..9c9b0fef51a --- /dev/null +++ b/src/lib/onboard/runtime-provider/podman-gpu.test.ts @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + normalizeNvidiaCdiDevice, + normalizePodmanCdiInventory, + qualifyPodmanGpuAttachments, +} from "./podman-gpu"; + +describe("Podman GPU attachment authority", () => { + it.each([ + ["all", "nvidia.com/gpu=all"], + ["0", "nvidia.com/gpu=0"], + ["1:0", "nvidia.com/gpu=1:0"], + ["GPU-deadbeef", "nvidia.com/gpu=GPU-deadbeef"], + ["nvidia.com/gpu=MIG-deadbeef", "nvidia.com/gpu=MIG-deadbeef"], + [ + "MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/1/0", + "nvidia.com/gpu=MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/1/0", + ], + ])("normalizes %s to canonical NVIDIA CDI identity", (requested, expected) => { + expect(normalizeNvidiaCdiDevice(requested)).toBe(expected); + }); + + it("qualifies only exact devices advertised by the injected endpoint", () => { + const attachments = qualifyPodmanGpuAttachments( + ["nvidia.com/gpu=all", "nvidia.com/gpu=0", "nvidia.com/gpu=GPU-deadbeef"], + ["0", "GPU-deadbeef"], + ); + + expect(attachments).toEqual([ + { kind: "cdi", device: "nvidia.com/gpu=0" }, + { kind: "cdi", device: "nvidia.com/gpu=GPU-deadbeef" }, + ]); + expect(Object.isFrozen(attachments)).toBe(true); + expect(Object.isFrozen(attachments[0])).toBe(true); + }); + + it("fails closed for missing, duplicate, raw, and malformed devices", () => { + expect(() => qualifyPodmanGpuAttachments([], ["all"])).toThrow("does not advertise"); + expect(() => + qualifyPodmanGpuAttachments(["nvidia.com/gpu=0"], ["0", "nvidia.com/gpu=0"]), + ).toThrow("duplicate NVIDIA CDI device"); + expect(() => normalizeNvidiaCdiDevice("/dev/nvidia0")).toThrow("safe NVIDIA CDI name"); + expect(() => normalizePodmanCdiInventory(["all", "nvidia.com/gpu=all"])).toThrow( + "duplicate NVIDIA device", + ); + }); +}); diff --git a/src/lib/onboard/runtime-provider/podman-gpu.ts b/src/lib/onboard/runtime-provider/podman-gpu.ts new file mode 100644 index 00000000000..960c2594776 --- /dev/null +++ b/src/lib/onboard/runtime-provider/podman-gpu.ts @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const NVIDIA_CDI_PREFIX = "nvidia.com/gpu="; +const CDI_DEVICE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/u; +const LEGACY_MIG_DEVICE_NAME = /^MIG-GPU-[A-Za-z0-9-]+\/[0-9]+\/[0-9]+$/u; + +export interface PodmanGpuAttachment { + readonly kind: "cdi"; + readonly device: string; +} + +/** + * Normalize one NVIDIA CDI device identity without consulting host state. + * Availability is proved separately against the exact Podman endpoint's + * qualified inventory. + */ +export function normalizeNvidiaCdiDevice(requestedDevice: string): string { + const requested = requestedDevice.trim(); + const device = requested.startsWith(NVIDIA_CDI_PREFIX) + ? requested + : `${NVIDIA_CDI_PREFIX}${requested}`; + const name = device.slice(NVIDIA_CDI_PREFIX.length); + if (!CDI_DEVICE_NAME.test(name) && !LEGACY_MIG_DEVICE_NAME.test(name)) { + throw new Error( + "Podman GPU device must be a safe NVIDIA CDI name such as 'all', '0', '1:0', 'GPU-...', or 'MIG-...'.", + ); + } + return device; +} + +export function normalizePodmanCdiInventory(devices: readonly string[]): readonly string[] { + if (!Array.isArray(devices) || devices.length > 256) { + throw new Error("Podman CDI inventory is invalid or exceeds its device limit."); + } + const normalized = devices.map(normalizeNvidiaCdiDevice).sort(); + if (new Set(normalized).size !== normalized.length) { + throw new Error("Podman CDI inventory contains a duplicate NVIDIA device."); + } + return Object.freeze(normalized); +} + +export function qualifyPodmanGpuAttachments( + availableDevices: readonly string[], + requestedDevices: readonly string[] = ["all"], +): readonly PodmanGpuAttachment[] { + const available = new Set(normalizePodmanCdiInventory(availableDevices)); + if (!Array.isArray(requestedDevices) || requestedDevices.length === 0) { + throw new Error("Podman GPU attachment requires at least one NVIDIA CDI device."); + } + const requested = requestedDevices.map(normalizeNvidiaCdiDevice); + if (new Set(requested).size !== requested.length) { + throw new Error("Podman GPU attachment contains a duplicate NVIDIA CDI device."); + } + for (const device of requested) { + if (!available.has(device)) { + throw new Error( + `Rootless Podman does not advertise the requested CDI device '${device}'. Refresh the NVIDIA CDI specification and retry.`, + ); + } + } + return Object.freeze(requested.map((device) => Object.freeze({ kind: "cdi" as const, device }))); +} diff --git a/src/lib/onboard/runtime-provider/podman-preflight.test.ts b/src/lib/onboard/runtime-provider/podman-preflight.test.ts index 25390bec419..9e4e31e721c 100644 --- a/src/lib/onboard/runtime-provider/podman-preflight.test.ts +++ b/src/lib/onboard/runtime-provider/podman-preflight.test.ts @@ -18,6 +18,7 @@ const INFO = JSON.stringify({ cgroupVersion: "v2", networkBackend: "netavark", security: { rootless: true }, + cdi: { devices: ["nvidia.com/gpu=all", "nvidia.com/gpu=0"] }, }, }); @@ -80,6 +81,7 @@ describe("Podman host preflight", () => { expect(qualifyPodmanHost(runtime, { platform: "linux", architecture: "x64" })).toEqual({ providerId: "podman", + authorityId: "test:podman-socket", clientVersion: "5.6.2", serverVersion: "5.6.2", rootless: true, @@ -87,6 +89,7 @@ describe("Podman host preflight", () => { os: "linux", architecture: "amd64", networkBackend: "netavark", + cdiDevices: ["nvidia.com/gpu=0", "nvidia.com/gpu=all"], }); expect(runtime.capture).toHaveBeenCalledWith(["info", "--format", "json"], 15_000); expect(runtime.capture).toHaveBeenCalledWith(["version", "--format", "json"], 10_000); @@ -111,6 +114,36 @@ describe("Podman host preflight", () => { ).toMatchObject({ architecture: "arm64" }); }); + it("combines endpoint-reported and separately qualified CDI inventories", () => { + expect( + qualifyPodmanHost(engine(), { + platform: "linux", + architecture: "x64", + additionalCdiDevices: ["nvidia.com/gpu=GPU-deadbeef"], + }), + ).toMatchObject({ + authorityId: "test:podman-socket", + cdiDevices: ["nvidia.com/gpu=0", "nvidia.com/gpu=GPU-deadbeef", "nvidia.com/gpu=all"], + }); + }); + + it("rejects malformed or duplicate qualified CDI inventory", () => { + expect(() => + qualifyPodmanHost(engine(), { + platform: "linux", + architecture: "x64", + additionalCdiDevices: ["/dev/nvidia0"], + }), + ).toThrow("safe NVIDIA CDI name"); + expect(() => + qualifyPodmanHost(engine(), { + platform: "linux", + architecture: "x64", + additionalCdiDevices: ["all"], + }), + ).toThrow("duplicate NVIDIA device"); + }); + it("rejects an API service architecture that differs from the host", () => { expect(() => qualifyPodmanHost(engine(), { platform: "linux", architecture: "arm64" })).toThrow( "does not match host 'arm64'", diff --git a/src/lib/onboard/runtime-provider/podman-preflight.ts b/src/lib/onboard/runtime-provider/podman-preflight.ts index 34da16f4f50..0307718f060 100644 --- a/src/lib/onboard/runtime-provider/podman-preflight.ts +++ b/src/lib/onboard/runtime-provider/podman-preflight.ts @@ -6,11 +6,14 @@ import type { ContainerEngineCommandResult, } from "../../adapters/container-engine"; import type { RuntimeProviderDoctorCheck } from "./contract"; +import { normalizePodmanCdiInventory } from "./podman-gpu"; export const MINIMUM_PODMAN_VERSION = "5.0.0"; export interface PodmanHostPreflightReceipt { readonly providerId: "podman"; + /** Exact endpoint identity of the operation-scoped engine that produced this receipt. */ + readonly authorityId: string; readonly clientVersion: string; readonly serverVersion: string; readonly rootless: true; @@ -18,11 +21,14 @@ export interface PodmanHostPreflightReceipt { readonly os: "linux"; readonly architecture: "amd64" | "arm64"; readonly networkBackend: string; + readonly cdiDevices: readonly string[]; } export interface PodmanHostPreflightOptions { readonly platform?: NodeJS.Platform; readonly architecture?: NodeJS.Architecture; + /** Additional devices from a separately qualified NVIDIA CDI inventory adapter. */ + readonly additionalCdiDevices?: readonly string[]; } export class PodmanHostPreflightError extends Error { @@ -111,6 +117,23 @@ function normalizeArchitecture(value: string): "amd64" | "arm64" | null { return null; } +function collectNvidiaCdiDevices(value: unknown, devices: Set): void { + if (typeof value === "string") { + if (value.startsWith("nvidia.com/gpu=")) devices.add(value); + return; + } + if (Array.isArray(value)) { + for (const entry of value) collectNvidiaCdiDevices(entry, devices); + return; + } + const source = record(value); + if (!source) return; + for (const [key, entry] of Object.entries(source)) { + if (key.startsWith("nvidia.com/gpu=")) devices.add(key); + collectNvidiaCdiDevices(entry, devices); + } +} + function hasSubordinateIdMapping(output: string): boolean { return output .trim() @@ -219,8 +242,14 @@ export function qualifyPodmanHost( } requireSubordinateIdMappings(engine); + const cdiDevices = new Set(); + collectNvidiaCdiDevices(host, cdiDevices); + for (const device of options.additionalCdiDevices ?? []) cdiDevices.add(device); + const qualifiedCdiDevices = normalizePodmanCdiInventory([...cdiDevices]); + return Object.freeze({ providerId: "podman", + authorityId: engine.authorityId, clientVersion, serverVersion, rootless: true, @@ -228,6 +257,7 @@ export function qualifyPodmanHost( os: "linux", architecture: normalizedArchitecture, networkBackend: textField(host, "networkBackend", "NetworkBackend") || "unknown", + cdiDevices: qualifiedCdiDevices, }); } diff --git a/src/lib/onboard/runtime-provider/registry.ts b/src/lib/onboard/runtime-provider/registry.ts index 1d0a31ae6aa..a207877cdb5 100644 --- a/src/lib/onboard/runtime-provider/registry.ts +++ b/src/lib/onboard/runtime-provider/registry.ts @@ -66,6 +66,7 @@ const MUTATION_OPERATIONS = new Set([ ]); const CONTAINER_ENGINE_OPERATIONS = new Set([ "host-doctor", + "host-local-inference", "gateway-inspection", "sandbox-lifecycle", "workload-cleanup", diff --git a/test/runtime-provider-source-shape.test.ts b/test/runtime-provider-source-shape.test.ts index 224dfcc66d5..c81261e9fa1 100644 --- a/test/runtime-provider-source-shape.test.ts +++ b/test/runtime-provider-source-shape.test.ts @@ -167,6 +167,7 @@ describe("runtime provider central source boundary", () => { "src/lib/onboard/runtime-provider/docker.ts", "src/lib/onboard/runtime-provider/persisted-engine-authority.ts", "src/lib/onboard/runtime-provider/persisted-engine-lifecycle.ts", + "src/lib/onboard/runtime-provider/podman-gpu.ts", "src/lib/onboard/runtime-provider/podman-lifecycle.ts", "src/lib/onboard/runtime-provider/podman-preflight.ts", "src/lib/onboard/runtime-provider/podman.ts", From d11f7081eb50140cbca6f675497cd967b93bfefe Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 1 Aug 2026 05:48:18 -0700 Subject: [PATCH 2/6] feat(runtime): translate Podman inference commands Signed-off-by: Aaron Erickson --- .../podman-inference-args.test.ts | 74 ++++++++++ .../runtime-provider/podman-inference-args.ts | 128 ++++++++++++++++++ test/runtime-provider-source-shape.test.ts | 1 + 3 files changed, 203 insertions(+) create mode 100644 src/lib/onboard/runtime-provider/podman-inference-args.test.ts create mode 100644 src/lib/onboard/runtime-provider/podman-inference-args.ts diff --git a/src/lib/onboard/runtime-provider/podman-inference-args.test.ts b/src/lib/onboard/runtime-provider/podman-inference-args.test.ts new file mode 100644 index 00000000000..100e7cceb91 --- /dev/null +++ b/src/lib/onboard/runtime-provider/podman-inference-args.test.ts @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { translatePodmanLocalInferenceArgs } from "./podman-inference-args"; + +const CDI_DEVICES = [ + "nvidia.com/gpu=all", + "nvidia.com/gpu=0", + "nvidia.com/gpu=1:0", + "nvidia.com/gpu=2", + "nvidia.com/gpu=GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "nvidia.com/gpu=MIG-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "nvidia.com/gpu=MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/1/0", +] as const; + +describe("Podman local inference command translation", () => { + it.each([ + ["all", ["nvidia.com/gpu=all"]], + ["device=0", ["nvidia.com/gpu=0"]], + ["device=1:0", ["nvidia.com/gpu=1:0"]], + [ + "device=GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + ["nvidia.com/gpu=GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"], + ], + [ + "device=MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/1/0", + ["nvidia.com/gpu=MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/1/0"], + ], + ['"device=0,2"', ["nvidia.com/gpu=0", "nvidia.com/gpu=2"]], + ])("preserves Docker selector %s as exact CDI devices", (selector, devices) => { + const translated = translatePodmanLocalInferenceArgs( + ["run", "--gpus", selector, "image"], + CDI_DEVICES, + ); + expect(translated.filter((value) => value.startsWith("nvidia.com/gpu="))).toEqual(devices); + expect(translated.filter((value) => value === "--device")).toHaveLength(devices.length); + expect(translated).not.toContain("--gpus"); + }); + + it("translates the NIM and vLLM subset without Docker name-filter leakage", () => { + expect( + translatePodmanLocalInferenceArgs( + ["run", "--gpus=all", "--filter", "name=^/nemoclaw-vllm$"], + CDI_DEVICES, + ), + ).toEqual(["run", "--device", "nvidia.com/gpu=all", "--filter", "name=^nemoclaw-vllm$"]); + expect( + translatePodmanLocalInferenceArgs(["run", "--device=nvidia.com/gpu=0", "image"], CDI_DEVICES), + ).toEqual(["run", "--device", "nvidia.com/gpu=0", "image"]); + }); + + it("fails closed instead of dropping unsupported Docker GPU modes", () => { + expect(() => + translatePodmanLocalInferenceArgs( + ["run", "--gpus", "capabilities=compute", "image"], + CDI_DEVICES, + ), + ).toThrow("cannot translate Docker GPU selector"); + expect(() => + translatePodmanLocalInferenceArgs(["run", "--gpus", "device=0,0"], CDI_DEVICES), + ).toThrow("duplicate NVIDIA CDI device"); + expect(() => + translatePodmanLocalInferenceArgs(["run", "--runtime", "nvidia"], CDI_DEVICES), + ).toThrow("refuses Docker's NVIDIA runtime mode"); + expect(() => + translatePodmanLocalInferenceArgs(["run", "--device", "/dev/nvidia0"], CDI_DEVICES), + ).toThrow("refuses raw NVIDIA device paths"); + expect(() => + translatePodmanLocalInferenceArgs(["run", "--gpus", "device=9"], CDI_DEVICES), + ).toThrow("does not advertise"); + }); +}); diff --git a/src/lib/onboard/runtime-provider/podman-inference-args.ts b/src/lib/onboard/runtime-provider/podman-inference-args.ts new file mode 100644 index 00000000000..478a8a7e6a8 --- /dev/null +++ b/src/lib/onboard/runtime-provider/podman-inference-args.ts @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { qualifyPodmanGpuAttachments } from "./podman-gpu"; + +const MAX_ARGUMENTS = 512; +const MAX_ARGUMENT_BYTES = 16 * 1024; + +function exactArgument(value: unknown, index: number): string { + if ( + typeof value !== "string" || + value.includes("\0") || + Buffer.byteLength(value, "utf8") > MAX_ARGUMENT_BYTES + ) { + throw new Error(`Podman local inference argument ${String(index)} is invalid.`); + } + return value; +} + +function stripExactDoubleQuotes(raw: string): string { + const trimmed = raw.trim(); + const startsQuoted = trimmed.startsWith('"'); + const endsQuoted = trimmed.endsWith('"'); + if (startsQuoted !== endsQuoted || (startsQuoted && trimmed.length < 2)) { + throw new Error(`Podman local inference cannot translate Docker GPU selector '${raw}' to CDI.`); + } + return startsQuoted ? trimmed.slice(1, -1) : trimmed; +} + +function translatedGpuDevices(selector: string, availableCdiDevices: readonly string[]): string[] { + const normalized = stripExactDoubleQuotes(selector); + const requested = + normalized === "all" + ? ["all"] + : normalized.startsWith("device=") + ? normalized.slice("device=".length).split(",") + : []; + if (requested.length === 0 || requested.some((device) => device.trim() === "")) { + throw new Error( + `Podman local inference cannot translate Docker GPU selector '${selector}' to CDI.`, + ); + } + return qualifyPodmanGpuAttachments( + availableCdiDevices, + requested.map((device) => device.trim()), + ).map((attachment) => attachment.device); +} + +function appendGpuDevices(target: string[], devices: readonly string[]): void { + for (const device of devices) target.push("--device", device); +} + +/** + * Translate the bounded Docker-compatible argument subset emitted by the + * existing NIM and vLLM launchers. NVIDIA GPU selection becomes exact CDI + * attachment; Docker-only runtime and raw-device modes fail closed. + */ +export function translatePodmanLocalInferenceArgs( + args: readonly string[], + availableCdiDevices: readonly string[], +): readonly string[] { + if (!Array.isArray(args) || args.length > MAX_ARGUMENTS) { + throw new Error("Podman local inference has too many command arguments."); + } + const source = args.map(exactArgument); + const translated: string[] = []; + for (let index = 0; index < source.length; index += 1) { + const value = source[index] ?? ""; + if (value === "--gpus") { + const selector = source[index + 1]; + if (selector === undefined) { + throw new Error("Podman local inference requires a GPU selector value."); + } + appendGpuDevices(translated, translatedGpuDevices(selector, availableCdiDevices)); + index += 1; + continue; + } + if (value.startsWith("--gpus=")) { + appendGpuDevices( + translated, + translatedGpuDevices(value.slice("--gpus=".length), availableCdiDevices), + ); + continue; + } + if ( + (value === "--runtime" && source[index + 1]?.toLowerCase() === "nvidia") || + value.toLowerCase() === "--runtime=nvidia" + ) { + throw new Error( + "Podman local inference refuses Docker's NVIDIA runtime mode; an exact CDI device is required.", + ); + } + if (value === "--device") { + const device = source[index + 1]; + if (device === undefined) { + throw new Error("Podman local inference requires a --device value."); + } + if (/^\/dev\/nvidia/u.test(device)) { + throw new Error( + "Podman local inference refuses raw NVIDIA device paths; an exact CDI device is required.", + ); + } + if (device.startsWith("nvidia.com/gpu=")) { + appendGpuDevices(translated, translatedGpuDevices(`device=${device}`, availableCdiDevices)); + index += 1; + continue; + } + } + if (value.startsWith("--device=/dev/nvidia")) { + throw new Error( + "Podman local inference refuses raw NVIDIA device paths; an exact CDI device is required.", + ); + } + if (value.startsWith("--device=nvidia.com/gpu=")) { + appendGpuDevices( + translated, + translatedGpuDevices(`device=${value.slice("--device=".length)}`, availableCdiDevices), + ); + continue; + } + if (value.startsWith("name=^/") && value.endsWith("$")) { + translated.push(`name=^${value.slice("name=^/".length)}`); + continue; + } + translated.push(value); + } + return Object.freeze(translated); +} diff --git a/test/runtime-provider-source-shape.test.ts b/test/runtime-provider-source-shape.test.ts index c81261e9fa1..4b86d0966f3 100644 --- a/test/runtime-provider-source-shape.test.ts +++ b/test/runtime-provider-source-shape.test.ts @@ -168,6 +168,7 @@ describe("runtime provider central source boundary", () => { "src/lib/onboard/runtime-provider/persisted-engine-authority.ts", "src/lib/onboard/runtime-provider/persisted-engine-lifecycle.ts", "src/lib/onboard/runtime-provider/podman-gpu.ts", + "src/lib/onboard/runtime-provider/podman-inference-args.ts", "src/lib/onboard/runtime-provider/podman-lifecycle.ts", "src/lib/onboard/runtime-provider/podman-preflight.ts", "src/lib/onboard/runtime-provider/podman.ts", From 9d8fee158d1b2000d12fef1a05bb739ad0f40643 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 1 Aug 2026 05:57:29 -0700 Subject: [PATCH 3/6] feat(runtime): define host-local inference receipts Signed-off-by: Aaron Erickson --- .../host-local-inference.test.ts | 115 +++++++++ .../runtime-provider/host-local-inference.ts | 239 ++++++++++++++++++ test/runtime-provider-source-shape.test.ts | 1 + 3 files changed, 355 insertions(+) create mode 100644 src/lib/onboard/runtime-provider/host-local-inference.test.ts create mode 100644 src/lib/onboard/runtime-provider/host-local-inference.ts diff --git a/src/lib/onboard/runtime-provider/host-local-inference.test.ts b/src/lib/onboard/runtime-provider/host-local-inference.test.ts new file mode 100644 index 00000000000..b1f83380752 --- /dev/null +++ b/src/lib/onboard/runtime-provider/host-local-inference.test.ts @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + normalizeHostLocalInferenceReceipt, + parseHostLocalInferenceReceipt, + serializeHostLocalInferenceReceipt, + type HostLocalInferenceReceipt, +} from "./host-local-inference"; + +const ENGINE_AUTHORITY = { + schemaVersion: 1, + providerId: "mxc", + operation: "host-local-inference", + engineId: "mxc", + authorityId: `mxc-endpoint:${"a".repeat(64)}`, + bindingSha256: "b".repeat(64), +} as const; + +function receipt(service: "ollama" | "nim" | "vllm" = "vllm"): HostLocalInferenceReceipt { + return { + schemaVersion: 1, + providerId: "mxc", + service, + engineAuthority: ENGINE_AUTHORITY, + endpoint: { + host: "host.openshell.internal", + port: service === "ollama" ? 11435 : 8000, + networkName: "openshell", + }, + runtime: + service === "ollama" + ? { kind: "host" } + : { + kind: "container", + runtimeId: "mxc-runtime:alpha", + name: `nemoclaw-${service}-alpha`, + imageRef: `nvcr.io/nvidia/${service}@sha256:${"c".repeat(64)}`, + gpu: { vendor: "nvidia", devices: ["nvidia.com/gpu=all"] }, + }, + }; +} + +describe("host-local inference receipt contract", () => { + it.each([ + "ollama", + "nim", + "vllm", + ] as const)("round-trips %s authority without Podman-specific state", (service) => { + const expected = normalizeHostLocalInferenceReceipt(receipt(service)); + const serialized = serializeHostLocalInferenceReceipt(expected); + + expect(parseHostLocalInferenceReceipt(serialized)).toEqual(expected); + expect(expected.providerId).toBe("mxc"); + expect(Object.isFrozen(expected)).toBe(true); + expect(Object.isFrozen(expected.endpoint)).toBe(true); + expect(Object.isFrozen(expected.runtime)).toBe(true); + }); + + it("rejects provider, operation, endpoint, image, and device authority drift", () => { + const base = receipt(); + expect(() => normalizeHostLocalInferenceReceipt({ ...base, providerId: "other" })).toThrow( + "does not match engine authority", + ); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...base, + engineAuthority: { ...ENGINE_AUTHORITY, operation: "sandbox-lifecycle" }, + }), + ).toThrow("wrong operation scope"); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...base, + endpoint: { ...base.endpoint, port: 0 }, + }), + ).toThrow("endpoint port is malformed"); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...base, + runtime: { ...base.runtime, imageRef: "nvcr.io/nvidia/vllm:latest" }, + }), + ).toThrow("runtime image reference is malformed"); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...base, + runtime: { + ...base.runtime, + gpu: { vendor: "nvidia", devices: ["/dev/nvidia0"] }, + }, + }), + ).toThrow("GPU device is malformed"); + }); + + it("rejects a host runtime for managed services and container runtime for Ollama", () => { + expect(() => + normalizeHostLocalInferenceReceipt({ ...receipt("nim"), runtime: { kind: "host" } }), + ).toThrow("only Ollama"); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...receipt("ollama"), + runtime: receipt("vllm").runtime, + }), + ).toThrow("Ollama must use host-process authority"); + }); + + it("rejects extensions and noncanonical serialized receipts", () => { + const base = receipt(); + expect(() => normalizeHostLocalInferenceReceipt({ ...base, extra: true })).toThrow( + "receipt schema is unsupported", + ); + expect(() => parseHostLocalInferenceReceipt(JSON.stringify(base))).toThrow("not canonical"); + }); +}); diff --git a/src/lib/onboard/runtime-provider/host-local-inference.ts b/src/lib/onboard/runtime-provider/host-local-inference.ts new file mode 100644 index 00000000000..21e7be10b52 --- /dev/null +++ b/src/lib/onboard/runtime-provider/host-local-inference.ts @@ -0,0 +1,239 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + normalizePersistedEngineAuthority, + type PersistedEngineAuthority, +} from "./persisted-engine-authority"; + +export const HOST_LOCAL_INFERENCE_RECEIPT_SCHEMA_VERSION = 1 as const; + +export type HostLocalInferenceService = "ollama" | "nim" | "vllm"; + +export interface HostLocalInferenceEndpointInput { + readonly networkName: string; + readonly hostPort: number; + readonly probeImageRef: string; +} + +export interface HostLocalInferenceMount { + readonly source: string; + readonly target: string; + readonly readOnly?: boolean; +} + +export interface HostLocalManagedInferenceInput extends HostLocalInferenceEndpointInput { + readonly service: "nim" | "vllm"; + readonly containerName: string; + readonly containerPort: number; + readonly imageRef: string; + readonly gpuDevices: readonly string[]; + /** Environment variable names forwarded from the current process; values are never persisted. */ + readonly environment?: readonly string[]; + readonly mounts?: readonly HostLocalInferenceMount[]; + readonly sharedMemory?: string; + readonly ipc?: "host" | "private"; + readonly command?: readonly string[]; +} + +export interface HostLocalInferenceEndpointAuthority { + readonly host: string; + readonly port: number; + readonly networkName: string; +} + +export type HostLocalInferenceRuntimeAuthority = + | { + readonly kind: "host"; + } + | { + readonly kind: "container"; + readonly runtimeId: string; + readonly name: string; + readonly imageRef: string; + readonly gpu: { + readonly vendor: "nvidia"; + readonly devices: readonly string[]; + }; + }; + +/** + * Secret-free durable proof for one host-local inference route. The injected + * provider owns command reconstruction; central consumers retain only this + * normalized endpoint and runtime authority. + */ +export interface HostLocalInferenceReceipt { + readonly schemaVersion: typeof HOST_LOCAL_INFERENCE_RECEIPT_SCHEMA_VERSION; + readonly providerId: string; + readonly service: HostLocalInferenceService; + readonly engineAuthority: PersistedEngineAuthority; + readonly endpoint: HostLocalInferenceEndpointAuthority; + readonly runtime: HostLocalInferenceRuntimeAuthority; +} + +export interface HostLocalManagedInferenceInspection { + readonly running: boolean; + readonly receipt: HostLocalInferenceReceipt; +} + +export interface HostLocalInferenceRuntime { + readonly providerId: string; + readonly services: readonly HostLocalInferenceService[]; + translateContainerArgs(args: readonly string[]): readonly string[]; + qualifyOllama(input: HostLocalInferenceEndpointInput): HostLocalInferenceReceipt; + startManaged(input: HostLocalManagedInferenceInput): HostLocalInferenceReceipt; + inspectManaged(receipt: HostLocalInferenceReceipt): HostLocalManagedInferenceInspection; + stopManaged(receipt: HostLocalInferenceReceipt): HostLocalManagedInferenceInspection; + /** Re-prove the same out-of-sandbox service before carrying it into a rebuild. */ + preserveForRebuild(receipt: HostLocalInferenceReceipt): HostLocalInferenceReceipt; +} + +const PROVIDER_ID = /^[a-z][a-z0-9-]{0,62}$/u; +const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; +const SAFE_HOST = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/u; +const RUNTIME_ID = /^[A-Za-z0-9][A-Za-z0-9._:/=+-]{0,511}$/u; +const OCI_DIGEST_REFERENCE = + /^(?:[A-Za-z0-9._-]+(?::[0-9]+)?\/)*(?:[A-Za-z0-9._-]+)@sha256:[a-f0-9]{64}$/u; +const CDI_DEVICE = /^nvidia\.com\/gpu=[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$/u; +const SERVICES = new Set(["ollama", "nim", "vllm"]); +const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u; +const MAX_SERIALIZED_BYTES = 32 * 1024; + +function fail(message: string): never { + throw new Error(`Host-local inference receipt is invalid: ${message}`); +} + +function exactRecord(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + fail(`${label} must be an object`); + } + return value as Record; +} + +function exactKeys(value: Record, keys: readonly string[], label: string): void { + if (Object.keys(value).sort().join(",") !== [...keys].sort().join(",")) { + fail(`${label} schema is unsupported`); + } +} + +function exactText(value: unknown, pattern: RegExp, label: string): string { + if ( + typeof value !== "string" || + value !== value.trim() || + CONTROL_CHARACTERS.test(value) || + !pattern.test(value) + ) { + fail(`${label} is malformed`); + } + return value; +} + +function exactPort(value: unknown, label: string): number { + if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > 65_535) { + fail(`${label} is malformed`); + } + return Number(value); +} + +function normalizeEndpoint(value: unknown): HostLocalInferenceEndpointAuthority { + const endpoint = exactRecord(value, "endpoint authority"); + exactKeys(endpoint, ["host", "networkName", "port"], "endpoint authority"); + return Object.freeze({ + host: exactText(endpoint.host, SAFE_HOST, "endpoint host"), + port: exactPort(endpoint.port, "endpoint port"), + networkName: exactText(endpoint.networkName, SAFE_NAME, "endpoint network"), + }); +} + +function normalizeRuntime( + service: HostLocalInferenceService, + value: unknown, +): HostLocalInferenceRuntimeAuthority { + const runtime = exactRecord(value, "runtime authority"); + if (runtime.kind === "host") { + exactKeys(runtime, ["kind"], "host runtime authority"); + if (service !== "ollama") fail("only Ollama may use host-process authority"); + return Object.freeze({ kind: "host" as const }); + } + if (runtime.kind !== "container") fail("runtime kind is unsupported"); + exactKeys(runtime, ["gpu", "imageRef", "kind", "name", "runtimeId"], "container authority"); + if (service === "ollama") fail("Ollama must use host-process authority"); + const gpu = exactRecord(runtime.gpu, "GPU authority"); + exactKeys(gpu, ["devices", "vendor"], "GPU authority"); + if (gpu.vendor !== "nvidia" || !Array.isArray(gpu.devices) || gpu.devices.length === 0) { + fail("GPU authority must identify NVIDIA devices"); + } + const devices = gpu.devices.map((device) => exactText(device, CDI_DEVICE, "GPU device")); + if (new Set(devices).size !== devices.length) fail("GPU devices must be unique"); + return Object.freeze({ + kind: "container" as const, + runtimeId: exactText(runtime.runtimeId, RUNTIME_ID, "runtime identity"), + name: exactText(runtime.name, SAFE_NAME, "runtime name"), + imageRef: exactText(runtime.imageRef, OCI_DIGEST_REFERENCE, "runtime image reference"), + gpu: Object.freeze({ vendor: "nvidia" as const, devices: Object.freeze(devices) }), + }); +} + +export function normalizeHostLocalInferenceReceipt(value: unknown): HostLocalInferenceReceipt { + const receipt = exactRecord(value, "receipt"); + exactKeys( + receipt, + ["endpoint", "engineAuthority", "providerId", "runtime", "schemaVersion", "service"], + "receipt", + ); + if (receipt.schemaVersion !== HOST_LOCAL_INFERENCE_RECEIPT_SCHEMA_VERSION) { + fail("schema version is unsupported"); + } + if ( + typeof receipt.service !== "string" || + !SERVICES.has(receipt.service as HostLocalInferenceService) + ) { + fail("service is unsupported"); + } + const service = receipt.service as HostLocalInferenceService; + const engineAuthority = normalizePersistedEngineAuthority(receipt.engineAuthority); + if (engineAuthority.operation !== "host-local-inference") { + fail("engine authority has the wrong operation scope"); + } + const providerId = exactText(receipt.providerId, PROVIDER_ID, "provider identity"); + if (engineAuthority.providerId !== providerId) { + fail("provider identity does not match engine authority"); + } + return Object.freeze({ + schemaVersion: HOST_LOCAL_INFERENCE_RECEIPT_SCHEMA_VERSION, + providerId, + service, + engineAuthority, + endpoint: normalizeEndpoint(receipt.endpoint), + runtime: normalizeRuntime(service, receipt.runtime), + }); +} + +export function serializeHostLocalInferenceReceipt(receipt: HostLocalInferenceReceipt): string { + const serialized = `${JSON.stringify(normalizeHostLocalInferenceReceipt(receipt))}\n`; + if (Buffer.byteLength(serialized, "utf8") > MAX_SERIALIZED_BYTES) { + fail("serialized receipt exceeds its bounded transport"); + } + return serialized; +} + +export function parseHostLocalInferenceReceipt(serialized: string): HostLocalInferenceReceipt { + if ( + serialized.length === 0 || + serialized.includes("\0") || + Buffer.byteLength(serialized, "utf8") > MAX_SERIALIZED_BYTES + ) { + fail("serialized receipt is empty or too large"); + } + let parsed: unknown; + try { + parsed = JSON.parse(serialized); + } catch { + fail("serialized receipt is not valid JSON"); + } + const receipt = normalizeHostLocalInferenceReceipt(parsed); + if (serializeHostLocalInferenceReceipt(receipt) !== serialized) { + fail("serialized receipt is not canonical"); + } + return receipt; +} diff --git a/test/runtime-provider-source-shape.test.ts b/test/runtime-provider-source-shape.test.ts index 4b86d0966f3..a4910b0b0b0 100644 --- a/test/runtime-provider-source-shape.test.ts +++ b/test/runtime-provider-source-shape.test.ts @@ -165,6 +165,7 @@ describe("runtime provider central source boundary", () => { "src/lib/onboard/runtime-provider/contract.ts", "src/lib/onboard/runtime-provider/current.ts", "src/lib/onboard/runtime-provider/docker.ts", + "src/lib/onboard/runtime-provider/host-local-inference.ts", "src/lib/onboard/runtime-provider/persisted-engine-authority.ts", "src/lib/onboard/runtime-provider/persisted-engine-lifecycle.ts", "src/lib/onboard/runtime-provider/podman-gpu.ts", From 6c23ebd40b87086a069962e5b35b861c65c86cfa Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 1 Aug 2026 06:17:23 -0700 Subject: [PATCH 4/6] feat(runtime): bind host-local inference specifications Signed-off-by: Aaron Erickson --- .../host-local-inference.test.ts | 32 +++++++++++++++++-- .../runtime-provider/host-local-inference.ts | 25 ++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/lib/onboard/runtime-provider/host-local-inference.test.ts b/src/lib/onboard/runtime-provider/host-local-inference.test.ts index b1f83380752..36857e76042 100644 --- a/src/lib/onboard/runtime-provider/host-local-inference.test.ts +++ b/src/lib/onboard/runtime-provider/host-local-inference.test.ts @@ -32,12 +32,16 @@ function receipt(service: "ollama" | "nim" | "vllm" = "vllm"): HostLocalInferenc }, runtime: service === "ollama" - ? { kind: "host" } + ? { + kind: "host", + probeImageRef: `quay.io/curl/curl@sha256:${"d".repeat(64)}`, + } : { kind: "container", runtimeId: "mxc-runtime:alpha", name: `nemoclaw-${service}-alpha`, imageRef: `nvcr.io/nvidia/${service}@sha256:${"c".repeat(64)}`, + specSha256: "d".repeat(64), gpu: { vendor: "nvidia", devices: ["nvidia.com/gpu=all"] }, }, }; @@ -95,7 +99,13 @@ describe("host-local inference receipt contract", () => { it("rejects a host runtime for managed services and container runtime for Ollama", () => { expect(() => - normalizeHostLocalInferenceReceipt({ ...receipt("nim"), runtime: { kind: "host" } }), + normalizeHostLocalInferenceReceipt({ + ...receipt("nim"), + runtime: { + kind: "host", + probeImageRef: `quay.io/curl/curl@sha256:${"d".repeat(64)}`, + }, + }), ).toThrow("only Ollama"); expect(() => normalizeHostLocalInferenceReceipt({ @@ -105,6 +115,24 @@ describe("host-local inference receipt contract", () => { ).toThrow("Ollama must use host-process authority"); }); + it("rejects mutable probe images and malformed managed specification digests", () => { + const ollama = receipt("ollama"); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...ollama, + runtime: { kind: "host", probeImageRef: "curlimages/curl:latest" }, + }), + ).toThrow("runtime image reference is malformed"); + + const vllm = receipt("vllm"); + expect(() => + normalizeHostLocalInferenceReceipt({ + ...vllm, + runtime: { ...vllm.runtime, specSha256: "mutable" }, + }), + ).toThrow("runtime specification digest is malformed"); + }); + it("rejects extensions and noncanonical serialized receipts", () => { const base = receipt(); expect(() => normalizeHostLocalInferenceReceipt({ ...base, extra: true })).toThrow( diff --git a/src/lib/onboard/runtime-provider/host-local-inference.ts b/src/lib/onboard/runtime-provider/host-local-inference.ts index 21e7be10b52..81043fcda32 100644 --- a/src/lib/onboard/runtime-provider/host-local-inference.ts +++ b/src/lib/onboard/runtime-provider/host-local-inference.ts @@ -45,12 +45,16 @@ export interface HostLocalInferenceEndpointAuthority { export type HostLocalInferenceRuntimeAuthority = | { readonly kind: "host"; + /** Immutable utility image used to prove endpoint reachability from the runtime network. */ + readonly probeImageRef: string; } | { readonly kind: "container"; readonly runtimeId: string; readonly name: string; readonly imageRef: string; + /** Secret-free digest of the complete provider-owned container specification. */ + readonly specSha256: string; readonly gpu: { readonly vendor: "nvidia"; readonly devices: readonly string[]; @@ -95,6 +99,7 @@ const RUNTIME_ID = /^[A-Za-z0-9][A-Za-z0-9._:/=+-]{0,511}$/u; const OCI_DIGEST_REFERENCE = /^(?:[A-Za-z0-9._-]+(?::[0-9]+)?\/)*(?:[A-Za-z0-9._-]+)@sha256:[a-f0-9]{64}$/u; const CDI_DEVICE = /^nvidia\.com\/gpu=[A-Za-z0-9][A-Za-z0-9_.:/-]{0,255}$/u; +const SHA256 = /^[a-f0-9]{64}$/u; const SERVICES = new Set(["ollama", "nim", "vllm"]); const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u; const MAX_SERIALIZED_BYTES = 32 * 1024; @@ -135,6 +140,10 @@ function exactPort(value: unknown, label: string): number { return Number(value); } +export function normalizeHostLocalInferenceImageRef(value: unknown): string { + return exactText(value, OCI_DIGEST_REFERENCE, "runtime image reference"); +} + function normalizeEndpoint(value: unknown): HostLocalInferenceEndpointAuthority { const endpoint = exactRecord(value, "endpoint authority"); exactKeys(endpoint, ["host", "networkName", "port"], "endpoint authority"); @@ -151,12 +160,19 @@ function normalizeRuntime( ): HostLocalInferenceRuntimeAuthority { const runtime = exactRecord(value, "runtime authority"); if (runtime.kind === "host") { - exactKeys(runtime, ["kind"], "host runtime authority"); + exactKeys(runtime, ["kind", "probeImageRef"], "host runtime authority"); if (service !== "ollama") fail("only Ollama may use host-process authority"); - return Object.freeze({ kind: "host" as const }); + return Object.freeze({ + kind: "host" as const, + probeImageRef: normalizeHostLocalInferenceImageRef(runtime.probeImageRef), + }); } if (runtime.kind !== "container") fail("runtime kind is unsupported"); - exactKeys(runtime, ["gpu", "imageRef", "kind", "name", "runtimeId"], "container authority"); + exactKeys( + runtime, + ["gpu", "imageRef", "kind", "name", "runtimeId", "specSha256"], + "container authority", + ); if (service === "ollama") fail("Ollama must use host-process authority"); const gpu = exactRecord(runtime.gpu, "GPU authority"); exactKeys(gpu, ["devices", "vendor"], "GPU authority"); @@ -169,7 +185,8 @@ function normalizeRuntime( kind: "container" as const, runtimeId: exactText(runtime.runtimeId, RUNTIME_ID, "runtime identity"), name: exactText(runtime.name, SAFE_NAME, "runtime name"), - imageRef: exactText(runtime.imageRef, OCI_DIGEST_REFERENCE, "runtime image reference"), + imageRef: normalizeHostLocalInferenceImageRef(runtime.imageRef), + specSha256: exactText(runtime.specSha256, SHA256, "runtime specification digest"), gpu: Object.freeze({ vendor: "nvidia" as const, devices: Object.freeze(devices) }), }); } From d4622df35c43ad98c304bc1a6d42e506c830c863 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 1 Aug 2026 06:44:34 -0700 Subject: [PATCH 5/6] fix(runtime): reject duplicate Podman CDI inventory Signed-off-by: Aaron Erickson --- .../runtime-provider/podman-preflight.test.ts | 7 +++++++ src/lib/onboard/runtime-provider/podman-preflight.ts | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lib/onboard/runtime-provider/podman-preflight.test.ts b/src/lib/onboard/runtime-provider/podman-preflight.test.ts index 9e4e31e721c..d0579b2575d 100644 --- a/src/lib/onboard/runtime-provider/podman-preflight.test.ts +++ b/src/lib/onboard/runtime-provider/podman-preflight.test.ts @@ -142,6 +142,13 @@ describe("Podman host preflight", () => { additionalCdiDevices: ["all"], }), ).toThrow("duplicate NVIDIA device"); + expect(() => + qualifyPodmanHost(engine(), { + platform: "linux", + architecture: "x64", + additionalCdiDevices: ["nvidia.com/gpu=0"], + }), + ).toThrow("duplicate NVIDIA device"); }); it("rejects an API service architecture that differs from the host", () => { diff --git a/src/lib/onboard/runtime-provider/podman-preflight.ts b/src/lib/onboard/runtime-provider/podman-preflight.ts index 0307718f060..b8e3e812635 100644 --- a/src/lib/onboard/runtime-provider/podman-preflight.ts +++ b/src/lib/onboard/runtime-provider/podman-preflight.ts @@ -117,9 +117,9 @@ function normalizeArchitecture(value: string): "amd64" | "arm64" | null { return null; } -function collectNvidiaCdiDevices(value: unknown, devices: Set): void { +function collectNvidiaCdiDevices(value: unknown, devices: string[]): void { if (typeof value === "string") { - if (value.startsWith("nvidia.com/gpu=")) devices.add(value); + if (value.startsWith("nvidia.com/gpu=")) devices.push(value); return; } if (Array.isArray(value)) { @@ -129,7 +129,7 @@ function collectNvidiaCdiDevices(value: unknown, devices: Set): void { const source = record(value); if (!source) return; for (const [key, entry] of Object.entries(source)) { - if (key.startsWith("nvidia.com/gpu=")) devices.add(key); + if (key.startsWith("nvidia.com/gpu=")) devices.push(key); collectNvidiaCdiDevices(entry, devices); } } @@ -242,10 +242,10 @@ export function qualifyPodmanHost( } requireSubordinateIdMappings(engine); - const cdiDevices = new Set(); + const cdiDevices: string[] = []; collectNvidiaCdiDevices(host, cdiDevices); - for (const device of options.additionalCdiDevices ?? []) cdiDevices.add(device); - const qualifiedCdiDevices = normalizePodmanCdiInventory([...cdiDevices]); + cdiDevices.push(...(options.additionalCdiDevices ?? [])); + const qualifiedCdiDevices = normalizePodmanCdiInventory(cdiDevices); return Object.freeze({ providerId: "podman", From 4b75eb4af615060a535c9354f17096109fa2fa93 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Sat, 1 Aug 2026 07:27:14 -0700 Subject: [PATCH 6/6] refactor(runtime): keep receipt slice domain-only Signed-off-by: Aaron Erickson --- .../host-local-inference.test.ts | 2 +- .../runtime-provider/host-local-inference.ts | 43 ------------------- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/src/lib/onboard/runtime-provider/host-local-inference.test.ts b/src/lib/onboard/runtime-provider/host-local-inference.test.ts index 36857e76042..9eee21cc525 100644 --- a/src/lib/onboard/runtime-provider/host-local-inference.test.ts +++ b/src/lib/onboard/runtime-provider/host-local-inference.test.ts @@ -4,10 +4,10 @@ import { describe, expect, it } from "vitest"; import { + type HostLocalInferenceReceipt, normalizeHostLocalInferenceReceipt, parseHostLocalInferenceReceipt, serializeHostLocalInferenceReceipt, - type HostLocalInferenceReceipt, } from "./host-local-inference"; const ENGINE_AUTHORITY = { diff --git a/src/lib/onboard/runtime-provider/host-local-inference.ts b/src/lib/onboard/runtime-provider/host-local-inference.ts index 81043fcda32..bab5d2e5df5 100644 --- a/src/lib/onboard/runtime-provider/host-local-inference.ts +++ b/src/lib/onboard/runtime-provider/host-local-inference.ts @@ -10,32 +10,6 @@ export const HOST_LOCAL_INFERENCE_RECEIPT_SCHEMA_VERSION = 1 as const; export type HostLocalInferenceService = "ollama" | "nim" | "vllm"; -export interface HostLocalInferenceEndpointInput { - readonly networkName: string; - readonly hostPort: number; - readonly probeImageRef: string; -} - -export interface HostLocalInferenceMount { - readonly source: string; - readonly target: string; - readonly readOnly?: boolean; -} - -export interface HostLocalManagedInferenceInput extends HostLocalInferenceEndpointInput { - readonly service: "nim" | "vllm"; - readonly containerName: string; - readonly containerPort: number; - readonly imageRef: string; - readonly gpuDevices: readonly string[]; - /** Environment variable names forwarded from the current process; values are never persisted. */ - readonly environment?: readonly string[]; - readonly mounts?: readonly HostLocalInferenceMount[]; - readonly sharedMemory?: string; - readonly ipc?: "host" | "private"; - readonly command?: readonly string[]; -} - export interface HostLocalInferenceEndpointAuthority { readonly host: string; readonly port: number; @@ -75,23 +49,6 @@ export interface HostLocalInferenceReceipt { readonly runtime: HostLocalInferenceRuntimeAuthority; } -export interface HostLocalManagedInferenceInspection { - readonly running: boolean; - readonly receipt: HostLocalInferenceReceipt; -} - -export interface HostLocalInferenceRuntime { - readonly providerId: string; - readonly services: readonly HostLocalInferenceService[]; - translateContainerArgs(args: readonly string[]): readonly string[]; - qualifyOllama(input: HostLocalInferenceEndpointInput): HostLocalInferenceReceipt; - startManaged(input: HostLocalManagedInferenceInput): HostLocalInferenceReceipt; - inspectManaged(receipt: HostLocalInferenceReceipt): HostLocalManagedInferenceInspection; - stopManaged(receipt: HostLocalInferenceReceipt): HostLocalManagedInferenceInspection; - /** Re-prove the same out-of-sandbox service before carrying it into a rebuild. */ - preserveForRebuild(receipt: HostLocalInferenceReceipt): HostLocalInferenceReceipt; -} - const PROVIDER_ID = /^[a-z][a-z0-9-]{0,62}$/u; const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; const SAFE_HOST = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/u;