Trim status startup gateway import graph
This commit is contained in:
parent
e2318d3cd1
commit
03c54162b8
@ -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", () => ({
|
||||
|
||||
@ -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 : "";
|
||||
|
||||
@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
101
src/gateway/connection-details.ts
Normal file
101
src/gateway/connection-details.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user