feat(runtime)!: require Node 26 across all installers, heal, and upgrade paths

Hermes now pins its toolchain to Node 26 everywhere. Every path that
installs, accepts, heals, or upgrades a Node runtime moves from the old
22-default / `^20.19 || >=22.12` floor to a single rule: Node >=26.

Installers:
- scripts/install.sh — NODE_VERSION=26; node_satisfies_build() collapses
  the two-branch Vite floor to `major >= 26`; user-facing messages updated.
- scripts/install.ps1 — $NodeVersion=26; Test-NodeVersionOk likewise;
  winget fallback switches OpenJS.NodeJS.LTS -> OpenJS.NodeJS (26 is
  Current, not LTS — the LTS manifest would reinstall a too-old Node).
- Dockerfile — node_source stage node:22-bookworm-slim -> node:26 (digest
  pinned, amd64 sha256:9e6f...bf73).
- nix/ was already on nodejs_26 (lib.nix, npm-12-0-2.nix); the checks.nix
  wrapper check ratchets from `>= 20` to `>= 26`.

Heal/upgrade paths:
- scripts/lib/node-bootstrap.sh — HERMES_NODE_TARGET_MAJOR default 22->26
  and HERMES_NODE_MIN_VERSION default 20->26, so heal_managed_node,
  _nb_install_bundled_node, and the fnm/proto/nvm/brew rungs all target 26
  and stop accepting an on-PATH Node below it. Both remain env-overridable.
- hermes_constants.py — _HERMES_NODE_TARGET_MAJOR fallback 22->26, which
  drives the Windows heal path's latest-v26.x download.

Version gates:
- package.json engines.node >=20 -> >=26; apps/desktop engines
  `^20.19.0 || >=22.12.0` -> `>=26.0.0`.
- CI setup-node: all five workflows 22 -> 26.
- Docs describing Hermes's own toolchain updated (windows-native, docker,
  acp, nix-setup, contributing). Skill docs describing third-party tools'
  own requirements are untouched.

Termux still installs via `pkg install nodejs` best-effort (nodejs.org
ships no Android tarballs); that path was never version-gated.

Verified: bash -n on both shell scripts, PowerShell AST parse of
install.ps1, latest-v26.x index resolves (node-v26.5.1), and the install
test suite — 18 tests across the 5 install/runtime test files — passes.
This commit is contained in:
ethernet 2026-08-01 21:04:48 -04:00
parent aa4ce7a507
commit 713a983e4a
19 changed files with 53 additions and 61 deletions

View File

@ -65,7 +65,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
node-version: 26
cache: npm
cache-dependency-path: website/package-lock.json

View File

@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
node-version: 26
cache: npm
cache-dependency-path: website/package-lock.json

View File

@ -39,7 +39,7 @@ jobs:
# ── Node ───────────────────────────────────────────────────────────
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
node-version: 26
cache: npm
- name: grab npm 12

View File

@ -67,7 +67,7 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
node-version: 26
cache: npm
- name: grab npm 12

View File

@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
node-version: 26
cache: npm
- name: grab npm 12
@ -66,7 +66,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
node-version: 26
cache: npm
- name: grab npm 12

View File

@ -41,14 +41,14 @@ RUN apt-get -o Acquire::Retries=3 update && \
make install
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source
# Node 22 LTS source stage. Debian trixie's bundled nodejs is pinned to 20.x
# Node 26 source stage. Debian trixie's bundled nodejs is pinned to 20.x
# which reached EOL in April 2026 — we copy node + npm + corepack from the
# upstream node:22 image instead so we can stay on a supported LTS without
# waiting for Debian 14 (forky, ~mid-2027). Bookworm-based slim image used
# so the produced binary links against glibc 2.36, which runs cleanly on
# our Debian 13 (trixie, glibc 2.41) runtime. Bumping to a new Node major
# is a one-line ARG change; see #4977.
FROM node:22-bookworm-slim@sha256:7af03b14a13c8cdd38e45058fd957bf00a72bbe17feac43b1c15a689c029c732 AS node_source
# upstream node:26 image instead (Hermes pins its toolchain to Node 26
# everywhere). Bookworm-based slim image used so the produced binary links
# against glibc 2.36, which runs cleanly on our Debian 13 (trixie, glibc
# 2.41) runtime. Bumping to a new Node major is a one-line ARG change; see
# #4977.
FROM node:26-bookworm-slim@sha256:9e6f9357d371591e32ab6f2d8a26d63bdd0d17c29eee3f4f3e7e454d9634bf73 AS node_source
FROM debian:13.4
# Disable Python stdout buffering to ensure logs are printed immediately.

