Skip to content

Commit bc46e01

Browse files
authored
Merge branch 'main' into ci/main-ci-optimizations
2 parents d2f9709 + 29b165e commit bc46e01

16 files changed

+685
-284
lines changed

src/bootstrap/node-extra-ca-certs.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import {
3-
isNvmNode,
3+
isNodeVersionManagerRuntime,
44
LINUX_CA_BUNDLE_PATHS,
55
resolveAutoNodeExtraCaCerts,
66
resolveLinuxSystemCaBundle,
@@ -34,17 +34,19 @@ describe("resolveLinuxSystemCaBundle", () => {
3434
});
3535
});
3636

37-
describe("isNvmNode", () => {
37+
describe("isNodeVersionManagerRuntime", () => {
3838
it("detects nvm via NVM_DIR", () => {
39-
expect(isNvmNode({ NVM_DIR: "/home/test/.nvm" }, "/usr/bin/node")).toBe(true);
39+
expect(isNodeVersionManagerRuntime({ NVM_DIR: "/home/test/.nvm" }, "/usr/bin/node")).toBe(true);
4040
});
4141

4242
it("detects nvm via execPath", () => {
43-
expect(isNvmNode({}, "/home/test/.nvm/versions/node/v22/bin/node")).toBe(true);
43+
expect(isNodeVersionManagerRuntime({}, "/home/test/.nvm/versions/node/v22/bin/node")).toBe(
44+
true,
45+
);
4446
});
4547

4648
it("returns false for non-nvm node paths", () => {
47-
expect(isNvmNode({}, "/usr/bin/node")).toBe(false);
49+
expect(isNodeVersionManagerRuntime({}, "/usr/bin/node")).toBe(false);
4850
});
4951
});
5052

src/bootstrap/node-extra-ca-certs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const LINUX_CA_BUNDLE_PATHS = [
66
"/etc/ssl/ca-bundle.pem",
77
] as const;
88

9-
type EnvMap = Record<string, string | undefined>;
9+
export type EnvMap = Record<string, string | undefined>;
1010
type AccessSyncFn = (path: string, mode?: number) => void;
1111

1212
export function resolveLinuxSystemCaBundle(
@@ -32,7 +32,7 @@ export function resolveLinuxSystemCaBundle(
3232
return undefined;
3333
}
3434

35-
export function isNvmNode(
35+
export function isNodeVersionManagerRuntime(
3636
env: EnvMap = process.env as EnvMap,
3737
execPath: string = process.execPath,
3838
): boolean {
@@ -57,7 +57,7 @@ export function resolveAutoNodeExtraCaCerts(
5757

5858
const platform = params.platform ?? process.platform;
5959
const execPath = params.execPath ?? process.execPath;
60-
if (platform !== "linux" || !isNvmNode(env, execPath)) {
60+
if (platform !== "linux" || !isNodeVersionManagerRuntime(env, execPath)) {
6161
return undefined;
6262
}
6363

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, it } from "vitest";
2+
import { LINUX_CA_BUNDLE_PATHS } from "./node-extra-ca-certs.js";
3+
import { resolveNodeStartupTlsEnvironment } from "./node-startup-env.js";
4+
5+
function allowOnly(path: string) {
6+
return (candidate: string) => {
7+
if (candidate !== path) {
8+
throw new Error("ENOENT");
9+
}
10+
};
11+
}
12+
13+
describe("resolveNodeStartupTlsEnvironment", () => {
14+
it("defaults macOS launch env values", () => {
15+
expect(
16+
resolveNodeStartupTlsEnvironment({
17+
env: {},
18+
platform: "darwin",
19+
}),
20+
).toEqual({
21+
NODE_EXTRA_CA_CERTS: "/etc/ssl/cert.pem",
22+
NODE_USE_SYSTEM_CA: "1",
23+
});
24+
});
25+
26+
it("keeps user-provided env values", () => {
27+
expect(
28+
resolveNodeStartupTlsEnvironment({
29+
env: {
30+
NODE_EXTRA_CA_CERTS: "/custom/ca.pem",
31+
NODE_USE_SYSTEM_CA: "0",
32+
},
33+
platform: "darwin",
34+
}),
35+
).toEqual({
36+
NODE_EXTRA_CA_CERTS: "/custom/ca.pem",
37+
NODE_USE_SYSTEM_CA: "0",
38+
});
39+
});
40+
41+
it("resolves Linux CA env for version-manager Node runtimes", () => {
42+
expect(
43+
resolveNodeStartupTlsEnvironment({
44+
env: { NVM_DIR: "/home/test/.nvm" },
45+
platform: "linux",
46+
execPath: "/usr/bin/node",
47+
accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[1]),
48+
}),
49+
).toEqual({
50+
NODE_EXTRA_CA_CERTS: LINUX_CA_BUNDLE_PATHS[1],
51+
NODE_USE_SYSTEM_CA: undefined,
52+
});
53+
});
54+
55+
it("can skip macOS defaults for CLI-only pre-start planning", () => {
56+
expect(
57+
resolveNodeStartupTlsEnvironment({
58+
env: {},
59+
platform: "darwin",
60+
includeDarwinDefaults: false,
61+
}),
62+
).toEqual({
63+
NODE_EXTRA_CA_CERTS: undefined,
64+
NODE_USE_SYSTEM_CA: undefined,
65+
});
66+
});
67+
68+
it("uses the Linux CA bundle heuristic when available", () => {
69+
const value = resolveNodeStartupTlsEnvironment({
70+
env: { NVM_DIR: "/home/test/.nvm" },
71+
platform: "linux",
72+
execPath: "/usr/bin/node",
73+
accessSync: allowOnly(LINUX_CA_BUNDLE_PATHS[2]),
74+
}).NODE_EXTRA_CA_CERTS;
75+
if (value !== undefined) {
76+
expect(LINUX_CA_BUNDLE_PATHS).toContain(value);
77+
}
78+
});
79+
});

src/bootstrap/node-startup-env.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { type EnvMap, resolveAutoNodeExtraCaCerts } from "./node-extra-ca-certs.js";
2+
3+
export type NodeStartupTlsEnvironment = {
4+
NODE_EXTRA_CA_CERTS?: string;
5+
NODE_USE_SYSTEM_CA?: string;
6+
};
7+
8+
export function resolveNodeStartupTlsEnvironment(
9+
params: {
10+
env?: EnvMap;
11+
platform?: NodeJS.Platform;
12+
execPath?: string;
13+
includeDarwinDefaults?: boolean;
14+
accessSync?: (path: string, mode?: number) => void;
15+
} = {},
16+
): NodeStartupTlsEnvironment {
17+
const env = params.env ?? (process.env as EnvMap);
18+
const platform = params.platform ?? process.platform;
19+
const includeDarwinDefaults = params.includeDarwinDefaults ?? true;
20+
21+
const nodeExtraCaCerts =
22+
env.NODE_EXTRA_CA_CERTS ??
23+
(platform === "darwin" && includeDarwinDefaults
24+
? "/etc/ssl/cert.pem"
25+
: resolveAutoNodeExtraCaCerts({
26+
env,
27+
platform,
28+
execPath: params.execPath,
29+
accessSync: params.accessSync,
30+
}));
31+
const nodeUseSystemCa =
32+
env.NODE_USE_SYSTEM_CA ?? (platform === "darwin" && includeDarwinDefaults ? "1" : undefined);
33+
34+
return {
35+
NODE_EXTRA_CA_CERTS: nodeExtraCaCerts,
36+
NODE_USE_SYSTEM_CA: nodeUseSystemCa,
37+
};
38+
}

src/cli/daemon-cli/install.test.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2-
import { captureFullEnv } from "../../test-utils/env.js";
32
import type { DaemonActionResponse } from "./response.js";
3+
import { captureFullEnv } from "../../test-utils/env.js";
44

5-
const resolveAutoNodeExtraCaCertsMock = vi.hoisted(() => vi.fn());
5+
const resolveNodeStartupTlsEnvironmentMock = vi.hoisted(() => vi.fn());
66
const loadConfigMock = vi.hoisted(() => vi.fn());
77
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
88
const resolveGatewayPortMock = vi.hoisted(() => vi.fn(() => 18789));
@@ -51,8 +51,8 @@ const service = vi.hoisted(() => ({
5151
readRuntime: vi.fn(async () => ({ status: "stopped" as const })),
5252
}));
5353

54-
vi.mock("../../bootstrap/node-extra-ca-certs.js", () => ({
55-
resolveAutoNodeExtraCaCerts: resolveAutoNodeExtraCaCertsMock,
54+
vi.mock("../../bootstrap/node-startup-env.js", () => ({
55+
resolveNodeStartupTlsEnvironment: resolveNodeStartupTlsEnvironmentMock,
5656
}));
5757

5858
vi.mock("../../config/config.js", () => ({
@@ -156,7 +156,7 @@ const envSnapshot = captureFullEnv();
156156
describe("runDaemonInstall", () => {
157157
beforeEach(() => {
158158
loadConfigMock.mockReset();
159-
resolveAutoNodeExtraCaCertsMock.mockReset();
159+
resolveNodeStartupTlsEnvironmentMock.mockReset();
160160
readConfigFileSnapshotMock.mockReset();
161161
resolveGatewayPortMock.mockClear();
162162
writeConfigFileMock.mockReset();
@@ -198,7 +198,10 @@ describe("runDaemonInstall", () => {
198198
installDaemonServiceAndEmitMock.mockResolvedValue(undefined);
199199
service.isLoaded.mockResolvedValue(false);
200200
service.readCommand.mockResolvedValue(null);
201-
resolveAutoNodeExtraCaCertsMock.mockReturnValue(undefined);
201+
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
202+
NODE_EXTRA_CA_CERTS: undefined,
203+
NODE_USE_SYSTEM_CA: undefined,
204+
});
202205
delete process.env.OPENCLAW_GATEWAY_TOKEN;
203206
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
204207
});
@@ -300,7 +303,10 @@ describe("runDaemonInstall", () => {
300303

301304
it("returns already-installed when the service already has the expected TLS env", async () => {
302305
service.isLoaded.mockResolvedValue(true);
303-
resolveAutoNodeExtraCaCertsMock.mockReturnValue("/etc/ssl/certs/ca-certificates.crt");
306+
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
307+
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
308+
NODE_USE_SYSTEM_CA: undefined,
309+
});
304310
service.readCommand.mockResolvedValue({
305311
programArguments: ["openclaw", "gateway", "run"],
306312
environment: {
@@ -316,7 +322,10 @@ describe("runDaemonInstall", () => {
316322

317323
it("reinstalls when an existing service is missing the nvm TLS CA bundle", async () => {
318324
service.isLoaded.mockResolvedValue(true);
319-
resolveAutoNodeExtraCaCertsMock.mockReturnValue("/etc/ssl/certs/ca-certificates.crt");
325+
resolveNodeStartupTlsEnvironmentMock.mockReturnValue({
326+
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
327+
NODE_USE_SYSTEM_CA: undefined,
328+
});
320329
service.readCommand.mockResolvedValue({
321330
programArguments: ["openclaw", "gateway", "run"],
322331
environment: {},
@@ -329,11 +338,13 @@ describe("runDaemonInstall", () => {
329338

330339
it("reinstalls when the installed service still runs from nvm even if the installer runtime does not", async () => {
331340
service.isLoaded.mockResolvedValue(true);
332-
resolveAutoNodeExtraCaCertsMock.mockImplementation(({ execPath }) =>
333-
typeof execPath === "string" && execPath.includes("/.nvm/")
334-
? "/etc/ssl/certs/ca-certificates.crt"
335-
: undefined,
336-
);
341+
resolveNodeStartupTlsEnvironmentMock.mockImplementation(({ execPath }) => ({
342+
NODE_EXTRA_CA_CERTS:
343+
typeof execPath === "string" && execPath.includes("/.nvm/")
344+
? "/etc/ssl/certs/ca-certificates.crt"
345+
: undefined,
346+
NODE_USE_SYSTEM_CA: undefined,
347+
}));
337348
service.readCommand.mockResolvedValue({
338349
programArguments: ["/home/test/.nvm/versions/node/v22.18.0/bin/node", "dist/entry.js"],
339350
environment: {},
@@ -342,7 +353,7 @@ describe("runDaemonInstall", () => {
342353
await runDaemonInstall({ json: true });
343354

344355
expect(installDaemonServiceAndEmitMock).toHaveBeenCalledTimes(1);
345-
expect(resolveAutoNodeExtraCaCertsMock).toHaveBeenCalledWith(
356+
expect(resolveNodeStartupTlsEnvironmentMock).toHaveBeenCalledWith(
346357
expect.objectContaining({
347358
execPath: "/home/test/.nvm/versions/node/v22.18.0/bin/node",
348359
}),

src/cli/daemon-cli/install.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { resolveAutoNodeExtraCaCerts } from "../../bootstrap/node-extra-ca-certs.js";
1+
import type { DaemonInstallOptions } from "./types.js";
2+
import type { DaemonInstallOptions } from "./types.js";
3+
import { resolveNodeStartupTlsEnvironment } from "../../bootstrap/node-startup-env.js";
24
import { buildGatewayInstallPlan } from "../../commands/daemon-install-helpers.js";
35
import {
46
DEFAULT_GATEWAY_DAEMON_RUNTIME,
@@ -16,7 +18,6 @@ import {
1618
failIfNixDaemonInstallMode,
1719
parsePort,
1820
} from "./shared.js";
19-
import type { DaemonInstallOptions } from "./types.js";
2021

2122
export async function runDaemonInstall(opts: DaemonInstallOptions) {
2223
const { json, stdout, warnings, emit, fail } = createDaemonInstallActionContext(opts.json);
@@ -146,14 +147,15 @@ async function gatewayServiceNeedsAutoNodeExtraCaCertsRefresh(params: {
146147
}
147148
const currentEnvironment = currentCommand.environment ?? {};
148149
const currentNodeExtraCaCerts = currentEnvironment.NODE_EXTRA_CA_CERTS?.trim();
149-
const expectedNodeExtraCaCerts = resolveAutoNodeExtraCaCerts({
150+
const expectedNodeExtraCaCerts = resolveNodeStartupTlsEnvironment({
150151
env: {
151152
...params.env,
152153
...currentEnvironment,
153154
NODE_EXTRA_CA_CERTS: undefined,
154155
},
155156
execPath: currentExecPath,
156-
});
157+
includeDarwinDefaults: false,
158+
}).NODE_EXTRA_CA_CERTS;
157159
if (!expectedNodeExtraCaCerts) {
158160
return false;
159161
}

0 commit comments

Comments
 (0)