fix(runtime): eliminate hardcoded 20128 port remnants and make loopback URLs dynamic (#13533)

* fix(runtime): eliminate hardcoded 20128 port remnants and make loopback URLs dynamic

- Make model assessment probe base URL resolve dynamically from getRuntimePorts() / env
- Support dynamic loopback in traffic inspector replay route and MITM handlers
- Make WebSocket live server allowlist dynamically include runtime PORT/DASHBOARD_PORT loopback origins
- Update CLI tools config/apply/letta-settings and tool-detector to adapt to configured runtime port
- Update client UI components (EndpointPageClient, ApiExplorerClient, RelayProxyClient) to use current window origin or dynamic port
- Make resolveOmniRouteBaseUrl, useDisplayBaseUrl, and wellKnown.ts respect configured port
- Update package.json electron:dev wait-on to use ${PORT:-20128}
- Add test coverage for custom port in resolveOmniRouteBaseUrl and liveServerAllowList
- Add changelog fragment for PR #13533

* fix(runtime): complete the truncated fallback comment in wellKnown.ts

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
ggdayup
2026-09-19 11:04:02 +08:00
committed by GitHub
parent bf0f213cf0
commit d715190bb0
25 changed files with 139 additions and 45 deletions

View File

@@ -0,0 +1 @@
- **fix(runtime):** eliminate hardcoded 20128 port remnants and make loopback URLs dynamic ([#13533](https://github.com/diegosouzapw/OmniRoute/pull/13533)) — thanks @ggdayup

View File

@@ -122,7 +122,7 @@
"lint:json": "node scripts/quality/run-eslint-json.mjs",
"lint:md": "npx --yes markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"!docs/i18n\" \"!docs/research\"",
"lint:prose": "vale docs",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:${PORT:-20128} && cd electron && npm run dev\"",
"electron:build": "npm run build && cd electron && npm run build",
"electron:build:win": "npm run build && cd electron && npm run build:win",
"electron:build:mac": "npm run build && cd electron && npm run build:mac",

View File

@@ -168,7 +168,9 @@ export default function APIPageClient({ machineId }: Readonly<APIPageClientProps
const [ngrokToken, setNgrokToken] = useState("");
const [showNgrokTunnel, setShowNgrokTunnel] = useState(true);
const [expandedTunnel, setExpandedTunnel] = useState<string | null>(null);
const [localApiUrl, setLocalApiUrl] = useState("http://localhost:20128/v1");
const [localApiUrl, setLocalApiUrl] = useState(
typeof window !== "undefined" ? `${window.location.origin}/v1` : "http://localhost:20128/v1"
);
const [lanUrls, setLanUrls] = useState<string[]>([]);
const [tailscaleIpUrl, setTailscaleIpUrl] = useState<string | null>(null);
const [activeEndpointTab, setActiveEndpointTab] = useState<EndpointTab>("apis");

View File

@@ -6,6 +6,7 @@ import Card from "@/shared/components/Card";
import Badge from "@/shared/components/Badge";
import Button from "@/shared/components/Button";
import { useNotificationStore } from "@/store/notificationStore";
import { useDisplayBaseUrl } from "@/shared/hooks";
interface RelayToken {
id: string;
@@ -23,6 +24,7 @@ interface RelayToken {
export default function RelayProxyClient() {
const t = useTranslations("relay");
const displayBaseUrl = useDisplayBaseUrl();
const [tokens, setTokens] = useState<RelayToken[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
@@ -193,7 +195,7 @@ export default function RelayProxyClient() {
<h2 className="text-sm font-semibold">{t("usage")}</h2>
<p className="text-xs text-text-muted">{t("usageDescription")}</p>
<pre className="text-xs bg-surface/50 border border-border rounded-lg p-3 overflow-x-auto">
{`curl http://localhost:20128/v1/relay/chat/completions \\
{`curl ${displayBaseUrl}/v1/relay/chat/completions \\
-H "Authorization: Bearer relay_..." \\
-H "Content-Type: application/json" \\
-d '{"model":"claude-sonnet-4","messages":[{"role":"user","content":"Hello"}]}'`}

View File

@@ -41,7 +41,8 @@ export default async function AgentBridgePage() {
try {
const base =
process.env.OMNIROUTE_BASE_URL ??
`http://127.0.0.1:${process.env.PORT ?? 20128}`;
process.env.BASE_URL ??
`http://127.0.0.1:${process.env.DASHBOARD_PORT ?? process.env.PORT ?? 20128}`;
const res = await fetch(`${base}/api/tools/agent-bridge/state`, {
cache: "no-store",
headers: { "x-internal-fetch": "1" },

View File

@@ -11,9 +11,18 @@ import {
import { validateBody } from "@/shared/validation/helpers";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
function getAssessBaseUrl(): string {
return (
process.env.OMNIROUTE_BASE_URL ??
process.env.OMNIROUTe_BASE_URL ??
process.env.BASE_URL ??
`http://localhost:${process.env.API_PORT ?? process.env.PORT ?? 20128}/v1`
);
}
const assessor = new Assessor(
process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? "",
process.env.OMNIROUTe_BASE_URL ?? "http://localhost:20128/v1"
process.env.OMNIROUTE_API_KEY ?? process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? "",
getAssessBaseUrl()
);
const categorizer = new Categorizer();
@@ -142,9 +151,12 @@ export async function GET(request: NextRequest) {
async function getAllModels(): Promise<Array<{ providerId: string; modelId: string }>> {
try {
const resp = await fetch("http://localhost:20128/v1/models", {
const baseUrl = getAssessBaseUrl();
const apiKey =
process.env.OMNIROUTE_API_KEY ?? process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? "";
const resp = await fetch(`${baseUrl}/models`, {
headers: {
Authorization: `Bearer ${process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? ""}`,
Authorization: `Bearer ${apiKey}`,
},
});
const data = (await resp.json()) as { data?: unknown };

View File

@@ -50,8 +50,14 @@ export async function POST(request: Request) {
const { toolId, baseUrl, apiKey, model, dryRun } = parsed.data;
const canonicalToolId = normalizeCliToolId(toolId);
const defaultPort = process.env.API_PORT || process.env.PORT || 20128;
const defaultBaseUrl =
process.env.OMNIROUTE_BASE_URL ||
process.env.BASE_URL ||
`http://localhost:${defaultPort}/v1`;
const result = await generateConfig(canonicalToolId, {
baseUrl: baseUrl || "http://localhost:20128/v1",
baseUrl: baseUrl || defaultBaseUrl,
apiKey,
model,
});

View File

@@ -16,7 +16,10 @@ export async function GET(request: Request) {
if (authError) return authError;
const { searchParams } = new URL(request.url);
const baseUrl = searchParams.get("baseUrl") || "http://localhost:20128/v1";
const defaultPort = process.env.API_PORT || process.env.PORT || 20128;
const defaultBaseUrl =
process.env.OMNIROUTE_BASE_URL || process.env.BASE_URL || `http://localhost:${defaultPort}/v1`;
const baseUrl = searchParams.get("baseUrl") || defaultBaseUrl;
const apiKey = searchParams.get("apiKey") || "";
if (!apiKey) {
@@ -46,9 +49,14 @@ export async function POST(request: Request) {
);
}
const { toolId, baseUrl, apiKey, model } = parsed.data;
const defaultPort = process.env.API_PORT || process.env.PORT || 20128;
const defaultBaseUrl =
process.env.OMNIROUTE_BASE_URL ||
process.env.BASE_URL ||
`http://localhost:${defaultPort}/v1`;
const result = await generateConfig(toolId, {
baseUrl: baseUrl || "http://localhost:20128/v1",
baseUrl: baseUrl || defaultBaseUrl,
apiKey,
model,
});

View File

@@ -74,7 +74,13 @@ const readAuthFile = async () => {
// ── Check if a base_url points to OmniRoute ──────────────────────────────
const isOmniRouteUrl = (baseUrl) => {
if (!baseUrl) return false;
return baseUrl.includes(":20128") || baseUrl.includes(":3000") || baseUrl.includes("omniroute");
const port = process.env.PORT || process.env.DASHBOARD_PORT;
return (
baseUrl.includes(":20128") ||
baseUrl.includes(":3000") ||
(!!port && baseUrl.includes(`:${port}`)) ||
baseUrl.includes("omniroute")
);
};
// ── Check if OmniRoute is configured ─────────────────────────────────────
@@ -122,10 +128,7 @@ export async function GET(request: Request) {
backendMode: settings.preferredBackendMode || "api",
});
} catch (error) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(error) } },
{ status: 500 }
);
return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
}
}
@@ -246,10 +249,7 @@ export async function POST(request: Request) {
needsRestart: true,
});
} catch (error) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(error) } },
{ status: 500 }
);
return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
}
}
@@ -321,9 +321,6 @@ export async function DELETE(request: Request) {
needsRestart: true,
});
} catch (error) {
return NextResponse.json(
{ error: { message: sanitizeErrorMessage(error) } },
{ status: 500 }
);
return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
}
}

View File

@@ -76,7 +76,7 @@ export async function POST(request: Request): Promise<Response> {
const chatBody = buildImproveChatBody(body);
// 5. Call /v1/chat/completions on ourselves (D8)
const port = process.env.PORT ?? "20128";
const port = process.env.API_PORT ?? process.env.PORT ?? "20128";
const baseUrl = process.env.OMNIROUTE_BASE_URL ?? `http://127.0.0.1:${port}`;
const upstreamUrl = `${baseUrl}/v1/chat/completions`;

View File

@@ -230,7 +230,8 @@ async function handleDisable(machineId: string, request: any) {
}
// Update Claude CLI settings to use local endpoint
const host = request.headers.get("host") || "localhost:20128";
const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || "20128";
const host = request.headers.get("host") || `localhost:${defaultPort}`;
await updateClaudeSettingsToLocal(machineId, host);
return NextResponse.json({

View File

@@ -15,7 +15,14 @@ interface Params {
params: Promise<{ id: string }>;
}
const OMNIROUTE_BASE = process.env.OMNIROUTE_BASE_URL ?? "http://127.0.0.1:20128";
function getOmnirouteBaseUrl(): string {
const port = process.env.API_PORT || process.env.PORT || 20128;
return (
process.env.OMNIROUTE_BASE_URL ||
process.env.BASE_URL ||
`http://127.0.0.1:${port}`
).replace(/\/+$/, "");
}
export async function POST(_request: Request, { params }: Params): Promise<Response> {
const { id } = await params;
@@ -27,7 +34,7 @@ export async function POST(_request: Request, { params }: Params): Promise<Respo
});
}
const url = `${OMNIROUTE_BASE}${entry.path}`;
const url = `${getOmnirouteBaseUrl()}${entry.path}`;
const replayHeaders: Record<string, string> = {
"content-type": "application/json",

View File

@@ -92,7 +92,9 @@ export function ApiExplorerClient() {
const t = useTranslations("docs");
const te = useTranslations("endpoint");
const [selected, setSelected] = useState<OpenApiEndpoint | null>(null);
const [baseUrl, setBaseUrl] = useState("http://localhost:20128");
const [baseUrl, setBaseUrl] = useState(
typeof window !== "undefined" ? window.location.origin : "http://localhost:20128"
);
const [apiKey, setApiKey] = useState("");
const [requestBody, setRequestBody] = useState("");
const [response, setResponse] = useState<string | null>(null);

View File

@@ -34,7 +34,9 @@ export class Assessor {
constructor(
apiKey: string,
baseUrl: string = "http://localhost:20128/v1",
baseUrl: string = process.env.OMNIROUTE_BASE_URL ??
process.env.BASE_URL ??
`http://localhost:${process.env.API_PORT ?? process.env.PORT ?? 20128}/v1`,
config: Partial<AssessmentConfig> = {}
) {
this.apiKey = apiKey;

View File

@@ -12,7 +12,12 @@ export interface LogStream {
}
export function createLogStream(options: LogStreamOptions = {}): LogStream {
const baseUrl = options.baseUrl || "http://localhost:20128";
const port = process.env.PORT || process.env.DASHBOARD_PORT || 20128;
const baseUrl =
options.baseUrl ||
process.env.OMNIROUTE_BASE_URL ||
process.env.BASE_URL ||
`http://localhost:${port}`;
const filters = options.filters || [];
const follow = options.follow ?? false;
const timeout = options.timeout || 30000;

View File

@@ -77,9 +77,11 @@ function expandHome(p: string): string {
function isConfigured(content: string, baseUrl: string): boolean {
const normalized = baseUrl.replace(/\/+$/, "");
const port = process.env.PORT || process.env.DASHBOARD_PORT;
return (
content.includes(normalized) ||
content.includes("localhost:20128") ||
(!!port && content.includes(`localhost:${port}`)) ||
content.includes("OMNIROUTE_BASE_URL")
);
}
@@ -170,7 +172,9 @@ export async function detectTool(id: string): Promise<DetectedTool | null> {
: getCliPrimaryConfigPath(tool.id) ||
(tool.id === "opencode" ? resolveOpencodeConfigPath() : "");
const configContents = await readConfigFile(configPath);
const configured = !!configContents && isConfigured(configContents, "http://localhost:20128");
const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || 20128;
const configured =
!!configContents && isConfigured(configContents, `http://localhost:${defaultPort}`);
const result: DetectedTool = {
id: canonicalId,
@@ -187,12 +191,14 @@ export async function detectTool(id: string): Promise<DetectedTool | null> {
try {
const roles = await getCurrentHermesAgentRoles();
const richRoles: Record<string, any> = {};
const currentPort = String(process.env.PORT || process.env.DASHBOARD_PORT || 20128);
Object.entries(roles).forEach(([role, info]) => {
const usingOmni =
info?.provider === "omniroute" ||
(info?.base_url || "").includes("20128") ||
(info?.base_url || "").includes("localhost:20128");
(info?.base_url || "").includes(currentPort) ||
(info?.base_url || "").includes("localhost");
richRoles[role] = {
model: info.model,

View File

@@ -10,5 +10,6 @@ export function getBaseUrl(request?: NextRequest | null): string {
if (process.env.OMNIROUTE_BASE_URL) return process.env.OMNIROUTE_BASE_URL;
// Direct route-handler invocation (unit tests, programmatic calls) passes no
// Request — fall back to the default local gateway origin instead of crashing.
return request?.nextUrl?.origin ?? "http://localhost:20128";
const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || 20128;
return request?.nextUrl?.origin ?? `http://localhost:${defaultPort}`;
}

View File

@@ -179,7 +179,9 @@ export abstract class MitmHandlerBase {
path: string,
headers: IncomingHttpHeaders,
): Promise<Response> {
const base = process.env.OMNIROUTE_BASE_URL ?? "http://127.0.0.1:20128";
const port = process.env.API_PORT || process.env.PORT || 20128;
const base =
process.env.OMNIROUTE_BASE_URL ?? process.env.BASE_URL ?? `http://127.0.0.1:${port}`;
const url = `${base.replace(/\/+$/, "")}${path}`;
const apiKey = process.env.ROUTER_API_KEY ?? "";

View File

@@ -42,7 +42,7 @@ const MITM_IDLE_TIMEOUT_MS =
const ROUTER_BASE_URL = (
process.env.OMNIROUTE_BASE_URL ||
process.env.BASE_URL ||
"http://localhost:20128"
`http://localhost:${process.env.API_PORT || process.env.PORT || 20128}`
)
.trim()
.replace(/\/+$/, "");

View File

@@ -3,10 +3,7 @@ import { PEER_IP_HEADER } from "@/server/authz/headers";
import { resolveStampedPeer } from "@/server/authz/peerStamp";
export type PublicOriginSource =
| "configured"
| "trusted-forwarded"
| "request-url"
| "direct-local-host";
"configured" | "trusted-forwarded" | "request-url" | "direct-local-host";
export interface PublicOriginCandidate {
origin: string;
@@ -200,7 +197,7 @@ function directLocalHostOrigin(request: Request): string | null {
if (classifyHostLocality(peer) === "remote") return null;
const rawHost = trustsForwardedHeaders(request)
? firstHeaderValue(request.headers.get("x-forwarded-host")) ?? request.headers.get("host")
? (firstHeaderValue(request.headers.get("x-forwarded-host")) ?? request.headers.get("host"))
: request.headers.get("host");
const host = sanitizeForwardedHost(rawHost);
if (!host) return null;
@@ -246,7 +243,8 @@ export function resolvePublicOrigin(request: Request): PublicOriginCandidate {
const requestOrigin = requestUrlOrigin(request);
if (requestOrigin) return { origin: requestOrigin, source: "request-url" };
return { origin: "http://localhost:20128", source: "request-url" };
const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || "20128";
return { origin: `http://localhost:${defaultPort}`, source: "request-url" };
}
export function validateBrowserMutationOrigin(request: Request): BrowserMutationOriginVerdict {

View File

@@ -45,7 +45,17 @@ export function parseCsvEnv(value: string | undefined | null): Set<string> {
*/
export function buildAllowedOrigins(env: NodeJS.ProcessEnv = process.env): Set<string> {
const extra = parseCsvEnv(env.LIVE_WS_ALLOWED_ORIGINS);
return new Set([...DEFAULT_ALLOWED_ORIGINS, ...extra]);
const runtimePort = env.PORT || env.DASHBOARD_PORT;
const dynamicDefaults: string[] = [];
if (runtimePort && runtimePort !== "20128") {
dynamicDefaults.push(
`http://127.0.0.1:${runtimePort}`,
`http://localhost:${runtimePort}`,
`http://[::1]:${runtimePort}`,
`http://0.0.0.0:${runtimePort}`
);
}
return new Set([...DEFAULT_ALLOWED_ORIGINS, ...dynamicDefaults, ...extra]);
}
/**

View File

@@ -209,7 +209,11 @@ export function resolveDisplayBaseUrl(
return joinOriginAndBasePath(configuredUrl, basePath);
}
const fallback = currentOrigin ?? configuredUrl ?? DEFAULT_DISPLAY_BASE_URL;
const portFallback =
typeof process !== "undefined" && (process.env.NEXT_PUBLIC_PORT || process.env.PORT)
? `http://localhost:${process.env.NEXT_PUBLIC_PORT || process.env.PORT}`
: DEFAULT_DISPLAY_BASE_URL;
const fallback = currentOrigin ?? configuredUrl ?? portFallback;
return joinOriginAndBasePath(fallback, basePath);
}

View File

@@ -4,6 +4,9 @@ type OmniRouteBaseUrlEnv = {
OMNIROUTE_BASE_URL?: string;
BASE_URL?: string;
NEXT_PUBLIC_BASE_URL?: string;
PORT?: string | number;
API_PORT?: string | number;
DASHBOARD_PORT?: string | number;
};
function normalizeBaseUrl(value?: string): string | null {
@@ -13,11 +16,14 @@ function normalizeBaseUrl(value?: string): string | null {
}
export function resolveOmniRouteBaseUrl(env: OmniRouteBaseUrlEnv = process.env): string {
const port = env.PORT || env.API_PORT || env.DASHBOARD_PORT;
const fallback = port ? `http://localhost:${port}` : DEFAULT_OMNIROUTE_BASE_URL;
return (
normalizeBaseUrl(env.OMNIROUTE_BASE_URL) ||
normalizeBaseUrl(env.BASE_URL) ||
normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) ||
DEFAULT_OMNIROUTE_BASE_URL
fallback
);
}

View File

@@ -50,3 +50,7 @@ test("resolveOmniRouteBaseUrl ignores blank values", () => {
test("resolveOmniRouteBaseUrl uses the default localhost fallback", () => {
assert.equal(resolveOmniRouteBaseUrl({}), DEFAULT_OMNIROUTE_BASE_URL);
});
test("resolveOmniRouteBaseUrl uses custom port when PORT env is set", () => {
assert.equal(resolveOmniRouteBaseUrl({ PORT: 37128 }), "http://localhost:37128");
});

View File

@@ -66,6 +66,18 @@ describe("buildAllowedOrigins", () => {
// Defaults remain.
assert.equal(out.has("http://localhost:20128"), true);
});
it("includes dynamic loopback origins when custom PORT is configured", () => {
const env = {
...EMPTY_ENV,
PORT: "37128",
};
const out = buildAllowedOrigins(env);
assert.equal(out.has("http://localhost:37128"), true);
assert.equal(out.has("http://127.0.0.1:37128"), true);
assert.equal(out.has("http://[::1]:37128"), true);
assert.equal(out.has("http://localhost:20128"), true);
});
});
describe("buildAllowedHosts", () => {
@@ -150,6 +162,11 @@ describe("isOriginAllowed", () => {
assert.equal(isOriginAllowed("http://100.96.135.160:20128", env), true);
});
it("does not treat a wildcard host as an allow-all origin policy", () => {
const env = { ...EMPTY_ENV, LIVE_WS_ALLOWED_HOSTS: "*" };
assert.equal(isOriginAllowed("http://100.90.139.116:37128", env), false);
});
it("does NOT accept a Tailscale Origin when LIVE_WS_ALLOWED_HOSTS is unset", () => {
// Critical security invariant: without explicit opt-in, the LAN/Tailscale
// surface is closed even though the listener is reachable.