Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion nemoclaw-blueprint/policies/presets/local-inference.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@

preset:
name: local-inference
description: "Local inference access (Ollama, vLLM) via host gateway"
description: "Local inference access (Ollama, vLLM, llama.cpp) through the OpenShell gateway"

network_policies:
local_inference:
name: local_inference
endpoints:
- host: host.openshell.internal
port: 8081
protocol: rest
enforcement: enforce
allowed_ips:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- host: host.openshell.internal
port: 11434
protocol: rest
Expand Down
7 changes: 7 additions & 0 deletions src/lib/adapters/http/curl-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import { assertEndpointResolvesPublic } from "../../inference/endpoint-ssrf-pref
import { validateCurlProbeArgs } from "./curl-args";

describe("validateCurlProbeArgs — credential-leak defence", () => {
it("allows a fixed response-byte cap for bounded llama.cpp probes (#8161)", () => {
expect(
validateCurlProbeArgs(["-sS", "--max-filesize", "262144", "http://127.0.0.1:8081/v1/models"])
.args,
).toEqual(["-sS", "--max-filesize", "262144"]);
});

it("rejects an inline Authorization header so credentials cannot reach argv", () => {
expect(() =>
validateCurlProbeArgs([
Expand Down
8 changes: 7 additions & 1 deletion src/lib/adapters/http/curl-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ const CURL_SAFE_FLAG_OPTIONS = new Set([
// genuinely need to follow redirects from a fixed, hardcoded host (e.g. the
// Ollama manifest probe) must opt in via CurlProbeArgOptions.allowRedirects.
const CURL_REDIRECT_FLAG_OPTIONS = new Set(["-L", "-sfL", "--location"]);
const CURL_SAFE_VALUE_OPTIONS = new Set(["--connect-timeout", "--max-time", "-X", "--request"]);
const CURL_SAFE_VALUE_OPTIONS = new Set([
"--connect-timeout",
"--max-time",
"--max-filesize",
"-X",
"--request",
]);
const CURL_FORBIDDEN_MULTI_TRANSFER_OPTIONS = new Set(["--next"]);
const CURL_SHORT_OPTIONS_WITH_VALUES = new Set(["-K", "-b", "-T", "-d", "-F", "-H", "-X"]);

Expand Down
21 changes: 21 additions & 0 deletions src/lib/core/ports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
parseGatewayPort,
parsePort,
validateHttpsPinRuntimeAdapterPort,
validateLlamaCppPortReservation,
validateOpenRouterRuntimeAdapterPort,
} from "./ports";

Expand Down Expand Up @@ -160,6 +161,26 @@ describe("parseGatewayPort", () => {
});
});

describe("validateLlamaCppPortReservation", () => {
it.each([
"gatewayPort",
"dashboardPort",
"vllmPort",
"ollamaPort",
"ollamaProxyPort",
"bedrockRuntimeAdapterPort",
"openrouterRuntimeAdapterPort",
"httpsPinRuntimeAdapterPort",
] as const)("rejects configured %s collision with fixed port 8081 (#8161)", (field) => {
expect(() =>
validateLlamaCppPortReservation({
...GATEWAY_VALIDATION_OPTIONS,
[field]: 8081,
}),
).toThrow(/fixed llama\.cpp inference port \(8081\)/);
});
});

describe("validateOpenRouterRuntimeAdapterPort", () => {
const ENV_KEY = "NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT";

Expand Down
51 changes: 51 additions & 0 deletions src/lib/core/ports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { LLAMA_CPP_PORT } from "../inference/llama-cpp/contract";

/**
* Central port configuration — override any port via environment variables.
* TypeScript counterpart of bin/lib/ports.js.
Expand Down Expand Up @@ -60,6 +62,8 @@ export const VLLM_PORT = parsePort("NEMOCLAW_VLLM_PORT", 8000);
export const OLLAMA_PORT = parsePort("NEMOCLAW_OLLAMA_PORT", 11434);
/** Ollama auth proxy port (default 11435, override via NEMOCLAW_OLLAMA_PROXY_PORT). */
export const OLLAMA_PROXY_PORT = parsePort("NEMOCLAW_OLLAMA_PROXY_PORT", 11435);
/** llama.cpp existing-server attachment port; fixed by the declarative serving contract. */
export { LLAMA_CPP_PORT };
/** Hermes OpenAI-compatible API port (manifest `forward_ports[1]` / start.sh `PUBLIC_PORT`); reserved — never a valid dashboard port, for any agent. (#4984) */
export const HERMES_OPENAI_API_PORT = 8642;
/** Bedrock Runtime adapter port (default 11436, override via NEMOCLAW_BEDROCK_RUNTIME_ADAPTER_PORT). */
Expand Down Expand Up @@ -90,6 +94,7 @@ export function validateGatewayPort(
}

const reservedDefaults = [
{ label: "llama.cpp inference", port: LLAMA_CPP_PORT },
{ label: "vLLM / NIM inference", port: 8000 },
{ label: "Ollama inference", port: 11434 },
{ label: "Ollama auth proxy", port: 11435 },
Expand Down Expand Up @@ -152,6 +157,7 @@ export function validateOpenRouterRuntimeAdapterPort(
}

const reservedDefaults = [
{ label: "llama.cpp inference", port: LLAMA_CPP_PORT },
{ label: "vLLM / NIM inference", port: 8000 },
{ label: "Ollama inference", port: 11434 },
{ label: "Ollama auth proxy", port: 11435 },
Expand Down Expand Up @@ -200,6 +206,7 @@ export function validateHttpsPinRuntimeAdapterPort(
}

const reservedDefaults = [
{ label: "llama.cpp inference", port: LLAMA_CPP_PORT },
{ label: "vLLM / NIM inference", port: 8000 },
{ label: "Ollama inference", port: 11434 },
{ label: "Ollama auth proxy", port: 11435 },
Expand Down Expand Up @@ -250,3 +257,47 @@ export const GATEWAY_PORT = parseGatewayPort("NEMOCLAW_GATEWAY_PORT", DEFAULT_GA
openrouterRuntimeAdapterPort: OPENROUTER_RUNTIME_ADAPTER_PORT,
httpsPinRuntimeAdapterPort: HTTPS_PIN_RUNTIME_ADAPTER_PORT,
});

/** Reject every configurable service collision with fixed llama.cpp attachment port 8081. */
export function validateLlamaCppPortReservation(
options: RuntimeAdapterPortValidationOptions,
): void {
const configuredPorts = [
{ envVar: "NEMOCLAW_GATEWAY_PORT", port: options.gatewayPort },
{ envVar: "NEMOCLAW_DASHBOARD_PORT", port: options.dashboardPort },
{ envVar: "NEMOCLAW_VLLM_PORT", port: options.vllmPort },
{ envVar: "NEMOCLAW_OLLAMA_PORT", port: options.ollamaPort },
{ envVar: "NEMOCLAW_OLLAMA_PROXY_PORT", port: options.ollamaProxyPort },
{
envVar: "NEMOCLAW_BEDROCK_RUNTIME_ADAPTER_PORT",
port: options.bedrockRuntimeAdapterPort,
},
{
envVar: "NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT",
port: options.openrouterRuntimeAdapterPort,
},
{
envVar: "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_PORT",
port: options.httpsPinRuntimeAdapterPort,
},
];
const conflict = configuredPorts.find(({ port }) => port === LLAMA_CPP_PORT);
if (conflict) {
throw new Error(
`Invalid port: ${conflict.envVar}="${LLAMA_CPP_PORT}" — conflicts with the fixed llama.cpp inference port (${LLAMA_CPP_PORT})`,
);
}
}

validateLlamaCppPortReservation({
gatewayPort: GATEWAY_PORT,
dashboardPort: DASHBOARD_PORT,
dashboardRangeStart: DASHBOARD_PORT_RANGE_START,
dashboardRangeEnd: DASHBOARD_PORT_RANGE_END,
vllmPort: VLLM_PORT,
ollamaPort: OLLAMA_PORT,
ollamaProxyPort: OLLAMA_PROXY_PORT,
bedrockRuntimeAdapterPort: BEDROCK_RUNTIME_ADAPTER_PORT,
openrouterRuntimeAdapterPort: OPENROUTER_RUNTIME_ADAPTER_PORT,
httpsPinRuntimeAdapterPort: HTTPS_PIN_RUNTIME_ADAPTER_PORT,
});
1 change: 1 addition & 0 deletions src/lib/credentials/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const KNOWN_CREDENTIAL_ENV_KEYS: readonly string[] = [
"GEMINI_API_KEY",
"COMPATIBLE_API_KEY",
"COMPATIBLE_ANTHROPIC_API_KEY",
"NEMOCLAW_LLAMACPP_LOCAL_TOKEN",
"BRAVE_API_KEY",
"TAVILY_API_KEY",
"GITHUB_TOKEN",
Expand Down
23 changes: 23 additions & 0 deletions src/lib/inference/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getSandboxInferenceConfig,
HERMES_PROVIDER_MODEL_OPTIONS,
INFERENCE_ROUTE_URL,
LLAMA_CPP_LOCAL_CREDENTIAL_ENV,
MANAGED_PROVIDER_ID,
OLLAMA_LOCAL_CREDENTIAL_ENV,
parseGatewayInference,
Expand Down Expand Up @@ -160,6 +161,28 @@ describe("inference selection config", () => {
expect(OLLAMA_LOCAL_CREDENTIAL_ENV).not.toBe(DEFAULT_ROUTE_CREDENTIAL_ENV);
});

it("maps llama.cpp attachment to inference.local with Chat Completions (#8161)", () => {
expect(getProviderSelectionConfig("llama-cpp-local", "team/model-alias")).toEqual({
endpointType: "custom",
endpointUrl: INFERENCE_ROUTE_URL,
ncpPartner: null,
model: "team/model-alias",
profile: DEFAULT_ROUTE_PROFILE,
credentialEnv: LLAMA_CPP_LOCAL_CREDENTIAL_ENV,
provider: "llama-cpp-local",
providerLabel: "Local llama.cpp",
});
expect(
getSandboxInferenceConfig("team/model-alias", "llama-cpp-local", "openai-responses"),
).toEqual({
providerKey: MANAGED_PROVIDER_ID,
primaryModelRef: `${MANAGED_PROVIDER_ID}/team/model-alias`,
inferenceBaseUrl: INFERENCE_ROUTE_URL,
inferenceApi: "openai-completions",
inferenceCompat: { supportsStore: false },
});
});

it("maps nvidia-nim to the sandbox inference route", () => {
expect(getProviderSelectionConfig("nvidia-nim", "nvidia/nemotron-3-super-120b-a12b")).toEqual({
endpointType: "custom",
Expand Down
10 changes: 10 additions & 0 deletions src/lib/inference/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { isSafeModelId, shouldSkipResponsesProbe } from "../validation";
import { LLAMA_CPP_CREDENTIAL_ENV } from "./llama-cpp/contract";
import { DEFAULT_OLLAMA_MODEL } from "./local";
import { OPENROUTER_CREDENTIAL_ENV, OPENROUTER_PROVIDER_NAME } from "./openrouter";

Expand Down Expand Up @@ -65,6 +66,7 @@ export const DEFAULT_ROUTE_CREDENTIAL_ENV = "OPENAI_API_KEY";
// never read the user's host OpenAI key for local providers. See GH #2519.
export const OLLAMA_LOCAL_CREDENTIAL_ENV = "NEMOCLAW_OLLAMA_PROXY_TOKEN";
export const VLLM_LOCAL_CREDENTIAL_ENV = "NEMOCLAW_VLLM_LOCAL_TOKEN";
export const LLAMA_CPP_LOCAL_CREDENTIAL_ENV = LLAMA_CPP_CREDENTIAL_ENV;
export const MANAGED_PROVIDER_ID = "inference";
export { DEFAULT_OLLAMA_MODEL };

Expand Down Expand Up @@ -215,6 +217,13 @@ export function getProviderSelectionConfig(
credentialEnv: OLLAMA_LOCAL_CREDENTIAL_ENV,
providerLabel: "Local Ollama",
};
case "llama-cpp-local":
return {
...base,
model: model || "llama-cpp-local",
credentialEnv: LLAMA_CPP_LOCAL_CREDENTIAL_ENV,
providerLabel: "Local llama.cpp",
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default:
return null;
}
Expand Down Expand Up @@ -276,6 +285,7 @@ export function getSandboxInferenceConfig(
};
break;
case "compatible-endpoint":
case "llama-cpp-local":
providerKey = MANAGED_PROVIDER_ID;
primaryModelRef = `${MANAGED_PROVIDER_ID}/${model}`;
inferenceCompat = {
Expand Down
48 changes: 48 additions & 0 deletions src/lib/inference/gateway-route-compatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,54 @@ describe("shared gateway inference route compatibility", () => {
});
});

it("preserves llama.cpp endpoint and completions API as durable route identity (#8161)", () => {
const result = discover(
discoveryRoute("llama-cpp-local", {
credentialEnv: "NEMOCLAW_LLAMACPP_LOCAL_TOKEN",
}),
[
sandbox("llama-peer", {
provider: "llama-cpp-local",
model: "team/model-alias",
endpointUrl: "http://127.0.0.1:8081/v1",
preferredInferenceApi: "openai-completions",
credentialEnv: "NEMOCLAW_LLAMACPP_LOCAL_TOKEN",
}),
],
);

expect(result).toEqual({
ok: true,
requiredModel: "team/model-alias",
requiredEndpointUrl: "http://127.0.0.1:8081/v1",
requiredInferenceApi: "openai-completions",
});
});

it("blocks a conflicting llama.cpp endpoint on a shared gateway (#8161)", () => {
const result = check(
route("llama-cpp-local", "team/model-alias", {
endpointUrl: "http://127.0.0.1:8081/v1",
preferredInferenceApi: "openai-completions",
credentialEnv: "NEMOCLAW_LLAMACPP_LOCAL_TOKEN",
}),
[
sandbox("llama-peer", {
provider: "llama-cpp-local",
model: "team/model-alias",
endpointUrl: "http://localhost:8082/v1",
preferredInferenceApi: "openai-completions",
credentialEnv: "NEMOCLAW_LLAMACPP_LOCAL_TOKEN",
}),
],
);

expect(result).toMatchObject({
ok: false,
conflicts: [{ sandboxName: "llama-peer", reason: "custom-endpoint" }],
});
});

it("blocks conflicting or unprovable discovery before a provider probe (#6315)", () => {
expect(discover(discoveryRoute("anthropic-prod"), [sandbox("stopped-peer")])).toMatchObject({
ok: false,
Expand Down
6 changes: 5 additions & 1 deletion src/lib/inference/gateway-route-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export type CurrentGatewayRouteDiscoveryPreflight = (
},
) => GatewayRouteDiscoveryResult;

const CUSTOM_ROUTE_PROVIDERS = new Set(["compatible-endpoint", "compatible-anthropic-endpoint"]);
const CUSTOM_ROUTE_PROVIDERS = new Set([
"compatible-endpoint",
"compatible-anthropic-endpoint",
"llama-cpp-local",
]);

const SUPPORTED_INFERENCE_APIS = new Set([
"openai-completions",
Expand Down
12 changes: 12 additions & 0 deletions src/lib/inference/llama-cpp/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/** Stable constants for operator-run llama.cpp existing-server attachment. */
export const LLAMA_CPP_SELECTION_KEY = "llama-cpp";
export const LLAMA_CPP_PROVIDER_NAME = "llama-cpp-local";
export const LLAMA_CPP_PROVIDER_LABEL = "Local llama.cpp";
export const LLAMA_CPP_CREDENTIAL_ENV = "NEMOCLAW_LLAMACPP_LOCAL_TOKEN";
export const LLAMA_CPP_PORT = 8081;
export const LLAMA_CPP_HOST_BASE_URL = `http://127.0.0.1:${LLAMA_CPP_PORT}`;
export const LLAMA_CPP_HOST_OPENAI_BASE_URL = `${LLAMA_CPP_HOST_BASE_URL}/v1`;
export const LLAMA_CPP_GATEWAY_BASE_URL = `http://host.openshell.internal:${LLAMA_CPP_PORT}/v1`;
Loading
Loading