From 2de49b52044df8a1b32ad30ee7faf89cf21b132d Mon Sep 17 00:00:00 2001 From: m3tam3re Date: Sat, 12 Sep 2026 13:58:56 +0200 Subject: [PATCH] fix(desktop): build hermes-desktop with corrected Electron 43.6.0 headers hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream hermes-agent v2026.8.31 pins sha256-f8bSb… for the artifacts.electronjs.org v43.6.0 headers tarball. The CDN swapped that artifact, breaking every desktop build with a fixed-output derivation hash mismatch (specified f8bSb… vs got xDgc5…). Local copy of upstream nix/desktop.nix with: - verified hash for electron 43.6.0 (nix store prefetch-file) - per-version hash map with a loud throw for unknown versions - icon/desktop-entry paths resolved against the flake input checkout Remove once upstream ships the corrected hash and the hermes-agent input is bumped past that release. --- profiles/contexts/desktop/default.nix | 17 +- profiles/contexts/desktop/hermes-desktop.nix | 244 +++++++++++++++++++ 2 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 profiles/contexts/desktop/hermes-desktop.nix diff --git a/profiles/contexts/desktop/default.nix b/profiles/contexts/desktop/default.nix index a4bdfbd..68d516a 100644 --- a/profiles/contexts/desktop/default.nix +++ b/profiles/contexts/desktop/default.nix @@ -4,7 +4,20 @@ m3taHomeInputs, pkgs, ... -}: { +}: let + # WORKAROUND (2026-09-12): hermes-agent v2026.8.31 pins a stale hash for + # the Electron 43.6.0 headers artifact (the CDN swapped the file). Build + # the desktop app from a local copy of upstream's nix/desktop.nix with + # the verified hash. Remove when upstream fixes it and the input is bumped. + hermesDesktopFixed = let + hermesAgent = m3taHomeInputs.hermes-agent.packages.${pkgs.stdenv.hostPlatform.system}.default; + in + pkgs.callPackage ./hermes-desktop.nix { + inherit hermesAgent; + hermesNpmLib = hermesAgent.hermesNpmLib; + hermesSrc = m3taHomeInputs.hermes-agent.outPath; + }; +in { imports = [ ./wm ./apps @@ -72,7 +85,7 @@ (element-desktop.override { commandLineArgs = "--password-store=gnome-libsecret"; }) - m3taHomeInputs.hermes-agent.packages.${pkgs.stdenv.hostPlatform.system}.desktop + hermesDesktopFixed launch-webapp file-roller gsettings-desktop-schemas diff --git a/profiles/contexts/desktop/hermes-desktop.nix b/profiles/contexts/desktop/hermes-desktop.nix new file mode 100644 index 0000000..6727cee --- /dev/null +++ b/profiles/contexts/desktop/hermes-desktop.nix @@ -0,0 +1,244 @@ +# hermes-desktop.nix — local copy of hermes-agent's nix/desktop.nix +# +# WORKAROUND for an upstream build failure (hermes-agent v2026.8.31): +# +# Upstream pins sha256-f8bSbLRmtbP93CJAvEBs+sHWDZ1xP2bcpLhC1EnOmZU= +# for https://artifacts.electronjs.org/headers/dist/v43.6.0/node-v43.6.0-headers.tar.gz +# The Electron CDN swapped that artifact, so the pinned hash no longer +# matches what the server serves today (verified 2026-09-12 via +# `nix store prefetch-file`). Upstream has fixed this class of breakage +# repeatedly ("fix electron headers sha" commits); main is not fixed yet. +# +# Differences from upstream nix/desktop.nix: +# 1. electron headers sha256 per electron version, with a clear throw +# for unknown versions (upstream hardcodes one hash). +# 2. icon.png / linux_desktop_entry.py resolve against `hermesSrc` +# (the flake input checkout) instead of relative repo paths. +# +# REMOVE THIS FILE and the hermesDesktopFixed let-binding in default.nix +# once upstream ships the corrected hash and the hermes-agent input is +# bumped past that release. +{ + pkgs, + lib, + stdenv, + makeWrapper, + hermesNpmLib, + electron, + hermesAgent, + python3, + # Checkout of the hermes-agent flake input (outPath), used for files + # that upstream references with relative paths inside its own repo. + hermesSrc, + # Environment to bake into the launcher. A GUI launcher reads none of the + # shell profile, so a variable that an interactive shell exports does not + # reach an app that the desktop menu starts. The Home Manager module passes + # HERMES_HOME and HERMES_MANAGED here, which gives the app the same state + # directory as the services. + extraEnv ? {}, + # Shell lines to run before the app starts. A secret belongs here and never + # in extraEnv: makeWrapper writes a --set value into the Nix store, which + # all users can read. A --run line reads the value from a runtime path at + # each start instead. + extraRun ? [], + ... +}: let + # Each flag goes on its own continued line, and the leading backslash is + # inside the generated string. An empty attribute set then adds no text + # at all, and cannot leave a backslash above a blank line. That fault ends the + # makeWrapper command early, and the next flag runs as a shell command. + extraEnvFlags = lib.concatMapStrings ( + name: " \\\n --set ${name} ${lib.escapeShellArg (toString extraEnv.${name})}" + ) (lib.attrNames extraEnv); + + extraRunFlags = lib.concatMapStrings (line: " \\\n --run ${lib.escapeShellArg line}") extraRun; + + # Electron's CDN has re-uploaded header artifacts for the same version + # before, which silently changes their hash. Keep one verified hash per + # electron version and fail loudly (instead of with a cryptic FOD + # mismatch) whenever a bump needs a new one. + electronHeaderHashes = { + "43.6.0" = "sha256-xDgc5PpkcLpWHnlqVcjBD3SxJKtkUoSGLnJaSSrxJtI="; + }; + + electronHeaders = pkgs.fetchurl { + url = "https://artifacts.electronjs.org/headers/dist/v${electron.version}/node-v${electron.version}-headers.tar.gz"; + sha256 = + electronHeaderHashes.${ + electron.version + } + or (throw '' + hermes-desktop workaround: no verified Electron headers hash for version ${electron.version}. + Run: nix store prefetch-file https://artifacts.electronjs.org/headers/dist/v${electron.version}/node-v${electron.version}-headers.tar.gz + and add it to electronHeaderHashes in profiles/contexts/desktop/hermes-desktop.nix (m3ta-home). + ''); + }; + + # node-pty ships no Electron-tagged prebuild we can trust to match this + # exact nixpkgs electron version, so it's always compiled from source + # against Electron's own headers (not whatever Node ran `npm`). + targetPlatform = + if stdenv.hostPlatform.isDarwin + then "darwin" + else if stdenv.hostPlatform.isLinux + then "linux" + else throw "hermes-desktop: unsupported host platform for node-pty staging"; + + targetArch = + if stdenv.hostPlatform.isAarch64 + then "arm64" + else if stdenv.hostPlatform.isx86_64 + then "x64" + else throw "hermes-desktop: unsupported host arch for node-pty staging"; + + # Build the renderer (dist/ + electron/ + package.json). + renderer = hermesNpmLib.buildNpmPackage { + dirs = [ + "apps/desktop" + "apps/shared" + ]; + pname = "hermes-desktop-renderer"; + + doCheck = true; + + buildPhase = '' + runHook preBuild + + mkdir -p apps/desktop/build + + patchShebangs . + + pushd apps/desktop + # typecheck :3 + npm exec -- tsc -b + + # build the renderer bundle + # vite's emptyOutDir wipes dist/ on every run + # so it has to be first + npm exec -- vite build + + # build the electron bundle + node scripts/bundle-electron-main.mjs + + # Compile node-pty against Electron's actual ABI (the nixpkgs + # `electron` we ship). Headers come from a pinned fetchurl input + # since the sandbox has no network here, so node-gyp's + # normal --disturl download path can't run. + mkdir -p "$TMPDIR/electron-headers" + tar -xzf ${electronHeaders} -C "$TMPDIR/electron-headers" --strip-components=1 + + ${lib.getExe hermesNpmLib.node-gyp} rebuild \ + --directory=../../node_modules/node-pty \ + --build-from-source \ + --runtime=electron \ + --target=${electron.version} \ + --nodedir="$TMPDIR/electron-headers" \ + --disturl="" \ + --offline + + # Target platform/arch come from stdenv.hostPlatform, not the + # build host's own process.platform/arch. + node scripts/stage-native-deps.mjs ${targetPlatform} ${targetArch} + popd + + runHook postBuild + ''; + + checkPhase = '' + runHook preCheck + + pushd apps/desktop + + npm run postbuild + + # validate staged node-pty native binary is present. + STAGED_PTY_NODE="./dist/node_modules/node-pty/build/Release/pty.node" + + if [ ! -f "$STAGED_PTY_NODE" ]; then + echo "FATAL: Missing staged node-pty native binary at $STAGED_PTY_NODE" + echo "node-pty must be compiled natively" + exit 1 + fi + + popd + + runHook postCheck + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out + # vite writes to apps/desktop/dist/ (we cd'd there in buildPhase). + # stage-native-deps.mjs stages node-pty into dist/node_modules/node-pty, + # so copying dist/ wholesale carries the native dep along with the + # esbuild bundle that require()s it. apps/desktop/build was created + # before the cd. + cp -rn apps/desktop/dist $out/ + + echo '{"schemaVersion":1,"commit":"nix-dummy-commit","branch":"nix","dirty":false,"source":"nix"}' > $out/install-stamp.json + + cp -n apps/desktop/package.json $out/ + runHook postInstall + ''; + }; +in + # Electron wrapper: nixpkgs' electron binary pointed at the renderer dir. + stdenv.mkDerivation { + pname = "hermes-desktop"; + inherit (renderer) version; + + dontUnpack = true; + dontBuild = true; + + nativeBuildInputs = [ + makeWrapper + python3 + ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/share/hermes-desktop $out/bin + cp -r ${renderer}/* $out/share/hermes-desktop/ + + # Standard nixpkgs pattern for electron-builder apps: patch process.resourcesPath + # to point to the app's directory. In Nix, unpackaged electron defaults this + # to the electron distribution's resources path, breaking extraResources lookups. + substituteInPlace $out/share/hermes-desktop/dist/electron-main.mjs \ + --replace-fail "process.resourcesPath" "'$out/share/hermes-desktop'" + + # Wrap the nixpkgs electron binary to launch our app. Set + # HERMES_DESKTOP_HERMES to the absolute path of the nix-built `hermes` + # binary so the desktop's resolver step 4 ("existing Hermes CLI on + # PATH") uses our fully wrapped binary — venv with all deps, + # bundled skills/plugins, runtime PATH (ripgrep/git/ffmpeg/etc). + # No reimplementation of the agent resolver in the wrapper. + makeWrapper ${lib.getExe electron} $out/bin/hermes-desktop \ + --add-flags "$out/share/hermes-desktop" \ + --set HERMES_DESKTOP_HERMES "${lib.getExe hermesAgent}" \ + --set ELECTRON_IS_DEV 0${extraEnvFlags}${extraRunFlags} + + # XDG launcher entry + mkdir -p $out/share/applications $out/share/icons/hicolor/1024x1024/apps + install -m 0644 ${hermesSrc}/apps/desktop/assets/icon.png \ + $out/share/icons/hicolor/1024x1024/apps/hermes.png + export PYTHONPATH=$(mktemp -d) + cp ${hermesSrc}/hermes_cli/linux_desktop_entry.py "$PYTHONPATH/linux_desktop_entry.py" + export DESKTOP_EXEC="$out/bin/hermes-desktop" + export DESKTOP_ICON="$out/share/icons/hicolor/1024x1024/apps/hermes.png" + python3 -c 'import os; from linux_desktop_entry import render_desktop_entry; print(render_desktop_entry(os.environ["DESKTOP_EXEC"], os.environ["DESKTOP_ICON"]))' > $out/share/applications/hermes.desktop + runHook postInstall + ''; + + passthru = { + inherit (renderer.passthru) packageJsonPath; + }; + + meta = with lib; { + description = "Native Electron desktop shell for Hermes Agent"; + homepage = "https://github.com/NousResearch/hermes-agent"; + license = licenses.mit; + platforms = platforms.unix; + mainProgram = "hermes-desktop"; + }; + }