fix(install): auto-write NODE_EXTRA_CA_CERTS to .env on nvm Node.js installs (Linux)#49091
fix(install): auto-write NODE_EXTRA_CA_CERTS to .env on nvm Node.js installs (Linux)#49091GodsBoy wants to merge 4 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR auto-detects nvm-installed Node.js on Linux and injects Key concerns:
Confidence Score: 3/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/daemon/service-env.ts
Line: 349-355
Comment:
**CA bundle path not validated before use on non-Debian Linux**
`/etc/ssl/certs/ca-certificates.crt` is the correct bundle location on Debian/Ubuntu/Alpine, but it does not exist on RHEL/Fedora/CentOS (where it lives at `/etc/pki/tls/certs/ca-bundle.crt`) or openSUSE (`/etc/ssl/ca-bundle.pem`). Assigning `NODE_EXTRA_CA_CERTS` to a path that does not exist on disk causes Node.js to throw an ENOENT error when any TLS connection is attempted — making things actively *worse* for users on those distros (all HTTPS calls fail rather than just returning incorrect CA validation). The same non-existent path is written to `.env` in `install.ts`.
A safer approach is to resolve the bundle at runtime by probing the known paths, or at minimum, skip setting `NODE_EXTRA_CA_CERTS` when the file is absent:
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/daemon/service-env.ts
Line: 353
Comment:
**`isNvmNode` called without `execPath` here but with it in `install.ts`**
In `resolveSharedServiceEnvironmentFields`, `isNvmNode(env)` only checks the `NVM_DIR` environment variable. In `install.ts`, the same detection uses `isNvmNode(params.env, process.execPath)`, which also inspects the binary path. If a user sources nvm without `NVM_DIR` being exported (uncommon, but valid), the systemd service environment will not receive `NODE_EXTRA_CA_CERTS` even though `install.ts` will have written it to `.env`. Passing `process.execPath` here as well would keep the two codepaths consistent.
```suggestion
: platform === "linux" && isNvmNode(env, process.execPath)
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 2589aa5 |
| const nodeCaCerts = | ||
| env.NODE_EXTRA_CA_CERTS ?? (platform === "darwin" ? "/etc/ssl/cert.pem" : undefined); | ||
| env.NODE_EXTRA_CA_CERTS ?? | ||
| (platform === "darwin" | ||
| ? "/etc/ssl/cert.pem" | ||
| : platform === "linux" && isNvmNode(env) | ||
| ? LINUX_SYSTEM_CA_BUNDLE | ||
| : undefined); |
There was a problem hiding this comment.
CA bundle path not validated before use on non-Debian Linux
/etc/ssl/certs/ca-certificates.crt is the correct bundle location on Debian/Ubuntu/Alpine, but it does not exist on RHEL/Fedora/CentOS (where it lives at /etc/pki/tls/certs/ca-bundle.crt) or openSUSE (/etc/ssl/ca-bundle.pem). Assigning NODE_EXTRA_CA_CERTS to a path that does not exist on disk causes Node.js to throw an ENOENT error when any TLS connection is attempted — making things actively worse for users on those distros (all HTTPS calls fail rather than just returning incorrect CA validation). The same non-existent path is written to .env in install.ts.
A safer approach is to resolve the bundle at runtime by probing the known paths, or at minimum, skip setting NODE_EXTRA_CA_CERTS when the file is absent:
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/daemon/service-env.ts
Line: 349-355
Comment:
**CA bundle path not validated before use on non-Debian Linux**
`/etc/ssl/certs/ca-certificates.crt` is the correct bundle location on Debian/Ubuntu/Alpine, but it does not exist on RHEL/Fedora/CentOS (where it lives at `/etc/pki/tls/certs/ca-bundle.crt`) or openSUSE (`/etc/ssl/ca-bundle.pem`). Assigning `NODE_EXTRA_CA_CERTS` to a path that does not exist on disk causes Node.js to throw an ENOENT error when any TLS connection is attempted — making things actively *worse* for users on those distros (all HTTPS calls fail rather than just returning incorrect CA validation). The same non-existent path is written to `.env` in `install.ts`.
A safer approach is to resolve the bundle at runtime by probing the known paths, or at minimum, skip setting `NODE_EXTRA_CA_CERTS` when the file is absent:
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in 66143c0: resolveLinuxSystemCaBundle() now probes /etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu/Alpine), /etc/pki/tls/certs/ca-bundle.crt (RHEL/Fedora/CentOS), and /etc/ssl/ca-bundle.pem (openSUSE). Returns undefined when none exists, so we never set NODE_EXTRA_CA_CERTS to a missing path.
src/daemon/service-env.ts
Outdated
| env.NODE_EXTRA_CA_CERTS ?? | ||
| (platform === "darwin" | ||
| ? "/etc/ssl/cert.pem" | ||
| : platform === "linux" && isNvmNode(env) |
There was a problem hiding this comment.
isNvmNode called without execPath here but with it in install.ts
In resolveSharedServiceEnvironmentFields, isNvmNode(env) only checks the NVM_DIR environment variable. In install.ts, the same detection uses isNvmNode(params.env, process.execPath), which also inspects the binary path. If a user sources nvm without NVM_DIR being exported (uncommon, but valid), the systemd service environment will not receive NODE_EXTRA_CA_CERTS even though install.ts will have written it to .env. Passing process.execPath here as well would keep the two codepaths consistent.
| : platform === "linux" && isNvmNode(env) | |
| : platform === "linux" && isNvmNode(env, process.execPath) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/daemon/service-env.ts
Line: 353
Comment:
**`isNvmNode` called without `execPath` here but with it in `install.ts`**
In `resolveSharedServiceEnvironmentFields`, `isNvmNode(env)` only checks the `NVM_DIR` environment variable. In `install.ts`, the same detection uses `isNvmNode(params.env, process.execPath)`, which also inspects the binary path. If a user sources nvm without `NVM_DIR` being exported (uncommon, but valid), the systemd service environment will not receive `NODE_EXTRA_CA_CERTS` even though `install.ts` will have written it to `.env`. Passing `process.execPath` here as well would keep the two codepaths consistent.
```suggestion
: platform === "linux" && isNvmNode(env, process.execPath)
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Intentionally kept env-only detection in resolveSharedServiceEnvironmentFields — this function builds env for a different process context (the systemd service), so process.execPath of the current process isn't representative. The service env builder should be conservative and rely on NVM_DIR in the passed env dict.
The install flow uses isNvmNode(params.env, process.execPath) because it runs in the actual nvm context and writes to .env (which dotenv loads at runtime). The .env backfill now also runs on the already-installed path (66143c0), covering users who don't pass --force.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2589aa5eac
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/daemon/service-env.ts
Outdated
| env.NODE_EXTRA_CA_CERTS ?? | ||
| (platform === "darwin" | ||
| ? "/etc/ssl/cert.pem" | ||
| : platform === "linux" && isNvmNode(env) |
There was a problem hiding this comment.
Use execPath fallback in Linux nvm detection
resolveSharedServiceEnvironmentFields only calls isNvmNode(env), so it never uses the new execPath-based detection path. In environments where NVM_DIR is not exported but the CLI is still running from ~/.nvm/.../node (common with non-interactive shells), buildServiceEnvironment will not set NODE_EXTRA_CA_CERTS, and the installed gateway service can still hit the same TLS fetch failed behavior this change is trying to fix.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
See reply on the greptile thread above — intentionally env-only in the service env builder. The install flow uses both env + execPath and now also runs on the already-installed path.
| // On Linux with nvm-installed Node, ensure NODE_EXTRA_CA_CERTS is in ~/.openclaw/.env | ||
| // so manual `openclaw gateway run` also picks up the system CA bundle via dotenv. | ||
| ensureNvmCaCertsInDotEnv({ env: process.env, json, warnings }); |
There was a problem hiding this comment.
Backfill .env even when install reports already-installed
The nvm .env backfill is executed only at the end of the successful install path, but runDaemonInstall returns early on the default already-installed path. That means users upgrading to this fix (who typically already have the service installed) can run openclaw gateway install and still never get NODE_EXTRA_CA_CERTS written to ~/.openclaw/.env, leaving manual gateway run sessions unfixed unless they discover they need --force.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 66143c0: ensureNvmCaCertsInDotEnv now runs on the already-installed early-return path too, so users upgrading get the .env entry without needing --force.
…already-installed
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66143c07b9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/daemon/service-env.ts
Outdated
| env.NODE_EXTRA_CA_CERTS ?? | ||
| (platform === "darwin" | ||
| ? "/etc/ssl/cert.pem" | ||
| : platform === "linux" && isNvmNode(env) |
There was a problem hiding this comment.
Pass execPath when detecting nvm for service env defaults
resolveSharedServiceEnvironmentFields only calls isNvmNode(env), so the new execPath fallback path is never used when building daemon service environments. On Linux hosts where NVM_DIR is not exported but the CLI is running from ~/.nvm/.../node (a common non-interactive shell setup), NODE_EXTRA_CA_CERTS is omitted from the installed service environment, so TLS fetch failed behavior can persist even after reinstalling.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 31dee69: resolveSharedServiceEnvironmentFields now accepts an injectable execPath parameter (defaults to process.execPath). Both buildServiceEnvironment and buildNodeServiceEnvironment pass it through, so nvm is detected via both NVM_DIR env var and exec path. Tests pass explicit execPath values for determinism.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31dee69df2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| : platform === "linux" && isNvmNode(env, execPath ?? process.execPath) | ||
| ? resolveLinuxSystemCaBundle() |
There was a problem hiding this comment.
Detect nvm from daemon runtime path, not installer runtime
The Linux CA fallback now keys off process.execPath, which is the CLI runtime, not necessarily the daemon runtime selected for install. In supported flows where install runs under Bun but the daemon is installed with --runtime node, resolveGatewayProgramArguments can still choose an nvm Node binary while this check sees Bun and returns false, so NODE_EXTRA_CA_CERTS is omitted and the TLS fetch failed issue persists. Fresh evidence: both buildGatewayInstallPlan and buildNodeInstallPlan call build*ServiceEnvironment without providing the resolved node path, so this branch always falls back to the installer process path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid edge case but very unlikely in practice: a user running install under Bun while the daemon uses nvm Node would need NVM_DIR set (or nvm sourced) to resolve the node binary. In that scenario NVM_DIR in the env dict would already trigger detection.
The resolved daemon runtime binary path isn't available when building the service environment since it's computed separately in resolveGatewayProgramArguments. Threading it back would add significant coupling for a marginal edge case. The .env backfill covers the common path. Noting as a potential follow-up if the edge case surfaces in practice.
Merge origin/main into fix/nvm-node-extra-ca-certs, resolving conflicts in service-env.ts where main added extraPathDirs and the PR added execPath. Both parameters are now threaded through resolveSharedServiceEnvironmentFields. Also fixes pre-existing msteams test type errors from main (missing updateActivity/deleteActivity/onReactionsAdded/onReactionsRemoved stubs).
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
24 similar comments
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
1 similar comment
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49a673b450
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| : platform === "linux" && isNvmNode(env, execPath ?? process.execPath) | ||
| ? resolveLinuxSystemCaBundle() |
There was a problem hiding this comment.
Detect nvm using the daemon runtime path
Use the daemon’s resolved Node binary path here instead of defaulting to process.execPath, otherwise Linux installs can miss NODE_EXTRA_CA_CERTS when the installer runtime differs from the service runtime. Fresh evidence: if resolvePreferredNodePath cannot return a supported system node (src/daemon/runtime-paths.ts:181-185), resolveGatewayProgramArguments falls back to PATH lookup (src/daemon/program-args.ts:171-174), which can pick ~/.nvm/.../node even when process.execPath is non-nvm; this branch then returns undefined and TLS failures persist for the installed daemon.
Useful? React with 👍 / 👎.
Summary
NVM_DIRenv var orprocess.execPathcontaining/.nvm/)NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crtin the systemd service environment when nvm is detected on Linux (same pattern as the existing macOS default)NODE_EXTRA_CA_CERTSto~/.openclaw/.envduringopenclaw gateway installso manualgateway runalso picks up the system CA bundle via dotenvFixes #49088
Test plan
isNvmNode()unit tests: NVM_DIR env detection, execPath detection, negative cases