Skip to content

Commit abf5dea

Browse files
committed
fix(daemon): filter missing service path fallbacks
1 parent bf4306d commit abf5dea

4 files changed

Lines changed: 147 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717
### Fixes
1818

1919
- CLI/Ollama: run local `infer model run` through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native `/api/chat` request. Fixes #72851. Thanks @TotalRes2020.
20+
- Daemon/service: only emit hard-coded version-manager paths such as `~/.volta/bin`, `~/.asdf/shims`, `~/.bun/bin`, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so `openclaw doctor` no longer flags `gateway.path.non-minimal` against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.
2021
- Channels/commands: make generated `/dock-*` commands switch the active session reply route through `session.identityLinks` instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.
2122
- Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.
2223
- Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no `gateway_start` hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.

docs/gateway/doctor.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,9 @@ That stages grounded durable candidates into the short-term dreaming store while
455455
</Accordion>
456456
<Accordion title="17. Gateway runtime best practices">
457457
Doctor warns when the gateway service runs on Bun or a version-managed Node path (`nvm`, `fnm`, `volta`, `asdf`, etc.). WhatsApp + Telegram channels require Node, and version-manager paths can break after upgrades because the service does not load your shell init. Doctor offers to migrate to a system Node install when available (Homebrew/apt/choco).
458+
459+
Newly installed or repaired services keep explicit environment roots (`NVM_DIR`, `FNM_DIR`, `VOLTA_HOME`, `ASDF_DATA_DIR`, `BUN_INSTALL`, `PNPM_HOME`) and stable user-bin directories, but guessed version-manager fallback directories are only written to the service PATH when those directories exist on disk. This keeps the generated supervisor PATH aligned with the same minimal-PATH audit doctor runs later.
460+
458461
</Accordion>
459462
<Accordion title="18. Config write + wizard metadata">
460463
Doctor persists any config changes and stamps wizard metadata to record the doctor run.

src/daemon/service-env.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ import {
1313
} from "./service-env.js";
1414

1515
describe("getMinimalServicePathParts - Linux user directories", () => {
16+
const allExist = (): boolean => true;
17+
const noneExist = (): boolean => false;
18+
1619
it("includes user bin directories when HOME is set on Linux", () => {
1720
const result = getMinimalServicePathParts({
1821
platform: "linux",
1922
home: "/home/testuser",
23+
existsSync: allExist,
2024
});
2125

2226
// Should include all common user bin directories
@@ -53,6 +57,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
5357
const result = getMinimalServicePathParts({
5458
platform: "linux",
5559
home: "/home/testuser",
60+
existsSync: allExist,
5661
});
5762

5863
const userDirIndex = result.indexOf("/home/testuser/.local/bin");
@@ -68,6 +73,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
6873
platform: "linux",
6974
home: "/home/testuser",
7075
extraDirs: ["/custom/bin"],
76+
existsSync: allExist,
7177
});
7278

7379
const extraDirIndex = result.indexOf("/custom/bin");
@@ -91,6 +97,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
9197
NVM_DIR: "/opt/nvm",
9298
FNM_DIR: "/opt/fnm",
9399
},
100+
existsSync: allExist,
94101
});
95102

96103
expect(result).toContain("/opt/pnpm");
@@ -107,6 +114,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
107114
const result = getMinimalServicePathParts({
108115
platform: "darwin",
109116
home: "/Users/testuser",
117+
existsSync: allExist,
110118
});
111119

112120
// Should include common user bin directories
@@ -138,6 +146,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
138146
NVM_DIR: "/Users/testuser/.nvm",
139147
PNPM_HOME: "/Users/testuser/Library/pnpm",
140148
},
149+
existsSync: allExist,
141150
});
142151

143152
// fnm uses aliases/default/bin (not current)
@@ -152,6 +161,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
152161
const result = getMinimalServicePathParts({
153162
platform: "darwin",
154163
home: "/Users/testuser",
164+
existsSync: allExist,
155165
});
156166

157167
// fnm on macOS defaults to ~/Library/Application Support/fnm
@@ -169,18 +179,105 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
169179
const result = getMinimalServicePathParts({
170180
platform: "win32",
171181
home: "C:\\Users\\testuser",
182+
existsSync: allExist,
172183
});
173184

