fix: relax gateway service path audit

This commit is contained in:
Peter Steinberger
2026-05-02 11:36:11 +01:00
parent 8f94bd9984
commit eb3e4f20a0
6 changed files with 92 additions and 12 deletions

View File

@@ -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.

View File

@@ -473,7 +473,7 @@ That stages grounded durable candidates into the short-term dreaming store while
<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.
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.
</Accordion>
<Accordion title="18. Config write + wizard metadata">

View File

@@ -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",

View File

@@ -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())

View File

@@ -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",

View File

@@ -32,6 +32,7 @@ type MinimalServicePathOptions = {
cwd?: string;
env?: Record<string, string | undefined>;
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<string, string | undefined> | undefined,
options: Pick<MinimalServicePathOptions, "cwd" | "home">,
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<string, string | undefined>,
existsSync: (candidate: string) => boolean = fs.existsSync,
options: Pick<MinimalServicePathOptions, "cwd" | "home"> = {},
options: Pick<MinimalServicePathOptions, "cwd" | "home" | "includeMissingUserBinDefaults"> = {},
): 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<string, string | undefined>,
existsSync: (candidate: string) => boolean = fs.existsSync,
options: Pick<MinimalServicePathOptions, "cwd" | "home"> = {},
options: Pick<MinimalServicePathOptions, "cwd" | "home" | "includeMissingUserBinDefaults"> = {},
): 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