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,
|
resolveMemorySearchConfig: mocks.resolveMemorySearchConfig,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../gateway/call.js", () => ({
|
vi.mock("../gateway/connection-details.js", () => ({
|
||||||
buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
|
buildGatewayConnectionDetailsFromConfig: mocks.buildGatewayConnectionDetails,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../gateway/probe.js", () => ({
|
vi.mock("../gateway/probe.js", () => ({
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import type { OpenClawConfig } from "../config/types.js";
|
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 { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||||
import { probeGateway } from "../gateway/probe.js";
|
import { probeGateway } from "../gateway/probe.js";
|
||||||
import type { MemoryProviderStatus } from "../memory/types.js";
|
import type { MemoryProviderStatus } from "../memory/types.js";
|
||||||
@ -20,7 +23,7 @@ export type MemoryPluginStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayProbeSnapshot = {
|
export type GatewayProbeSnapshot = {
|
||||||
gatewayConnection: ReturnType<typeof buildGatewayConnectionDetails>;
|
gatewayConnection: GatewayConnectionDetails;
|
||||||
remoteUrlMissing: boolean;
|
remoteUrlMissing: boolean;
|
||||||
gatewayMode: "local" | "remote";
|
gatewayMode: "local" | "remote";
|
||||||
gatewayProbeAuth: {
|
gatewayProbeAuth: {
|
||||||
@ -60,7 +63,7 @@ export async function resolveGatewayProbeSnapshot(params: {
|
|||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
opts: { timeoutMs?: number; all?: boolean };
|
opts: { timeoutMs?: number; all?: boolean };
|
||||||
}): Promise<GatewayProbeSnapshot> {
|
}): Promise<GatewayProbeSnapshot> {
|
||||||
const gatewayConnection = buildGatewayConnectionDetails({ config: params.cfg });
|
const gatewayConnection = buildGatewayConnectionDetailsFromConfig({ config: params.cfg });
|
||||||
const isRemoteMode = params.cfg.gateway?.mode === "remote";
|
const isRemoteMode = params.cfg.gateway?.mode === "remote";
|
||||||
const remoteUrlRaw =
|
const remoteUrlRaw =
|
||||||
typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url : "";
|
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,
|
getMemorySearchManager: mocks.getMemorySearchManager,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../gateway/connection-details.js", () => ({
|
||||||
|
buildGatewayConnectionDetailsFromConfig: mocks.buildGatewayConnectionDetails,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../gateway/call.js", () => ({
|
vi.mock("../gateway/call.js", () => ({
|
||||||
buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
|
|
||||||
callGateway: mocks.callGateway,
|
callGateway: mocks.callGateway,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import {
|
import { loadConfig, resolveConfigPath, resolveStateDir } from "../config/config.js";
|
||||||
loadConfig,
|
|
||||||
resolveConfigPath,
|
|
||||||
resolveGatewayPort,
|
|
||||||
resolveStateDir,
|
|
||||||
} from "../config/config.js";
|
|
||||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||||
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
|
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
|
||||||
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
|
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
|
||||||
@ -18,6 +13,10 @@ import {
|
|||||||
} from "../utils/message-channel.js";
|
} from "../utils/message-channel.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { GatewayClient } from "./client.js";
|
import { GatewayClient } from "./client.js";
|
||||||
|
import {
|
||||||
|
buildGatewayConnectionDetailsFromConfig,
|
||||||
|
type GatewayConnectionDetails,
|
||||||
|
} from "./connection-details.js";
|
||||||
import {
|
import {
|
||||||
GatewaySecretRefUnavailableError,
|
GatewaySecretRefUnavailableError,
|
||||||
resolveGatewayCredentialsFromConfig,
|
resolveGatewayCredentialsFromConfig,
|
||||||
@ -32,7 +31,6 @@ import {
|
|||||||
resolveLeastPrivilegeOperatorScopesForMethod,
|
resolveLeastPrivilegeOperatorScopesForMethod,
|
||||||
type OperatorScope,
|
type OperatorScope,
|
||||||
} from "./method-scopes.js";
|
} from "./method-scopes.js";
|
||||||
import { isSecureWebSocketUrl } from "./net.js";
|
|
||||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||||
|
|
||||||
type CallGatewayBaseOptions = {
|
type CallGatewayBaseOptions = {
|
||||||
@ -73,14 +71,6 @@ export type CallGatewayOptions = CallGatewayBaseOptions & {
|
|||||||
scopes?: OperatorScope[];
|
scopes?: OperatorScope[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayConnectionDetails = {
|
|
||||||
url: string;
|
|
||||||
urlSource: string;
|
|
||||||
bindDetail?: string;
|
|
||||||
remoteFallbackNote?: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function shouldAttachDeviceIdentityForGatewayCall(params: {
|
function shouldAttachDeviceIdentityForGatewayCall(params: {
|
||||||
url: string;
|
url: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
@ -156,86 +146,12 @@ export function buildGatewayConnectionDetails(
|
|||||||
} = {},
|
} = {},
|
||||||
): GatewayConnectionDetails {
|
): GatewayConnectionDetails {
|
||||||
const config = options.config ?? loadConfig();
|
const config = options.config ?? loadConfig();
|
||||||
const configPath =
|
return buildGatewayConnectionDetailsFromConfig({
|
||||||
options.configPath ?? resolveConfigPath(process.env, resolveStateDir(process.env));
|
config,
|
||||||
const isRemoteMode = config.gateway?.mode === "remote";
|
url: options.url,
|
||||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
configPath: options.configPath,
|
||||||
const tlsEnabled = config.gateway?.tls?.enabled === true;
|
urlSource: options.urlSource,
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GatewayRemoteSettings = {
|
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 {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
loadConfig: loadConfigMock,
|
loadConfig: loadConfigMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../config/paths.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../config/paths.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
resolveGatewayPort: resolveGatewayPortMock,
|
resolveGatewayPort: resolveGatewayPortMock,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user