mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 07:20:59 +00:00
refactor: centralize node startup tls planning
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isNvmNode,
|
||||
isNodeVersionManagerRuntime,
|
||||
LINUX_CA_BUNDLE_PATHS,
|
||||
resolveAutoNodeExtraCaCerts,
|
||||
resolveLinuxSystemCaBundle,
|
||||
@@ -34,17 +34,19 @@ describe("resolveLinuxSystemCaBundle", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNvmNode", () => {
|
||||
describe("isNodeVersionManagerRuntime", () => {
|
||||
it("detects nvm via NVM_DIR", () => {
|
||||
expect(isNvmNode({ NVM_DIR: "/home/test/.nvm" }, "/usr/bin/node")).toBe(true);
|
||||
expect(isNodeVersionManagerRuntime({ NVM_DIR: "/home/test/.nvm" }, "/usr/bin/node")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects nvm via execPath", () => {
|
||||
expect(isNvmNode({}, "/home/test/.nvm/versions/node/v22/bin/node")).toBe(true);
|
||||
expect(isNodeVersionManagerRuntime({}, "/home/test/.nvm/versions/node/v22/bin/node")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for non-nvm node paths", () => {
|
||||
expect(isNvmNode({}, "/usr/bin/node")).toBe(false);
|
||||
expect(isNodeVersionManagerRuntime({}, "/usr/bin/node")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export const LINUX_CA_BUNDLE_PATHS = [
|
||||
"/etc/ssl/ca-bundle.pem",
|
||||
] as const;
|
||||
|
||||
type EnvMap = Record<string, string | undefined>;
|
||||
export type EnvMap = Record<string, string | undefined>;
|
||||
type AccessSyncFn = (path: string, mode?: number) => void;
|
||||
|
||||
export function resolveLinuxSystemCaBundle(
|
||||
@@ -32,7 +32,7 @@ export function resolveLinuxSystemCaBundle(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isNvmNode(
|
||||
export function isNodeVersionManagerRuntime(
|
||||
env: EnvMap = process.env as EnvMap,
|
||||
execPath: string = process.execPath,
|
||||
): boolean {
|
||||
@@ -57,7 +57,7 @@ export function resolveAutoNodeExtraCaCerts(
|
||||
|
||||
const platform = params.platform ?? process.platform;
|
||||
const execPath = params.execPath ?? process.execPath;
|
||||
if (platform !== "linux" || !isNvmNode(env, execPath)) {
|
||||
if (platform !== "linux" || !isNodeVersionManagerRuntime(env, execPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
79
src/bootstrap/node-startup-env.test.ts
Normal file
79
src/bootstrap/node-startup-env.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LINUX_CA_BUNDLE_PATHS } from "./node-extra-ca-certs.js";
|
||||
import { resolveNodeStartupTlsEnvironment } from "./node-startup-env.js";
|
||||
|
||||
function allowOnly(path: string) {
|
||||
return (candidate: string) => {
|
||||
if (candidate !== path) {
|
||||
throw new Error("ENOENT");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveNodeStartupTlsEnvironment", () => {
|
||||
it("defaults macOS launch env values", () => {
|
||||
expect(
|
||||
resolveNodeStartupTlsEnvironment({
|
||||
env: {},
|
||||
platform: "darwin",
|
||||
}),
|
||||
).toEqual({
|
||||
NODE_EXTRA_CA_CERTS: "/etc/ssl/cert.pem",
|
||||
NODE_USE_SYSTEM_CA: "1",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps user-provided env values", () => {
|
||||
expect(
|
||||
resolveNodeStartupTlsEnvironment({
|
||||
env: {
|
||||
NODE_EXTRA_CA_CERTS: "/custom/ca.pem",
|
||||
NODE_USE_SYSTEM_CA: "0",
|
||||
},
|
||||
platform: "darwin",
|
||||
}),
|
||||
).toEqual({
|
||||
NODE_EXTRA_CA_CERTS: "/custom/ca.pem",
|
||||
NODE_USE_SYSTEM_CA: "0",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves Linux CA env for version-manager Node runtimes", () => {
|
||||
expect(
|
||||
resolveNodeStartupTlsEnvironment({
|
||||
env: { NVM_DIR: "/home/test/.nvm" },
|
||||
platform: "linux",
|
||||
execPath: "/usr/bin/node",
|
||||
accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[1]),
|
||||
}),
|
||||
).toEqual({
|
||||
NODE_EXTRA_CA_CERTS: LINUX_CA_BUNDLE_PATHS[1],
|
||||
NODE_USE_SYSTEM_CA: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("can skip macOS defaults for CLI-only pre-start planning", () => {
|
||||
expect(
|
||||
resolveNodeStartupTlsEnvironment({
|
||||
env: {},
|
||||
platform: "darwin",
|
||||
includeDarwinDefaults: false,
|
||||
}),
|
||||
).toEqual({
|
||||
NODE_EXTRA_CA_CERTS: undefined,
|
||||
NODE_USE_SYSTEM_CA: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the Linux CA bundle heuristic when available", () => {
|
||||
const value = resolveNodeStartupTlsEnvironment({
|
||||
env: { NVM_DIR: "/home/test/.nvm" },
|
||||
platform: "linux",
|
||||
execPath: "/usr/bin/node",
|
||||
accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[2]),
|
||||
}).NODE_EXTRA_CA_CERTS;
|
||||
if (value !== undefined) {
|
||||
expect(LINUX_CA_BUNDLE_PATHS).toContain(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
38
src/bootstrap/node-startup-env.ts
Normal file
38
src/bootstrap/node-startup-env.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { type EnvMap, resolveAutoNodeExtraCaCerts } from "./node-extra-ca-certs.js";
|
||||
|
||||
export type NodeStartupTlsEnvironment = {
|
||||
NODE_EXTRA_CA_CERTS?: string;
|
||||
NODE_USE_SYSTEM_CA?: string;
|
||||
};
|
||||
|
||||
export function resolveNodeStartupTlsEnvironment(
|
||||
params: {
|
||||
env?: EnvMap;
|
||||
platform?: NodeJS.Platform;
|
||||
execPath?: string;
|
||||
includeDarwinDefaults?: boolean;
|
||||
accessSync?: (path: string, mode?: number) => void;
|
||||
} = {},
|
||||
): NodeStartupTlsEnvironment {
|
||||
const env = params.env ?? (process.env as EnvMap);
|
||||
const platform = params.platform ?? process.platform;
|
||||
const includeDarwinDefaults = params.includeDarwinDefaults ?? true;
|
||||
|
||||
const nodeExtraCaCerts =
|
||||
env.NODE_EXTRA_CA_CERTS ??
|
||||
(platform === "darwin" && includeDarwinDefaults
|
||||
? "/etc/ssl/cert.pem"
|
||||
: resolveAutoNodeExtraCaCerts({
|
||||
env,
|
||||
platform,
|
||||
execPath: params.execPath,
|
||||
accessSync: params.accessSync,
|
||||
}));
|
||||
const nodeUseSystemCa =
|
||||
env.NODE_USE_SYSTEM_CA ?? (platform === "darwin" && includeDarwinDefaults ? "1" : undefined);
|
||||
|
||||
return {
|
||||
NODE_EXTRA_CA_CERTS: nodeExtraCaCerts,
|
||||
NODE_USE_SYSTEM_CA: nodeUseSystemCa,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureFullEnv } from "../../test-utils/env.js";
|
||||
import type { DaemonActionResponse } from "./response.js";
|
||||
import { captureFullEnv } from "../../test-utils/env.js";
|
||||
|
||||
const resolveAutoNodeExtraCaCertsMock = vi.hoisted(() => vi.fn());
|
||||
const resolveNodeStartupTlsEnvironmentMock = vi.hoisted(() => vi.fn());
|
||||
const loadConfigMock = vi.hoisted(() => vi.fn());
|
||||
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
|
||||
const resolveGatewayPortMock = vi.hoisted(() => vi.fn(() => 18789));
|
||||
@@ -51,8 +51,8 @@ const service = vi.hoisted(() => ({
|
||||
readRuntime: vi.fn(async () => ({ status: "stopped" as const })),
|
||||
}));
|
||||
|
||||
vi.mock("../../bootstrap/node-extra-ca-certs.js", () => ({
|
||||
resolveAutoNodeExtraCaCerts: resolveAutoNodeExtraCaCertsMock,
|
||||
vi.mock("../../bootstrap/node-startup-env.js", () => ({
|
||||
resolveNodeStartupTlsEnvironment: resolveNodeStartupTlsEnvironmentMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
@@ -156,7 +156,7 @@ const envSnapshot = captureFullEnv();
|
||||
describe("runDaemonInstall", () => {
|
||||
beforeEach(() => {
|
||||
loadConfigMock.mockReset();
|
||||
resolveAutoNodeExtraCaCertsMock.mockReset();
|
||||
resolveNodeStartupTlsEnvironmentMock.mockReset();
|
||||
readConfigFileSnapshotMock.mockReset();
|
||||
resolveGatewayPortMock.mockClear();
|
||||
writeConfigFileMock.mockReset();
|
||||
@@ -198,7 +198,10 @@ describe("runDaemonInstall", () => {
|
||||
installDaemonServiceAndEmitMock.mockResolvedValue(undefined);
|
||||
service.isLoaded.mockResolvedValue(false);
|
||||
service.readCommand.mockResolvedValue(null);
|
||||
resolveAutoNodeExtraCaCertsMock.mockReturnValue(undefined);
|
||||
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
|
||||
NODE_EXTRA_CA_CERTS: undefined,
|
||||
NODE_USE_SYSTEM_CA: undefined,
|
||||
});
|
||||
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
});
|
||||
@@ -300,7 +303,10 @@ describe("runDaemonInstall", () => {
|
||||
|
||||
it("returns already-installed when the service already has the expected TLS env", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
resolveAutoNodeExtraCaCertsMock.mockReturnValue("/etc/ssl/certs/ca-certificates.crt");
|
||||
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
|
||||
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
|
||||
NODE_USE_SYSTEM_CA: undefined,
|
||||
});
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {
|
||||
@@ -316,7 +322,10 @@ describe("runDaemonInstall", () => {
|
||||
|
||||
it("reinstalls when an existing service is missing the nvm TLS CA bundle", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
resolveAutoNodeExtraCaCertsMock.mockReturnValue("/etc/ssl/certs/ca-certificates.crt");
|
||||
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
|
||||
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
|
||||
NODE_USE_SYSTEM_CA: undefined,
|
||||
});
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["openclaw", "gateway", "run"],
|
||||
environment: {},
|
||||
@@ -329,11 +338,13 @@ describe("runDaemonInstall", () => {
|
||||
|
||||
it("reinstalls when the installed service still runs from nvm even if the installer runtime does not", async () => {
|
||||
service.isLoaded.mockResolvedValue(true);
|
||||
resolveAutoNodeExtraCaCertsMock.mockImplementation(({ execPath }) =>
|
||||
typeof execPath === "string" && execPath.includes("/.nvm/")
|
||||
? "/etc/ssl/certs/ca-certificates.crt"
|
||||
: undefined,
|
||||
);
|
||||
resolveNodeStartupTlsEnvironmentMock.mockImplementation(({ execPath }) => ({
|
||||
NODE_EXTRA_CA_CERTS:
|
||||
typeof execPath === "string" && execPath.includes("/.nvm/")
|
||||
? "/etc/ssl/certs/ca-certificates.crt"
|
||||
: undefined,
|
||||
NODE_USE_SYSTEM_CA: undefined,
|
||||
}));
|
||||
service.readCommand.mockResolvedValue({
|
||||
programArguments: ["/home/test/.nvm/versions/node/v22.18.0/bin/node", "dist/entry.js"],
|
||||
environment: {},
|
||||
@@ -342,7 +353,7 @@ describe("runDaemonInstall", () => {
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
|
||||
expect(resolveAutoNodeExtraCaCertsMock).toHaveBeenCalledWith(
|
||||
expect(resolveNodeStartupTlsEnvironmentMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
execPath: "/home/test/.nvm/versions/node/v22.18.0/bin/node",
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { resolveAutoNodeExtraCaCerts } from "../../bootstrap/node-extra-ca-certs.js";
|
||||
import type { DaemonInstallOptions } from "./types.js";
|
||||
import type { DaemonInstallOptions } from "./types.js";
|
||||
import { resolveNodeStartupTlsEnvironment } from "../../bootstrap/node-startup-env.js";
|
||||
import { buildGatewayInstallPlan } from "../../commands/daemon-install-helpers.js";
|
||||
import {
|
||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||
@@ -16,7 +18,6 @@ import {
|
||||
failIfNixDaemonInstallMode,
|
||||
parsePort,
|
||||
} from "./shared.js";
|
||||
import type { DaemonInstallOptions } from "./types.js";
|
||||
|
||||
export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
const { json, stdout, warnings, emit, fail } = createDaemonInstallActionContext(opts.json);
|
||||
@@ -146,14 +147,15 @@ async function gatewayServiceNeedsAutoNodeExtraCaCertsRefresh(params: {
|
||||
}
|
||||
const currentEnvironment = currentCommand.environment ?? {};
|
||||
const currentNodeExtraCaCerts = currentEnvironment.NODE_EXTRA_CA_CERTS?.trim();
|
||||
const expectedNodeExtraCaCerts = resolveAutoNodeExtraCaCerts({
|
||||
const expectedNodeExtraCaCerts = resolveNodeStartupTlsEnvironment({
|
||||
env: {
|
||||
...params.env,
|
||||
...currentEnvironment,
|
||||
NODE_EXTRA_CA_CERTS: undefined,
|
||||
},
|
||||
execPath: currentExecPath,
|
||||
});
|
||||
includeDarwinDefaults: false,
|
||||
}).NODE_EXTRA_CA_CERTS;
|
||||
if (!expectedNodeExtraCaCerts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
buildServiceEnvironment,
|
||||
getMinimalServicePathParts,
|
||||
getMinimalServicePathPartsFromEnv,
|
||||
isNvmNode,
|
||||
isNodeVersionManagerRuntime,
|
||||
resolveLinuxSystemCaBundle,
|
||||
} from "./service-env.js";
|
||||
|
||||
@@ -534,17 +534,19 @@ describe("resolveGatewayStateDir", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isNvmNode", () => {
|
||||
describe("isNodeVersionManagerRuntime", () => {
|
||||
it("returns true when NVM_DIR env var is set", () => {
|
||||
expect(isNvmNode({ NVM_DIR: "/home/user/.nvm" })).toBe(true);
|
||||
expect(isNodeVersionManagerRuntime({ NVM_DIR: "/home/user/.nvm" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when execPath contains /.nvm/", () => {
|
||||
expect(isNvmNode({}, "/home/user/.nvm/versions/node/v22.22.0/bin/node")).toBe(true);
|
||||
expect(isNodeVersionManagerRuntime({}, "/home/user/.nvm/versions/node/v22.22.0/bin/node")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when neither NVM_DIR nor nvm execPath", () => {
|
||||
expect(isNvmNode({}, "/usr/bin/node")).toBe(false);
|
||||
expect(isNodeVersionManagerRuntime({}, "/usr/bin/node")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
isNvmNode,
|
||||
resolveAutoNodeExtraCaCerts,
|
||||
isNodeVersionManagerRuntime,
|
||||
resolveLinuxSystemCaBundle,
|
||||
} from "../bootstrap/node-extra-ca-certs.js";
|
||||
import { resolveNodeStartupTlsEnvironment } from "../bootstrap/node-startup-env.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import {
|
||||
GATEWAY_SERVICE_KIND,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
resolveNodeWindowsTaskName,
|
||||
} from "./constants.js";
|
||||
|
||||
export { isNvmNode, resolveLinuxSystemCaBundle };
|
||||
export { isNodeVersionManagerRuntime, resolveLinuxSystemCaBundle };
|
||||
|
||||
export type MinimalServicePathOptions = {
|
||||
platform?: NodeJS.Platform;
|
||||
@@ -346,16 +346,11 @@ function resolveSharedServiceEnvironmentFields(
|
||||
// cannot locate the system CA bundle. Default to /etc/ssl/cert.pem so TLS verification
|
||||
// works correctly when running as a LaunchAgent without extra user configuration.
|
||||
// On Linux, nvm-installed Node may need the host CA bundle injected before startup.
|
||||
const nodeCaCerts =
|
||||
env.NODE_EXTRA_CA_CERTS ??
|
||||
(platform === "darwin"
|
||||
? "/etc/ssl/cert.pem"
|
||||
: resolveAutoNodeExtraCaCerts({
|
||||
env,
|
||||
platform,
|
||||
execPath,
|
||||
}));
|
||||
const nodeUseSystemCa = env.NODE_USE_SYSTEM_CA ?? (platform === "darwin" ? "1" : undefined);
|
||||
const startupTlsEnv = resolveNodeStartupTlsEnvironment({
|
||||
env,
|
||||
platform,
|
||||
execPath,
|
||||
});
|
||||
return {
|
||||
stateDir,
|
||||
configPath,
|
||||
@@ -367,7 +362,7 @@ function resolveSharedServiceEnvironmentFields(
|
||||
? undefined
|
||||
: buildMinimalServicePath({ env, platform, extraDirs: extraPathDirs }),
|
||||
proxyEnv,
|
||||
nodeCaCerts,
|
||||
nodeUseSystemCa,
|
||||
nodeCaCerts: startupTlsEnv.NODE_EXTRA_CA_CERTS,
|
||||
nodeUseSystemCa: startupTlsEnv.NODE_USE_SYSTEM_CA,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveAutoNodeExtraCaCerts } from "./bootstrap/node-extra-ca-certs.js";
|
||||
import { resolveNodeStartupTlsEnvironment } from "./bootstrap/node-startup-env.js";
|
||||
import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js";
|
||||
import { isTruthyEnvValue } from "./infra/env.js";
|
||||
|
||||
@@ -45,10 +45,11 @@ export function buildCliRespawnPlan(
|
||||
|
||||
const autoNodeExtraCaCerts =
|
||||
params.autoNodeExtraCaCerts ??
|
||||
resolveAutoNodeExtraCaCerts({
|
||||
resolveNodeStartupTlsEnvironment({
|
||||
env,
|
||||
execPath,
|
||||
});
|
||||
includeDarwinDefaults: false,
|
||||
}).NODE_EXTRA_CA_CERTS;
|
||||
if (
|
||||
autoNodeExtraCaCerts &&
|
||||
!isTruthyEnvValue(env[OPENCLAW_NODE_EXTRA_CA_CERTS_READY]) &&
|
||||
|
||||
Reference in New Issue
Block a user