openclaw/node-version.mjs
Peter Steinberger cd90e0619e
fix(daemon): node install reports a supported Node runtime as unsupported when the working directory is unreadable (#131998)
* fix(daemon): stop reporting failed runtime probes as unsupported runtimes

The daemon runtime probe wrapped its exec in a bare catch that returned
`supported: false`, so any failure to *run* the probe was laundered into a
verdict that the runtime itself was unsupported. Operators on a perfectly
good Node install were told to install a Node version they already had.

Observed on Ubuntu 26.04 with Node 26.8.1: `openclaw node install` failed
with "No supported Node runtime was selected for the daemon" whenever the
process cwd was not readable by the service user (e.g. `runuser -u openclaw`
inheriting root's 0700 home over SSH), because every child spawn then fails
EACCES. The version logic was never wrong -- resolveSystemNodeInfo returned
supported:true and resolvePreferredNodePath returned /usr/bin/node when
probed directly on the affected host.

Node and Bun probes now share one resolver returning a closed
supported | unsupported | probe-failed union. A failed probe retains its
cause, executable, and cwd, and selection propagates that instead of falling
through to Node-upgrade advice.

Also:
- Derive the supported-version wording from NODE_RELEASE_FLOORS via a new
  exported SUPPORTED_NODE_VERSIONS, replacing six hand-copied spellings that
  omitted the >=25.9.0 line and told operators to downgrade.
- Forward OPENCLAW_WRAPPER through the node-host install path; the documented
  escape hatch was previously gateway-daemon-only.

Production LOC net +6 (+156/-150); consolidating the duplicated Node/Bun
probes paid for the new failure handling.

* docs(cli): drop machine-local path from node probe-failure guidance

ClawSweeper P3: docs/AGENTS.md requires generic docs content with no local
paths. The probe-failure recovery example prescribed a specific directory;
state the readability requirement instead.
2026-08-28 11:43:11 -07:00

84 lines
3.2 KiB
JavaScript

// Zero-dependency Node release-version contract shared by source and packaged entry points.
const NODE_RELEASE_VERSION_RE =
/^v?((?:0|[1-9]\d*))\.((?:0|[1-9]\d*))\.((?:0|[1-9]\d*))(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
const NODE_RELEASE_FLOORS = [
{ major: 22, minor: 22, patch: 3 },
{ major: 24, minor: 15, patch: 0 },
{ major: 25, minor: 9, patch: 0 },
];
const HIGHEST_RELEASE_FLOOR = NODE_RELEASE_FLOORS[NODE_RELEASE_FLOORS.length - 1];
// Render diagnostics from the same release floors used by the runtime guard.
export const SUPPORTED_NODE_VERSIONS = `${NODE_RELEASE_FLOORS.map(
({ major, minor, patch }, index) =>
`>=${major}.${minor}.${patch}${index < NODE_RELEASE_FLOORS.length - 1 ? ` <${major + 1}` : ""}`,
)
.join(", ")
.replace(/, ([^,]+)$/, ", or $1")} (Node 26 recommended)`;
/** Parses an anchored release SemVer, allowing a leading v and valid build metadata. */
export function parseNodeReleaseVersion(value) {
if (typeof value !== "string") {
return null;
}
const match = NODE_RELEASE_VERSION_RE.exec(value.trim());
if (!match) {
return null;
}
const version = {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
};
return Object.values(version).every(Number.isSafeInteger) ? version : null;
}
export function isNodeVersionAtLeast(version, minimum) {
if (!version) {
return false;
}
if (version.major !== minimum.major) {
return version.major > minimum.major;
}
if (version.minor !== minimum.minor) {
return version.minor > minimum.minor;
}
return version.patch >= minimum.patch;
}
/** Checks OpenClaw's supported release lines. Node 23 remains unsupported. */
export function isSupportedOpenClawNodeVersion(value) {
const version = parseNodeReleaseVersion(value);
if (!version) {
return false;
}
const minimum = NODE_RELEASE_FLOORS.find((floor) => floor.major === version.major);
return minimum
? isNodeVersionAtLeast(version, minimum)
: version.major > HIGHEST_RELEASE_FLOOR.major;
}
function renderProcessNodeVersionCheck() {
const patternSource = JSON.stringify(NODE_RELEASE_VERSION_RE.source);
const releaseFloors = JSON.stringify(NODE_RELEASE_FLOORS);
return `((value) => {
if (typeof value !== "string") return false;
const match = new RegExp(${patternSource}, "u").exec(value.trim());
if (!match) return false;
const version = { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
if (!Object.values(version).every(Number.isSafeInteger)) return false;
const floors = ${releaseFloors};
const minimum = floors.find((floor) => floor.major === version.major);
const atLeast = (floor) =>
version.major > floor.major ||
(version.major === floor.major &&
(version.minor > floor.minor ||
(version.minor === floor.minor && version.patch >= floor.patch)));
return minimum ? atLeast(minimum) : version.major > floors[floors.length - 1].major;
})(process.versions.node)`;
}
// Worker bootstrap runs before OpenClaw is transferred. Carry the canonical
// release policy as a self-contained expression instead of a second parser.
export const PROCESS_NODE_VERSION_CHECK = renderProcessNodeVersionCheck();