View File

@ -8,7 +8,7 @@
"type": "module",
"main": "dist/electron-main.mjs",
"engines": {
"node": "^20.19.0 || >=22.12.0"
"node": ">=26.0.0"
},
"scripts": {
"clean": "npm run clean:e2e && npm run clean:renderer && npm run clean:electron",

View File

@ -317,7 +317,7 @@ def _candidate_node_command_names(command: str) -> list[str]:
return [f"{base}.cmd", f"{base}.exe", base]
_HERMES_NODE_TARGET_MAJOR = int(os.environ.get("HERMES_NODE_TARGET_MAJOR", "22"))
_HERMES_NODE_TARGET_MAJOR = int(os.environ.get("HERMES_NODE_TARGET_MAJOR", "26"))
_managed_node_heal_attempted = False
_NODE_BOOTSTRAP_SCRIPT = Path(__file__).resolve().parent / "scripts" / "lib" / "node-bootstrap.sh"

View File

@ -256,8 +256,8 @@ json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2)
echo "ok" > $out/result
'';
# Verify HERMES_NODE is set in wrapper and points to Node 20+
# (string-width uses the /v regex flag which requires Node 20+)
# Verify HERMES_NODE is set in wrapper and points to Node 26+
# (Hermes pins its toolchain to Node 26 everywhere)
hermes-node = pkgs.runCommand "hermes-node-version" { } ''
set -e
echo "=== Checking HERMES_NODE in wrapper ==="
@ -270,9 +270,9 @@ json.dump(sorted(leaf_paths(DEFAULT_CONFIG)), sys.stdout, indent=2)
echo "PASS: HERMES_NODE executable at $HERMES_NODE"
NODE_MAJOR=$("$HERMES_NODE" --version | sed 's/^v//' | cut -d. -f1)
test "$NODE_MAJOR" -ge 20 || \
(echo "FAIL: Node v$NODE_MAJOR < 20, TUI needs /v regex flag support"; exit 1)
echo "PASS: Node v$NODE_MAJOR >= 20"
test "$NODE_MAJOR" -ge 26 || \
(echo "FAIL: Node v$NODE_MAJOR < 26, Hermes requires Node 26"; exit 1)
echo "PASS: Node v$NODE_MAJOR >= 26"
echo "=== All HERMES_NODE checks passed ==="
mkdir -p $out

4
package-lock.json generated
View File

