Trim status startup gateway import graph

This commit is contained in:
Junebugg1214 2026-03-20 23:36:08 -04:00
parent e2318d3cd1
commit 03c54162b8
6 changed files with 131 additions and 101 deletions

View File

@ -134,8 +134,8 @@ vi.mock("../agents/memory-search.js", () => ({
resolveMemorySearchConfig: mocks.resolveMemorySearchConfig,
}));
vi.mock("../gateway/call.js", () => ({
buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
vi.mock("../gateway/connection-details.js", () => ({
buildGatewayConnectionDetailsFromConfig: mocks.buildGatewayConnectionDetails,
}));
vi.mock("../gateway/probe.js", () => ({

View File

@ -1,6 +1,9 @@
import { existsSync } from "node:fs";
import type { OpenClawConfig } from "../config/types.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import {
buildGatewayConnectionDetailsFromConfig,
type GatewayConnectionDetails,
} from "../gateway/connection-details.js";
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
import { probeGateway } from "../gateway/probe.js";
import type { MemoryProviderStatus } from "../memory/types.js";
@ -20,7 +23,7 @@ export type MemoryPluginStatus = {
};
export type GatewayProbeSnapshot = {
gatewayConnection: ReturnType<typeof buildGatewayConnectionDetails>;
gatewayConnection: GatewayConnectionDetails;
remoteUrlMissing: boolean;
gatewayMode: "local" | "remote";
gatewayProbeAuth: {
@ -60,7 +63,7 @@ export async function resolveGatewayProbeSnapshot(params: {
cfg: OpenClawConfig;
opts: { timeoutMs?: number; all?: boolean };
}): Promise<GatewayProbeSnapshot> {
const gatewayConnection = buildGatewayConnectionDetails({ config: params.cfg });
const gatewayConnection = buildGatewayConnectionDetailsFromConfig({ config: params.cfg });
const isRemoteMode = params.cfg.gateway?.mode === "remote";
const remoteUrlRaw =
typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url : "";

View File

@ -79,8 +79,11 @@ vi.mock("./status.scan.deps.runtime.js", () => ({
getMemorySearchManager: mocks.getMemorySearchManager,
}));
vi.mock("../gateway/connection-details.js", () => ({
buildGatewayConnectionDetailsFromConfig: mocks.buildGatewayConnectionDetails,
}));
vi.mock("../gateway/call.js", () => ({
buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
callGateway: mocks.callGateway,
}));

View File

@ -1,11 +1,6 @@
import { randomUUID } from "node:crypto";
import type { OpenClawConfig } from "../config/config.js";
import {
loadConfig,
resolveConfigPath,
resolveGatewayPort,
resolveStateDir,
} from "../config/config.js";
import { loadConfig, resolveConfigPath, resolveStateDir } from "../config/config.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
@ -18,6 +13,10 @@ import {
} from "../utils/message-channel.js";
import { VERSION } from "../version.js";
import { GatewayClient } from "./client.js";
import {
buildGatewayConnectionDetailsFromConfig,
type GatewayConnectionDetails,
} from "./connection-details.js";
import {
GatewaySecretRefUnavailableError,
resolveGatewayCredentialsFromConfig,
@ -32,7 +31,6 @@ import {
resolveLeastPrivilegeOperatorScopesForMethod,
type OperatorScope,
} from "./method-scopes.js";
import { isSecureWebSocketUrl } from "./net.js";
import { PROTOCOL_VERSION } from "./protocol/index.js";
type CallGatewayBaseOptions = {
@ -73,14 +71,6 @@ export type CallGatewayOptions = CallGatewayBaseOptions & {
scopes?: OperatorScope[];
};
export type GatewayConnectionDetails = {
url: string;
urlSource: string;
bindDetail?: string;
remoteFallbackNote?: string;
message: string;
};
function shouldAttachDeviceIdentityForGatewayCall(params: {
url: string;
token?: string;
@ -156,86 +146,12 @@ export function buildGatewayConnectionDetails(
} = {},
): GatewayConnectionDetails {
const config = options.config ?? loadConfig();
const configPath =
options.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode ? config.gateway?.remote : undefined;
const tlsEnabled = config.gateway?.tls?.enabled === true;
const localPort = resolveGatewayPort(config);
const bindMode = config.gateway?.bind ?? "loopback";
const scheme = tlsEnabled ? "wss" : "ws";
// Self-connections should always target loopback; bind mode only controls listener exposure.
const localUrl = `${scheme}://127.0.0.1:${localPort}`;
const cliUrlOverride =
typeof options.url === "string" && options.url.trim().length > 0
? options.url.trim()
: undefined;
const envUrlOverride = cliUrlOverride
? undefined
: (trimToUndefined(process.env.OPENCLAW_GATEWAY_URL) ??
trimToUndefined(process.env.CLAWDBOT_GATEWAY_URL));
const urlOverride = cliUrlOverride ?? envUrlOverride;
const remoteUrl =
typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined;
const remoteMisconfigured = isRemoteMode && !urlOverride && !remoteUrl;
const urlSourceHint =
options.urlSource ?? (cliUrlOverride ? "cli" : envUrlOverride ? "env" : undefined);
const url = urlOverride || remoteUrl || localUrl;
const urlSource = urlOverride
? urlSourceHint === "env"
? "env OPENCLAW_GATEWAY_URL"
: "cli --url"
: remoteUrl
? "config gateway.remote.url"
: remoteMisconfigured
? "missing gateway.remote.url (fallback local)"
: "local loopback";
const bindDetail = !urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
const remoteFallbackNote = remoteMisconfigured
? "Warn: gateway.mode=remote but gateway.remote.url is missing; set gateway.remote.url or switch gateway.mode=local."
: undefined;
const allowPrivateWs = process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1";
// Security check: block ALL insecure ws:// to non-loopback addresses (CWE-319, CVSS 9.8)
// This applies to the FINAL resolved URL, regardless of source (config, CLI override, etc).
// Both credentials and chat/conversation data must not be transmitted over plaintext to remote hosts.
if (!isSecureWebSocketUrl(url, { allowPrivateWs })) {
throw new Error(
[
`SECURITY ERROR: Gateway URL "${url}" uses plaintext ws:// to a non-loopback address.`,
"Both credentials and chat data would be exposed to network interception.",
`Source: ${urlSource}`,
`Config: ${configPath}`,
"Fix: Use wss:// for remote gateway URLs.",
"Safe remote access defaults:",
"- keep gateway.bind=loopback and use an SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host)",
"- or use Tailscale Serve/Funnel for HTTPS remote access",
allowPrivateWs
? undefined
: "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1",
"Doctor: openclaw doctor --fix",
"Docs: https://docs.openclaw.ai/gateway/remote",
].join("\n"),
);
}
const message = [
`Gateway target: ${url}`,
`Source: ${urlSource}`,
`Config: ${configPath}`,
bindDetail,
remoteFallbackNote,
]
.filter(Boolean)
.join("\n");
return {
url,
urlSource,
bindDetail,
remoteFallbackNote,
message,
};
return buildGatewayConnectionDetailsFromConfig({
config,
url: options.url,
configPath: options.configPath,
urlSource: options.urlSource,
});
}
type GatewayRemoteSettings = {

View File

@ -0,0 +1,101 @@
import { resolveConfigPath, resolveGatewayPort, resolveStateDir } from "../config/paths.js";
import type { OpenClawConfig } from "../config/types.js";
import { isSecureWebSocketUrl } from "./net.js";
export type GatewayConnectionDetails = {
url: string;
urlSource: string;
bindDetail?: string;
remoteFallbackNote?: string;
message: string;
};
function trimToUndefined(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
export function buildGatewayConnectionDetailsFromConfig(options: {
config: OpenClawConfig;
url?: string;
configPath?: string;
urlSource?: "cli" | "env";
}): GatewayConnectionDetails {
const configPath =
options.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
const isRemoteMode = options.config.gateway?.mode === "remote";
const remote = isRemoteMode ? options.config.gateway?.remote : undefined;
const tlsEnabled = options.config.gateway?.tls?.enabled === true;
const localPort = resolveGatewayPort(options.config);
const bindMode = options.config.gateway?.bind ?? "loopback";
const scheme = tlsEnabled ? "wss" : "ws";
// Self-connections should always target loopback; bind mode only controls listener exposure.
const localUrl = `${scheme}://127.0.0.1:${localPort}`;
const cliUrlOverride =
typeof options.url === "string" && options.url.trim().length > 0
? options.url.trim()
: undefined;
const envUrlOverride = cliUrlOverride
? undefined
: (trimToUndefined(process.env.OPENCLAW_GATEWAY_URL) ??
trimToUndefined(process.env.CLAWDBOT_GATEWAY_URL));
const urlOverride = cliUrlOverride ?? envUrlOverride;
const remoteUrl =
typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined;
const remoteMisconfigured = isRemoteMode && !urlOverride && !remoteUrl;
const urlSourceHint =
options.urlSource ?? (cliUrlOverride ? "cli" : envUrlOverride ? "env" : undefined);
const url = urlOverride || remoteUrl || localUrl;
const urlSource = urlOverride
? urlSourceHint === "env"
? "env OPENCLAW_GATEWAY_URL"
: "cli --url"
: remoteUrl
? "config gateway.remote.url"
: remoteMisconfigured
? "missing gateway.remote.url (fallback local)"
: "local loopback";
const bindDetail = !urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
const remoteFallbackNote = remoteMisconfigured
? "Warn: gateway.mode=remote but gateway.remote.url is missing; set gateway.remote.url or switch gateway.mode=local."
: undefined;
const allowPrivateWs = process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1";
if (!isSecureWebSocketUrl(url, { allowPrivateWs })) {
throw new Error(
[
`SECURITY ERROR: Gateway URL "${url}" uses plaintext ws:// to a non-loopback address.`,
"Both credentials and chat data would be exposed to network interception.",
`Source: ${urlSource}`,
`Config: ${configPath}`,
"Fix: Use wss:// for remote gateway URLs.",
"Safe remote access defaults:",
"- keep gateway.bind=loopback and use an SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host)",
"- or use Tailscale Serve/Funnel for HTTPS remote access",
allowPrivateWs
? undefined
: "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1",
"Doctor: openclaw doctor --fix",
"Docs: https://docs.openclaw.ai/gateway/remote",
].join("\n"),
);
}
const message = [
`Gateway target: ${url}`,
`Source: ${urlSource}`,
`Config: ${configPath}`,
bindDetail,
remoteFallbackNote,
]
.filter(Boolean)
.join("\n");
return {
url,
urlSource,
bindDetail,
remoteFallbackNote,
message,
};
}

View File

@ -12,6 +12,13 @@ vi.mock("../config/config.js", async (importOriginal) => {
return {
...actual,
loadConfig: loadConfigMock,
};
});
vi.mock("../config/paths.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/paths.js")>();
return {
...actual,
resolveGatewayPort: resolveGatewayPortMock,
};
});