Skip to content
Merged
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
251 changes: 111 additions & 140 deletions src/lib/core/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export interface RuntimeAdapterPortValidationOptions extends GatewayPortValidati
gatewayPort: number;
}

type PortValidationOptions = GatewayPortValidationOptions | RuntimeAdapterPortValidationOptions;

/** Default OpenShell gateway port when NEMOCLAW_GATEWAY_PORT is unset. */
export const DEFAULT_GATEWAY_PORT = 8080;

/**
* The default port the OpenClaw dashboard listens on inside the sandbox.
* The sandbox image is built with CHAT_UI_URL=http://127.0.0.1:SANDBOX_DASHBOARD_PORT
Expand Down Expand Up @@ -97,59 +102,127 @@ export const HTTPS_PIN_RUNTIME_ADAPTER_PORT = parsePort(
11438,
);

export function validateGatewayPort(
interface ServicePortDefinition {
readonly envVar: string | null;
readonly label: string;
readonly defaultPort: number;
readonly reserveDefault: boolean;
readonly configuredPort: (options: PortValidationOptions) => number | undefined;
}

/**
* Services that participate in host-port collision validation. The gateway
* default is reusable after the gateway is reconfigured, while the dashboard
* allocation range and inference and adapter defaults remain reserved.
*/
const SERVICE_PORT_CATALOG: readonly ServicePortDefinition[] = [
{
envVar: "NEMOCLAW_GATEWAY_PORT",
label: "OpenShell gateway",
defaultPort: DEFAULT_GATEWAY_PORT,
reserveDefault: false,
configuredPort: (options) => ("gatewayPort" in options ? options.gatewayPort : undefined),
},
{
envVar: "NEMOCLAW_DASHBOARD_PORT",
label: "dashboard",
defaultPort: SANDBOX_DASHBOARD_PORT,
reserveDefault: false,
configuredPort: (options) => options.dashboardPort,
},
{
envVar: "NEMOCLAW_VLLM_PORT",
label: "vLLM / NIM inference",
defaultPort: 8000,
reserveDefault: true,
configuredPort: (options) => options.vllmPort,
},
{
envVar: null,
label: "llama.cpp inference",
defaultPort: LLAMA_CPP_PORT,
reserveDefault: true,
configuredPort: () => undefined,
},
{
envVar: "NEMOCLAW_OLLAMA_PORT",
label: "Ollama inference",
defaultPort: 11434,
reserveDefault: true,
configuredPort: (options) => options.ollamaPort,
},
{
envVar: "NEMOCLAW_OLLAMA_PROXY_PORT",
label: "Ollama auth proxy",
defaultPort: 11435,
reserveDefault: true,
configuredPort: (options) => options.ollamaProxyPort,
},
{
envVar: "NEMOCLAW_BEDROCK_RUNTIME_ADAPTER_PORT",
label: "Bedrock Runtime adapter",
defaultPort: 11436,
reserveDefault: true,
configuredPort: (options) => options.bedrockRuntimeAdapterPort,
},
{
envVar: "NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT",
label: "OpenRouter Runtime adapter",
defaultPort: 11437,
reserveDefault: true,
configuredPort: (options) => options.openrouterRuntimeAdapterPort,
},
{
envVar: "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_PORT",
label: "HTTPS Pin Runtime adapter",
defaultPort: 11438,
reserveDefault: true,
configuredPort: (options) => options.httpsPinRuntimeAdapterPort,
},
];

function validateServicePort(
envVar: string,
port: number,
options: GatewayPortValidationOptions,
options: PortValidationOptions,
serviceEnvVar: string,
): void {
if (port >= options.dashboardRangeStart && port <= options.dashboardRangeEnd) {
throw new Error(
`Invalid port: ${envVar}="${port}" — must not overlap the ${options.dashboardRangeStart}-${options.dashboardRangeEnd} dashboard port range`,
);
}

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 },
{ label: "Bedrock Runtime adapter", port: 11436 },
{ label: "OpenRouter Runtime adapter", port: 11437 },
{ label: "HTTPS Pin Runtime adapter", port: 11438 },
];
const reservedDefault = reservedDefaults.find((entry) => entry.port === port);
const reservedDefault = SERVICE_PORT_CATALOG.find(
(entry) => entry.reserveDefault && entry.envVar !== serviceEnvVar && entry.defaultPort === port,
);
if (reservedDefault) {
throw new Error(
`Invalid port: ${envVar}="${port}" — must not overlap the ${reservedDefault.label} default port (${reservedDefault.port})`,
`Invalid port: ${envVar}="${port}" — must not overlap the ${reservedDefault.label} default port (${reservedDefault.defaultPort})`,
);
}