@ -29,7 +29,7 @@
"typescript-eslint": "8.64.0"
},
"engines": {
"node": ">=20.0.0",
"node": ">=26.0.0",
"npm": ">=12.0.0"
}
},
@ -168,7 +168,7 @@
"wait-on": "9.0.10"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
"node": ">=26.0.0"
}
},
"apps/desktop/node_modules/ignore": {

View File

@ -53,7 +53,7 @@
"brace-expansion": "5.0.8"
},
"engines": {
"node": ">=20.0.0",
"node": ">=26.0.0",
"npm": ">=12.0.0"
},
"allowScripts": {

View File

@ -150,7 +150,7 @@ $PythonVersion = "3.11"
# interpreters, so this list also matches a pre-existing system Python. Single
# source of truth shared by Test-Python's fallback and Resolve-AvailablePythonVersion.
$PythonFallbackVersions = @("3.12", "3.13", "3.10")
$NodeVersion = "22"
$NodeVersion = "26"
# Stage-protocol version. Bumped only for genuinely breaking changes to the
# manifest schema, stage-name set semantics, or stdout JSON shape. Adding a
@ -1089,11 +1089,10 @@ function Set-GitBashEnvVar {
Write-Info "If needed, set HERMES_GIT_BASH_PATH manually to your bash.exe path."
}
# The desktop build runs Vite ^8, which refuses to start on Node outside
# `^20.19 || >=22.12` -- older Node lacks node:util.styleText, so `vite build`
# crashes with a SyntaxError that surfaces only as the opaque "Build desktop
# app ... exit code 1" install failure. Returns $true when a `node --version`
# string clears that floor.
# Hermes requires Node 26 across every install: the desktop build's toolchain
# floor is pinned there and the managed runtime, heal, and upgrade paths all
# provision latest-v26.x. Returns $true when a `node --version` string clears
# that floor.
function Test-NodeVersionOk {
param([string]$Version)
try {
@ -1101,9 +1100,7 @@ function Test-NodeVersionOk {
} catch {
return $false
}
if ($v.Major -eq 20 -and $v.Minor -ge 19) { return $true }
if ($v.Major -ge 22 -and ($v.Major -gt 22 -or $v.Minor -ge 12)) { return $true }
return $false
return ($v.Major -ge 26)
}
function Test-Node {
@ -1117,7 +1114,7 @@ function Test-Node {
$script:HasNode = $true
return $true
}
Write-Warn "Node.js $version is too old for the desktop build (need ^20.19 or >=22.12)"
Write-Warn "Node.js $version is too old (Hermes requires Node >=26)"
}
# Prefer a Hermes-managed Node from a previous run over a too-old system one.
@ -1208,7 +1205,7 @@ function Test-Node {
# even after a "successful" install. The OpenJS manifest does
# publish an arm64 installer, so this is safe.
$wingetArgs = @(
'install','OpenJS.NodeJS.LTS','--silent',
'install','OpenJS.NodeJS','--silent',
'--accept-package-agreements','--accept-source-agreements'
)
if ((Get-WindowsArch) -eq 'arm64') {
@ -2923,7 +2920,7 @@ function Install-Desktop {
# Always re-resolve Node here. Stages run in separate PowerShell processes,
# so $script:HasNode from Stage-Node isn't visible; more importantly Test-Node
# enforces the build floor (^20.19 || >=22.12) and prepends the Hermes-managed
# enforces the build floor (Node >=26) and prepends the Hermes-managed
# Node to PATH, so the build never runs on a too-old system Node -- the cause
# of the opaque "Build desktop app ... exit code 1" failure (Vite crashes on
# old Node).

View File

@ -57,7 +57,7 @@ else
INSTALL_DIR_EXPLICIT=false
fi
PYTHON_VERSION="3.11"
NODE_VERSION="22"
NODE_VERSION="26"
# FHS-style root install layout (set by resolve_install_layout when applicable):
# code at /usr/local/lib/hermes-agent, command at /usr/local/bin/hermes,
@ -780,21 +780,16 @@ check_git() {
exit 1
}
# The desktop build runs Vite ^8, which refuses to start on Node outside
# `^20.19 || >=22.12` — older Node lacks `node:util.styleText`, so `vite build`
# crashes with a SyntaxError that surfaces only as the opaque "Build desktop
# app … exit code 1" install failure. Returns 0 when the given `node --version`
# string clears that floor; anything below it is replaced with the Hermes-
# managed Node $NODE_VERSION LTS.
# Hermes requires Node 26 across every install: the desktop build's toolchain
# floor is pinned there and the managed runtime, heal, and upgrade paths all
# provision latest-v26.x. Returns 0 when the given `node --version` string
# clears that floor; anything below it is replaced with the Hermes-managed
# Node $NODE_VERSION.
node_satisfies_build() {
local ver="${1#v}"
local major="${ver%%.*}"
local minor="${ver#*.}"; minor="${minor%%.*}"
case "$major" in ''|*[!0-9]*) return 1 ;; esac
case "$minor" in ''|*[!0-9]*) minor=0 ;; esac
if [ "$major" -eq 20 ] && [ "$minor" -ge 19 ]; then return 0; fi
if [ "$major" -ge 22 ] && { [ "$major" -gt 22 ] || [ "$minor" -ge 12 ]; }; then return 0; fi
return 1
[ "$major" -ge 26 ]
}
check_node() {
@ -820,7 +815,7 @@ check_node() {
fi
if command -v node &> /dev/null; then
log_warn "Node.js $(node --version) is too old for the desktop build (need ^20.19 or >=22.12) — installing Hermes-managed Node $NODE_VERSION LTS..."
log_warn "Node.js $(node --version) is too old (Hermes requires Node >=26) — installing Hermes-managed Node $NODE_VERSION..."
elif [ "$DISTRO" = "termux" ]; then
log_info "Node.js not found — installing Node.js via pkg..."
else
@ -869,7 +864,7 @@ install_node() {
;;
esac
# Resolve the latest v22.x.x tarball name from the index page
# Resolve the latest v${NODE_VERSION}.x.x tarball name from the index page
local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/"
local tarball_name
tarball_name=$(curl -fsSL "$index_url" \
@ -2928,7 +2923,7 @@ install_desktop() {
# with no app and a confusing "couldn't find a built desktop" at launch.
# Always re-resolve Node here. Stages run in separate processes, so we can't
# trust an earlier check; more importantly check_node now enforces the build
# floor (^20.19 || >=22.12) and prepends the Hermes-managed Node to PATH, so
# floor (Node >=26) and prepends the Hermes-managed Node to PATH, so
# the build never runs on a too-old system Node — the cause of the opaque
# "Build desktop app … exit code 1" failure (Vite crashes on old Node).
check_node

View File

@ -18,13 +18,13 @@
# if [ "$HERMES_NODE_AVAILABLE" = true ]; then ...; fi
#
# Env inputs (set before sourcing to override defaults):
# HERMES_NODE_MIN_VERSION (default: 20) — accepted on PATH
# HERMES_NODE_TARGET_MAJOR (default: 22) — installed when we install
# HERMES_NODE_MIN_VERSION (default: 26) — accepted on PATH
# HERMES_NODE_TARGET_MAJOR (default: 26) — installed when we install
# HERMES_HOME (default: $HOME/.hermes)
# ============================================================================
HERMES_NODE_MIN_VERSION="${HERMES_NODE_MIN_VERSION:-20}"
HERMES_NODE_TARGET_MAJOR="${HERMES_NODE_TARGET_MAJOR:-22}"
HERMES_NODE_MIN_VERSION="${HERMES_NODE_MIN_VERSION:-26}"
HERMES_NODE_TARGET_MAJOR="${HERMES_NODE_TARGET_MAJOR:-26}"
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
HERMES_NODE_AVAILABLE=false

View File

@ -36,7 +36,7 @@ We value contributions in this order:
| **Git** | With the `git-lfs` extension installed |
| **Python 3.113.13** | uv will install it if missing |
| **uv** | Fast Python package manager ([install](https://docs.astral.sh/uv/)) |
| **Node.js 20+** | Optional — needed for browser tools and WhatsApp bridge (matches root `package.json` engines) |
| **Node.js 26+** | Optional — needed for browser tools and WhatsApp bridge (matches root `package.json` engines) |
### Install with the standard installer

View File

@ -772,7 +772,7 @@ nix develop
# Shell provides:
# - Python 3.12 + uv (deps installed into .venv on first entry)
# - Node.js 22, ripgrep, git, openssh, ffmpeg on PATH
# - Node.js 26, ripgrep, git, openssh, ffmpeg on PATH
# - Stamp-file optimization: re-entry is near-instant if deps haven't changed
hermes setup

View File

@ -472,7 +472,7 @@ docker run -d \
The official image is based on `debian:13.4` and includes:
- Python 3.13 with dependencies synced from the lockfile via `uv sync --frozen --no-install-project` for the baked extras (`all`, `messaging`, Anthropic/Bedrock/Azure identity, Hindsight, Matrix), followed by a no-dependency editable install of Hermes itself.
- Node.js 22 + npm (for browser automation, WhatsApp bridge, TUI/Desktop bundles, and workspace build tooling)
- Node.js 26 + npm (for browser automation, WhatsApp bridge, TUI/Desktop bundles, and workspace build tooling)
- Playwright with Chromium (`npx playwright install --with-deps chromium --only-shell`)
- ripgrep, ffmpeg, git, and `xz-utils` as system utilities
- **`docker-cli`** — so agents running inside the container can drive the host's Docker daemon (bind-mount `/var/run/docker.sock` to opt in) for `docker build`, `docker run`, container inspection, etc.

View File

@ -89,7 +89,7 @@ This is the standalone command. The terminal-auth flow (`hermes acp --setup`) al
What it does:
- Installs Node.js 22 LTS into `~/.hermes/node/` if missing
- Installs Node.js 26 into `~/.hermes/node/` if missing
- `npm install -g agent-browser @askjo/camofox-browser` into that prefix (no sudo needed — `npm`'s `--prefix` points at the user-writable Hermes-managed Node)
- Installs Playwright Chromium, or uses a detected system Chrome/Chromium when available

View File

@ -56,7 +56,7 @@ On first launch (and on demand when a missing tool is detected), Hermes runs a s
| Dependency | Why Hermes needs it |
|---|---|
| **PortableGit** | Provides `bash.exe` for the terminal tool and `git` for in-session clones. Provisioned at install time, not by `dep_ensure`. |
| **Node.js 22** | Required for the browser tool (`agent-browser`), the TUI's web bridge, and the WhatsApp bridge. |
| **Node.js 26** | Required for the browser tool (`agent-browser`), the TUI's web bridge, and the WhatsApp bridge. |
| **ffmpeg** | Audio format conversion for TTS / voice messages. |
| **ripgrep** | Fast file search — falls back to `grep` if unavailable. |
| **npm packages** | `agent-browser`, Playwright Chromium, and any per-toolset Node deps are installed once at first browser-tool use. |
@ -69,7 +69,7 @@ Top-to-bottom, in order:
1. **Bootstraps `uv`** — Astral's fast Python manager. Installed to `%USERPROFILE%\.local\bin`.
2. **Installs Python 3.11** via `uv`. No existing Python needed.
3. **Installs Node.js 22** (winget if available, else a portable Node tarball unpacked under `%LOCALAPPDATA%\hermes\node`). Used for the browser tool and the WhatsApp bridge.
3. **Installs Node.js 26** (winget if available, else a portable Node tarball unpacked under `%LOCALAPPDATA%\hermes\node`). Used for the browser tool and the WhatsApp bridge.
4. **Installs portable Git** — if `git` is already on PATH the installer uses it; otherwise it downloads a trimmed, self-contained **PortableGit** (~45 MB, from the official `git-for-windows` release) to `%LOCALAPPDATA%\hermes\git`. No admin, no Windows installer registry, no interference with anything else on the box.
5. **Clones the repo** to `%LOCALAPPDATA%\hermes\hermes-agent` and creates a virtualenv inside it.
6. **Tiered `uv pip install`** — tries `.[all]` first, falls back to progressively smaller sets (`[messaging,dashboard,ext]` → `[messaging]``.`) if a `git+https` dep flakes on rate-limited GitHub. Prevents "single flake drops you to a bare install" failure mode.
@ -306,7 +306,7 @@ You set it in the current process only; close and reopen the shell, or set it at
Chromium is auto-installed on first run. If the install failed (rate-limited GitHub, Playwright CDN hiccup), run `hermes doctor` — it will surface the missing Chromium and print the exact `npx playwright install chromium` command to fix it.
**`agent-browser` fails with a weird Node version error.**
The installer provisions Node 22 at `%LOCALAPPDATA%\hermes\node` but your PATH may have an older system Node 18 first. Either move Hermes's node dir earlier on PATH, or delete the system install if you don't use Node elsewhere.
The installer provisions Node 26 at `%LOCALAPPDATA%\hermes\node` but your PATH may have an older system Node 18 first. Either move Hermes's node dir earlier on PATH, or delete the system install if you don't use Node elsewhere.
**Chinese / Japanese / Arabic characters show as `?` in the CLI.**
The UTF-8 stdio shim didn't activate. Check that `HERMES_DISABLE_WINDOWS_UTF8` is NOT set (`Get-ChildItem env:HERMES_DISABLE_WINDOWS_UTF8`). If it's empty and you still see `?`, the console host (very old `cmd.exe`) may not support UTF-8 at all — switch to Windows Terminal.