diff --git a/src/lib/inference/llama-cpp/managed-installer.test.ts b/src/lib/inference/llama-cpp/managed-installer.test.ts index f037dbfe96f..10a107f390f 100644 --- a/src/lib/inference/llama-cpp/managed-installer.test.ts +++ b/src/lib/inference/llama-cpp/managed-installer.test.ts @@ -735,6 +735,7 @@ describe("managed llama.cpp installer", () => { expect(createLifecycle).toHaveBeenCalledWith( expect.objectContaining({ bindings: expect.objectContaining({ + hostPort: selected.recipe.spec.serve.port, imageReference: selected.recipe.spec.runtime.image, }), contract: expect.objectContaining({ diff --git a/src/lib/inference/llama-cpp/managed-installer.ts b/src/lib/inference/llama-cpp/managed-installer.ts index ca1fb588f03..6007cb52ea2 100644 --- a/src/lib/inference/llama-cpp/managed-installer.ts +++ b/src/lib/inference/llama-cpp/managed-installer.ts @@ -24,7 +24,7 @@ import { isLlamaCppServingRecipe } from "../serving/adapter-registry"; import { loadManagedInferenceCatalog } from "../serving/catalog-loader"; import type { ResolvedLlamaCppInferenceSelection } from "../serving/types"; import { buildVllmDockerEnv } from "../vllm-docker-env"; -import { LLAMA_CPP_CREDENTIAL_ENV, LLAMA_CPP_PORT } from "./contract"; +import { LLAMA_CPP_CREDENTIAL_ENV } from "./contract"; import { acquireVerifiedLlamaCppGguf, verifyLlamaCppGgufCacheEntry } from "./gguf-acquisition"; import { compileLlamaCppGgufCachePlan } from "./gguf-cache-plan"; import type { @@ -395,7 +395,7 @@ function lifecycleFor(input: { bindings: { apiKeyHostPath: input.paths.apiKeyPath, containerName: MANAGED_LLAMA_CPP_CONTAINER_NAME, - hostPort: LLAMA_CPP_PORT, + hostPort: recipe.spec.serve.port, imageReference: recipe.spec.runtime.image, model: input.artifact, network: { isolation: "docker-internal", name: MANAGED_LLAMA_CPP_NETWORK_NAME }, @@ -488,6 +488,7 @@ export async function installManagedLlamaCpp( const checkPort = options.checkPort ?? checkPortAvailable; const log = options.log ?? ((message: string) => console.log(message)); const recipe = selection.recipe; + const hostPort = recipe.spec.serve.port; let engine: ContainerEngine | null = null; let operation: HostLocalInferenceOperation | null = null; let owner: ReturnType["owner"] | null = null; @@ -523,10 +524,10 @@ export async function installManagedLlamaCpp( assertContainerNameAvailable(engine, persistedReceipt); if (persistedReceipt === null && pending.length === 0) { - const port = await checkPort(LLAMA_CPP_PORT); + const port = await checkPort(hostPort); if (!port.ok) { throw new Error( - `Managed llama.cpp port ${String(LLAMA_CPP_PORT)} is unavailable: ${port.reason}`, + `Managed llama.cpp port ${String(hostPort)} is unavailable: ${port.reason}`, ); } } @@ -608,10 +609,10 @@ export async function installManagedLlamaCpp( if (receipt !== null) { receipt = lifecycle.resume(receipt); } else { - const port = await checkPort(LLAMA_CPP_PORT); + const port = await checkPort(hostPort); if (!port.ok) { throw new Error( - `Managed llama.cpp port ${String(LLAMA_CPP_PORT)} is unavailable: ${port.reason}`, + `Managed llama.cpp port ${String(hostPort)} is unavailable: ${port.reason}`, ); } const transactionId = randomBytes(32).toString("hex"); @@ -664,6 +665,7 @@ export async function resumeManagedLlamaCppRuntime( const engine = operation.engine; const verify = options.verifyGguf ?? verifyLlamaCppGgufCacheEntry; const checkPort = options.checkPort ?? checkPortAvailable; + const hostPort = selection.recipe.spec.serve.port; const plan = compileLlamaCppGgufCachePlan(selection.recipe); const cacheRoot = ensureSharedHuggingFaceCache(homeDir); const artifact = await verify(plan, cacheRoot); @@ -706,11 +708,9 @@ export async function resumeManagedLlamaCppRuntime( receipt = loadManagedLlamaCppReceipt(paths); } if (receipt === null) { - const port = await checkPort(LLAMA_CPP_PORT); + const port = await checkPort(hostPort); if (!port.ok) { - throw new Error( - `Managed llama.cpp port ${String(LLAMA_CPP_PORT)} is unavailable: ${port.reason}`, - ); + throw new Error(`Managed llama.cpp port ${String(hostPort)} is unavailable: ${port.reason}`); } loadOrCreateManagedLlamaCppApiKey(paths); const transactionId = randomBytes(32).toString("hex"); diff --git a/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.test.ts b/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.test.ts index bc9c2b0ee89..d803ff1bef5 100644 --- a/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.test.ts +++ b/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.test.ts @@ -387,7 +387,13 @@ interface DockerFixture { readonly seed: (journal: HostLocalCreateJournalRecord, running: boolean) => void; } -function dockerFixture(): DockerFixture { +function dockerFixture( + configuredHostPort = "", + publishedHostPort?: string, + publishedHostIp = "127.0.0.1", + publishedBindingCount = 1, +): DockerFixture { + const effectivePublishedHostPort = publishedHostPort ?? (configuredHostPort || "49152"); let networkId = NETWORK_ID; let networkPresent = false; let networkTransactionId = TRANSACTION_ID; @@ -435,7 +441,9 @@ function dockerFixture(): DockerFixture { HostConfig: { NetworkMode: "nemoclaw-llama-cpp-internal", RestartPolicy: { Name: "unless-stopped", MaximumRetryCount: 0 }, - PortBindings: { "8081/tcp": [{ HostIp: "127.0.0.1", HostPort: "" }] }, + PortBindings: { + "8081/tcp": [{ HostIp: "127.0.0.1", HostPort: configuredHostPort }], + }, ReadonlyRootfs: !hardeningDrift, CapDrop: ["ALL"], SecurityOpt: ["no-new-privileges:true"], @@ -463,7 +471,12 @@ function dockerFixture(): DockerFixture { NetworkSettings: { Networks: { "nemoclaw-llama-cpp-internal": { NetworkID: networkId } }, Ports: { - "8081/tcp": startedOnce ? [{ HostIp: "127.0.0.1", HostPort: "49152" }] : null, + "8081/tcp": startedOnce + ? Array.from({ length: publishedBindingCount }, () => ({ + HostIp: publishedHostIp, + HostPort: effectivePublishedHostPort, + })) + : null, }, }, Mounts: [ @@ -687,6 +700,10 @@ function dockerFixture(): DockerFixture { }; } +function dockerCommandPrefixes(fixture: DockerFixture): unknown[] { + return fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2)); +} + function options( fixture: DockerFixture, store = journalStore(), @@ -771,15 +788,16 @@ function preparedJournal(): HostLocalCreateJournalRecord { } describe("dormant Docker llama.cpp managed lifecycle", () => { - it("journals create/start/finalize and serves the provider-neutral lifecycle in a test-only bundle (#8395)", () => { - const fixture = dockerFixture(); + it("journals a product install on its declared loopback host port (#8544)", () => { + const fixture = dockerFixture("8081"); const store = journalStore(); - const lifecycle = controller(fixture, store); + const lifecycle = createDockerLlamaCppManagedLifecycle( + options(fixture, store, { ...bindings(), hostPort: 8081 }), + ); const writer = receiptWriter(); const receipt = lifecycle.start(writer); const serialized = serializeHostLocalInferenceReceipt(receipt); - - expect(receipt.endpoint.port).toBe(49152); + expect(receipt.endpoint.port).toBe(8081); expect(receipt.runtime).toMatchObject({ kind: "container", runtimeId: RUNTIME_ID, @@ -795,26 +813,53 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { expect(serialized).not.toContain(apiKeyPath); expect(serialized).not.toContain("filesystemIdentity"); expect(serialized).not.toContain("test-only-secret"); - - expect(serializeHostLocalInferenceReceipt(parseHostLocalInferenceReceipt(serialized))).toBe( - serialized, + const roundTrip = serializeHostLocalInferenceReceipt( + parseHostLocalInferenceReceipt(serialized), ); + expect(roundTrip).toBe(serialized); expect(lifecycle.runtime.inspectManaged(receipt).running).toBe(true); expect(lifecycle.runtime.stopManaged(receipt).running).toBe(false); expect(lifecycle.runtime.prepareDestroy(receipt)).toEqual(receipt); expect(lifecycle.runtime.destroy(receipt).status).toBe("removed"); 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) => { + const [fixture, store] = [dockerFixture(configured, published), journalStore()]; + const lifecycle = createDockerLlamaCppManagedLifecycle( + options(fixture, store, { ...bindings(), hostPort: 8081 }), + ); + expect(() => lifecycle.start(receiptWriter())).toThrow(/binding/u); + 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)", () => { + for (const args of [ + ["8081", "8082", "0.0.0.0", 1], + ["8081", "invalid", "127.0.0.1", 1], + ["8081", "8082", "127.0.0.1", 2], + ] as const) { + const [fixture, store] = [dockerFixture(args[0], args[1], args[2], args[3]), journalStore()]; + const lifecycle = createDockerLlamaCppManagedLifecycle( + options(fixture, store, { ...bindings(), hostPort: 8081 }), + ); + expect(() => lifecycle.start(receiptWriter())).toThrow("Exact rollback also failed"); + const calls = fixture.capture.mock.calls.map((call) => call[0]); + expect(store.list()).not.toEqual([]); + expect(calls).not.toContainEqual(["rm", "--force", RUNTIME_ID]); + } + }); it("uses the declarative readiness timeout as both curl retry budget and capture budget", () => { const fixture = dockerFixture(); const lifecycle = createDockerLlamaCppManagedLifecycle({ ...options(fixture), readinessTimeoutSeconds: 37, }); - lifecycle.start(receiptWriter()); - const probe = fixture.capture.mock.calls.find(([args]) => args[0] === "run"); expect(probe).toBeDefined(); const [args, timeoutMs] = probe!; @@ -831,10 +876,8 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { ).toEqual(["--retry-max-time", "37"]); expect(timeoutMs).toBe(52_000); }); - it("rejects an invalid declarative readiness timeout before inspection or mutation", () => { const fixture = dockerFixture(); - expect(() => createDockerLlamaCppManagedLifecycle({ ...options(fixture), @@ -948,10 +991,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { }); expect(() => controller(fixture, store).start(receiptWriter())).toThrow("filesystem identity"); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).toContainEqual(["rm", "--force"]); }); it("rolls back pathname replacement from inside Docker create capture before persistence (#8395)", () => { @@ -965,10 +1005,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { expect(() => controller(fixture, store).start(writer)).toThrow("filesystem identity"); expect(writer.writeExact).not.toHaveBeenCalled(); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).toContainEqual(["rm", "--force"]); }); it("rolls back an API-key root swap-and-restore inside Docker create capture (#8395)", () => { @@ -990,10 +1027,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { expect(() => controller(fixture, store).start(writer)).toThrow("API-key file changed"); expect(writer.writeExact).not.toHaveBeenCalled(); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).toContainEqual(["rm", "--force"]); }); it("rolls back malformed create output and readiness failure before receipt prepare (#8395)", () => { @@ -1007,10 +1041,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { arrangeFailure[failure](fixture); expect(() => controller(fixture, store).start(receiptWriter())).toThrow(); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).toContainEqual(["rm", "--force"]); } }); @@ -1023,10 +1054,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { "prepare receipt failed", ); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).toContainEqual(["rm", "--force"]); }); it("preserves and replays when receipt preparation commits then throws (#8414)", () => { @@ -1039,10 +1067,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { expect(() => lifecycle.start(writer)).toThrow("prepare receipt outcome unknown"); expect(store.load(TRANSACTION_ID)?.phase).toBe("receipt-prepared"); expect(writer.writeExact).not.toHaveBeenCalled(); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["rm", "--force"]); expect(lifecycle.recoverUnfinished(writer)).toEqual({ recovered: [TRANSACTION_ID], failures: [], @@ -1072,10 +1097,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { phase: "receipt-prepared", serializedReceipt: committed, }); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["rm", "--force"]); expect(lifecycle.recoverUnfinished(writer)).toEqual({ recovered: [TRANSACTION_ID], failures: [], @@ -1236,14 +1258,8 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { failures: [], }); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).toContainEqual([ - "network", - "rm", - ]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).toContainEqual(["network", "rm"]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["rm", "--force"]); }); it("holds an absent uncertain network intent through grace and refuses another transaction", () => { @@ -1264,10 +1280,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { const foreign = lifecycle.recoverUnfinished(receiptWriter()); expect(foreign.recovered).toEqual([]); expect(foreign.failures[0]?.message).toContain("exact internal Docker network"); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "network", - "rm", - ]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["network", "rm"]); expect(store.load(TRANSACTION_ID)?.phase).toBe("network-creating"); }); @@ -1296,10 +1309,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { expect(recovery.recovered).toEqual([]); expect(recovery.failures[0]?.message).toContain("network inspection failed"); expect(store.load(TRANSACTION_ID)).toEqual(journal); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "network", - "rm", - ]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["network", "rm"]); }); it("accepts an exact alternate Docker network-absence response during rollback", () => { @@ -1323,10 +1333,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { ).recoverUnfinished(receiptWriter()), ).toEqual({ recovered: [TRANSACTION_ID], failures: [] }); expect(store.list()).toEqual([]); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "network", - "rm", - ]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["network", "rm"]); }); it("recovers prepared and exact creating/created/started journals without touching finalized ownership (#8395)", () => { @@ -1386,10 +1393,7 @@ describe("dormant Docker llama.cpp managed lifecycle", () => { expect(recovery.recovered).toEqual([]); expect(recovery.failures).toHaveLength(1); expect(store.load(TRANSACTION_ID)).not.toBeNull(); - expect(fixture.capture.mock.calls.map((call) => call[0]?.slice(0, 2))).not.toContainEqual([ - "rm", - "--force", - ]); + expect(dockerCommandPrefixes(fixture)).not.toContainEqual(["rm", "--force"]); } }); diff --git a/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.ts b/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.ts index 5d9c87b6a1b..9d111354e3c 100644 --- a/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.ts +++ b/src/lib/onboard/runtime-provider/docker-llama-cpp-managed-lifecycle.ts @@ -271,10 +271,20 @@ function parseLabels(value: unknown): Readonly> { return Object.freeze(labels); } +function parsePublishedPortBinding(value: unknown): Record | 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", ): DockerContainerInspection { let parsed: unknown; try { @@ -308,17 +318,27 @@ function parseInspection( 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" || configuredPort.HostPort !== "") { + if (configuredPort.HostIp !== "127.0.0.1") { throw new Error("Docker llama.cpp configured host port is not loopback-only."); } - const bindings = ports[portKey]; - const published = - Array.isArray(bindings) && bindings.length === 1 - ? record(bindings[0], "Docker llama.cpp published port") - : null; + 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."); + } if (!Array.isArray(source.Mounts)) { throw new Error("Docker llama.cpp inspection returned malformed mounts."); } @@ -386,7 +406,7 @@ function parseInspection( status: stateStatus, networkId: exactId(attached.NetworkID, "Docker attached network identity"), networkName, - hostPort: published === null ? null : exactPort(published.HostPort), + hostPort: publishedHostPort, mounts: Object.freeze(mounts), hardening: Object.freeze({ user: String(config.User ?? ""), @@ -424,6 +444,8 @@ function inspectContainer( target: string, contract: LlamaCppHostLocalLaunchContract, networkName: string, + hostPort: number | undefined, + portValidation: "exact" | "cleanup" = "exact", ): DockerContainerInspection | null { const result = engine.capture(["container", "inspect", target], INSPECT_TIMEOUT_MS); const escapedTarget = target.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); @@ -434,7 +456,13 @@ function inspectContainer( if (!result.error && result.status === 1 && exactAbsent.test(result.stderr.trim())) { return null; } - return parseInspection(requireSuccess("container inspection", result), contract, networkName); + return parseInspection( + requireSuccess("container inspection", result), + contract, + networkName, + hostPort, + portValidation, + ); } function currentUid(): bigint { @@ -920,6 +948,8 @@ function rollbackExact( target, options.contract, options.bindings.network.name, + options.bindings.hostPort, + "cleanup", ); if (container === null && record.phase === "creating" && uncertainRecoveryUnixMs !== undefined) { if ( @@ -935,6 +965,8 @@ function rollbackExact( target, options.contract, options.bindings.network.name, + options.bindings.hostPort, + "cleanup", ); } if (container !== null) { @@ -949,6 +981,8 @@ function rollbackExact( owned.id, options.contract, options.bindings.network.name, + options.bindings.hostPort, + "cleanup", ) !== null ) { throw new Error("Docker llama.cpp exact rollback left the owned runtime present."); @@ -1206,6 +1240,7 @@ export function createDockerLlamaCppManagedLifecycle( authorized.receipt.runtime.runtimeId, options.contract, options.bindings.network.name, + options.bindings.hostPort, ); if (inspected === null) throw new Error("Docker llama.cpp owned runtime is absent."); const container = requireOwnedContainer(inspected, options, authorized.journal); @@ -1323,6 +1358,7 @@ export function createDockerLlamaCppManagedLifecycle( normalized.runtime.runtimeId, options.contract, options.bindings.network.name, + options.bindings.hostPort, ); const journal = options.journalStore.load(normalized.runtime.model.generation); if (existing !== null || journal !== null) authorizeReceipt(normalized, true); @@ -1338,6 +1374,7 @@ export function createDockerLlamaCppManagedLifecycle( normalized.runtime.runtimeId, options.contract, options.bindings.network.name, + options.bindings.hostPort, ); if (existing === null) { const journal = options.journalStore.load(normalized.runtime.model.generation); @@ -1377,6 +1414,7 @@ export function createDockerLlamaCppManagedLifecycle( inspected.container.id, options.contract, options.bindings.network.name, + options.bindings.hostPort, ) !== null ) { throw new Error("Docker llama.cpp removal left the exact runtime present."); @@ -1473,6 +1511,7 @@ export function createDockerLlamaCppManagedLifecycle( options.bindings.containerName, options.contract, options.bindings.network.name, + options.bindings.hostPort, ) !== null ) { throw new Error("Docker llama.cpp container name is already in use."); @@ -1554,6 +1593,7 @@ export function createDockerLlamaCppManagedLifecycle( options.bindings.containerName, options.contract, options.bindings.network.name, + options.bindings.hostPort, ); if (create.error || create.status !== 0 || created === null) { throw new Error( @@ -1580,6 +1620,7 @@ export function createDockerLlamaCppManagedLifecycle( created.id, options.contract, options.bindings.network.name, + options.bindings.hostPort, ); if (started === null || !started.running) { throw new Error("Docker llama.cpp start did not leave the exact runtime running.");