mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 00:52:10 -06:00
fix(nodes): preserve gateway connections and local inference (#114857)
This commit is contained in:
committed by
GitHub
parent
3806c3866b
commit
9bcc44301b
@@ -121,6 +121,7 @@ describe("runNodeDaemonInstall", () => {
|
||||
|
||||
expect(mocks.buildNodeInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contextPath: undefined,
|
||||
tls: false,
|
||||
tlsFingerprint: undefined,
|
||||
}),
|
||||
@@ -149,11 +150,35 @@ describe("runNodeDaemonInstall", () => {
|
||||
|
||||
expect(mocks.buildNodeInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contextPath: "/saved",
|
||||
tls: true,
|
||||
tlsFingerprint: "saved-fingerprint",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("installs an explicitly plaintext node for a saved TLS gateway", async () => {
|
||||
await runNodeDaemonInstall({ force: true, tls: false });
|
||||
|
||||
expect(mocks.buildNodeInstallPlan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
host: "saved-gateway.local",
|
||||
port: 18789,
|
||||
contextPath: "/saved",
|
||||
tls: false,
|
||||
tlsFingerprint: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a TLS fingerprint when installing an explicitly plaintext node", async () => {
|
||||
await runNodeDaemonInstall({ force: true, tls: false, tlsFingerprint: "new-fingerprint" });
|
||||
|
||||
expect(mocks.buildNodeInstallPlan).not.toHaveBeenCalled();
|
||||
expect(mocks.runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("--no-tls cannot be combined with --tls-fingerprint"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runNodeDaemonStatus", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Node-host daemon lifecycle commands for install, status, start, stop, and restart.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { colorize } from "../../../packages/terminal-core/src/theme.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
@@ -33,10 +32,10 @@ import {
|
||||
failIfNixDaemonInstallMode,
|
||||
filterDaemonEnv,
|
||||
formatRuntimeStatus,
|
||||
parsePort,
|
||||
resolveRuntimeStatusColor,
|
||||
} from "../daemon-cli/shared.js";
|
||||
import { formatInvalidConfigPort, formatInvalidPortOption } from "../error-format.js";
|
||||
import { resolveNodeGatewayOptions } from "./gateway-options.js";
|
||||
|
||||
type NodeDaemonInstallOptions = {
|
||||
host?: string;
|
||||
@@ -78,30 +77,6 @@ function buildNodeRuntimeHints(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveNodeDefaults(
|
||||
opts: NodeDaemonInstallOptions,
|
||||
config: Awaited<ReturnType<typeof loadNodeHostConfig>>,
|
||||
) {
|
||||
// CLI flags override node-host config; missing values fall back to loopback Gateway defaults.
|
||||
const savedHost = config?.gateway?.host || "127.0.0.1";
|
||||
const host = normalizeOptionalString(opts.host) || savedHost;
|
||||
const retargeted = opts.host !== undefined || opts.port !== undefined;
|
||||
const portOverride = parsePort(opts.port);
|
||||
if (opts.port !== undefined && portOverride === null) {
|
||||
return { host, port: null, retargeted, endpointChanged: false };
|
||||
}
|
||||
const savedPort = config?.gateway?.port ?? 18789;
|
||||
const port = portOverride ?? savedPort;
|
||||
const endpointChanged =
|
||||
(opts.host !== undefined && host !== savedHost) ||
|
||||
(opts.port !== undefined && port !== savedPort);
|
||||
const explicitContextPath = opts.contextPath !== undefined;
|
||||
const contextPath =
|
||||
normalizeOptionalString(opts.contextPath) ||
|
||||
(explicitContextPath || retargeted ? undefined : config?.gateway?.contextPath);
|
||||
return { host, port, contextPath, retargeted, endpointChanged };
|
||||
}
|
||||
|
||||
export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
const { json, stdout, warnings, emit, fail } = createDaemonInstallActionContext(opts.json);
|
||||
if (failIfNixDaemonInstallMode(fail)) {
|
||||
@@ -109,7 +84,7 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
}
|
||||
|
||||
const config = await loadNodeHostConfig();
|
||||
const { host, port, contextPath, endpointChanged } = resolveNodeDefaults(opts, config);
|
||||
const { host, port, contextPath, tls, tlsFingerprint } = resolveNodeGatewayOptions(opts, config);
|
||||
if (!Number.isFinite(port ?? Number.NaN) || (port ?? 0) <= 0 || (port ?? 0) > 65_535) {
|
||||
fail(
|
||||
opts.port !== undefined
|
||||
@@ -118,6 +93,10 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (opts.tls === false && opts.tlsFingerprint !== undefined) {
|
||||
fail("--no-tls cannot be combined with --tls-fingerprint");
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeRaw = opts.runtime ? opts.runtime : DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||
if (!isGatewayDaemonRuntime(runtimeRaw)) {
|
||||
@@ -148,19 +127,14 @@ export async function runNodeDaemonInstall(opts: NodeDaemonInstallOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tlsFingerprint =
|
||||
normalizeOptionalString(opts.tlsFingerprint) ||
|
||||
(endpointChanged ? undefined : config?.gateway?.tlsFingerprint);
|
||||
const inheritedTls = endpointChanged ? undefined : config?.gateway?.tls;
|
||||
const tls = Boolean(opts.tls) || Boolean(tlsFingerprint) || Boolean(inheritedTls);
|
||||
const { programArguments, workingDirectory, environment, environmentValueSources, description } =
|
||||
await buildNodeInstallPlan({
|
||||
env: process.env,
|
||||
host,
|
||||
port: port ?? 18789,
|
||||
contextPath,
|
||||
tls,
|
||||
tlsFingerprint: tlsFingerprint || undefined,
|
||||
tls: Boolean(tls),
|
||||
tlsFingerprint,
|
||||
nodeId: opts.nodeId,
|
||||
displayName: opts.displayName,
|
||||
installedAppsSharing: opts.shareInstalledApps,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { NodeHostConfig } from "../../node-host/config.js";
|
||||
import { parsePort } from "../daemon-cli/shared.js";
|
||||
|
||||
type NodeGatewayOptions = {
|
||||
host?: string;
|
||||
port?: string | number;
|
||||
contextPath?: string;
|
||||
tls?: boolean;
|
||||
tlsFingerprint?: string;
|
||||
};
|
||||
|
||||
export function resolveNodeGatewayOptions(
|
||||
options: NodeGatewayOptions,
|
||||
config: NodeHostConfig | null,
|
||||
) {
|
||||
const savedHost = config?.gateway?.host || "127.0.0.1";
|
||||
const savedPort = config?.gateway?.port ?? 18789;
|
||||
const host = normalizeOptionalString(options.host) || savedHost;
|
||||
const port = options.port === undefined ? savedPort : parsePort(options.port);
|
||||
const endpointChanged = host !== savedHost || (port !== null && port !== savedPort);
|
||||
const tlsFingerprint =
|
||||
options.tls === false
|
||||
? undefined
|
||||
: (normalizeOptionalString(options.tlsFingerprint) ??
|
||||
(endpointChanged ? undefined : config?.gateway?.tlsFingerprint));
|
||||
const tls =
|
||||
typeof options.tls === "boolean"
|
||||
? options.tls
|
||||
: Boolean(tlsFingerprint) || (endpointChanged ? undefined : config?.gateway?.tls);
|
||||
const contextPath =
|
||||
normalizeOptionalString(options.contextPath) ??
|
||||
(options.contextPath !== undefined || endpointChanged
|
||||
? undefined
|
||||
: config?.gateway?.contextPath);
|
||||
|
||||
return { host, port, contextPath, tls, tlsFingerprint };
|
||||
}
|
||||
@@ -106,6 +106,62 @@ describe("registerNodeCli", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["host", ["--host", "10.0.0.2"]],
|
||||
["port", ["--port", "19001"]],
|
||||
])("preserves saved gateway settings when the explicit %s is unchanged", async (_name, args) => {
|
||||
daemonMocks.loadNodeHostConfig.mockResolvedValue({
|
||||
version: 1,
|
||||
nodeId: "node-existing",
|
||||
gateway: {
|
||||
host: "10.0.0.2",
|
||||
port: 19001,
|
||||
tls: true,
|
||||
tlsFingerprint: "saved-fingerprint",
|
||||
contextPath: "/saved",
|
||||
},
|
||||
});
|
||||
|
||||
await createProgram().parseAsync(["node", "run", ...args], { from: "user" });
|
||||
|
||||
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gatewayHost: "10.0.0.2",
|
||||
gatewayPort: 19001,
|
||||
gatewayTls: true,
|
||||
gatewayTlsFingerprint: "saved-fingerprint",
|
||||
gatewayContextPath: "/saved",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["host", ["--host", "10.0.0.3"]],
|
||||
["port", ["--port", "19002"]],
|
||||
])("clears saved gateway settings when the explicit %s changes", async (_name, args) => {
|
||||
daemonMocks.loadNodeHostConfig.mockResolvedValue({
|
||||
version: 1,
|
||||
nodeId: "node-existing",
|
||||
gateway: {
|
||||
host: "10.0.0.2",
|
||||
port: 19001,
|
||||
tls: true,
|
||||
tlsFingerprint: "saved-fingerprint",
|
||||
contextPath: "/saved",
|
||||
},
|
||||
});
|
||||
|
||||
await createProgram().parseAsync(["node", "run", ...args], { from: "user" });
|
||||
|
||||
expect(daemonMocks.runNodeHost).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
gatewayTls: undefined,
|
||||
gatewayTlsFingerprint: undefined,
|
||||
gatewayContextPath: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("inherits saved TLS settings only when using the saved gateway endpoint", async () => {
|
||||
daemonMocks.loadNodeHostConfig.mockResolvedValue({
|
||||
version: 1,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Commander registration for foreground node host and node service lifecycle commands.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { Command } from "commander";
|
||||
import { formatDocsLink } from "../../../packages/terminal-core/src/links.js";
|
||||
import { theme } from "../../../packages/terminal-core/src/theme.js";
|
||||
@@ -7,7 +6,6 @@ import { loadNodeHostConfig } from "../../node-host/config.js";
|
||||
import { runNodeHost } from "../../node-host/runner.js";
|
||||
import { runNodeHostWorker } from "../../node-host/worker.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { parsePort } from "../daemon-cli/shared.js";
|
||||
import { formatInvalidPortOption } from "../error-format.js";
|
||||
import { formatHelpExamples } from "../help-format.js";
|
||||
import {
|
||||
@@ -18,16 +16,9 @@ import {
|
||||
runNodeDaemonStop,
|
||||
runNodeDaemonUninstall,
|
||||
} from "./daemon.js";
|
||||
import { resolveNodeGatewayOptions } from "./gateway-options.js";
|
||||
import { runNodeIdentityShow } from "./identity.js";
|
||||
|
||||
function parsePortOption(value: unknown, fallback: number): number | null {
|
||||
// Undefined keeps config/default port; invalid explicit input returns null for CLI errors.
|
||||
if (value === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
return parsePort(value);
|
||||
}
|
||||
|
||||
export function registerNodeCli(program: Command) {
|
||||
const node = program
|
||||
.command("node")
|
||||
@@ -69,38 +60,26 @@ export function registerNodeCli(program: Command) {
|
||||
.option("--no-share-installed-apps", "Disable installed application sharing")
|
||||
.action(async (opts) => {
|
||||
const existing = await loadNodeHostConfig();
|
||||
const host =
|
||||
normalizeOptionalString(opts.host as string | undefined) ||
|
||||
existing?.gateway?.host ||
|
||||
"127.0.0.1";
|
||||
const port = parsePortOption(opts.port, existing?.gateway?.port ?? 18789);
|
||||
const { host, port, contextPath, tls, tlsFingerprint } = resolveNodeGatewayOptions(
|
||||
opts,
|
||||
existing,
|
||||
);
|
||||
if (port === null) {
|
||||
defaultRuntime.error(formatInvalidPortOption("--port"));
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const retargetedGateway = opts.host !== undefined || opts.port !== undefined;
|
||||
const explicitContextPath = opts.contextPath !== undefined;
|
||||
const explicitTlsDisabled = opts.tls === false;
|
||||
if (explicitTlsDisabled && opts.tlsFingerprint !== undefined) {
|
||||
if (opts.tls === false && opts.tlsFingerprint !== undefined) {
|
||||
defaultRuntime.error("--no-tls cannot be combined with --tls-fingerprint");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const tlsFingerprint =
|
||||
explicitTlsDisabled || retargetedGateway
|
||||
? opts.tlsFingerprint
|
||||
: (opts.tlsFingerprint ?? existing?.gateway?.tlsFingerprint);
|
||||
const inheritedTls = retargetedGateway ? undefined : existing?.gateway?.tls;
|
||||
await runNodeHost({
|
||||
gatewayHost: host,
|
||||
gatewayPort: port,
|
||||
gatewayTls:
|
||||
typeof opts.tls === "boolean" ? opts.tls : Boolean(tlsFingerprint) || inheritedTls,
|
||||
gatewayTls: tls,
|
||||
gatewayTlsFingerprint: tlsFingerprint,
|
||||
gatewayContextPath:
|
||||
normalizeOptionalString(opts.contextPath as string | undefined) ??
|
||||
(explicitContextPath || retargetedGateway ? undefined : existing?.gateway?.contextPath),
|
||||
gatewayContextPath: contextPath,
|
||||
nodeId: opts.nodeId,
|
||||
displayName: opts.displayName,
|
||||
installedAppsSharing: opts.shareInstalledApps,
|
||||
@@ -129,7 +108,8 @@ export function registerNodeCli(program: Command) {
|
||||
.option("--host <host>", "Gateway host")
|
||||
.option("--port <port>", "Gateway port")
|
||||
.option("--context-path <path>", "Gateway WebSocket context path (e.g. /openclaw-gw)")
|
||||
.option("--tls", "Use TLS for the gateway connection", false)
|
||||
.option("--tls", "Use TLS for the gateway connection")
|
||||
.option("--no-tls", "Disable TLS for the gateway connection")
|
||||
.option("--tls-fingerprint <sha256>", "Expected TLS certificate fingerprint (sha256)")
|
||||
.option("--node-id <id>", "Override the generated node instance id")
|
||||
.option("--display-name <name>", "Override node display name")
|
||||
|
||||
Reference in New Issue
Block a user