174185
// Windows returns empty array (uses existing PATH)
175186
expect(result).toEqual([]);
176187
});
188+
189+
it("omits hard-coded version-manager fallbacks on Linux when missing", () => {
190+
const result = getMinimalServicePathParts({
191+
platform: "linux",
192+
home: "/home/testuser",
193+
existsSync: noneExist,
194+
});
195+
196+
expect(result).toContain("/home/testuser/.local/bin");
197+
expect(result).toContain("/home/testuser/.npm-global/bin");
198+
expect(result).toContain("/home/testuser/bin");
199+
expect(result).toContain("/home/testuser/.nix-profile/bin");
200+
expect(result).not.toContain("/home/testuser/.volta/bin");
201+
expect(result).not.toContain("/home/testuser/.asdf/shims");
202+
expect(result).not.toContain("/home/testuser/.bun/bin");
203+
expect(result).not.toContain("/home/testuser/.nvm/current/bin");
204+
expect(result).not.toContain("/home/testuser/.local/share/fnm/aliases/default/bin");
205+
expect(result).not.toContain("/home/testuser/.local/share/fnm/current/bin");
206+
expect(result).not.toContain("/home/testuser/.fnm/aliases/default/bin");
207+
expect(result).not.toContain("/home/testuser/.fnm/current/bin");
208+
expect(result).not.toContain("/home/testuser/.local/share/pnpm");
209+
});
210+
211+
it("omits hard-coded version-manager fallbacks on macOS when missing", () => {
212+
const result = getMinimalServicePathParts({
213+
platform: "darwin",
214+
home: "/Users/testuser",
215+
existsSync: noneExist,
216+
});
217+
218+
expect(result).toContain("/Users/testuser/.local/bin");
219+
expect(result).toContain("/Users/testuser/.npm-global/bin");
220+
expect(result).toContain("/Users/testuser/bin");
221+
expect(result).toContain("/Users/testuser/.nix-profile/bin");
222+
expect(result).not.toContain("/Users/testuser/.volta/bin");
223+
expect(result).not.toContain("/Users/testuser/.asdf/shims");
224+
expect(result).not.toContain("/Users/testuser/.bun/bin");
225+
expect(result).not.toContain(
226+
"/Users/testuser/Library/Application Support/fnm/aliases/default/bin",
227+
);
228+
expect(result).not.toContain("/Users/testuser/.fnm/aliases/default/bin");
229+
expect(result).not.toContain("/Users/testuser/Library/pnpm");
230+
expect(result).not.toContain("/Users/testuser/.local/share/pnpm");
231+
});
232+
233+
it("keeps env-configured roots when fallback directories are missing", () => {
234+
const result = getMinimalServicePathPartsFromEnv({
235+
platform: "linux",
236+
env: {
237+
HOME: "/home/testuser",
238+
PNPM_HOME: "/opt/pnpm",
239+
VOLTA_HOME: "/opt/volta",
240+
BUN_INSTALL: "/opt/bun",
241+
ASDF_DATA_DIR: "/opt/asdf",
242+
NVM_DIR: "/opt/nvm",
243+
FNM_DIR: "/opt/fnm",
244+
},
245+
existsSync: noneExist,
246+
});
247+
248+
expect(result).toContain("/opt/pnpm");
249+
expect(result).toContain("/opt/volta/bin");
250+
expect(result).toContain("/opt/bun/bin");
251+
expect(result).toContain("/opt/asdf/shims");
252+
expect(result).toContain("/opt/nvm/current/bin");
253+
expect(result).toContain("/opt/fnm/aliases/default/bin");
254+
expect(result).toContain("/opt/fnm/current/bin");
255+
});
256+
257+
it("emits only existing hard-coded version-manager fallbacks", () => {
258+
const exists = (candidate: string) =>
259+
candidate === "/home/testuser/.volta/bin" ||
260+
candidate === "/home/testuser/.local/share/fnm/aliases/default/bin";
261+
const result = getMinimalServicePathParts({
262+
platform: "linux",
263+
home: "/home/testuser",
264+
existsSync: exists,
265+
});
266+
267+
expect(result).toContain("/home/testuser/.volta/bin");
268+
expect(result).toContain("/home/testuser/.local/share/fnm/aliases/default/bin");
269+
expect(result).not.toContain("/home/testuser/.bun/bin");
270+
expect(result).not.toContain("/home/testuser/.asdf/shims");
271+
expect(result).not.toContain("/home/testuser/.fnm/aliases/default/bin");
272+
});
177273
});
178274

179275
describe("getMinimalServicePathParts - Nix Home Manager", () => {
180276
it("falls back to default Nix profile when NIX_PROFILES is absent on Linux", () => {
181277
const result = getMinimalServicePathParts({
182278
platform: "linux",
183279
home: "/home/testuser",
280+
existsSync: () => true,
184281
});
185282

186283
expect(result).toContain("/home/testuser/.nix-profile/bin");
@@ -190,6 +287,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
190287
const result = getMinimalServicePathParts({
191288
platform: "darwin",
192289
home: "/Users/testuser",
290+
existsSync: () => true,
193291
});
194292

195293
expect(result).toContain("/Users/testuser/.nix-profile/bin");
@@ -202,6 +300,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
202300
HOME: "/home/testuser",
203301
NIX_PROFILES: "/nix/var/nix/profiles/default /home/testuser/.nix-profile",
204302
},
303+
existsSync: () => true,
205304
});
206305

207306
const userIdx = result.indexOf("/home/testuser/.nix-profile/bin");
@@ -218,6 +317,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
218317
HOME: "/Users/testuser",
219318
NIX_PROFILES: "/nix/var/nix/profiles/default /Users/testuser/.nix-profile",
220319
},
320+
existsSync: () => true,
221321
});
222322

