From eb3e4f20a0af500f2b746c83a2c9bf9ccba13891 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 11:36:11 +0100 Subject: [PATCH] fix: relax gateway service path audit --- CHANGELOG.md | 1 + docs/gateway/doctor.md | 2 +- src/daemon/service-audit.test.ts | 47 ++++++++++++++++++++++++++++++++ src/daemon/service-audit.ts | 6 +++- src/daemon/service-env.test.ts | 14 ++++++++++ src/daemon/service-env.ts | 34 ++++++++++++++++------- 6 files changed, 92 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 856eb027b10..1836f665699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Memory/markdown: replace CRLF managed blocks in place and collapse duplicate marker blocks without rewriting unmanaged markdown, so Dreaming and Memory Wiki files self-heal from repeated generated sections. Fixes #75491; supersedes #75495, #75810, and #76008. Thanks @asaenokkostya-coder, @ottodeng, @everettjf, and @lrg913427-dot. - Agents/tools: return critical tool-loop circuit-breaker stops as blocked tool results instead of thrown tool failures, so models see the guardrail and stop retrying the same call. Thanks @rayraiser. - Model commands: clarify direct and inline `/model` acknowledgements for non-default selections as session-scoped. Thanks @addu2612. +- Doctor/gateway: stop warning that non-existent, unconfigured user-bin directories are required in the Gateway service PATH. Fixes #76017. Thanks @xiphis. - TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019. - Memory Wiki: accept relative Markdown links that include the `.md` suffix during broken-wikilink validation, avoiding false positives for native render-mode links. Thanks @Kenneth8128. - OpenAI Codex: show the device-pairing code in the interactive SSH/headless prompt while keeping the short-lived code out of persistent runtime logs. Fixes #74212. Thanks @da22le123. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 696a7a233f8..32265ca6fa7 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -473,7 +473,7 @@ That stages grounded durable candidates into the short-term dreaming store while 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. + 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. The audit accepts existing stable user-bin directories and explicit environment roots; it does not warn that missing, unconfigured `$HOME/.npm-global/bin`, `$HOME/bin`, or `$HOME/.nix-profile/bin` entries are required. diff --git a/src/daemon/service-audit.test.ts b/src/daemon/service-audit.test.ts index 1fc1bd215b9..a6c932c7ef6 100644 --- a/src/daemon/service-audit.test.ts +++ b/src/daemon/service-audit.test.ts @@ -1,3 +1,6 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { auditGatewayServiceConfig, @@ -120,6 +123,50 @@ describe("auditGatewayServiceConfig", () => { ).toBe(false); }); + it("does not require missing unconfigured user-bin defaults in gateway service PATH", async () => { + const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-service-audit-home-")); + try { + const localBin = path.join(home, ".local/bin"); + await fs.mkdir(localBin, { recursive: true }); + const servicePath = [ + localBin, + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "/bin", + ].join(":"); + + const audit = await auditGatewayServiceConfig({ + env: { HOME: home }, + platform: "darwin", + command: { + programArguments: ["/usr/bin/node", "gateway"], + environment: { PATH: servicePath }, + }, + }); + + expect(hasIssue(audit, SERVICE_AUDIT_CODES.gatewayPathMissingDirs)).toBe(false); + } finally { + await fs.rm(home, { recursive: true, force: true }); + } + }); + + it("still requires explicit env-configured tool roots in gateway service PATH", async () => { + const audit = await auditGatewayServiceConfig({ + env: { HOME: "/tmp/openclaw-testuser", PNPM_HOME: "/opt/pnpm" }, + platform: "linux", + command: { + programArguments: ["/usr/bin/node", "gateway"], + environment: { PATH: "/usr/local/bin:/usr/bin:/bin" }, + }, + }); + + const issue = audit.issues.find( + (entry) => entry.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs, + ); + expect(issue?.message).toContain("/opt/pnpm"); + }); + it("accepts Linux fnm aliases/default without requiring the legacy current symlink", async () => { const env = { HOME: "/tmp/openclaw-testuser", diff --git a/src/daemon/service-audit.ts b/src/daemon/service-audit.ts index 8e4506283ed..a27086a8c85 100644 --- a/src/daemon/service-audit.ts +++ b/src/daemon/service-audit.ts @@ -433,7 +433,11 @@ function auditGatewayServicePath( return; } - const expected = getMinimalServicePathPartsFromEnv({ platform, env }); + const expected = getMinimalServicePathPartsFromEnv({ + platform, + env, + includeMissingUserBinDefaults: false, + }); const parts = servicePath .split(getPathModule(platform).delimiter) .map((entry) => entry.trim()) diff --git a/src/daemon/service-env.test.ts b/src/daemon/service-env.test.ts index aecbb329b98..003221df962 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -231,6 +231,20 @@ describe("getMinimalServicePathParts - Linux user directories", () => { expect(result).not.toContain("/Users/testuser/.local/share/pnpm"); }); + it("can omit missing stable user-bin defaults for service PATH audits", () => { + const result = getMinimalServicePathPartsFromEnv({ + platform: "darwin", + env: { HOME: "/Users/testuser" }, + existsSync: (candidate) => candidate === "/Users/testuser/.local/bin", + includeMissingUserBinDefaults: false, + }); + + expect(result).toContain("/Users/testuser/.local/bin"); + expect(result).not.toContain("/Users/testuser/.npm-global/bin"); + expect(result).not.toContain("/Users/testuser/bin"); + expect(result).not.toContain("/Users/testuser/.nix-profile/bin"); + }); + it("keeps env-configured roots when fallback directories are missing", () => { const result = getMinimalServicePathPartsFromEnv({ platform: "linux", diff --git a/src/daemon/service-env.ts b/src/daemon/service-env.ts index 090140f6eea..a5a0705819f 100644 --- a/src/daemon/service-env.ts +++ b/src/daemon/service-env.ts @@ -32,6 +32,7 @@ type MinimalServicePathOptions = { cwd?: string; env?: Record; existsSync?: (candidate: string) => boolean; + includeMissingUserBinDefaults?: boolean; }; type BuildServicePathOptions = MinimalServicePathOptions & { @@ -168,10 +169,14 @@ function addCommonUserBinDirs( dirs: string[], home: string, existsSync: (candidate: string) => boolean, + includeMissingDefaults: boolean, ): void { - dirs.push(`${home}/.local/bin`); - dirs.push(`${home}/.npm-global/bin`); - dirs.push(`${home}/bin`); + const addDefault = includeMissingDefaults + ? (candidate: string) => dirs.push(candidate) + : (candidate: string) => addExistingDir(dirs, candidate, existsSync); + addDefault(`${home}/.local/bin`); + addDefault(`${home}/.npm-global/bin`); + addDefault(`${home}/bin`); addExistingDir(dirs, `${home}/.volta/bin`, existsSync); addExistingDir(dirs, `${home}/.asdf/shims`, existsSync); addExistingDir(dirs, `${home}/.bun/bin`, existsSync); @@ -196,6 +201,8 @@ function addNixProfileBinDirs( home: string, env: Record | undefined, options: Pick, + includeMissingDefault: boolean, + existsSync: (candidate: string) => boolean, ): void { const nixProfiles = env?.NIX_PROFILES?.trim(); if (nixProfiles) { @@ -203,7 +210,12 @@ function addNixProfileBinDirs( addEnvConfiguredBinDir(dirs, appendSubdir(profile, "bin"), options); } } else { - dirs.push(`${home}/.nix-profile/bin`); + const defaultProfileBin = `${home}/.nix-profile/bin`; + if (includeMissingDefault) { + dirs.push(defaultProfileBin); + } else { + addExistingDir(dirs, defaultProfileBin, existsSync); + } } } @@ -229,7 +241,7 @@ function resolveDarwinUserBinDirs( home: string | undefined, env?: Record, existsSync: (candidate: string) => boolean = fs.existsSync, - options: Pick = {}, + options: Pick = {}, ): string[] { if (!home) { return []; @@ -237,6 +249,7 @@ function resolveDarwinUserBinDirs( const dirs: string[] = []; const pathOptions = { ...options, home }; + const includeMissingUserBinDefaults = options.includeMissingUserBinDefaults ?? true; // Env-configured bin roots (override defaults when present). // Note: FNM_DIR on macOS defaults to ~/Library/Application Support/fnm @@ -250,10 +263,10 @@ function resolveDarwinUserBinDirs( // pnpm: binary is directly in PNPM_HOME (not in bin subdirectory) // Common user bin directories - addCommonUserBinDirs(dirs, home, existsSync); + addCommonUserBinDirs(dirs, home, existsSync, includeMissingUserBinDefaults); // Nix Home Manager (cross-platform) - addNixProfileBinDirs(dirs, home, env, pathOptions); + addNixProfileBinDirs(dirs, home, env, pathOptions, includeMissingUserBinDefaults, existsSync); // Node version managers - macOS specific paths // nvm: no stable default path, depends on user's shell configuration @@ -275,7 +288,7 @@ function resolveLinuxUserBinDirs( home: string | undefined, env?: Record, existsSync: (candidate: string) => boolean = fs.existsSync, - options: Pick = {}, + options: Pick = {}, ): string[] { if (!home) { return []; @@ -283,6 +296,7 @@ function resolveLinuxUserBinDirs( const dirs: string[] = []; const pathOptions = { ...options, home }; + const includeMissingUserBinDefaults = options.includeMissingUserBinDefaults ?? true; // Env-configured bin roots (override defaults when present). addCommonEnvConfiguredBinDirs(dirs, env, pathOptions); @@ -291,10 +305,10 @@ function resolveLinuxUserBinDirs( addEnvConfiguredBinDir(dirs, appendSubdir(env?.FNM_DIR, "current/bin"), pathOptions); // Common user bin directories - addCommonUserBinDirs(dirs, home, existsSync); + addCommonUserBinDirs(dirs, home, existsSync, includeMissingUserBinDefaults); // Nix Home Manager (cross-platform) - addNixProfileBinDirs(dirs, home, env, pathOptions); + addNixProfileBinDirs(dirs, home, env, pathOptions, includeMissingUserBinDefaults, existsSync); // Node version managers addExistingDir(dirs, `${home}/.nvm/current/bin`, existsSync); // nvm with current symlink