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
10 changes: 10 additions & 0 deletions src/lib/inference/llama-cpp/host-local-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import path from "node:path";

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

import { LLAMA_CPP_PORT } from "./contract";
import {
buildLlamaCppHostLocalDockerArgv,
type LlamaCppHostLocalLaunchContract,
Expand Down Expand Up @@ -159,6 +160,15 @@ describe("llama.cpp host-local runtime materializer", () => {
expect(argv.join("\n")).not.toContain("huggingface.co");
});

it("publishes the fixed loopback host port when the bindings pin one", () => {
const argv = buildLlamaCppHostLocalDockerArgv(contract(), {
...bindings(),
hostPort: LLAMA_CPP_PORT,
});

expect(valuesAfter(argv, "--publish")).toEqual([`127.0.0.1:${String(LLAMA_CPP_PORT)}:8081`]);
});

it("takes launch settings from the declared contract instead of code defaults (#8144)", () => {
const input = contract();
const changed = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import type { ContainerEngine } from "../../adapters/container-engine";
import { LLAMA_CPP_PORT } from "../../inference/llama-cpp/contract";
import type { LlamaCppGgufCachePlan } from "../../inference/llama-cpp/gguf-cache-plan";
/* Test-only reconstruction of the exact immutable command for recovery fixtures. */
import {
buildLlamaCppHostLocalServerArgv,
type LlamaCppHostLocalLaunchContract,
type LlamaCppHostLocalRuntimeBindings,
} from "../../inference/llama-cpp/host-local-runtime";
import {
createDockerLlamaCppManagedLifecycle,
Expand All @@ -32,6 +32,7 @@ import {
} from "./host-local-inference";
import type { PersistedEngineAuthorityStore } from "./persisted-engine-authority";

const HOST_PORT = String(LLAMA_CPP_PORT);
const MODEL_DIGEST = `sha256:${"a".repeat(64)}`;
const IMAGE = `ghcr.io/nvidia/nemoclaw/llama-cpp-server@sha256:${"c".repeat(64)}`;
const PROBE_IMAGE = `quay.io/curl/curl@sha256:${"d".repeat(64)}`;
Expand Down Expand Up @@ -231,10 +232,11 @@ function keyRootIdentitySha256(): string {
});
}

function bindings(): LlamaCppHostLocalRuntimeBindings {
function bindings(): DockerLlamaCppManagedLifecycleOptions["bindings"] {
return {
apiKeyHostPath: apiKeyPath,
containerName: "nemoclaw-llama-cpp",
hostPort: LLAMA_CPP_PORT,
imageReference: IMAGE,
model: {
digest: MODEL_DIGEST,
Expand Down Expand Up @@ -388,7 +390,7 @@ interface DockerFixture {
}

function dockerFixture(
configuredHostPort = "",
configuredHostPort = HOST_PORT,
publishedHostPort?: string,
publishedHostIp = "127.0.0.1",
publishedBindingCount = 1,
Expand Down Expand Up @@ -824,21 +826,22 @@ describe("dormant Docker llama.cpp managed lifecycle", () => {
expect(lifecycle.runtime.destroy(receipt).status).toBe("already-absent");
});
it.each([
["configured", "8082", undefined],
["published", "8081", "8082"],
] as const)("rolls back exact ownership for %s loopback port drift (#8544)", (_kind, configured, published) => {
["configured", "8082", undefined, /bound host port/u],
["published", "8081", "8082", /declared binding/u],
] as const)("rolls back exact ownership for %s loopback port drift (#8544)", (_kind, configured, published, expectedError) => {
const [fixture, store] = [dockerFixture(configured, published), journalStore()];
const lifecycle = createDockerLlamaCppManagedLifecycle(
options(fixture, store, { ...bindings(), hostPort: 8081 }),
);
expect(() => lifecycle.start(receiptWriter())).toThrow(/binding/u);
expect(() => lifecycle.start(receiptWriter())).toThrow(expectedError);
const calls = fixture.capture.mock.calls.map((call) => call[0]);
expect(calls).toContainEqual(["rm", "--force", RUNTIME_ID]);
expect(calls).toContainEqual(["network", "rm", NETWORK_ID]);
expect(store.list()).toEqual([]);
});
it("fails closed on non-loopback, malformed, or multiple published bindings during port-drift rollback (#8544)", () => {
it("rejects and cleans up malformed or non-loopback published bindings (#8544)", () => {
for (const args of [
[HOST_PORT, HOST_PORT, "0.0.0.0", 1],
["8081", "8082", "0.0.0.0", 1],
["8081", "invalid", "127.0.0.1", 1],
["8081", "8082", "127.0.0.1", 2],
Expand All @@ -847,10 +850,11 @@ describe("dormant Docker llama.cpp managed lifecycle", () => {
const lifecycle = createDockerLlamaCppManagedLifecycle(
options(fixture, store, { ...bindings(), hostPort: 8081 }),
);
expect(() => lifecycle.start(receiptWriter())).toThrow("Exact rollback also failed");
expect(() => lifecycle.start(receiptWriter())).toThrow(/port|binding/u);
const calls = fixture.capture.mock.calls.map((call) => call[0]);
expect(store.list()).not.toEqual([]);
expect(calls).not.toContainEqual(["rm", "--force", RUNTIME_ID]);
expect(store.list()).toEqual([]);
expect(calls).toContainEqual(["rm", "--force", RUNTIME_ID]);
expect(calls).toContainEqual(["network", "rm", NETWORK_ID]);
}
});
it("uses the declarative readiness timeout as both curl retry budget and capture budget", () => {
Expand Down
136 changes: 57 additions & 79 deletions src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ interface DockerContainerInspection {
};
}

type DockerContainerInspectionMode = "runtime" | "cleanup";

interface StableFileIdentity {
readonly dev: bigint;
readonly ino: bigint;
Expand Down Expand Up @@ -271,21 +273,13 @@ function parseLabels(value: unknown): Readonly<Record<string, string>> {
return Object.freeze(labels);
}

function parsePublishedPortBinding(value: unknown): Record<string, unknown> | null {
if (value === null) return null;
if (!Array.isArray(value) || value.length !== 1) {
throw new Error("Docker llama.cpp container has unexpected published ports.");
}
return record(value[0], "Docker llama.cpp published port");
}

function parseInspection(
output: string,
contract: LlamaCppHostLocalLaunchContract,
networkName: string,
hostPort: number | undefined,
portValidation: "exact" | "cleanup",
bindings: DockerLlamaCppManagedLifecycleOptions["bindings"],
mode: DockerContainerInspectionMode,
): DockerContainerInspection {
const networkName = bindings.network.name;
let parsed: unknown;
try {
parsed = JSON.parse(output);
Expand All @@ -307,37 +301,43 @@ function parseInspection(
throw new Error("Docker llama.cpp container has unexpected network attachments.");
}
const attached = record(networks[networkName], "Docker llama.cpp network attachment");
const ports = record(networkSettings.Ports, "Docker llama.cpp published ports");
const portKey = `${String(contract.serve.port)}/tcp`;
const configuredPorts = record(hostConfig.PortBindings, "Docker llama.cpp configured ports");
if (Object.keys(configuredPorts).length !== 1) {
throw new Error("Docker llama.cpp container has extra configured ports.");
}
const configuredBindings = configuredPorts[portKey];
if (!Array.isArray(configuredBindings) || configuredBindings.length !== 1) {
throw new Error("Docker llama.cpp container has unexpected configured ports.");
}
const configuredPort = record(configuredBindings[0], "Docker llama.cpp configured port");
if (configuredPort.HostIp !== "127.0.0.1") {
throw new Error("Docker llama.cpp configured host port is not loopback-only.");
}
const configuredHostPort =
configuredPort.HostPort === "" ? null : exactPort(configuredPort.HostPort);
if (portValidation === "exact" && configuredHostPort !== (hostPort ?? null)) {
throw new Error("Docker llama.cpp configured host port does not match its loopback binding.");
}
const published = parsePublishedPortBinding(ports[portKey]);
if (published !== null && published.HostIp !== "127.0.0.1") {
throw new Error("Docker llama.cpp host port is not loopback-only.");
}
const publishedHostPort = published === null ? null : exactPort(published.HostPort);
if (
portValidation === "exact" &&
hostPort !== undefined &&
publishedHostPort !== null &&
publishedHostPort !== hostPort
) {
throw new Error("Docker llama.cpp published host port differs from its declared binding.");
let hostPort: number | null = null;
if (mode === "runtime") {
const ports = record(networkSettings.Ports, "Docker llama.cpp published ports");
const portKey = `${String(contract.serve.port)}/tcp`;
const configuredPorts = record(hostConfig.PortBindings, "Docker llama.cpp configured ports");
if (Object.keys(configuredPorts).length !== 1) {
throw new Error("Docker llama.cpp container has extra configured ports.");
}
const configuredBindings = configuredPorts[portKey];
if (!Array.isArray(configuredBindings) || configuredBindings.length !== 1) {
throw new Error("Docker llama.cpp container has unexpected configured ports.");
}
const configuredPort = record(configuredBindings[0], "Docker llama.cpp configured port");
if (configuredPort.HostIp !== "127.0.0.1") {
throw new Error("Docker llama.cpp configured host port is not loopback-only.");
}
if (configuredPort.HostPort !== String(bindings.hostPort)) {
throw new Error("Docker llama.cpp configured host port is not the bound host port.");
}
const publishedBindings = ports[portKey];
if (
publishedBindings !== null &&
(!Array.isArray(publishedBindings) || publishedBindings.length !== 1)
) {
throw new Error("Docker llama.cpp container has unexpected published ports.");
}
const published =
publishedBindings === null
? null
: record(publishedBindings[0], "Docker llama.cpp published port");
if (published !== null && published.HostIp !== "127.0.0.1") {
throw new Error("Docker llama.cpp host port is not loopback-only.");
}
hostPort = published === null ? null : exactPort(published.HostPort);
if (hostPort !== null && hostPort !== bindings.hostPort) {
throw new Error("Docker llama.cpp published host port differs from its declared binding.");
}
}
if (!Array.isArray(source.Mounts)) {
throw new Error("Docker llama.cpp inspection returned malformed mounts.");
Expand Down Expand Up @@ -406,7 +406,7 @@ function parseInspection(
status: stateStatus,
networkId: exactId(attached.NetworkID, "Docker attached network identity"),
networkName,
hostPort: publishedHostPort,
hostPort,
mounts: Object.freeze(mounts),
hardening: Object.freeze({
user: String(config.User ?? ""),
Expand Down Expand Up @@ -443,9 +443,8 @@ function inspectContainer(
engine: ContainerEngine,
target: string,
contract: LlamaCppHostLocalLaunchContract,
networkName: string,
hostPort: number | undefined,
portValidation: "exact" | "cleanup" = "exact",
bindings: DockerLlamaCppManagedLifecycleOptions["bindings"],
mode: DockerContainerInspectionMode = "runtime",
): DockerContainerInspection | null {
const result = engine.capture(["container", "inspect", target], INSPECT_TIMEOUT_MS);
const escapedTarget = target.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
Expand All @@ -456,13 +455,7 @@ function inspectContainer(
if (!result.error && result.status === 1 && exactAbsent.test(result.stderr.trim())) {
return null;
}
return parseInspection(
requireSuccess("container inspection", result),
contract,
networkName,
hostPort,
portValidation,
);
return parseInspection(requireSuccess("container inspection", result), contract, bindings, mode);
}

function currentUid(): bigint {
Expand Down Expand Up @@ -947,8 +940,7 @@ function rollbackExact(
options.engine,
target,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
"cleanup",
);
if (container === null && record.phase === "creating" && uncertainRecoveryUnixMs !== undefined) {
Expand All @@ -964,8 +956,7 @@ function rollbackExact(
options.engine,
target,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
"cleanup",
);
}
Expand All @@ -976,14 +967,8 @@ function rollbackExact(
captureMutation(options, lease, execution, ["rm", "--force", owned.id], MUTATION_TIMEOUT_MS),
);
if (
inspectContainer(
options.engine,
owned.id,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
"cleanup",
) !== null
inspectContainer(options.engine, owned.id, options.contract, options.bindings, "cleanup") !==
null
) {
throw new Error("Docker llama.cpp exact rollback left the owned runtime present.");
}
Expand Down Expand Up @@ -1239,8 +1224,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
authorized.receipt.runtime.runtimeId,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
);
if (inspected === null) throw new Error("Docker llama.cpp owned runtime is absent.");
const container = requireOwnedContainer(inspected, options, authorized.journal);
Expand Down Expand Up @@ -1357,8 +1341,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
normalized.runtime.runtimeId,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
);
const journal = options.journalStore.load(normalized.runtime.model.generation);
if (existing !== null || journal !== null) authorizeReceipt(normalized, true);
Expand All @@ -1373,8 +1356,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
normalized.runtime.runtimeId,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
);
if (existing === null) {
const journal = options.journalStore.load(normalized.runtime.model.generation);
Expand Down Expand Up @@ -1413,8 +1395,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
inspected.container.id,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
) !== null
) {
throw new Error("Docker llama.cpp removal left the exact runtime present.");
Expand Down Expand Up @@ -1510,8 +1491,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
options.bindings.containerName,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
) !== null
) {
throw new Error("Docker llama.cpp container name is already in use.");
Expand Down Expand Up @@ -1592,8 +1572,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
options.bindings.containerName,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
);
if (create.error || create.status !== 0 || created === null) {
throw new Error(
Expand All @@ -1619,8 +1598,7 @@ export function createDockerLlamaCppManagedLifecycle(
options.engine,
created.id,
options.contract,
options.bindings.network.name,
options.bindings.hostPort,
options.bindings,
);
if (started === null || !started.running) {
throw new Error("Docker llama.cpp start did not leave the exact runtime running.");
Expand Down
2 changes: 1 addition & 1 deletion src/lib/onboard/runtime-provider/host-local-inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export interface HostLocalLlamaCppLifecycleInput {
readonly authorityStore: PersistedEngineAuthorityStore;
readonly apiKeyRootHostPath: string;
readonly bindingSha256: string;
readonly bindings: LlamaCppHostLocalRuntimeBindings;
readonly bindings: LlamaCppHostLocalRuntimeBindings & { readonly hostPort: number };
readonly cacheRootHostPath: string;
readonly contract: LlamaCppHostLocalLaunchContract;
readonly engine: ContainerEngine;
Expand Down
Loading