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
13 changes: 13 additions & 0 deletions src/lib/actions/sandbox/destroy-host-local-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,19 @@ describe("sandbox destroy host-local inference transaction", () => {
expect(runtimeProvider.destroy).not.toHaveBeenCalled();
});

it("rejects a malformed durable receipt before sandbox deletion", async () => {
const runtimeProvider = provider();
const entry = sandbox("alpha", receipt(), { hostLocalInferenceReceipt: "not-json" });
const { result, runOpenshell, stopInferenceResources } = await runDestroy(runtimeProvider, {
entry,
});

expect(result).toMatchObject({ ok: false });
expect(runOpenshell).not.toHaveBeenCalled();
expect(stopInferenceResources).not.toHaveBeenCalled();
expect(runtimeProvider.destroy).not.toHaveBeenCalled();
});

it("preserves authority when the registry row is missing or reused after deletion", async () => {
const missingProvider = provider();
const missing = await runDestroy(missingProvider, { currentAfterDelete: null });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

import { hostLocalInferenceReceipt } from "../../../../test/helpers/host-local-inference-receipt";
import { createInMemoryRuntimeProviderBundle } from "../../../../test/helpers/runtime-provider-bundle";
Expand Down Expand Up @@ -197,14 +197,9 @@ function successfulRestore(

describe("snapshot command host-local inference authority", () => {
beforeEach(() => {
vi.clearAllMocks();
harness.events.length = 0;
});

afterEach(() => {
vi.restoreAllMocks();
});

it("re-proves authority before restore, at the mutation fence, and after success", async () => {
const receipt = receiptAtPort(8000);
harness.setRegistryEntry(sandbox(receipt));
Expand Down
14 changes: 14 additions & 0 deletions src/lib/inference/llama-cpp/managed-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,20 @@ describe("managed llama.cpp installer", () => {
const createLifecycle = vi.fn(() => lifecycle);
const operation = managedOperation(harness.engine, createLifecycle);
const runtimeProvider = managedRuntimeProvider(harness.engine, createLifecycle);
const mismatchedOperation = managedOperation(
{ ...harness.engine, engineId: "other-engine" },
createLifecycle,
);

expect(() =>
rehydrateManagedLlamaCppLifecycle({
runtimeProvider,
runtimeOwnerSandboxName: "spark-agent",
homeDir,
operation: mismatchedOperation,
}),
).toThrow("returned mismatched host-local-inference authority");
expect(createLifecycle).not.toHaveBeenCalled();

const rehydrated = rehydrateManagedLlamaCppLifecycle({
runtimeProvider,
Expand Down
14 changes: 6 additions & 8 deletions src/lib/inference/llama-cpp/managed-installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,15 +512,13 @@ export function rehydrateManagedLlamaCppLifecycle(
const receipt = loadManagedLlamaCppReceipt(paths);
if (!receipt) throw new Error("Managed llama.cpp private receipt is unavailable.");
const selection = resolveManagedLlamaCppOwnerSelection(owner);
const operation =
options.operation ??
requireRuntimeProviderHostLocalInferenceOperation(options.runtimeProvider, "llama-cpp", {
env: options.env ?? process.env,
});
const operation = requireRuntimeProviderHostLocalInferenceOperation(
options.runtimeProvider,
"llama-cpp",
{ env: options.env ?? process.env },
options.operation,
);
operation.assertAuthority();
if (operation.providerId !== options.runtimeProvider.identity.id) {
throw new Error("Managed llama.cpp runtime provider changed during lifecycle rehydration.");
}
const current = currentManagedLlamaCppArtifact(selection, homeDir);
return Object.freeze({
lifecycle: lifecycleFor({
Expand Down
14 changes: 8 additions & 6 deletions src/lib/inference/llama-cpp/managed-lifecycle-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ function temporaryHome(): string {
describe("managed llama.cpp lifecycle adapter", () => {
it("uses one exact operation and exposes rollback only before publication", () => {
const homeDir = temporaryHome();
const gatewayPort = 8080;
const harness = engineHarness();
createManagedState(homeDir, harness.engine);
const receipt = loadManagedLlamaCppReceipt(managedLlamaCppStatePaths(homeDir))!;
createManagedState(homeDir, harness.engine, { gatewayPort });
const receipt = loadManagedLlamaCppReceipt(managedLlamaCppStatePaths(homeDir, gatewayPort))!;
const operation: HostLocalInferenceOperation = {
providerId: "docker",
engine: harness.engine,
Expand Down Expand Up @@ -92,7 +93,7 @@ describe("managed llama.cpp lifecycle adapter", () => {
runtimeOwnerSandboxName: "spark-agent",
expectedModel: "llama-cpp-model",
expectedReceipt: receipt,
gatewayPort: 8080,
gatewayPort,
homeDir,
operation,
rehydrate: vi.fn(
Expand Down Expand Up @@ -130,9 +131,10 @@ describe("managed llama.cpp lifecycle adapter", () => {

it("uses the retained receipt for an idempotent destroy retry after private state is gone", () => {
const homeDir = temporaryHome();
const gatewayPort = 8080;
const harness = engineHarness();
createManagedState(homeDir, harness.engine);
const paths = managedLlamaCppStatePaths(homeDir);
createManagedState(homeDir, harness.engine, { gatewayPort });
const paths = managedLlamaCppStatePaths(homeDir, gatewayPort);
const receipt = loadManagedLlamaCppReceipt(paths)!;
fs.rmSync(paths.stateDir, { recursive: true });
const operation: HostLocalInferenceOperation = {
Expand All @@ -159,7 +161,7 @@ describe("managed llama.cpp lifecycle adapter", () => {
runtimeOwnerSandboxName: "spark-agent",
expectedModel: "llama-cpp-model",
expectedReceipt: receipt,
gatewayPort: 8080,
gatewayPort,
homeDir,
operation,
finalizeCleanup: (owner, expected, options) =>
Expand Down
3 changes: 2 additions & 1 deletion src/lib/onboard/runtime-provider/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ export function requireRuntimeProviderHostLocalInferenceOperation(
bundle: RuntimeProviderBundle,
service: HostLocalInferenceService,
input: HostLocalInferenceOperationInput,
candidate?: HostLocalInferenceOperation,
): HostLocalInferenceOperation {
const surface = bundle.hostLocalInference;
if (
Expand All @@ -749,7 +750,7 @@ export function requireRuntimeProviderHostLocalInferenceOperation(
`Runtime provider '${bundle.identity.id}' does not provide an operation-scoped host-local-inference engine for ${service}.`,
);
}
const operation = surface.createOperation(input);
const operation = candidate ?? surface.createOperation(input);
if (
operation.providerId !== bundle.identity.id ||
operation.engine.operation !== "host-local-inference" ||
Expand Down
7 changes: 6 additions & 1 deletion src/lib/onboard/setup-inference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,12 @@ function resolveHostLocalInferenceRoute(
}
const operation =
request.service === "llama-cpp"
? request.adapter.operation
? requireRuntimeProviderHostLocalInferenceOperation(
providerBundle,
request.service,
{ env: hostLocalInferenceOperationEnvironment(request.service) },
request.adapter.operation,
)
: requireRuntimeProviderHostLocalInferenceOperation(providerBundle, request.service, {
env: hostLocalInferenceOperationEnvironment(request.service),
acceleration:
Expand Down
33 changes: 33 additions & 0 deletions test/onboard-host-local-inference-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,39 @@ describe("onboard host-local inference routing", () => {
},
);

it("rejects injected llama.cpp engine drift before startup", async () => {
const route = llamaFixture("openclaw");
const request = route.selection.request as Extract<
HostLocalInferenceStartupSelection["request"],
{ service: "llama-cpp" }
>;
const selection: HostLocalInferenceStartupSelection = {
...route.selection,
request: {
...request,
adapter: {
...request.adapter,
operation: {
...request.adapter.operation,
engine: { ...request.adapter.operation.engine, engineId: "other-engine" },
},
},
},
};
const harness = createHarness();

await expect(
harness.setupInference(SANDBOX, MODEL, "llama-cpp-local", null, null, null, [], {
hostLocalInference: selection,
}),
).rejects.toThrow("EXIT_CALLED:1");

expect(route.prepareStartup).not.toHaveBeenCalled();
expect(route.prepareGatewayMutation).not.toHaveBeenCalled();
expect(harness.commands).toEqual([]);
expect(harness.errors.join(" ")).toContain("mismatched host-local-inference authority");
});

it.each([
["nim", 8001],
["vllm", 8000],
Expand Down
Loading