- nix dev shell: pnpm-workspace.yaml (overrides, catalog, allowBuilds)
joins the package.json stamp, so a settings-only change relocks.
- desktop-pane-fixture: resolve vitest/config from apps/desktop; pnpm
links it only there.
- short-session hang repro: name the lockfile in the target metadata, so
an npm-era target fails as a lockfile mismatch rather than a bare hash.
- test_mirrors: the pnpm mirror test prefetches from the registry and
needs allow_network, like its siblings.
pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.
Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).
The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.
User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
backend.sessionTokenFile used to be rendered into a launcher that read
the file, exported the token and execed hermes -- because hermes took
the token only as an env var. That launcher is gone: hermes reads
HERMES_DASHBOARD_SESSION_TOKEN_FILE itself, so the module now sets the
path in the unit environment (systemd Environment=, launchd
EnvironmentVariables) and builds the plain hermes argv whenever no wait
is configured. Nothing renders the path through a shell any more, which
also retires the escapeShellArg-versus-${XDG_RUNTIME_DIR} bug class for
the backend -- and the same expansion fix lands in the desktop wrapper,
which still reads the token itself. The option insists on an absolute
path; a secret manager's placeholder would pass through unexpanded.
Depends on the HERMES_DASHBOARD_SESSION_TOKEN_FILE backend support;
against an older backend the service would mint a random token and the
desktop would fail to attach with no nix-side error.
The Nix package had no Webapp bundle, and `hermes webapp` could not build
one in the read-only store. nix/webapp.nix builds it in the npm workspace
the hook prepares (no native rebuild, no Electron), exposed as
`.#webapp`. The agent assembler takes it as a `webapp` frontend: the
wrapper sets HERMES_WEBAPP_DIST to it, as HERMES_WEB_DIST is set for the
dashboard.
hermes_cli/webapp.py serves a HERMES_WEBAPP_DIST bundle as shipped: no
freshness check, no build lock, never rebuilt or marked stale, and
`--force-build` says it cannot rebuild it.
Moving the shipped groups to defaultDependencyGroups dropped
extraDependencyGroups from the base callPackage arguments, so an
override (prev: { extraDependencyGroups = prev.extraDependencyGroups ++ [...]; })
that evaluated on main threw 'attribute extraDependencyGroups missing'.
The base package now passes an explicit empty list, and the
extras-extend-package check appends without 'or [ ]' so the shape stays
covered.
Greptile: "Custom extras lack coverage". The extras-extend-package cases
all started from the default package, whose groups now live in
defaultDependencyGroups, so reverting effectivePackage to the old
replacing override still passed every case. Add a custom `package`
built with extraDependencyGroups + extraPythonPackages, set the module's
extras on top, and assert the venv and the package equal one built with
both lists. Eval-only.
Module and .override extras now append to the selected package's
defaultDependencyGroups. Say so in nix-setup.md (en + zh-Hans) and the
option description, point smaller builds at .#minimal or
defaultDependencyGroups = [ ], and replace examples that named groups the
default already ships ("messaging", "voice") or that no longer exist
("honcho").
extras-extend-package named "langfuse", which left pyproject.toml when
Langfuse moved to the plugin catalog; uv2nix throws on an unknown group,
so the check aborted at eval. The desktop check's "exa" override is now
shipped by `full`, so its effective package equalled the default and it
no longer exercised a customised package. Both use "google-meet", a real
extra outside full's defaultDependencyGroups; the header example drops
"honcho" (also moved to the catalog) for the same group.
pm's macOS ffmpeg (martin-riedl) is a single binary with no ffprobe; moving
runtimeDeps to it dropped the ffprobe nixpkgs' ffmpeg used to provide, so
transcription could not measure duration and skipped splitting long
recordings. Darwin's runtime PATH links ffprobe alone from ffmpeg-headless,
keeping the pinned ffmpeg first.
devVenv copied only Python-shebang scripts, dropping the dev group's native
ruff and ty. Link every other command as-is, skipping the activate scripts
and devPython's interpreter links. dev-venv-self-contained runs both.
nixpkgs' patchelf 0.19.2 corrupts executables whose headers it must move
when --set-interpreter runs before --set-rpath, which is autoPatchelf's
order: the new read-only LOAD overlaps the RW one, .dynamic lands read-only
and ld.so faults (NixOS/patchelf#674). The pinned node segfaulted on start,
so every npm build (hermes-tui first) died with exit 139 after the flake
update moved patchelf off 0.15.
Apply upstream's fix (NixOS/patchelf#672, merged, not yet released) to the
patchelf the pm derivations use. Linux only; darwin derivations unchanged.
cage, ghostty, grim and libglvnd, and the sandbox's Electron LD_LIBRARY_PATH,
pulled alsa-lib, which refuses to evaluate on darwin. They are Linux-only now.
pm-packages ran autoPatchelf over darwin artifacts and crashed on the Mach-O
ffmpeg; ELF patching is Linux-only. It also drops Nix's env-vars dump from
$out, and npm-pinned takes the node lib.nix passes instead of defaulting.
The shell pointed HERMES_PYTHON at the bare pm-pinned interpreter and reached
the uv2nix site-packages only through PYTHONPATH. Every consumer that replaces
PYTHONPATH (Ink's gateway bootstrap, image attachments, run_tests.sh's env -i)
got an interpreter with no dependencies, so 'hermes --tui' in the shell failed
with 'No module named ruamel'.
devVenv re-homes uv2nix's editable venv onto the pinned interpreter: a real
pyvenv.cfg whose home is the lock's python, devPython's site-packages, and
console scripts re-shebanged onto it. HERMES_PYTHON, VIRTUAL_ENV, python,
pytest and pm are now one interpreter and the shell sets no PYTHONPATH.
The shipped package and the shell share one tool list: runtimeDeps takes
ripgrep and ffmpeg from pm/lock.json too. Reverts the run_tests.sh PYTHONPATH
passthrough, which leaked a caller's PYTHONPATH into every HERMES_PYTHON run.
The dev-venv-self-contained check runs the venv under env -i.
hermesNpmLib.nodejs already puts the pinned npm over the pinned node; the
separate npmPinned + pmPackages.node pair relied on PATH order to get the same.
The dev shell took uv, node, ripgrep and ffmpeg from nixpkgs, so it ran
different versions than pm/lock.json ships (uv 0.11.32 vs 0.13.5, node
26.5.0 vs 26.12.1, ffmpeg 8.1.2 vs 9.0.1). Compose its tool set from
nix/pm-packages.nix instead, and wrap the pinned npm onto the pinned node
so `npm` and `node` are one generation.
pm-packages.nix also shipped every artifact nested under its archive's
versioned directory ($out/node-v26.12.1-linux-x64/bin/node), so no consumer
could reach them through PATH; flatten that and link root-level executables
into $out/bin.
git and openssh stay nixpkgs: the lock carries no Linux artifact for either.
Settles TUI-23 and TUI-24 on one launch path: the helpers ship with the TUI
product (dist/scripts, salvaged from #139423) AND boot the install's own
dependency generation (salvaged from #138898). Combined as cherry-picked they
broke each other: a packaged helper's parents[2] is the product dir, not the
Python root, so activate_dependencies looked up the wrong install. The client
now passes the root it already puts on PYTHONPATH as HERMES_PYTHON_SRC_ROOT and
both helpers use it.
A helper interpreter that still cannot load Hermes (no committed generation,
missing module) prints the named verdict `gateway unavailable:
bootstrap_dependencies`; gatewayClient reports it once and stops scheduling
reconnects instead of retrying every 30 s forever. nix/checks.nix asserts the
helpers ship in the Nix TUI output.
Refs #134889 (TUI-23, TUI-24)
Refs #139574
Refs #139208Fixes#138878
12 conflicted files, resolved in three lanes:
- gateway/run.py: take the branch split; main's tirith removal and spool-replay
ordering ported into run_runtime_init.py / run_bootstrap.py.
- config migrations: main keeps v50 (tirith); the branch's bot-chat timeout removal
becomes v51 (_config_version 51, template + test updated).
- hermes_state_usage.py: main's voice_chat task route threaded through the branch's
owner transaction; worker.persist usage allowlist accepts task.
- tui_gateway/server.py: branch side; main's edits applied where the code now lives
(agent_factory, session_registry). Desktop sessions get the catalog toolset on the
authority path; daemon-run finite CLI turns are not watched turns; lazy session info
reports nous/welcome on the free tier.
- Desktop/Ink: one branch-child spinner fix (main's route-token check); main's
uninstall i18n keys kept, pool keys stay removed; voice_turn forwarded and accepted
by the authority submit (surface_v1.voice_turn).
- chat_ws / PTY keepalive test: main's stricter test kept.
tirith ran `tirith check` on every terminal command, default-on with fail-open, as a
pinned PM binary auto-downloaded at startup. Measured against the shipped 0.4.2 binary
it caught 14/16 attack commands but flagged 12/24 everyday agent commands
(`echo $((6*7))`, `"$VAR/x.sh"`, `gh api | python3`, LAN curls, `.dev` hosts,
`pip install` via analysis_incomplete), and in cron/-q deny mode each of those is a
hard block. It only ever saw terminal strings (execute_code, writes and the browser
bypass it), fail-open meant no protection wherever the binary could not install
(Windows, Termux, x64 musl), and it produced a steady issue/PR stream (fail_open
semantics four times, install loops filling disks, a SIGSEGV hang, session-approval
scoping, third-party promo text on approval cards).
Removed: tools/tirith_security.py, the tirith branches of the command guard (the
is_tirith tuple, session-only "always" downgrade, import-error/fail-open paths,
unattended tirith pass), the CLI and gateway startup installers, the PM package and
lock pins, the nix runtime dep, security.tirith_* defaults, the CLI warning string
and three tirith tips (gaps refilled from the tail so translations survive), plus
the gateway's "no automated assessor" warning that only existed to point at tirith.
Config v50 drops security.tirith_* and enables nothing in its place. Hermes ships
no tirith plugin; if tirith upstream publishes one, it goes through normal catalog
admission under their name.
npm runs the `postbuild` hook after every `npm run build`, and the desktop
compiler (scripts/build/desktop.mjs) already runs checkDistBuilt on the
scratch product before it publishes dist/. Every update and every pack
therefore re-verified the bytes it had just verified; with the per-chunk
syntax check that was a second ~960-process pass (#123216).
Drop the hook; nix's checkPhase calls the guard directly.
The default and messaging packages carried their groups in
extraDependencyGroups, so the documented overlay path
`pkgs.hermes-agent.override { extraDependencyGroups = [ ... ]; }`
replaced them and rebuilt the venv as minimal. Give the package its own
defaultDependencyGroups argument, set it from packages.nix, and keep
extraDependencyGroups purely additive (deduplicated, devPython too).
Replace module-extras-extend-package with extras-extend-package, which
covers the overlay and module paths and asserts positive effects as well
as preserved ones, so an implementation that ignores the extras fails.
effectivePackage overrode the package with a plain attrset, replacing its
extraDependencyGroups. The default package is minimal plus the provider
groups (anthropic, messaging, ...), so setting extraPythonPackages alone
rebuilt the venv as minimal and dropped them. Use the function form of
override and append instead; add a module-level check that the default
venv is unchanged.
* fix(nix): stamp _config_version into the declarative config.yaml
The NixOS and Home Manager modules rendered config.yaml without a
_config_version key. Hermes reads a missing key as version 0, so every
boot of a fresh declarative install ran the post-update migrate step,
which logged "This config predates version 12 ... can no longer be
auto-migrated". Hermes cannot fix this itself: in managed mode
save_config() refuses to write config.yaml, so migrate_config() can
never persist the stamp.
The module now builds the generated config with the package's own
interpreter and adds DEFAULT_CONFIG["_config_version"], so the stamp
always matches the package it ships with. An explicit
settings._config_version still wins. A user-supplied configFile is
installed unchanged.
* test(nix): a generated config.yaml is current to the packaged Hermes
Merges the module's generated config into an empty HERMES_HOME and asks the packaged check_config_version() whether it is up to date. Fails on the parent commit (v0 vs v46).
Packaged windows claim `com.nousresearch.hermes` as their Wayland app id
(electron-builder bakes product-identity.cjs's `appId` into
extraMetadata.desktopName; Electron hands that string to the compositor
verbatim), while the entry was written as `hermes.desktop` with
`StartupWMClass=Hermes` — so GNOME matched neither StartupWMClass nor a
`<app_id>.desktop` file name and every launch fell back to the placeholder
icon, with the raw app id in the tooltip.
- write `<app_id>.desktop` with `StartupWMClass=<app_id>` (Name= stays "Hermes")
- retire a leftover `hermes.desktop` once the new entry is on disk, and only
when the file still names this app and launcher management is enabled;
foreign files at that path are left alone
- nix/desktop.nix derives the entry file name from the module instead of
hardcoding it
- tests: the installed entry carries the app id, legacy retirement, foreign-file
preservation, opt-out preservation, plus a node probe asserting
APP_ID == product-identity.cjs appId
Known consequence: an existing taskbar pin points at the old entry id and has to
be re-added once.
Main's desktop-backend check waits for a spawned `hermes serve` HERMES_BACKEND_READY sentinel;
Desktop now attaches to `hermes gateway ensure` and the daemon serves the dashboard. Desktop
logs the backend it chose and its attach readiness, and the check asserts those: the pinned
CLI wins over the mutable install and the gateway comes up.
The desktop derivation compiled node-pty against a hand-pinned fetchurl
of Electron's node headers, whose URL is templated off electron.version
but whose sha256 is hardcoded. Every nixpkgs electron bump swaps the
tarball out from under that pinned hash, failing the fixed-output
derivation at build time — and only there, never in the networked
Docker/npm ci paths.
Use nixpkgs' own electron.headers derivation instead. It is
version-locked to the electron package, so it tracks every bump
automatically with no hand-pinned hash to go stale, needs no network,
and is already the --nodedir layout (so the manual fetch + tar
extraction both go away). Same pattern as signal-desktop,
github-desktop, session-desktop, rstudio in nixpkgs.
Fixes#61443
Co-authored-by: ak2k <19240940+ak2k@users.noreply.github.com>
The extra removal left CI, the Docker image and the nix package still
requesting `hindsight`. Once the extra is gone, `--extra hindsight` and
extraDependencyGroups = [ "hindsight" ] ask for something that no longer
exists. Drop them the same way 73c598e319 originally did: remove it from
the CI extras lists, the Docker sealed-venv build and the nix default
groups, and point the nix examples/check at honcho. Also remove the
stray blank line left in the exclude-newer table.
KittenTTS 0.8.1 requires misaki[en]>=0.9.4. PyPI's misaki 0.9.4 caps
Python below 3.13. NousResearch/misaki f03fd2be73 is upstream main with
the cap raised to <3.15 and no code change, so the extra declares
misaki[en] at that commit and uv resolves KittenTTS's requirement to it.
misaki[en] also lists spacy-curated-transformers. The newest release
that fits spacy 3.8, 0.3.1, caps Python below 3.14, so PM's `uv pip
check` rejected every 3.14 venv that carried it. Nothing imports it:
only transformer spaCy models use it, and KittenTTS never builds a
misaki G2P. A never-true override marker removes it from the lock.
torch, transformers, spaCy and num2words stay: misaki.en imports them
at module level and KittenTTS imports misaki.en.
Native bundles sync with --all-extras, so un-gating the extra would put
KittenTTS, torch and spaCy in every payload. [tool.hermes] opt-in-extras
names the extras that only an explicit selection installs. PM adds
--no-extra for each of them to an all-extras sync, so bundles and their
recorded feature list leave kittentts out, and setup still installs it
through sync_venv when the user picks it.
The extra is gated on platform instead of Python: onnxruntime has no
Intel macOS wheel and torch has no Windows ARM64 wheel. Nix gets the
hatchling build backend the misaki git source needs.
Verified on macOS arm64 with CPython 3.14.7 and uv 0.12.3:
uv sync --frozen --extra kittentts from this lock, then uv pip check
reports all 126 packages compatible. _generate_kittentts writes a
3.87 s 24 kHz WAV (RMS 3661), and no spaCy model is downloaded. A dry-run
all-extras sync with --no-extra kittentts selects none of kittentts,
misaki, torch, transformers or spaCy. The new build test fails without
the environment change and passes with it.
This reverts commit 694fe220a4.
misaki[en] also requires spacy-curated-transformers. With spacy 3.8
(thinc 8.3) only its 0.3.x line resolves, and 0.3.1 declares
requires-python <3.14. uv's lock ignores that upper bound, but PM's
dependency validation rejects it, so the win32-x64 bundle failed with
"spacy-curated-transformers requires Python >=3.9, <3.14". Restore the
<3.13 gate until misaki's English stack installs cleanly on 3.14.
KittenTTS 0.8.1 requires misaki[en]>=0.9.4, and PyPI's misaki 0.9.4
declares requires-python <3.13, so the extra was gated off the managed
3.14 interpreter. NousResearch/misaki f03fd2be73 is upstream main
(0.9.4 + "Enable Python 3.13") with the cap raised to <3.15 and no code
change. The extra now declares misaki[en] at that commit directly, so
uv resolves KittenTTS's transitive requirement to the fork. A
[tool.uv.sources] entry would not reach a transitive dependency, and
an override would bypass pip installs of the extra.
The extra is gated on platform instead of Python: onnxruntime has no
Intel macOS wheel and torch (pulled by misaki[en]) has no Windows ARM64
wheel, the same gate piper already carries.
Nix: uv.lock records no build backend for the git source, so
buildSystemOverrides supplies hatchling for misaki. Without it the
opt-in build fails with "No module named 'hatchling'". The default
package derivation is unchanged; kittentts is in no Nix bundle.
Verified on macOS arm64 with CPython 3.14.7: uv sync --frozen --extra
kittentts from this lock installs misaki from the fork commit, and
tools.tts_tool_local._generate_kittentts writes a 3.87 s 24 kHz WAV.
pm/lock.json lists provenance sidecars (checksums.txt, .sig/.pem/.asc)
next to the tirith and iron-proxy archives; pm verifies them at install
time. nix/pm-packages.nix fed every artifact to srcs, and stdenv's
unpackPhase has no unpacker for a .txt, so `nix build .#pm-tirith`
failed with "do not know how to unpack source archive". `nix flake
check` only evaluated the pm derivations, which is why CI stayed green.
srcs now keeps the suffixes pm/store.py::extract unpacks; the
fixed-output hash already pins the archive. A new check builds the two
sidecar-bearing pins so the regression cannot hide behind evaluation.
- pm.environment owns _RESOLVER_MARKERS: the streaming uv runner imported
pm.workspace, whose tomllib import fails on the 3.10 system python that
bootstraps the Docker arm64 image (No module named 'tomllib'). Invariant test
proves runtime staging needs neither tomllib nor pm.workspace.
- run_tests.sh forwards the MSVC/SDK/Rust/OpenSSL toolchain variables through
its env -i scrub so PM tests that compile ruamel-yaml-clib on Windows arm64
find cl.exe (previously 'Visual C++ 14.0 or greater is required').
- nix desktop-backend check: the spawned backend outlives cage's process group
and kept writing under the temp HERMES_HOME during rmtree; stop every
process bound to the throwaway HOME before cleanup.
- windows: test_launcher_runtime_selection imports runtime_state from
hermes_cli (moved in bbec973514); the ' spaced ' suffix row loses its
trailing space on win32 (the filesystem strips it).
- macOS: test_sealed_worker_command copies the interpreter into the payload
(the escape guard resolves symlinks) and links the host lib tree.
apps/desktop/product-identity.cjs now resolves the channel request through
scripts/msix-shared.mjs (and its content-types table); nix flake check failed
with Cannot find module '../../scripts/msix-shared.mjs'.
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
The scope-dispatching cron worker runs inside the container there, so a
host user manager would start for nothing (as PR #110641 by @liuhao1024
also gated it). nix-setup.md gains the note operators need when they
declare the user themselves.
The NixOS module produces the one topology the restart-safe cron worker
cannot run in: a system service (`User=hermes`, `isSystemUser = true`)
whose uid has no systemd user manager. `run_one_job` hands every fire to
`_launch_external_cron_worker` → `restart_safe_gateway_child_argv`, which
requires a transient `systemd-run --user --scope` and fails closed when it
cannot get one:
Restart-safe cron worker dispatch failed: cannot create restart-safe
systemd scope for gateway child: systemd-run --user --scope is
unavailable (usually no reachable user D-Bus session at
/run/user/995/bus). On a system-level service install, run
`sudo loginctl enable-linger <gateway-user>` and restart the gateway.
So on a stock `services.hermes-agent.enable = true` host, no cron job runs
at all — every fire is recorded as a failed execution before an agent
starts, and the occurrence is consumed, so a weekly job does not retry for
another week. The error names the remedy, but nothing in the module applies
it, and the terminal-worker path with the same cgroup-isolation goal
degrades gracefully instead (`process_registry.py`: "worker shares the
gateway cgroup"), so nothing else on the host looks wrong.
Three parts, because linger alone leaves a startup race:
- `linger = lib.mkDefault true` on the created user: the declarative
`loginctl enable-linger`, which gives the uid a user manager and with it
/run/user/<uid>/bus. mkDefault, so an operator can still refuse it.
- after/wants linger-users.service, the unit that runs enable-linger.
- a bounded preStart wait for the socket. logind starts user@<uid>.service
asynchronously, and `run_gateway` resolves XDG_RUNTIME_DIR /
DBUS_SESSION_BUS_ADDRESS exactly once at startup (802f0f9), so a bus that
appears after ExecStart is one the process never sees for its lifetime.
Non-fatal after 10s: a gateway without cron beats no gateway.
All three are gated on `lingerEnabled`, read back off `config` rather than
assumed: with `createUser = false` the operator owns the user, nothing here
knows whether they lingered it, and the unit must not block on a bus that
may never arrive.
Test: nix/checks.nix `cron-worker-user-scope` relates the unit's `User=` to
that user's `linger`, requires the ordering and the wait when it lingers,
and requires neither when the module does not own the user. Red on the
parent commit for the first three arms.
Needs nixpkgs >= 25.05 for `users.manageLingering`.
The Nix desktop selected a legacy user venv before its explicit backend command. That runtime passed the version probe but failed agent initialization with a missing tools.kanban_toolset_context module.
Resolve the deployment command before the managed-install fallback. Add a headless packaged-desktop check with a competing legacy launcher; it fails on the old artifact and passes inside the Nix sandbox after the fix.
Build TUI, web, desktop UI and runnable agent products from explicit
prepared inputs. Keep dependency preparation separate from distribution
packaging, with PM and native builds sharing uv environment construction.
Docker copies compiled frontend products instead of build dependencies.
Nix retains uv2nix environments and consumes shared assembly through store
references. Native desktop and Termux use the same launcher and frontend
contracts. Preserve the independent PM runtime and source imports from
arbitrary working directories.
Keep failed frontend builds from replacing the previous product, reject
source/output overlap, and bound dependency-process output draining.
Include hermes_wisdom in the Nix wheel: real CLI smoke tests exposed its
missing package declaration on the base revision too.
Verified focused Python and JavaScript suites, Docker build/runtime checks,
Nix desktop and CLI/ACP checks, standalone TUI and packaged Electron PTY,
and real full-Chromium interaction. Native signed installers, Android device
installation and the full repository suite remain CI verification.