fix(daemon): filter missing service path fallbacks

This commit is contained in:
Peter Steinberger
2026-04-27 23:16:04 +01:00
parent bf4306d1b0
commit abf5dea7dd
4 changed files with 147 additions and 18 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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.
- 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.
- 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.
- 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.
- 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.

View File

@@ -455,6 +455,9 @@ That stages grounded durable candidates into the short-term dreaming store while
</Accordion>
<Accordion title="17. Gateway runtime best practices">
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).
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.
</Accordion>
<Accordion title="18. Config write + wizard metadata">
Doctor persists any config changes and stamps wizard metadata to record the doctor run.

View File

@@ -13,10 +13,14 @@ import {
} from "./service-env.js";
describe("getMinimalServicePathParts - Linux user directories", () => {
const allExist = (): boolean => true;
const noneExist = (): boolean => false;
it("includes user bin directories when HOME is set on Linux", () => {
const result = getMinimalServicePathParts({
platform: "linux",
home: "/home/testuser",
existsSync: allExist,
});
// Should include all common user bin directories
@@ -53,6 +57,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
const result = getMinimalServicePathParts({
platform: "linux",
home: "/home/testuser",
existsSync: allExist,
});
const userDirIndex = result.indexOf("/home/testuser/.local/bin");
@@ -68,6 +73,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
platform: "linux",
home: "/home/testuser",
extraDirs: ["/custom/bin"],
existsSync: allExist,
});
const extraDirIndex = result.indexOf("/custom/bin");
@@ -91,6 +97,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
NVM_DIR: "/opt/nvm",
FNM_DIR: "/opt/fnm",
},
existsSync: allExist,
});
expect(result).toContain("/opt/pnpm");
@@ -107,6 +114,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
const result = getMinimalServicePathParts({
platform: "darwin",
home: "/Users/testuser",
existsSync: allExist,
});
// Should include common user bin directories
@@ -138,6 +146,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
NVM_DIR: "/Users/testuser/.nvm",
PNPM_HOME: "/Users/testuser/Library/pnpm",
},
existsSync: allExist,
});
// fnm uses aliases/default/bin (not current)
@@ -152,6 +161,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
const result = getMinimalServicePathParts({
platform: "darwin",
home: "/Users/testuser",
existsSync: allExist,
});
// fnm on macOS defaults to ~/Library/Application Support/fnm
@@ -169,11 +179,97 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
const result = getMinimalServicePathParts({
platform: "win32",
home: "C:\\Users\\testuser",
existsSync: allExist,
});
// Windows returns empty array (uses existing PATH)
expect(result).toEqual([]);
});
it("omits hard-coded version-manager fallbacks on Linux when missing", () => {
const result = getMinimalServicePathParts({
platform: "linux",
home: "/home/testuser",
existsSync: noneExist,
});
expect(result).toContain("/home/testuser/.local/bin");
expect(result).toContain("/home/testuser/.npm-global/bin");
expect(result).toContain("/home/testuser/bin");
expect(result).toContain("/home/testuser/.nix-profile/bin");
expect(result).not.toContain("/home/testuser/.volta/bin");
expect(result).not.toContain("/home/testuser/.asdf/shims");
expect(result).not.toContain("/home/testuser/.bun/bin");
expect(result).not.toContain("/home/testuser/.nvm/current/bin");
expect(result).not.toContain("/home/testuser/.local/share/fnm/aliases/default/bin");
expect(result).not.toContain("/home/testuser/.local/share/fnm/current/bin");
expect(result).not.toContain("/home/testuser/.fnm/aliases/default/bin");
expect(result).not.toContain("/home/testuser/.fnm/current/bin");
expect(result).not.toContain("/home/testuser/.local/share/pnpm");
});
it("omits hard-coded version-manager fallbacks on macOS when missing", () => {
const result = getMinimalServicePathParts({
platform: "darwin",
home: "/Users/testuser",
existsSync: noneExist,
});
expect(result).toContain("/Users/testuser/.local/bin");
expect(result).toContain("/Users/testuser/.npm-global/bin");
expect(result).toContain("/Users/testuser/bin");
expect(result).toContain("/Users/testuser/.nix-profile/bin");
expect(result).not.toContain("/Users/testuser/.volta/bin");
expect(result).not.toContain("/Users/testuser/.asdf/shims");
expect(result).not.toContain("/Users/testuser/.bun/bin");
expect(result).not.toContain(
"/Users/testuser/Library/Application Support/fnm/aliases/default/bin",
);
expect(result).not.toContain("/Users/testuser/.fnm/aliases/default/bin");
expect(result).not.toContain("/Users/testuser/Library/pnpm");
expect(result).not.toContain("/Users/testuser/.local/share/pnpm");
});
it("keeps env-configured roots when fallback directories are missing", () => {
const result = getMinimalServicePathPartsFromEnv({
platform: "linux",
env: {
HOME: "/home/testuser",
PNPM_HOME: "/opt/pnpm",
VOLTA_HOME: "/opt/volta",
BUN_INSTALL: "/opt/bun",
ASDF_DATA_DIR: "/opt/asdf",
NVM_DIR: "/opt/nvm",
FNM_DIR: "/opt/fnm",
},
existsSync: noneExist,
});
expect(result).toContain("/opt/pnpm");
expect(result).toContain("/opt/volta/bin");
expect(result).toContain("/opt/bun/bin");
expect(result).toContain("/opt/asdf/shims");
expect(result).toContain("/opt/nvm/current/bin");
expect(result).toContain("/opt/fnm/aliases/default/bin");
expect(result).toContain("/opt/fnm/current/bin");
});
it("emits only existing hard-coded version-manager fallbacks", () => {
const exists = (candidate: string) =>
candidate === "/home/testuser/.volta/bin" ||
candidate === "/home/testuser/.local/share/fnm/aliases/default/bin";
const result = getMinimalServicePathParts({
platform: "linux",
home: "/home/testuser",
existsSync: exists,
});
expect(result).toContain("/home/testuser/.volta/bin");
expect(result).toContain("/home/testuser/.local/share/fnm/aliases/default/bin");
expect(result).not.toContain("/home/testuser/.bun/bin");
expect(result).not.toContain("/home/testuser/.asdf/shims");
expect(result).not.toContain("/home/testuser/.fnm/aliases/default/bin");
});
});
describe("getMinimalServicePathParts - Nix Home Manager", () => {
@@ -181,6 +277,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
const result = getMinimalServicePathParts({
platform: "linux",
home: "/home/testuser",
existsSync: () => true,
});
expect(result).toContain("/home/testuser/.nix-profile/bin");
@@ -190,6 +287,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
const result = getMinimalServicePathParts({
platform: "darwin",
home: "/Users/testuser",
existsSync: () => true,
});
expect(result).toContain("/Users/testuser/.nix-profile/bin");
@@ -202,6 +300,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
HOME: "/home/testuser",
NIX_PROFILES: "/nix/var/nix/profiles/default /home/testuser/.nix-profile",
},
existsSync: () => true,
});
const userIdx = result.indexOf("/home/testuser/.nix-profile/bin");
@@ -218,6 +317,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
HOME: "/Users/testuser",
NIX_PROFILES: "/nix/var/nix/profiles/default /Users/testuser/.nix-profile",
},
existsSync: () => true,
});
const userIdx = result.indexOf("/Users/testuser/.nix-profile/bin");
@@ -234,6 +334,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
HOME: "/home/testuser",
NIX_PROFILES: "/nix/var/nix/profiles/per-user/testuser/profile",
},
existsSync: () => true,
});
expect(result).toContain("/nix/var/nix/profiles/per-user/testuser/profile/bin");
@@ -246,6 +347,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
HOME: "/Users/testuser",
NIX_PROFILES: "/nix/var/nix/profiles/per-user/testuser/profile",
},
existsSync: () => true,
});
expect(result).toContain("/nix/var/nix/profiles/per-user/testuser/profile/bin");
@@ -259,6 +361,7 @@ describe("getMinimalServicePathParts - Nix Home Manager", () => {
NIX_PROFILES:
"/nix/var/nix/profiles/default /nix/var/nix/profiles/per-user/testuser/custom /home/testuser/.nix-profile",
},
existsSync: () => true,
});
const userIdx = result.indexOf("/home/testuser/.nix-profile/bin");
@@ -299,6 +402,7 @@ describe("buildMinimalServicePath", () => {
const result = buildMinimalServicePath({
platform: "linux",
env: { HOME: "/home/alice" },
existsSync: () => true,
});
const parts = splitPath(result, "linux");
@@ -332,6 +436,7 @@ describe("buildMinimalServicePath", () => {
const result = buildMinimalServicePath({
platform: "linux",
env: { HOME: "/home/bob" },
existsSync: () => true,
});
const parts = splitPath(result, "linux");
@@ -366,6 +471,7 @@ describe("buildMinimalServicePath", () => {
platform: "linux",
extraDirs: ["/home/alice/.nvm/versions/node/v22.22.0/bin"],
env: { HOME: "/home/alice" },
existsSync: () => true,
});
const parts = splitPath(result, "linux");

View File

@@ -1,3 +1,4 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
@@ -29,6 +30,7 @@ export type MinimalServicePathOptions = {
extraDirs?: string[];
home?: string;
env?: Record<string, string | undefined>;
existsSync?: (candidate: string) => boolean;
};
type BuildServicePathOptions = MinimalServicePathOptions & {
@@ -68,13 +70,27 @@ function appendSubdir(base: string | undefined, subdir: string): string | undefi
return base.endsWith(`/${subdir}`) ? base : path.posix.join(base, subdir);
}
function addCommonUserBinDirs(dirs: string[], home: string): void {
function addExistingDir(
dirs: string[],
candidate: string,
existsSync: (candidate: string) => boolean,
): void {
if (existsSync(candidate)) {
dirs.push(candidate);
}
}
function addCommonUserBinDirs(
dirs: string[],
home: string,
existsSync: (candidate: string) => boolean,
): void {
dirs.push(`${home}/.local/bin`);
dirs.push(`${home}/.npm-global/bin`);
dirs.push(`${home}/bin`);
dirs.push(`${home}/.volta/bin`);
dirs.push(`${home}/.asdf/shims`);
dirs.push(`${home}/.bun/bin`);
addExistingDir(dirs, `${home}/.volta/bin`, existsSync);
addExistingDir(dirs, `${home}/.asdf/shims`, existsSync);
addExistingDir(dirs, `${home}/.bun/bin`, existsSync);
}
function addCommonEnvConfiguredBinDirs(
@@ -126,6 +142,7 @@ function resolveSystemPathDirs(platform: NodeJS.Platform): string[] {
export function resolveDarwinUserBinDirs(
home: string | undefined,
env?: Record<string, string | undefined>,
existsSync: (candidate: string) => boolean = fs.existsSync,
): string[] {
if (!home) {
return [];
@@ -145,7 +162,7 @@ export function resolveDarwinUserBinDirs(
// pnpm: binary is directly in PNPM_HOME (not in bin subdirectory)
// Common user bin directories
addCommonUserBinDirs(dirs, home);
addCommonUserBinDirs(dirs, home, existsSync);
// Nix Home Manager (cross-platform)
addNixProfileBinDirs(dirs, home, env);
@@ -153,11 +170,11 @@ export function resolveDarwinUserBinDirs(
// Node version managers - macOS specific paths
// nvm: no stable default path, depends on user's shell configuration
// fnm: macOS default is ~/Library/Application Support/fnm, not ~/.fnm
dirs.push(`${home}/Library/Application Support/fnm/aliases/default/bin`); // fnm default
dirs.push(`${home}/.fnm/aliases/default/bin`); // fnm if customized to ~/.fnm
addExistingDir(dirs, `${home}/Library/Application Support/fnm/aliases/default/bin`, existsSync); // fnm default
addExistingDir(dirs, `${home}/.fnm/aliases/default/bin`, existsSync); // fnm if customized to ~/.fnm
// pnpm: macOS default is ~/Library/pnpm, not ~/.local/share/pnpm
dirs.push(`${home}/Library/pnpm`); // pnpm default
dirs.push(`${home}/.local/share/pnpm`); // pnpm XDG fallback
addExistingDir(dirs, `${home}/Library/pnpm`, existsSync); // pnpm default
addExistingDir(dirs, `${home}/.local/share/pnpm`, existsSync); // pnpm XDG fallback
return dirs;
}
@@ -169,6 +186,7 @@ export function resolveDarwinUserBinDirs(
export function resolveLinuxUserBinDirs(
home: string | undefined,
env?: Record<string, string | undefined>,
existsSync: (candidate: string) => boolean = fs.existsSync,
): string[] {
if (!home) {
return [];
@@ -183,18 +201,18 @@ export function resolveLinuxUserBinDirs(
addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "current/bin"));
// Common user bin directories
addCommonUserBinDirs(dirs, home);
addCommonUserBinDirs(dirs, home, existsSync);
// Nix Home Manager (cross-platform)
addNixProfileBinDirs(dirs, home, env);
// Node version managers
dirs.push(`${home}/.nvm/current/bin`); // nvm with current symlink
dirs.push(`${home}/.local/share/fnm/aliases/default/bin`); // fnm default
dirs.push(`${home}/.local/share/fnm/current/bin`); // fnm legacy current symlink
dirs.push(`${home}/.fnm/aliases/default/bin`); // fnm if customized to ~/.fnm
dirs.push(`${home}/.fnm/current/bin`); // fnm legacy current symlink
dirs.push(`${home}/.local/share/pnpm`); // pnpm global bin
addExistingDir(dirs, `${home}/.nvm/current/bin`, existsSync); // nvm with current symlink
addExistingDir(dirs, `${home}/.local/share/fnm/aliases/default/bin`, existsSync); // fnm default
addExistingDir(dirs, `${home}/.local/share/fnm/current/bin`, existsSync); // fnm legacy current symlink
addExistingDir(dirs, `${home}/.fnm/aliases/default/bin`, existsSync); // fnm if customized to ~/.fnm
addExistingDir(dirs, `${home}/.fnm/current/bin`, existsSync); // fnm legacy current symlink
addExistingDir(dirs, `${home}/.local/share/pnpm`, existsSync); // pnpm global bin
return dirs;
}
@@ -210,11 +228,12 @@ export function getMinimalServicePathParts(options: MinimalServicePathOptions =
const systemDirs = resolveSystemPathDirs(platform);
// Add user bin directories for version managers (npm global, nvm, fnm, volta, etc.)
const existsSync = options.existsSync ?? fs.existsSync;
const userDirs =
platform === "linux"
? resolveLinuxUserBinDirs(options.home, options.env)
? resolveLinuxUserBinDirs(options.home, options.env, existsSync)
: platform === "darwin"
? resolveDarwinUserBinDirs(options.home, options.env)
? resolveDarwinUserBinDirs(options.home, options.env, existsSync)
: [];
const add = (dir: string) => {