diff --git a/src/lib/core/ports.ts b/src/lib/core/ports.ts index a3de3361225..fe43e128bc8 100644 --- a/src/lib/core/ports.ts +++ b/src/lib/core/ports.ts @@ -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 @@ -97,10 +102,90 @@ 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( @@ -108,48 +193,36 @@ 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 }, - { 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, @@ -165,48 +238,7 @@ 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( @@ -214,52 +246,8 @@ export function validateHttpsPinRuntimeAdapterPort( 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, @@ -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})`,