fix(config): keep named profiles off the default gateway port (#122751)

* fix(config): derive profile gateway ports like the Mac app instead of defaulting to 18789

* fix(config): use canonical profile normalization
This commit is contained in:
Peter Steinberger
2026-08-12 11:47:44 -07:00
committed by GitHub
parent d17264f5fd
commit 8bbc0a9bc7
8 changed files with 48 additions and 24 deletions
@@ -82,6 +82,8 @@ struct AppProfile: Equatable, Sendable {
var defaultGatewayPort: Int {
guard let name else { return 18789 }
// Keep byte-for-byte aligned with src/config/paths.ts resolveGatewayPort so the app and CLI
// connect to the same profile Gateway.
var hash: UInt32 = 2_166_136_261
for byte in name.utf8 {
hash = (hash ^ UInt32(byte)) &* 16_777_619
+2 -3
View File
@@ -3,8 +3,8 @@ import {
resolveGatewayPublicOrigin,
type OpenClawConfig,
} from "openclaw/plugin-sdk/config-contracts";
import { resolveGatewayPort } from "openclaw/plugin-sdk/core";
const DEFAULT_GATEWAY_PORT = 18789;
type ViewerBaseUrlFieldName = "baseUrl" | "viewerBaseUrl";
export function buildViewerUrl(params: {
@@ -55,8 +55,7 @@ export function normalizeViewerBaseUrl(
function resolveGatewayBaseUrl(config: OpenClawConfig): string {
const scheme = config.gateway?.tls?.enabled ? "https" : "http";
const port =
typeof config.gateway?.port === "number" ? config.gateway.port : DEFAULT_GATEWAY_PORT;
const port = resolveGatewayPort(config);
const customHost = config.gateway?.customBindHost?.trim();
if (config.gateway?.bind === "custom" && customHost) {
@@ -1,6 +1,7 @@
// Mattermost plugin module implements interactions behavior.
import { createHmac } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { resolveGatewayPort } from "openclaw/plugin-sdk/core";
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
import {
normalizeOptionalString,
@@ -134,7 +135,7 @@ export function computeInteractionCallbackUrl(
if (callbackBaseUrl) {
return `${normalizeCallbackBaseUrl(callbackBaseUrl)}${path}`;
}
const port = typeof cfg?.gateway?.port === "number" ? cfg.gateway.port : 18789;
const port = resolveGatewayPort(cfg);
let host =
cfg?.gateway?.customBindHost && !isWildcardBindHost(cfg.gateway.customBindHost)
? cfg.gateway.customBindHost.trim()
@@ -2,7 +2,6 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const listSkillCommandsForAgents = vi.hoisted(() => vi.fn());
const parseTcpPort = vi.hoisted(() => vi.fn());
const fetchMattermostUserTeams = vi.hoisted(() => vi.fn());
const normalizeMattermostBaseUrl = vi.hoisted(() => vi.fn((value: string | undefined) => value));
const isSlashCommandsEnabled = vi.hoisted(() => vi.fn());
@@ -13,7 +12,6 @@ const activateSlashCommands = vi.hoisted(() => vi.fn());
vi.mock("./runtime-api.js", () => ({
listSkillCommandsForAgents,
parseTcpPort,
}));
vi.mock("./client.js", async () => {
@@ -60,7 +58,6 @@ describe("mattermost monitor slash", () => {
beforeEach(() => {
listSkillCommandsForAgents.mockReset();
parseTcpPort.mockReset();
fetchMattermostUserTeams.mockReset();
normalizeMattermostBaseUrl.mockClear();
isSlashCommandsEnabled.mockReset();
@@ -95,7 +92,6 @@ describe("mattermost monitor slash", () => {
vi.stubEnv("OPENCLAW_GATEWAY_PORT", "18888");
resolveSlashCommandConfig.mockReturnValue({ enabled: true, nativeSkills: true });
isSlashCommandsEnabled.mockReturnValue(true);
parseTcpPort.mockReturnValue(18888);
fetchMattermostUserTeams.mockResolvedValue([{ id: "team-1" }, { id: "team-2" }]);
resolveCallbackUrl.mockReturnValue("https://openclaw.test/slash");
listSkillCommandsForAgents.mockReturnValue([
@@ -171,7 +167,6 @@ describe("mattermost monitor slash", () => {
vi.stubEnv("OPENCLAW_GATEWAY_PORT", "65536");
resolveSlashCommandConfig.mockReturnValue({ enabled: true, nativeSkills: false });
isSlashCommandsEnabled.mockReturnValue(true);
parseTcpPort.mockReturnValue(null);
fetchMattermostUserTeams.mockResolvedValue([{ id: "team-1" }]);
resolveCallbackUrl.mockReturnValue("https://openclaw.test/slash");
registerSlashCommands.mockResolvedValue([{ token: "token-1", trigger: "ping" }]);
@@ -185,7 +180,6 @@ describe("mattermost monitor slash", () => {
botUserId: "bot-user",
});
expect(parseTcpPort).toHaveBeenCalledWith("65536");
expect(resolveCallbackUrl).toHaveBeenCalledWith(
expect.objectContaining({ gatewayPort: 18789 }),
);
@@ -194,7 +188,6 @@ describe("mattermost monitor slash", () => {
it("warns on loopback callback urls and reports partial team failures", async () => {
resolveSlashCommandConfig.mockReturnValue({ enabled: true, nativeSkills: false });
isSlashCommandsEnabled.mockReturnValue(true);
parseTcpPort.mockReturnValue(null);
fetchMattermostUserTeams.mockResolvedValue([{ id: "team-1" }, { id: "team-2" }]);
resolveCallbackUrl.mockReturnValue("http://127.0.0.1:18789/slash");
registerSlashCommands
@@ -1,4 +1,5 @@
// Mattermost plugin module implements monitor slash behavior.
import { resolveGatewayPort } from "openclaw/plugin-sdk/core";
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
import type { ResolvedMattermostAccount } from "./accounts.js";
import {
@@ -6,12 +7,7 @@ import {
normalizeMattermostBaseUrl,
type MattermostClient,
} from "./client.js";
import {
listSkillCommandsForAgents,
parseTcpPort,
type OpenClawConfig,
type RuntimeEnv,
} from "./runtime-api.js";
import { listSkillCommandsForAgents, type OpenClawConfig, type RuntimeEnv } from "./runtime-api.js";
import {
DEFAULT_COMMAND_SPECS,
isSlashCommandsEnabled,
@@ -150,11 +146,9 @@ export async function registerMattermostMonitorSlashCommands(params: {
try {
const teams = await fetchMattermostUserTeams(params.client, params.botUserId);
const envPort = parseTcpPort(process.env.OPENCLAW_GATEWAY_PORT);
const slashGatewayPort = envPort ?? params.cfg.gateway?.port ?? 18789;
const slashCallbackUrl = resolveCallbackUrl({
config: slashConfig,
gatewayPort: slashGatewayPort,
gatewayPort: resolveGatewayPort(params.cfg),
gatewayHost: params.cfg.gateway?.customBindHost ?? undefined,
});
@@ -37,4 +37,3 @@ export {
readRequestBodyWithLimit,
} from "openclaw/plugin-sdk/webhook-ingress";
export { isTrustedProxyAddress, resolveClientIp } from "openclaw/plugin-sdk/core";
export { parseTcpPort } from "openclaw/plugin-sdk/number-runtime";
+27 -1
View File
@@ -322,10 +322,36 @@ describe("oauth paths", () => {
describe("gateway port resolution", () => {
it("prefers numeric env values over config", () => {
expect(
resolveGatewayPort({ gateway: { port: 19002 } }, envWith({ OPENCLAW_GATEWAY_PORT: "19001" })),
resolveGatewayPort(
{ gateway: { port: 19002 } },
envWith({ OPENCLAW_GATEWAY_PORT: "19001", OPENCLAW_PROFILE: "work" }),
),
).toBe(19001);
expect(
resolveGatewayPort({ gateway: { port: 19002 } }, envWith({ OPENCLAW_PROFILE: "work" })),
).toBe(19002);
});
it.each([
{ profile: "ct2", expected: 45696 },
{ profile: "p1402", expected: 55636 },
{ profile: "p2380", expected: 55636 },
])("derives the byte-exact profile port for $profile", ({ profile, expected }) => {
const port = resolveGatewayPort({}, envWith({ OPENCLAW_PROFILE: profile }));
expect(port).toBe(expected);
expect(port).toBeGreaterThanOrEqual(20000);
expect(port).toBeLessThan(60000);
});
it.each([undefined, "default", "Default", "../escape"])(
"keeps the default port for profile %j",
(profile) => {
expect(resolveGatewayPort({}, envWith({ OPENCLAW_PROFILE: profile }))).toBe(
DEFAULT_GATEWAY_PORT,
);
},
);
it("accepts Compose-style IPv4 host publish values from env", () => {
expect(
resolveGatewayPort(
+12 -2
View File
@@ -2,7 +2,7 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolveProfileStateDir } from "../cli/profile-utils.js";
import { normalizeProfileName, resolveProfileStateDir } from "../cli/profile-utils.js";
import { resolveGatewayNativeServiceIdentityConflict } from "../daemon/constants.js";
import { resolveHomeRelativePath, resolveRequiredHomeDir } from "../infra/home-dir.js";
import { parseTcpPort } from "../infra/tcp-port.js";
@@ -468,5 +468,15 @@ export function resolveGatewayPort(
return configPort;
}
}
return DEFAULT_GATEWAY_PORT;
const profile = normalizeProfileName(env.OPENCLAW_PROFILE);
if (!profile) {
return DEFAULT_GATEWAY_PORT;
}
// Keep byte-for-byte aligned with AppProfile.defaultGatewayPort in
// apps/macos/Sources/OpenClaw/AppProfile.swift so both surfaces connect to the same Gateway.
let hash = 2_166_136_261;
for (const byte of Buffer.from(profile, "utf8")) {
hash = Math.imul(hash ^ byte, 16_777_619) >>> 0;
}
return 20_000 + (hash % 40_000);
}