mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(gateway): align handshake client timeouts
This commit is contained in:
@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/TUI: keep `chat.history` off model-catalog discovery so initial Gateway-backed TUI history loads cannot block behind slow provider/plugin model scans on low-core hosts. Refs #73524. Thanks @harshcatsystems-collab.
|
||||
- Channels/WhatsApp: flag recently reconnected linked accounts in channel status even when the socket is currently healthy, so flapping WhatsApp Web sessions no longer look clean after a brief reconnect. Refs #73602. Thanks @Vksh07.
|
||||
- Gateway: expose `gateway.handshakeTimeoutMs` in config, schema, and docs while preserving `OPENCLAW_HANDSHAKE_TIMEOUT_MS` precedence, so loaded or low-powered hosts can tune local WebSocket pre-auth handshakes without patching dist files. Supersedes #51282; refs #73592 and #73652. Thanks @henry-the-frog.
|
||||
- Gateway/TUI/status: align configured and env-based WebSocket handshake budgets across local clients, probes, and fallback RPCs while preserving explicit status timeouts and paired-device auth fallback, so slow local gateways are not marked unreachable by a shorter client watchdog. Refs #73524, #73535, #73592, and #73602. Thanks @harshcatsystems-collab, @DJBlackhawk, and @Vksh07.
|
||||
- Agents/model selection: resolve slash-form aliases before provider/model parsing and keep alias-resolved primary models subject to transient provider cooldowns, so cron and persisted sessions do not retry cooled-down raw aliases. Fixes #73573 and #73657. Thanks @akai-shuuichi and @hashslingers.
|
||||
- Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.
|
||||
- Agents/Claude CLI doctor: scope workspace and project-dir checks to agents that actually use the Claude CLI runtime, so non-default Claude agents no longer make the default agent look Claude-backed. Fixes #73903. Thanks @bobfreeman1989.
|
||||
|
||||
@@ -550,19 +550,19 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
The reference client in `src/gateway/client.ts` uses these defaults. Values are
|
||||
stable across protocol v3 and are the expected baseline for third-party clients.
|
||||
|
||||
| Constant | Default | Source |
|
||||
| ----------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` |
|
||||
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
|
||||
| Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (clamp `250`–`15_000`) |
|
||||
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
|
||||
| Max reconnect backoff | `30_000` ms | `src/gateway/client.ts` (`scheduleReconnect`) |
|
||||
| Fast-retry clamp after device-token close | `250` ms | `src/gateway/client.ts` |
|
||||
| Force-stop grace before `terminate()` | `250` ms | `FORCE_STOP_TERMINATE_GRACE_MS` |
|
||||
| `stopAndWait()` default timeout | `1_000` ms | `STOP_AND_WAIT_TIMEOUT_MS` |
|
||||
| Default tick interval (pre `hello-ok`) | `30_000` ms | `src/gateway/client.ts` |
|
||||
| Tick-timeout close | code `4000` when silence exceeds `tickIntervalMs * 2` | `src/gateway/client.ts` |
|
||||
| `MAX_PAYLOAD_BYTES` | `25 * 1024 * 1024` (25 MB) | `src/gateway/server-constants.ts` |
|
||||
| Constant | Default | Source |
|
||||
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` |
|
||||
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
|
||||
| Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) |
|
||||
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
|
||||
| Max reconnect backoff | `30_000` ms | `src/gateway/client.ts` (`scheduleReconnect`) |
|
||||
| Fast-retry clamp after device-token close | `250` ms | `src/gateway/client.ts` |
|
||||
| Force-stop grace before `terminate()` | `250` ms | `FORCE_STOP_TERMINATE_GRACE_MS` |
|
||||
| `stopAndWait()` default timeout | `1_000` ms | `STOP_AND_WAIT_TIMEOUT_MS` |
|
||||
| Default tick interval (pre `hello-ok`) | `30_000` ms | `src/gateway/client.ts` |
|
||||
| Tick-timeout close | code `4000` when silence exceeds `tickIntervalMs * 2` | `src/gateway/client.ts` |
|
||||
| `MAX_PAYLOAD_BYTES` | `25 * 1024 * 1024` (25 MB) | `src/gateway/server-constants.ts` |
|
||||
|
||||
The server advertises the effective `policy.tickIntervalMs`, `policy.maxPayload`,
|
||||
and `policy.maxBufferedBytes` in `hello-ok`; clients should honor those values
|
||||
|
||||
@@ -58,6 +58,7 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
|
||||
url: bootstrap.url,
|
||||
token: bootstrap.auth.token,
|
||||
password: bootstrap.auth.password,
|
||||
preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs,
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
clientDisplayName: "ACP",
|
||||
clientVersion: "acp",
|
||||
|
||||
@@ -138,6 +138,43 @@ describe("probeGatewayStatus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards configured handshake timeout to the connect probe and status RPC", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
probeGatewayMock.mockReset();
|
||||
callGatewayMock.mockResolvedValueOnce({ status: "ok" });
|
||||
probeGatewayMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
auth: {
|
||||
role: "operator",
|
||||
scopes: ["operator.admin"],
|
||||
capability: "admin_capable",
|
||||
},
|
||||
});
|
||||
const config = { gateway: { handshakeTimeoutMs: 30_000 } };
|
||||
|
||||
await probeGatewayStatus({
|
||||
url: "ws://127.0.0.1:19191",
|
||||
token: "temp-token",
|
||||
config,
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
timeoutMs: 30_000,
|
||||
requireRpc: true,
|
||||
});
|
||||
|
||||
expect(probeGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to read-only when the status RPC succeeds but the auth probe is inconclusive", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
probeGatewayMock.mockReset();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "../../config/types.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { withProgress } from "../progress.js";
|
||||
|
||||
@@ -27,8 +28,10 @@ export async function probeGatewayStatus(opts: {
|
||||
url: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
config?: OpenClawConfig;
|
||||
tlsFingerprint?: string;
|
||||
timeoutMs: number;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
json?: boolean;
|
||||
requireRpc?: boolean;
|
||||
configPath?: string;
|
||||
@@ -50,6 +53,9 @@ export async function probeGatewayStatus(opts: {
|
||||
password: opts.password,
|
||||
},
|
||||
tlsFingerprint: opts.tlsFingerprint,
|
||||
...(opts.preauthHandshakeTimeoutMs !== undefined
|
||||
? { preauthHandshakeTimeoutMs: opts.preauthHandshakeTimeoutMs }
|
||||
: {}),
|
||||
timeoutMs: opts.timeoutMs,
|
||||
includeDetails: false,
|
||||
};
|
||||
@@ -60,6 +66,7 @@ export async function probeGatewayStatus(opts: {
|
||||
token: opts.token,
|
||||
password: opts.password,
|
||||
tlsFingerprint: opts.tlsFingerprint,
|
||||
...(opts.config ? { config: opts.config } : {}),
|
||||
method: "status",
|
||||
timeoutMs: opts.timeoutMs,
|
||||
...(opts.configPath ? { configPath: opts.configPath } : {}),
|
||||
|
||||
@@ -231,6 +231,31 @@ describe("gatherDaemonStatus", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses configured handshake timeout as the default daemon probe budget", async () => {
|
||||
daemonLoadedConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
tls: { enabled: true },
|
||||
handshakeTimeoutMs: 30_000,
|
||||
auth: { token: "daemon-token" },
|
||||
},
|
||||
};
|
||||
|
||||
await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: true,
|
||||
deep: false,
|
||||
});
|
||||
|
||||
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: daemonLoadedConfig,
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reuses the shared CLI config snapshot when the daemon uses the same config path", async () => {
|
||||
serviceReadCommand.mockResolvedValueOnce({
|
||||
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],
|
||||
|
||||
@@ -478,7 +478,9 @@ export async function gatherDaemonStatus(
|
||||
.catch(() => [])
|
||||
: [];
|
||||
|
||||
const timeoutMs = parseStrictPositiveInteger(opts.rpc.timeout ?? "10000") ?? 10_000;
|
||||
const timeoutMs =
|
||||
parseStrictPositiveInteger(opts.rpc.timeout ?? undefined) ??
|
||||
Math.max(10_000, daemonCfg.gateway?.handshakeTimeoutMs ?? 0);
|
||||
|
||||
const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true;
|
||||
const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled;
|
||||
@@ -513,10 +515,12 @@ export async function gatherDaemonStatus(
|
||||
url: gateway.probeUrl,
|
||||
token: daemonProbeAuth?.token,
|
||||
password: daemonProbeAuth?.password,
|
||||
config: daemonCfg,
|
||||
tlsFingerprint:
|
||||
shouldUseLocalTlsRuntime && tlsRuntime?.enabled
|
||||
? tlsRuntime.fingerprintSha256
|
||||
: undefined,
|
||||
preauthHandshakeTimeoutMs: daemonCfg.gateway?.handshakeTimeoutMs,
|
||||
timeoutMs,
|
||||
json: opts.rpc.json,
|
||||
requireRpc: opts.requireRpc,
|
||||
|
||||
@@ -794,6 +794,28 @@ describe("gateway-status command", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses configured handshake timeout as the default local probe budget", async () => {
|
||||
const { runtime } = createRuntimeCapture();
|
||||
probeGateway.mockClear();
|
||||
readBestEffortConfig.mockResolvedValueOnce({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
handshakeTimeoutMs: 30_000,
|
||||
auth: { mode: "token", token: "ltok" },
|
||||
},
|
||||
} as never);
|
||||
|
||||
await gatewayStatusCommand({ json: true }, asRuntimeEnv(runtime));
|
||||
|
||||
expect(probeGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps inactive local loopback probes on the short timeout in remote mode", async () => {
|
||||
const { runtime } = createRuntimeCapture();
|
||||
probeGateway.mockClear();
|
||||
|
||||
@@ -53,7 +53,8 @@ export async function gatewayStatusCommand(
|
||||
const startedAt = Date.now();
|
||||
const cfg = await readBestEffortConfig();
|
||||
const rich = isRich() && opts.json !== true;
|
||||
const overallTimeoutMs = parseTimeoutMs(opts.timeout, 3000);
|
||||
const defaultTimeoutMs = Math.max(3000, cfg.gateway?.handshakeTimeoutMs ?? 0);
|
||||
const overallTimeoutMs = parseTimeoutMs(opts.timeout, defaultTimeoutMs);
|
||||
const wideAreaDomain = resolveWideAreaDiscoveryDomain({
|
||||
configDomain: cfg.discovery?.wideArea?.domain,
|
||||
});
|
||||
|
||||
@@ -131,6 +131,7 @@ export async function runGatewayStatusProbePass(params: {
|
||||
target.kind === "localLoopback" && target.url.startsWith("wss://")
|
||||
? params.localTlsFingerprint
|
||||
: undefined,
|
||||
preauthHandshakeTimeoutMs: params.cfg.gateway?.handshakeTimeoutMs,
|
||||
timeoutMs: resolveProbeBudgetMs(params.overallTimeoutMs, target),
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -238,6 +238,91 @@ describe("resolveGatewayProbeSnapshot", () => {
|
||||
expect(result.gatewayProbeAuthWarning).toBe("warn");
|
||||
});
|
||||
|
||||
it("keeps the local status RPC fallback timeout aligned with configured handshake timeout", async () => {
|
||||
mocks.resolveGatewayProbeTarget.mockReturnValue({
|
||||
mode: "local",
|
||||
gatewayMode: "local",
|
||||
remoteUrlMissing: false,
|
||||
});
|
||||
mocks.probeGateway.mockResolvedValue({
|
||||
ok: false,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
connectLatencyMs: null,
|
||||
error: "timeout",
|
||||
close: null,
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "unknown",
|
||||
},
|
||||
health: null,
|
||||
status: null,
|
||||
presence: null,
|
||||
configSnapshot: null,
|
||||
});
|
||||
mocks.callGateway.mockResolvedValue({ sessions: 1 });
|
||||
|
||||
await resolveGatewayProbeSnapshot({
|
||||
cfg: { gateway: { handshakeTimeoutMs: 30_000 } },
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(mocks.probeGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
expect(mocks.callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { gateway: { handshakeTimeoutMs: 30_000 } },
|
||||
timeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not raise an explicit local status RPC fallback timeout", async () => {
|
||||
mocks.resolveGatewayProbeTarget.mockReturnValue({
|
||||
mode: "local",
|
||||
gatewayMode: "local",
|
||||
remoteUrlMissing: false,
|
||||
});
|
||||
mocks.probeGateway.mockResolvedValue({
|
||||
ok: false,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
connectLatencyMs: null,
|
||||
error: "timeout",
|
||||
close: null,
|
||||
auth: {
|
||||
role: null,
|
||||
scopes: [],
|
||||
capability: "unknown",
|
||||
},
|
||||
health: null,
|
||||
status: null,
|
||||
presence: null,
|
||||
configSnapshot: null,
|
||||
});
|
||||
mocks.callGateway.mockResolvedValue({ sessions: 1 });
|
||||
|
||||
await resolveGatewayProbeSnapshot({
|
||||
cfg: { gateway: { handshakeTimeoutMs: 30_000 } },
|
||||
opts: { timeoutMs: 1000 },
|
||||
});
|
||||
|
||||
expect(mocks.probeGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
timeoutMs: 1000,
|
||||
}),
|
||||
);
|
||||
expect(mocks.callGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
timeoutMs: 1000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lets callGateway reuse paired-device auth for local status RPC fallback", async () => {
|
||||
mocks.resolveGatewayProbeTarget.mockReturnValue({
|
||||
mode: "local",
|
||||
|
||||
@@ -120,10 +120,12 @@ async function applyLocalStatusRpcFallback(params: {
|
||||
password?: string;
|
||||
};
|
||||
timeoutMs: number;
|
||||
timeoutMsExplicit: boolean;
|
||||
}): Promise<GatewayProbeResult | null> {
|
||||
if (!shouldTryLocalStatusRpcFallback(params)) {
|
||||
return params.gatewayProbe;
|
||||
}
|
||||
const boundedFallbackTimeoutMs = Math.min(2000, Math.max(1000, params.timeoutMs));
|
||||
const status = await loadGatewayCallModule()
|
||||
.then(({ callGateway }) =>
|
||||
callGateway({
|
||||
@@ -131,7 +133,9 @@ async function applyLocalStatusRpcFallback(params: {
|
||||
method: "status",
|
||||
token: params.gatewayProbeAuth.token,
|
||||
password: params.gatewayProbeAuth.password,
|
||||
timeoutMs: Math.min(2000, Math.max(1000, params.timeoutMs)),
|
||||
timeoutMs: params.timeoutMsExplicit
|
||||
? boundedFallbackTimeoutMs
|
||||
: Math.max(params.cfg.gateway?.handshakeTimeoutMs ?? 0, boundedFallbackTimeoutMs),
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
}),
|
||||
@@ -206,13 +210,19 @@ export async function resolveGatewayProbeSnapshot(params: {
|
||||
)
|
||||
: { auth: {}, warning: undefined };
|
||||
let gatewayProbeAuthWarning = gatewayProbeAuthResolution.warning;
|
||||
const probeTimeoutMs = Math.min(params.opts.all ? 5000 : 2500, params.opts.timeoutMs ?? 10_000);
|
||||
const defaultProbeTimeoutMs = Math.max(
|
||||
params.opts.all ? 5000 : 2500,
|
||||
params.cfg.gateway?.handshakeTimeoutMs ?? 0,
|
||||
);
|
||||
const timeoutMsExplicit = params.opts.timeoutMs !== undefined;
|
||||
const probeTimeoutMs = params.opts.timeoutMs ?? defaultProbeTimeoutMs;
|
||||
const initialGatewayProbe = shouldProbe
|
||||
? await loadProbeGatewayModule()
|
||||
.then(({ probeGateway }) =>
|
||||
probeGateway({
|
||||
url: gatewayConnection.url,
|
||||
auth: gatewayProbeAuthResolution.auth,
|
||||
preauthHandshakeTimeoutMs: params.cfg.gateway?.handshakeTimeoutMs,
|
||||
timeoutMs: probeTimeoutMs,
|
||||
detailLevel: params.opts.detailLevel ?? "presence",
|
||||
}),
|
||||
@@ -226,6 +236,7 @@ export async function resolveGatewayProbeSnapshot(params: {
|
||||
gatewayProbe: initialGatewayProbe,
|
||||
gatewayProbeAuth: gatewayProbeAuthResolution.auth,
|
||||
timeoutMs: probeTimeoutMs,
|
||||
timeoutMsExplicit,
|
||||
});
|
||||
if (
|
||||
(params.opts.mergeAuthWarningIntoProbeError ?? true) &&
|
||||
|
||||
@@ -27,6 +27,7 @@ let lastClientOptions: {
|
||||
token?: string;
|
||||
password?: string;
|
||||
tlsFingerprint?: string;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
clientName?: string;
|
||||
clientDisplayName?: string;
|
||||
mode?: string;
|
||||
@@ -61,6 +62,7 @@ vi.mock("./client.js", () => ({
|
||||
url?: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
clientName?: string;
|
||||
clientDisplayName?: string;
|
||||
mode?: string;
|
||||
@@ -101,6 +103,7 @@ class StubGatewayClient {
|
||||
url?: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
clientName?: string;
|
||||
clientDisplayName?: string;
|
||||
mode?: string;
|
||||
@@ -838,6 +841,51 @@ describe("callGateway error details", () => {
|
||||
expect(errMessage).toContain("Bind: loopback");
|
||||
});
|
||||
|
||||
it("keeps the default wrapper timeout aligned with configured handshake timeout", async () => {
|
||||
startMode = "silent";
|
||||
getRuntimeConfig.mockReturnValue({
|
||||
gateway: { mode: "local", bind: "loopback", handshakeTimeoutMs: 30_000 },
|
||||
});
|
||||
setGatewayNetworkDefaults();
|
||||
|
||||
vi.useFakeTimers();
|
||||
let errMessage = "";
|
||||
const promise = callGateway({ method: "health" }).catch((caught) => {
|
||||
errMessage = caught instanceof Error ? caught.message : String(caught);
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(errMessage).toBe("");
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
await promise;
|
||||
|
||||
expect(errMessage).toContain("gateway timeout after 30000ms");
|
||||
});
|
||||
|
||||
it("keeps the default wrapper timeout aligned with env handshake timeout", async () => {
|
||||
const envSnapshot = captureEnv(["OPENCLAW_HANDSHAKE_TIMEOUT_MS"]);
|
||||
try {
|
||||
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS = "30000";
|
||||
startMode = "silent";
|
||||
setLocalLoopbackGatewayConfig();
|
||||
|
||||
vi.useFakeTimers();
|
||||
let errMessage = "";
|
||||
const promise = callGateway({ method: "health" }).catch((caught) => {
|
||||
errMessage = caught instanceof Error ? caught.message : String(caught);
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(errMessage).toBe("");
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
await promise;
|
||||
|
||||
expect(errMessage).toContain("gateway timeout after 30000ms");
|
||||
} finally {
|
||||
envSnapshot.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not overflow very large timeout values", async () => {
|
||||
startMode = "silent";
|
||||
setLocalLoopbackGatewayConfig();
|
||||
@@ -866,6 +914,17 @@ describe("callGateway error details", () => {
|
||||
expect(lastRequestOptions?.opts?.timeoutMs).toBe(45_000);
|
||||
});
|
||||
|
||||
it("passes configured gateway handshake timeout to the client watchdog", async () => {
|
||||
getRuntimeConfig.mockReturnValue({
|
||||
gateway: { mode: "local", bind: "loopback", handshakeTimeoutMs: 30_000 },
|
||||
});
|
||||
setGatewayNetworkDefaults();
|
||||
|
||||
await callGateway({ method: "health" });
|
||||
|
||||
expect(lastClientOptions?.preauthHandshakeTimeoutMs).toBe(30_000);
|
||||
});
|
||||
|
||||
it("does not inject wrapper timeout defaults into expectFinal requests", async () => {
|
||||
setLocalLoopbackGatewayConfig();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
type GatewayRemoteCredentialPrecedence,
|
||||
} from "./credentials.js";
|
||||
import { canSkipGatewayConfigLoad } from "./explicit-connection-policy.js";
|
||||
import { resolvePreauthHandshakeTimeoutMs } from "./handshake-timeouts.js";
|
||||
import {
|
||||
CLI_DEFAULT_OPERATOR_SCOPES,
|
||||
isGatewayMethodClassified,
|
||||
@@ -318,12 +319,30 @@ type ResolvedGatewayCallContext = {
|
||||
remotePasswordFallback?: GatewayRemoteCredentialFallback;
|
||||
};
|
||||
|
||||
function resolveGatewayCallTimeout(timeoutValue: unknown): {
|
||||
function resolveGatewayCallTimeout(
|
||||
timeoutValue: unknown,
|
||||
configuredHandshakeTimeoutMs?: number | null,
|
||||
): {
|
||||
timeoutMs: number;
|
||||
safeTimerTimeoutMs: number;
|
||||
} {
|
||||
const hasConfiguredHandshakeTimeout =
|
||||
typeof configuredHandshakeTimeoutMs === "number" &&
|
||||
Number.isFinite(configuredHandshakeTimeoutMs) &&
|
||||
configuredHandshakeTimeoutMs > 0;
|
||||
const hasEnvHandshakeTimeout =
|
||||
Boolean(process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS) ||
|
||||
Boolean(process.env.VITEST && process.env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS);
|
||||
const resolvedHandshakeTimeoutMs =
|
||||
hasConfiguredHandshakeTimeout || hasEnvHandshakeTimeout
|
||||
? resolvePreauthHandshakeTimeoutMs({ configuredTimeoutMs: configuredHandshakeTimeoutMs })
|
||||
: undefined;
|
||||
const timeoutMs =
|
||||
typeof timeoutValue === "number" && Number.isFinite(timeoutValue) ? timeoutValue : 10_000;
|
||||
typeof timeoutValue === "number" && Number.isFinite(timeoutValue)
|
||||
? timeoutValue
|
||||
: typeof resolvedHandshakeTimeoutMs === "number" && resolvedHandshakeTimeoutMs > 10_000
|
||||
? resolvedHandshakeTimeoutMs
|
||||
: 10_000;
|
||||
const safeTimerTimeoutMs = resolveSafeTimeoutDelayMs(timeoutMs);
|
||||
return { timeoutMs, safeTimerTimeoutMs };
|
||||
}
|
||||
@@ -505,12 +524,22 @@ async function executeGatewayRequestWithScopes<T>(params: {
|
||||
token?: string;
|
||||
password?: string;
|
||||
tlsFingerprint?: string;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
timeoutMs: number;
|
||||
safeTimerTimeoutMs: number;
|
||||
connectionDetails: GatewayConnectionDetails;
|
||||
}): Promise<T> {
|
||||
const { opts, scopes, url, token, password, tlsFingerprint, timeoutMs, safeTimerTimeoutMs } =
|
||||
params;
|
||||
const {
|
||||
opts,
|
||||
scopes,
|
||||
url,
|
||||
token,
|
||||
password,
|
||||
tlsFingerprint,
|
||||
preauthHandshakeTimeoutMs,
|
||||
timeoutMs,
|
||||
safeTimerTimeoutMs,
|
||||
} = params;
|
||||
// Yield to the event loop before starting the WebSocket connection.
|
||||
// On Windows with large dist bundles, heavy synchronous module loading
|
||||
// can starve the event loop, preventing timely processing of the
|
||||
@@ -539,6 +568,7 @@ async function executeGatewayRequestWithScopes<T>(params: {
|
||||
token,
|
||||
password,
|
||||
tlsFingerprint,
|
||||
preauthHandshakeTimeoutMs,
|
||||
instanceId: opts.instanceId ?? randomUUID(),
|
||||
clientName: opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI,
|
||||
clientDisplayName: resolveGatewayClientDisplayName(opts),
|
||||
@@ -593,8 +623,11 @@ async function callGatewayWithScopes<T = Record<string, unknown>>(
|
||||
opts: CallGatewayBaseOptions,
|
||||
scopes: OperatorScope[],
|
||||
): Promise<T> {
|
||||
const { timeoutMs, safeTimerTimeoutMs } = resolveGatewayCallTimeout(opts.timeoutMs);
|
||||
const context = resolveGatewayCallContext(opts);
|
||||
const { timeoutMs, safeTimerTimeoutMs } = resolveGatewayCallTimeout(
|
||||
opts.timeoutMs,
|
||||
context.config.gateway?.handshakeTimeoutMs,
|
||||
);
|
||||
const resolvedCredentials = await resolveGatewayCredentials(context);
|
||||
ensureExplicitGatewayAuth({
|
||||
urlOverride: context.urlOverride,
|
||||
@@ -621,6 +654,7 @@ async function callGatewayWithScopes<T = Record<string, unknown>>(
|
||||
token,
|
||||
password,
|
||||
tlsFingerprint,
|
||||
preauthHandshakeTimeoutMs: context.config.gateway?.handshakeTimeoutMs,
|
||||
timeoutMs,
|
||||
safeTimerTimeoutMs,
|
||||
connectionDetails,
|
||||
|
||||
@@ -51,6 +51,7 @@ describe("resolveGatewayClientBootstrap", () => {
|
||||
expect(result).toEqual({
|
||||
url: "wss://override.example/ws",
|
||||
urlSource: "cli --url",
|
||||
preauthHandshakeTimeoutMs: undefined,
|
||||
auth: {
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
@@ -84,4 +85,18 @@ describe("resolveGatewayClientBootstrap", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("carries configured preauth handshake timeout for GatewayClient callers", async () => {
|
||||
mockState.buildGatewayConnectionDetails.mockReturnValue({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "local loopback",
|
||||
});
|
||||
|
||||
const result = await resolveGatewayClientBootstrap({
|
||||
config: { gateway: { handshakeTimeoutMs: 30_000 } } as never,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(result.preauthHandshakeTimeoutMs).toBe(30_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ export async function resolveGatewayClientBootstrap(params: {
|
||||
}): Promise<{
|
||||
url: string;
|
||||
urlSource: string;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
auth: {
|
||||
token?: string;
|
||||
password?: string;
|
||||
@@ -41,6 +42,7 @@ export async function resolveGatewayClientBootstrap(params: {
|
||||
return {
|
||||
url: connection.url,
|
||||
urlSource: connection.urlSource,
|
||||
preauthHandshakeTimeoutMs: params.config.gateway?.handshakeTimeoutMs,
|
||||
auth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +127,11 @@ export type GatewayClientOptions = {
|
||||
connectChallengeTimeoutMs?: number;
|
||||
/** @deprecated Use connectChallengeTimeoutMs. */
|
||||
connectDelayMs?: number;
|
||||
/**
|
||||
* Server-side pre-auth handshake budget. Config-derived local clients use
|
||||
* this to keep the connect-challenge watchdog aligned with the gateway.
|
||||
*/
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
tickWatchMinIntervalMs?: number;
|
||||
requestTimeoutMs?: number;
|
||||
token?: string;
|
||||
@@ -190,9 +195,14 @@ function isGatewayClientStoppedError(err: unknown): boolean {
|
||||
}
|
||||
|
||||
export function resolveGatewayClientConnectChallengeTimeoutMs(
|
||||
opts: Pick<GatewayClientOptions, "connectChallengeTimeoutMs" | "connectDelayMs">,
|
||||
opts: Pick<
|
||||
GatewayClientOptions,
|
||||
"connectChallengeTimeoutMs" | "connectDelayMs" | "preauthHandshakeTimeoutMs"
|
||||
>,
|
||||
): number {
|
||||
return resolveConnectChallengeTimeoutMs(readConnectChallengeTimeoutOverride(opts));
|
||||
return resolveConnectChallengeTimeoutMs(readConnectChallengeTimeoutOverride(opts), {
|
||||
configuredTimeoutMs: opts.preauthHandshakeTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
const FORCE_STOP_TERMINATE_GRACE_MS = 250;
|
||||
|
||||
@@ -94,6 +94,17 @@ describe("GatewayClient", () => {
|
||||
connectChallengeTimeoutMs: 5_000,
|
||||
}),
|
||||
).toBe(5_000);
|
||||
expect(
|
||||
resolveGatewayClientConnectChallengeTimeoutMs({
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
}),
|
||||
).toBe(30_000);
|
||||
expect(
|
||||
resolveGatewayClientConnectChallengeTimeoutMs({
|
||||
connectChallengeTimeoutMs: 45_000,
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
}),
|
||||
).toBe(30_000);
|
||||
});
|
||||
|
||||
test("closes on missing ticks", async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("gateway handshake timeouts", () => {
|
||||
expect(clampConnectChallengeTimeoutMs(0)).toBe(MIN_CONNECT_CHALLENGE_TIMEOUT_MS);
|
||||
expect(clampConnectChallengeTimeoutMs(2_000)).toBe(2_000);
|
||||
expect(clampConnectChallengeTimeoutMs(20_000)).toBe(MAX_CONNECT_CHALLENGE_TIMEOUT_MS);
|
||||
expect(clampConnectChallengeTimeoutMs(30_000, 30_000)).toBe(30_000);
|
||||
});
|
||||
|
||||
test("prefers OPENCLAW_HANDSHAKE_TIMEOUT_MS and falls back on the test-only env", () => {
|
||||
@@ -107,17 +108,38 @@ describe("gateway handshake timeouts", () => {
|
||||
|
||||
test("resolveConnectChallengeTimeoutMs falls back to env override", () => {
|
||||
const original = process.env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS;
|
||||
const originalHandshake = process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS;
|
||||
try {
|
||||
process.env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS = "5000";
|
||||
expect(resolveConnectChallengeTimeoutMs()).toBe(5_000);
|
||||
// Explicit value still takes precedence over env
|
||||
expect(resolveConnectChallengeTimeoutMs(3_000)).toBe(3_000);
|
||||
process.env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS = "";
|
||||
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS = "30000";
|
||||
expect(resolveConnectChallengeTimeoutMs()).toBe(30_000);
|
||||
} finally {
|
||||
if (original === undefined) {
|
||||
delete process.env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS = original;
|
||||
}
|
||||
if (originalHandshake === undefined) {
|
||||
delete process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.OPENCLAW_HANDSHAKE_TIMEOUT_MS = originalHandshake;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveConnectChallengeTimeoutMs follows configured preauth timeout", () => {
|
||||
expect(
|
||||
resolveConnectChallengeTimeoutMs(undefined, { env: {}, configuredTimeoutMs: 30_000 }),
|
||||
).toBe(30_000);
|
||||
expect(resolveConnectChallengeTimeoutMs(45_000, { env: {}, configuredTimeoutMs: 30_000 })).toBe(
|
||||
30_000,
|
||||
);
|
||||
expect(resolveConnectChallengeTimeoutMs(0, { env: {}, configuredTimeoutMs: 30_000 })).toBe(
|
||||
MIN_CONNECT_CHALLENGE_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,13 @@ export const DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15_000;
|
||||
export const MIN_CONNECT_CHALLENGE_TIMEOUT_MS = 250;
|
||||
export const MAX_CONNECT_CHALLENGE_TIMEOUT_MS = DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS;
|
||||
|
||||
export function clampConnectChallengeTimeoutMs(timeoutMs: number): number {
|
||||
export function clampConnectChallengeTimeoutMs(
|
||||
timeoutMs: number,
|
||||
maxTimeoutMs = MAX_CONNECT_CHALLENGE_TIMEOUT_MS,
|
||||
): number {
|
||||
return Math.max(
|
||||
MIN_CONNECT_CHALLENGE_TIMEOUT_MS,
|
||||
Math.min(MAX_CONNECT_CHALLENGE_TIMEOUT_MS, timeoutMs),
|
||||
Math.min(Math.max(MIN_CONNECT_CHALLENGE_TIMEOUT_MS, maxTimeoutMs), timeoutMs),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,15 +25,32 @@ export function getConnectChallengeTimeoutMsFromEnv(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveConnectChallengeTimeoutMs(timeoutMs?: number | null): number {
|
||||
function normalizePositiveTimeoutMs(timeoutMs: unknown): number | undefined {
|
||||
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
||||
? timeoutMs
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolveConnectChallengeTimeoutMs(
|
||||
timeoutMs?: number | null,
|
||||
params?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
configuredTimeoutMs?: number | null;
|
||||
},
|
||||
): number {
|
||||
const configuredPreauthTimeoutMs = resolvePreauthHandshakeTimeoutMs({
|
||||
env: params?.env,
|
||||
configuredTimeoutMs: params?.configuredTimeoutMs,
|
||||
});
|
||||
const maxTimeoutMs = Math.max(DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS, configuredPreauthTimeoutMs);
|
||||
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
|
||||
return clampConnectChallengeTimeoutMs(timeoutMs);
|
||||
return clampConnectChallengeTimeoutMs(timeoutMs, maxTimeoutMs);
|
||||
}
|
||||
const envOverride = getConnectChallengeTimeoutMsFromEnv();
|
||||
const envOverride = getConnectChallengeTimeoutMsFromEnv(params?.env);
|
||||
if (envOverride !== undefined) {
|
||||
return clampConnectChallengeTimeoutMs(envOverride);
|
||||
return clampConnectChallengeTimeoutMs(envOverride, Math.max(maxTimeoutMs, envOverride));
|
||||
}
|
||||
return DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS;
|
||||
return clampConnectChallengeTimeoutMs(configuredPreauthTimeoutMs, maxTimeoutMs);
|
||||
}
|
||||
|
||||
export function getPreauthHandshakeTimeoutMsFromEnv(env: NodeJS.ProcessEnv = process.env): number {
|
||||
@@ -58,8 +78,8 @@ export function resolvePreauthHandshakeTimeoutMs(params?: {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
const configured = params?.configuredTimeoutMs;
|
||||
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
||||
const configured = normalizePositiveTimeoutMs(params?.configuredTimeoutMs);
|
||||
if (configured !== undefined) {
|
||||
return configured;
|
||||
}
|
||||
return DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS;
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function createOperatorApprovalsGatewayClient(
|
||||
url: bootstrap.url,
|
||||
token: bootstrap.auth.token,
|
||||
password: bootstrap.auth.password,
|
||||
preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs,
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
clientDisplayName: params.clientDisplayName,
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
|
||||
@@ -144,6 +144,7 @@ export async function probeGateway(opts: {
|
||||
url: string;
|
||||
auth?: GatewayProbeAuth;
|
||||
timeoutMs: number;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
includeDetails?: boolean;
|
||||
detailLevel?: "none" | "presence" | "full";
|
||||
tlsFingerprint?: string;
|
||||
@@ -255,6 +256,7 @@ export async function probeGateway(opts: {
|
||||
token: opts.auth?.token,
|
||||
password: opts.auth?.password,
|
||||
tlsFingerprint: opts.tlsFingerprint,
|
||||
preauthHandshakeTimeoutMs: opts.preauthHandshakeTimeoutMs,
|
||||
scopes: [READ_SCOPE],
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
clientVersion: "dev",
|
||||
|
||||
@@ -114,6 +114,7 @@ export class OpenClawChannelBridge {
|
||||
url: bootstrap.url,
|
||||
token: bootstrap.auth.token,
|
||||
password: bootstrap.auth.password,
|
||||
preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs,
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
clientDisplayName: "OpenClaw MCP",
|
||||
clientVersion: VERSION,
|
||||
|
||||
@@ -222,6 +222,7 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise<void> {
|
||||
url,
|
||||
token: token || undefined,
|
||||
password: password || undefined,
|
||||
preauthHandshakeTimeoutMs: cfg.gateway?.handshakeTimeoutMs,
|
||||
instanceId: nodeId,
|
||||
clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||
clientDisplayName: displayName,
|
||||
|
||||
@@ -166,9 +166,24 @@ describe("resolveGatewayConnection", () => {
|
||||
expect(result).toEqual({
|
||||
url: "wss://override.example/ws",
|
||||
...expected,
|
||||
preauthHandshakeTimeoutMs: undefined,
|
||||
allowInsecureLocalOperatorUi: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("carries configured handshake timeout to the TUI client connection", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
mode: "local",
|
||||
handshakeTimeoutMs: 30_000,
|
||||
auth: { token: "config-token" },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await resolveGatewayConnection({});
|
||||
|
||||
expect(result.preauthHandshakeTimeoutMs).toBe(30_000);
|
||||
});
|
||||
it("uses config auth token for local mode when both config and env tokens are set", async () => {
|
||||
loadConfig.mockReturnValue({ gateway: { mode: "local", auth: { token: "config-token" } } });
|
||||
|
||||
@@ -504,6 +519,7 @@ describe("GatewayChatClient", () => {
|
||||
const client = new GatewayChatClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: "test-token",
|
||||
preauthHandshakeTimeoutMs: 30_000,
|
||||
allowInsecureLocalOperatorUi: true,
|
||||
});
|
||||
|
||||
@@ -519,6 +535,10 @@ describe("GatewayChatClient", () => {
|
||||
(client as unknown as { client: { opts: { deviceIdentity?: unknown } } }).client.opts
|
||||
.deviceIdentity,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(client as unknown as { client: { opts: { preauthHandshakeTimeoutMs?: number } } }).client
|
||||
.opts.preauthHandshakeTimeoutMs,
|
||||
).toBe(30_000);
|
||||
});
|
||||
|
||||
it("retries startup-unavailable chat history until the gateway finishes booting", async () => {
|
||||
|
||||
@@ -49,6 +49,7 @@ type ResolvedGatewayConnection = {
|
||||
url: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
preauthHandshakeTimeoutMs?: number;
|
||||
allowInsecureLocalOperatorUi?: boolean;
|
||||
};
|
||||
|
||||
@@ -117,6 +118,7 @@ export class GatewayChatClient implements TuiBackend {
|
||||
url: connection.url,
|
||||
token: connection.token,
|
||||
password: connection.password,
|
||||
preauthHandshakeTimeoutMs: connection.preauthHandshakeTimeoutMs,
|
||||
clientName: GATEWAY_CLIENT_NAMES.TUI,
|
||||
clientDisplayName: "openclaw-tui",
|
||||
clientVersion: VERSION,
|
||||
@@ -284,6 +286,7 @@ export async function resolveGatewayConnection(
|
||||
url,
|
||||
token: explicitAuth.token,
|
||||
password: explicitAuth.password,
|
||||
preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
@@ -302,6 +305,7 @@ export async function resolveGatewayConnection(
|
||||
url,
|
||||
token: resolved.token,
|
||||
password: resolved.password,
|
||||
preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs,
|
||||
allowInsecureLocalOperatorUi: false,
|
||||
};
|
||||
}
|
||||
@@ -317,6 +321,7 @@ export async function resolveGatewayConnection(
|
||||
url,
|
||||
token: resolved.token,
|
||||
password: resolved.password,
|
||||
preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
@@ -341,6 +346,7 @@ export async function resolveGatewayConnection(
|
||||
url,
|
||||
token: resolved.token,
|
||||
password: resolved.password,
|
||||
preauthHandshakeTimeoutMs: config.gateway?.handshakeTimeoutMs,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user