const conflicts = [
{ 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 = conflicts.find((entry) => entry.port === port);
const conflict = SERVICE_PORT_CATALOG.find(
(entry) =>
entry.envVar !== null &&
entry.envVar !== serviceEnvVar &&
entry.configuredPort(options) === port,
);
if (conflict) {
throw new Error(
`Invalid port: ${envVar}="${port}" — conflicts with ${conflict.envVar} (${conflict.port})`,
`Invalid port: ${envVar}="${port}" — conflicts with ${conflict.envVar} (${port})`,
);
}
}

export function validateGatewayPort(
envVar: string,
port: number,
options: GatewayPortValidationOptions,
): void {
validateServicePort(envVar, port, options, "NEMOCLAW_GATEWAY_PORT");
}

export function parseGatewayPort(
envVar: string,
fallback: number,
Expand All @@ -165,101 +238,16 @@ export function validateOpenRouterRuntimeAdapterPort(
port: number,
options: RuntimeAdapterPortValidationOptions,
): void {
if (port >= options.dashboardRangeStart && port <= options.dashboardRangeEnd) {
throw new Error(
`Invalid port: ${envVar}="${port}" — must not overlap the ${options.dashboardRangeStart}-${options.dashboardRangeEnd} dashboard port range`,
);
}

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 },
{ label: "Bedrock Runtime adapter", port: 11436 },
{ label: "HTTPS Pin Runtime adapter", port: 11438 },
];
const reservedDefault = reservedDefaults.find((entry) => entry.port === port);
if (reservedDefault) {
throw new Error(
`Invalid port: ${envVar}="${port}" — must not overlap the ${reservedDefault.label} default port (${reservedDefault.port})`,
);
}

const conflicts = [
{ 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_HTTPS_PIN_RUNTIME_ADAPTER_PORT",
port: options.httpsPinRuntimeAdapterPort,
},
];
const conflict = conflicts.find((entry) => entry.port === port);
if (conflict) {
throw new Error(
`Invalid port: ${envVar}="${port}" — conflicts with ${conflict.envVar} (${conflict.port})`,
);
}
validateServicePort(envVar, port, options, "NEMOCLAW_OPENROUTER_RUNTIME_ADAPTER_PORT");
}

export function validateHttpsPinRuntimeAdapterPort(
envVar: string,
port: number,
options: RuntimeAdapterPortValidationOptions,
): void {
if (port >= options.dashboardRangeStart && port <= options.dashboardRangeEnd) {
throw new Error(
`Invalid port: ${envVar}="${port}" — must not overlap the ${options.dashboardRangeStart}-${options.dashboardRangeEnd} dashboard port range`,
);
}

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 },
{ label: "Bedrock Runtime adapter", port: 11436 },
{ label: "OpenRouter Runtime adapter", port: 11437 },
];
const reservedDefault = reservedDefaults.find((entry) => entry.port === port);
if (reservedDefault) {
throw new Error(
`Invalid port: ${envVar}="${port}" — must not overlap the ${reservedDefault.label} default port (${reservedDefault.port})`,
);
}

const conflicts = [
{ 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,
},
];
const conflict = conflicts.find((entry) => entry.port === port);
if (conflict) {
throw new Error(
`Invalid port: ${envVar}="${port}" — conflicts with ${conflict.envVar} (${conflict.port})`,
);
}
validateServicePort(envVar, port, options, "NEMOCLAW_HTTPS_PIN_RUNTIME_ADAPTER_PORT");
}

/** Default OpenShell gateway port when NEMOCLAW_GATEWAY_PORT is unset. */
export const DEFAULT_GATEWAY_PORT = 8080;
/** OpenShell gateway port (default 8080, override via NEMOCLAW_GATEWAY_PORT). */
export const GATEWAY_PORT = parseGatewayPort("NEMOCLAW_GATEWAY_PORT", DEFAULT_GATEWAY_PORT, {
dashboardPort: DASHBOARD_PORT,
Expand All @@ -277,26 +265,9 @@ export const GATEWAY_PORT = parseGatewayPort("NEMOCLAW_GATEWAY_PORT", DEFAULT_GA
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);
const conflict = SERVICE_PORT_CATALOG.find(
(entry) => entry.envVar !== null && entry.configuredPort(options) === 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})`,
Expand Down
Loading