diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0bcc8d750..f14438c75a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai when the local control service is missing. Fixes #66637. - Gateway/update: fail package updates when the restarted managed gateway reports the wrong version, avoiding false-success mixed-version restarts after macOS LaunchAgent updates. Fixes #71835. Thanks @abhinas90 and @jsompis. - Plugins/runtime deps: surface activated plugin load failures in health and fail package-update restart verification or doctor repair when bundled runtime deps still cannot load, avoiding false-success repairs. (#71883) Thanks @Solvely-Colin. +- Gateway/Linux: include fnm `aliases/default/bin` in generated service PATHs and let doctor accept either modern fnm aliases or the legacy `current/bin` symlink, avoiding false PATH repair prompts. Fixes #68169. Thanks @richard-scott. - WhatsApp: remove ack reactions after a visible reply when `messages.removeAckAfterReply` is enabled, matching other reaction-capable channels. Fixes #26183. Thanks @MrUnforsaken. - Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo. - Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd. diff --git a/src/daemon/service-audit.test.ts b/src/daemon/service-audit.test.ts index f7e87b6a518..449ea36d3a0 100644 --- a/src/daemon/service-audit.test.ts +++ b/src/daemon/service-audit.test.ts @@ -112,6 +112,44 @@ describe("auditGatewayServiceConfig", () => { ).toBe(false); }); + it("accepts Linux fnm aliases/default without requiring the legacy current symlink", async () => { + const env = { HOME: "/home/testuser", FNM_DIR: "/home/testuser/.local/share/fnm" }; + const pathParts = buildMinimalServicePath({ platform: "linux", env }) + .split(":") + .filter((entry) => !entry.includes("/fnm/current/bin")); + const audit = await auditGatewayServiceConfig({ + env, + platform: "linux", + command: { + programArguments: ["/usr/bin/node", "gateway"], + environment: { PATH: pathParts.join(":") }, + }, + }); + + expect( + audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs), + ).toBe(false); + }); + + it("accepts Linux fnm current symlink without requiring aliases/default", async () => { + const env = { HOME: "/home/testuser", FNM_DIR: "/home/testuser/.local/share/fnm" }; + const pathParts = buildMinimalServicePath({ platform: "linux", env }) + .split(":") + .filter((entry) => !entry.includes("/fnm/aliases/default/bin")); + const audit = await auditGatewayServiceConfig({ + env, + platform: "linux", + command: { + programArguments: ["/usr/bin/node", "gateway"], + environment: { PATH: pathParts.join(":") }, + }, + }); + + expect( + audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs), + ).toBe(false); + }); + it("flags gateway token mismatch when service token is stale", async () => { const audit = await createGatewayAudit({ expectedGatewayToken: "new-token", diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index aafa85349e9..ee174b2f700 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -260,6 +260,26 @@ function normalizePathEntry(entry: string, platform: NodeJS.Platform): string { return normalized; } +function getEquivalentMinimalPathEntries( + entry: string, + platform: NodeJS.Platform, + normalizedExpected: Set, +): string[] { + if (platform !== "linux") { + return []; + } + const equivalent = entry.endsWith("/aliases/default/bin") + ? `${entry.slice(0, -"/aliases/default/bin".length)}/current/bin` + : entry.endsWith("/current/bin") + ? `${entry.slice(0, -"/current/bin".length)}/aliases/default/bin` + : undefined; + if (!equivalent) { + return []; + } + const normalizedEquivalent = normalizePathEntry(equivalent, platform); + return normalizedExpected.has(normalizedEquivalent) ? [equivalent] : []; +} + function auditGatewayServicePath( command: GatewayServiceCommand, issues: ServiceConfigIssue[], @@ -288,7 +308,12 @@ function auditGatewayServicePath( const normalizedExpected = new Set(expected.map((entry) => normalizePathEntry(entry, platform))); const missing = expected.filter((entry) => { const normalized = normalizePathEntry(entry, platform); - return !normalizedParts.has(normalized); + if (normalizedParts.has(normalized)) { + return false; + } + return !getEquivalentMinimalPathEntries(entry, platform, normalizedExpected).some( + (equivalent) => normalizedParts.has(normalizePathEntry(equivalent, platform)), + ); }); if (missing.length > 0) { issues.push({ diff --git a/src/daemon/service-env.test.ts b/src/daemon/service-env.test.ts index 79216dadcf0..1396b769f3c 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -24,6 +24,9 @@ describe("getMinimalServicePathParts - Linux user directories", () => { expect(result).toContain("/home/testuser/.npm-global/bin"); expect(result).toContain("/home/testuser/bin"); expect(result).toContain("/home/testuser/.nvm/current/bin"); + expect(result).toContain("/home/testuser/.local/share/fnm/aliases/default/bin"); + expect(result).toContain("/home/testuser/.local/share/fnm/current/bin"); + expect(result).toContain("/home/testuser/.fnm/aliases/default/bin"); expect(result).toContain("/home/testuser/.fnm/current/bin"); expect(result).toContain("/home/testuser/.volta/bin"); expect(result).toContain("/home/testuser/.asdf/shims"); @@ -96,6 +99,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => { expect(result).toContain("/opt/volta/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"); }); @@ -302,6 +306,7 @@ describe("buildMinimalServicePath", () => { expect(parts).toContain("/home/alice/.local/bin"); expect(parts).toContain("/home/alice/.npm-global/bin"); expect(parts).toContain("/home/alice/.nvm/current/bin"); + expect(parts).toContain("/home/alice/.local/share/fnm/aliases/default/bin"); // Verify system directories are also included expect(parts).toContain("/usr/local/bin"); diff --git a/src/daemon/service-env.ts b/src/daemon/service-env.ts index 3460af66865..baa652501be 100644 --- a/src/daemon/service-env.ts +++ b/src/daemon/service-env.ts @@ -197,6 +197,7 @@ export function resolveLinuxUserBinDirs( // Env-configured bin roots (override defaults when present). addCommonEnvConfiguredBinDirs(dirs, env); addNonEmptyDir(dirs, appendSubdir(env?.NVM_DIR, "current/bin")); + addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "aliases/default/bin")); addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "current/bin")); // Common user bin directories @@ -207,7 +208,10 @@ export function resolveLinuxUserBinDirs( // Node version managers dirs.push(`${home}/.nvm/current/bin`); // nvm with current symlink - dirs.push(`${home}/.fnm/current/bin`); // fnm + 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 return dirs;