223323
const userIdx = result.indexOf("/Users/testuser/.nix-profile/bin");
@@ -234,6 +334,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
234334
HOME: "/home/testuser",
235335
NIX_PROFILES: "/nix/var/nix/profiles/per-user/testuser/profile",
236336
},
337+
existsSync: () => true,
237338
});
238339

239340
expect(result).toContain("/nix/var/nix/profiles/per-user/testuser/profile/bin");
@@ -246,6 +347,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
246347
HOME: "/Users/testuser",
247348
NIX_PROFILES: "/nix/var/nix/profiles/per-user/testuser/profile",
248349
},
350+
existsSync: () => true,
249351
});
250352

251353
expect(result).toContain("/nix/var/nix/profiles/per-user/testuser/profile/bin");
@@ -259,6 +361,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
259361
NIX_PROFILES:
260362
"/nix/var/nix/profiles/default /nix/var/nix/profiles/per-user/testuser/custom /home/testuser/.nix-profile",
261363
},
364+
existsSync: () => true,
262365
});
263366

264367
const userIdx = result.indexOf("/home/testuser/.nix-profile/bin");
@@ -299,6 +402,7 @@ describe("buildMinimalServicePath", () => {
299402
const result = buildMinimalServicePath({
300403
platform: "linux",
301404
env: { HOME: "/home/alice" },
405+
existsSync: () => true,
302406
});
303407
const parts = splitPath(result, "linux");
304408

@@ -332,6 +436,7 @@ describe("buildMinimalServicePath", () => {
332436
const result = buildMinimalServicePath({
333437
platform: "linux",
334438
env: { HOME: "/home/bob" },
439+
existsSync: () => true,
335440
});
336441
const parts = splitPath(result, "linux");
337442

@@ -366,6 +471,7 @@ describe("buildMinimalServicePath", () => {
366471
platform: "linux",
367472
extraDirs: ["/home/alice/.nvm/versions/node/v22.22.0/bin"],
368473
env: { HOME: "/home/alice" },
474+
existsSync: () => true,
369475
});
370476
const parts = splitPath(result, "linux");
371477

src/daemon/service-env.ts

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from "node:fs";
12
import os from "node:os";
23
import path from "node:path";
34
import {
@@ -29,6 +30,7 @@ export type MinimalServicePathOptions = {
2930
extraDirs?: string[];
3031
home?: string;
3132
env?: Record<string, string | undefined>;
33+
existsSync?: (candidate: string) => boolean;
3234
};
3335

3436
type BuildServicePathOptions = MinimalServicePathOptions & {
@@ -68,13 +70,27 @@ function appendSubdir(base: string | undefined, subdir: string): string | undefi
6870
return base.endsWith(`/${subdir}`) ? base : path.posix.join(base, subdir);
6971
}
7072

71-
function addCommonUserBinDirs(dirs: string[], home: string): void {
73+
function addExistingDir(
74+
dirs: string[],
75+
candidate: string,
76+
existsSync: (candidate: string) => boolean,
77+
): void {
78+
if (existsSync(candidate)) {
79+
dirs.push(candidate);
80+
}
81+
}
82+
83+
function addCommonUserBinDirs(
84+
dirs: string[],
85+
home: string,
86+
existsSync: (candidate: string) => boolean,
87+
): void {
7288
dirs.push(`${home}/.local/bin`);
7389
dirs.push(`${home}/.npm-global/bin`);
7490
dirs.push(`${home}/bin`);
75-
dirs.push(`${home}/.volta/bin`);
76-
dirs.push(`${home}/.asdf/shims`);
77-
dirs.push(`${home}/.bun/bin`);
91+
addExistingDir(dirs, `${home}/.volta/bin`, existsSync);
92+
addExistingDir(dirs, `${home}/.asdf/shims`, existsSync);
93+
addExistingDir(dirs, `${home}/.bun/bin`, existsSync);
7894
}
7995

8096
function addCommonEnvConfiguredBinDirs(
@@ -126,6 +142,7 @@ function resolveSystemPathDirs(platform: NodeJS.Platform): string[] {
126142
export function resolveDarwinUserBinDirs(
127143
home: string | undefined,
128144
env?: Record<string, string | undefined>,
145+
existsSync: (candidate: string) => boolean = fs.existsSync,
129146
): string[] {
130147
if (!home) {
131148
return [];
@@ -145,19 +162,19 @@ export function resolveDarwinUserBinDirs(
145162
// pnpm: binary is directly in PNPM_HOME (not in bin subdirectory)
146163

147164
// Common user bin directories
148-
addCommonUserBinDirs(dirs, home);
165+
addCommonUserBinDirs(dirs, home, existsSync);
149166

150167
// Nix Home Manager (cross-platform)
151168
addNixProfileBinDirs(dirs, home, env);
152169

153170
// Node version managers - macOS specific paths
154171
// nvm: no stable default path, depends on user's shell configuration
155172
// fnm: macOS default is ~/Library/Application Support/fnm, not ~/.fnm
156-
dirs.push(`${home}/Library/Application Support/fnm/aliases/default/bin`); // fnm default
157-
dirs.push(`${home}/.fnm/aliases/default/bin`); // fnm if customized to ~/.fnm
173+
addExistingDir(dirs, `${home}/Library/Application Support/fnm/aliases/default/bin`, existsSync); // fnm default
174+
addExistingDir(dirs, `${home}/.fnm/aliases/default/bin`, existsSync); // fnm if customized to ~/.fnm
158175
// pnpm: macOS default is ~/Library/pnpm, not ~/.local/share/pnpm
159-
dirs.push(`${home}/Library/pnpm`); // pnpm default
160-
dirs.push(`${home}/.local/share/pnpm`); // pnpm XDG fallback
176+
addExistingDir(dirs, `${home}/Library/pnpm`, existsSync); // pnpm default
177+
addExistingDir(dirs, `${home}/.local/share/pnpm`, existsSync); // pnpm XDG fallback
161178

162179
return dirs;
163180
}
@@ -169,6 +186,7 @@ export function resolveDarwinUserBinDirs(
169186
export function resolveLinuxUserBinDirs(
170187
home: string | undefined,
171188
env?: Record<string, string | undefined>,
189+
existsSync: (candidate: string) => boolean = fs.existsSync,
172190
): string[] {
173191
if (!home) {
174192
return [];
@@ -183,18 +201,18 @@ export function resolveLinuxUserBinDirs(
183201
addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "current/bin"));
184202

185203
// Common user bin directories
186-
addCommonUserBinDirs(dirs, home);
204+
addCommonUserBinDirs(dirs, home, existsSync);
187205

188206
// Nix Home Manager (cross-platform)
189207
addNixProfileBinDirs(dirs, home, env);
190208

191209
// Node version managers
192-
dirs.push(`${home}/.nvm/current/bin`); // nvm with current symlink
193-
dirs.push(`${home}/.local/share/fnm/aliases/default/bin`); // fnm default
194-
dirs.push(`${home}/.local/share/fnm/current/bin`); // fnm legacy current symlink
195-
dirs.push(`${home}/.fnm/aliases/default/bin`); // fnm if customized to ~/.fnm
196-
dirs.push(`${home}/.fnm/current/bin`); // fnm legacy current symlink
197-
dirs.push(`${home}/.local/share/pnpm`); // pnpm global bin
210+
addExistingDir(dirs, `${home}/.nvm/current/bin`, existsSync); // nvm with current symlink
211+
addExistingDir(dirs, `${home}/.local/share/fnm/aliases/default/bin`, existsSync); // fnm default
212+
addExistingDir(dirs, `${home}/.local/share/fnm/current/bin`, existsSync); // fnm legacy current symlink
213+
addExistingDir(dirs, `${home}/.fnm/aliases/default/bin`, existsSync); // fnm if customized to ~/.fnm
214+
addExistingDir(dirs, `${home}/.fnm/current/bin`, existsSync); // fnm legacy current symlink
215+
addExistingDir(dirs, `${home}/.local/share/pnpm`, existsSync); // pnpm global bin
198216

199217
return dirs;
200218
}
@@ -210,11 +228,12 @@ export function getMinimalServicePathParts(options: MinimalServicePathOptions =
210228
const systemDirs = resolveSystemPathDirs(platform);
211229

212230
// Add user bin directories for version managers (npm global, nvm, fnm, volta, etc.)
231+
const existsSync = options.existsSync ?? fs.existsSync;
213232
const userDirs =
214233
platform === "linux"
215-
? resolveLinuxUserBinDirs(options.home, options.env)
234+
? resolveLinuxUserBinDirs(options.home, options.env, existsSync)
216235
: platform === "darwin"
217-
? resolveDarwinUserBinDirs(options.home, options.env)
236+
? resolveDarwinUserBinDirs(options.home, options.env, existsSync)
218237
: [];
219238

220239
const add = (dir: string) => {

0 commit comments

Comments
 (0)