Removes the global `uppercase` + `font-mondwest` from the App.tsx root
that forced every page to opt-out, replaces stacked-alpha text colors
with semantic tokens for WCAG-AA contrast across all 7 themes, and
applies the new `text-display` utility from @nous-research/ui@0.16.0
on intentional brand chrome (page titles, sidebar headings, segmented
filters) only. Bumps every sub-12px arbitrary text size to text-xs.
Also widens the dashboard plugin routes (/api/dashboard/agent-plugins/
{name:path}/...) so category-namespaced plugins like observability/
langfuse and image_gen/openai can be enable/disabled from the dashboard
— previously the FE encodeURIComponent-ed the slash and the backend
{name} route rejected it. _validate_plugin_name still blocks .. and
backslash, and strips leading/trailing slash.
Touches sessions/env/keys page chrome and adds two new i18n keys
(`overview`, `showMore`/`showLess`) across all 18 locales.
Squashes 19 commits from PR #28832.
Co-authored-by: Hermes <noreply@nousresearch.com>
Remove the stale Babel compiler config and direct Babel dev dependencies from the TUI package.
Regenerate the npm lockfile and refresh the Nix fetchNpmDeps hash for the trimmed dependency graph.
* feat(web): migrate dashboard checkboxes to @nous-research/ui + DS polish
Replaces the hand-rolled shadcn-style `Checkbox` in `web/src/components/ui/`
with the Nous DS `Checkbox` (Radix-backed) from `@nous-research/ui`, bumps
the DS to 0.14.2, and picks up two regressions surfaced by the bump.
Checkbox migration
- bump `@nous-research/ui` 0.14.0 → ^0.14.2 and remove
`web/src/components/ui/checkbox.tsx`
- migrate `ProfilesPage` and `ModelPickerDialog` to the DS Checkbox API
(`onCheckedChange`, paired `<Label htmlFor>`)
- expose `Checkbox` on the dashboard plugin SDK
(`web/src/plugins/registry.ts`) so plugin bundles can use the same
DS component
- migrate the kanban dashboard plugin's 7 native `<input type="checkbox">`
call sites to the SDK `Checkbox`, with a native-input fallback shim so
the bundle still renders against older hosts that predate the SDK export
Fix: missing font registrations after the 0.14.x split
- import `@nous-research/ui/styles/fonts.css` before `globals.css` in
`web/src/index.css`. As of 0.14.x, `globals.css` only declares the
`--font-*` variables (Collapse, Mondwest, Rules Compressed/Expanded);
the `@font-face` registrations now live in a separate `fonts.css`, so
without this import the DS components silently fall back to a system
font stack and look unstyled.
Fix: right-align page header toolbars on sm+ viewports
- The mobile dashboard polish in #28127 flipped four pages'
`setEnd(...)` wrappers from `justify-end` to `w-full ... justify-start`
so toolbars stack below the title and align left on small screens.
But the outer `end` slot in `PageHeaderProvider` already has
`sm:justify-end`, and that has no effect when its only child is
`w-full` — once a flex child fills the row, the parent's `justify-*`
can't move it. The toolbar pinned to the *left* of the right-side
`sm:max-w-md` (~448px) slot, making the buttons appear to float a
couple-hundred pixels off the right edge on Analytics, Models, Logs,
and Plugins.
- Re-add `sm:justify-end` on the inner wrapper of each affected page,
preserving the mobile stacked layout.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(nix): update web npmDeps hash for package-lock bump
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(nix): refresh npm lockfile hashes
* chore(ci): re-trigger checks after nix lockfile hash fix
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(web): mobile dashboard UX polish
Bottom sheets for sidebar theme/language pickers on narrow viewports with
enter/exit animation and drag-to-close; inline header badges beside titles;
bottom padding on the route outlet for scroll clearance; profiles loading uses a
unicode braille spinner; align profile/cron card actions to the top; viewport-fit
cover and supporting layout tweaks across dashboard pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Nix web npm hash and mobile sheet accessibility.
Align fetchNpmDeps in nix/web.nix with web/package-lock.json for CI. Improve BottomPickSheet backdrop labeling, avoid aria-hidden on the dialog during exit animation, and wire theme/language sheets with listbox semantics and localized dismiss labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The container entrypoint ran `chown -R` on $HERMES_HOME every start.
`chown` strips the setgid bit (kernel security behavior), destroying
the 2770 permissions the NixOS activation script sets for group access
by hostUsers. This caused PermissionError for interactive CLI users
even though they were in the hermes group.
Replace with `find ... ! -user $UID -exec chown` which only touches
files with wrong ownership, leaving correctly-owned directories and
their permission bits intact.
Affects: container.enable + container.hostUsers + addToSystemPackages
Related: #19795, #19788, #9383
Expose the dependency-groups parameter from python.nix through
hermes-agent.nix and the NixOS module, allowing users to opt into
pyproject.toml optional extras (e.g. hindsight, voice, matrix) that
are resolved by uv inside the sealed venv.
Unlike extraPythonPackages (which appends to PYTHONPATH and requires
collision checking), extraDependencyGroups resolves the full dependency
graph in a single uv pass — no PYTHONPATH patching, no version
conflicts, no collision risk.
When to use which:
- extraDependencyGroups: enable a pyproject.toml optional extra
- extraPythonPackages: add an external Python plugin not in pyproject.toml
Usage:
services.hermes-agent.extraDependencyGroups = [ "hindsight" ];
Or via overlay:
pkgs.hermes-agent.override { extraDependencyGroups = [ "hindsight" ]; }
Refs: #8873, #9194
The fix-lockfiles script used 'nix build .#tui.npmDeps' to detect stale
hashes. This always succeeds when the OLD derivation is cached in Cachix
or cache.nixos.org — even when the source package-lock.json has changed.
Fix: use prefetch-npm-deps to compute the hash directly from the lockfile
and compare against what's in the nix file. Falls back to nix build only
if prefetch-npm-deps fails.
- Add _validate_plugin_name() guard on all {name} path param endpoints
(rejects /, \, .. before reaching plugin logic)
- Strip after_install_path from install response (no internal paths to client)
- Update nix/tui.nix lockfile hash to match committed package-lock.json
Replace the tsc + babel pipeline with a single esbuild invocation that
produces a self-contained dist/entry.js. The nix TUI derivation no
longer copies node_modules — only dist/ + package.json ship, shrinking
the output from hundreds of MB to ~2.9 MB.
- ui-tui/scripts/build.mjs: new esbuild bundler. Aliases @hermes/ink
to source (esbuild's __esm helper doesn't await nested async init,
which breaks lazy-assigned exports like 'render' when re-exporting
through a prebuilt submodule). Stubs react-devtools-core (dev-only).
Injects a createRequire shim for transitive CJS deps. Strips the
shebang from src/entry.tsx because Nix patchShebangs mangles
'/usr/bin/env -S node --max-old-space-size=8192 --expose-gc' — it
drops the 'node' token. The Python launcher always invokes node
explicitly, so the shebang is redundant.
- nix/tui.nix: installPhase no longer copies node_modules or the
@hermes/ink packages dir.
- nix/checks.nix: drop the 'node_modules present' assertion.
- hermes_cli/main.py: _tui_need_npm_install short-circuits when
dist/entry.js exists and no package-lock.json is present. That is
the prebuilt-bundle layout (nix / packaged release) and there is
nothing to install. Without this, the launcher tried to npm install
in a non-existent site-packages/ui-tui path.
* change(nix): dedupe nix lockfile checking scripts in ci
* feat(nix): make .#fix-lockfiles run --apply if no args passed
* fix(nix): use same nodejs version everywhere & small lints
- prevent lockfile thrashing while using nix :3
- use lib.getExe instead of raw /bin/ paths
- use inputs'.self instead of passing system in manually
* fix(nix): update lock files yet again (hopefully for the last time)
* fix(nix): align indentation of collision check echo
---------
Co-authored-by: Hermes Agent <hermes@nousresearch.com>
feat(gateway): refine Platform._missing_ and platform-connected dispatch
Restricts plugin-name acceptance to bundled plugin scan + registry
(no arbitrary string -> enum-pollution), pulls per-platform connectivity
checks into a _PLATFORM_CONNECTED_CHECKERS lambda map with a clean
_is_platform_connected method, and adds tests covering the checker map,
plugin platform interface, and IRC setup wizard.
Nix-built hermes only copied skills/ into the output, so bundled platform
plugins weren't discoverable when running `nix run` (IRC invisible, no
plugin.yaml files present). Mirror the bundled-skills pattern:
- packages.nix: cleanSourceWith plugins/, copy to
$out/share/hermes-agent/plugins, set HERMES_BUNDLED_PLUGINS on every
wrapper.
- checks.nix: new bundled-plugins check verifying the directory, a
sample manifest, and the wrapper env var.
- hermes_cli.plugins.get_bundled_plugins_dir(): central helper that
honors HERMES_BUNDLED_PLUGINS with a dev-checkout fallback. Used by
plugins.py, plugins_cmd.py, gateway.py, and web_server.py so every
call site resolves the same path.
check_for_updates() looked at __file__.parent.parent for a .git dir to
diff against origin/main. A nix-built hermes lives in /nix/store with
no .git there, so the check fell through to whatever editable-install
dev checkout last populated ~/.hermes/.update_check, producing stale
"X commits behind" warnings right after a fresh `nix run --refresh`.
Embed the locked flake rev into the wrapper as HERMES_REVISION (only
on
clean builds — dirty refs don't represent any upstream commit). When
set, banner.py compares it to upstream main via `git ls-remote`
instead
of inspecting a local checkout, and the cache key includes the rev so
nix updates invalidate immediately. Without local history we can't
count commits, so the message is a plain "update available" with no
suggested command — nix users may install via `nix run`, profile,
system flake, or home-manager, and we don't know which.
Also bump web/package-lock.json npmDepsHash via `nix run
.#fix-lockfiles`.
Round 1 of #17174 hit `nix-lockfile-check` failure. Root cause was
NOT a stale hash — the primary `nix (ubuntu-latest)` and
`nix (macos-latest)` builds passed. GitHub's Magic Nix Cache returned
HTTP 418 (rate-limited / throttled) mid-run, so the rebuild bailed
with `some outputs of '/nix/store/...-npm-deps.drv' are not valid,
so checking is not possible` — no `got:` line for the script to
extract.
The script then incorrectly treated this as 'build failed with no
hash mismatch' and exited 1, breaking the lint on every PR whenever
the cache is throttled.
Now we recognize the throttling/cache-disabled signature and skip
that entry with a warning. A real stale hash still surfaces in the
primary `.#$ATTR` build (separate CI job), so we don't lose
coverage.
`web/package-lock.json` was updated by the design-system refactor
(merged via #17007 + follow-ups: spinner / select / badges / buttons)
without bumping `nix/web.nix::npmDeps.hash`, breaking nix builds on
every PR + main since 2026-04-28T18:46.
Hash sourced from the actual `Check flake` failure output:
specified: sha256-AahWmJ9gDQ9pMPa1FYwUjYdO2mOi6JM9Mst27E0vp68=
got: sha256-+B2+Fe4djPzHHcUXRx+m0cuyaopAhW0PcHsMgYfV5VE=
Standalone single-file fix so it can land fast and clear nix on
every other open PR.
* ci(nix): auto-fix stale npm hashes on push to main
When a PR merges to main with updated package-lock.json or package.json
in ui-tui/ or web/, the new auto-fix-main job detects stale npmDepsHash
values and pushes a fix commit directly to main.
This eliminates the recurring manual hash-bump PRs (#15420, #15314,
#15272, #15244) by reusing the existing fix-lockfiles --apply pipeline.
The fix commit only touches nix/*.nix files, which are outside the push
path filter (package-lock.json / package.json), so it cannot re-trigger
itself.
Closes#15314
* fix(ci): use GitHub App token for auto-fix-main push
GITHUB_TOKEN commits are invisible to workflow triggers (GitHub's
infinite-loop prevention). The auto-fix-main job pushes directly to
main, so the fix commit never triggered downstream nix.yml verification.
Mint a short-lived token via the repo's GitHub App (daimon-nous, APP_ID
+ APP_PRIVATE_KEY secrets) so the push is treated as a real event and
nix.yml fires to verify the corrected hashes.
Tested via workflow_dispatch dry-run: app token minted successfully,
checkout with app token succeeded, fix job correctly gated.
Resolves review feedback from Bugbot (r3144569551).
* ci(nix): rename lockfile check job for required status check
Rename 'check' → 'nix-lockfile-check' so the status check name is
unambiguous when added as a required check on main.
* fix(ci): harden auto-fix-main against races, loops, and silent failures
Address adversarial review findings:
1. Race condition (#1): Job-level concurrency with cancel-in-progress
collapses back-to-back pushes; ref: main checkout always gets latest
branch state; explicit push target (origin HEAD:main).
2. Loop prevention (#2): File-whitelist check before commit aborts if
any file outside nix/{tui,web}.nix was modified, preventing
accidental self-triggering.
3. Silent infra failures (#8): nix-lockfile-check now fails explicitly
when fix-lockfiles exits without reporting stale status (catches nix
setup failures, network errors, script bugs that bypass continue-on-error).
4. Commit traceability (#11): Auto-fix commits include source SHA and
workflow run URL in the commit body.
5. Explicit push target (#12): git push origin HEAD:main instead of
bare git push.
---------
Co-authored-by: alt-glitch <alt-glitch@users.noreply.github.com>
* fix(nix): make extraPackages actually work — wire into per-user profile
#17030 deprecated extraPackages because it only set the systemd service
PATH, which the terminal backend's login-shell snapshot discards.
Instead of deprecating, fix it: set users.users.${cfg.user}.packages
so NixOS builds a per-user profile at /etc/profiles/per-user/hermes/bin.
This path is included in PATH by /etc/set-environment, which the login
shell sources, so the terminal backend's snapshot picks it up.
One line of actual logic:
users.users.${cfg.user}.packages = cfg.extraPackages;
Verified in a NixOS VM test: su - hermes -c 'which hello' resolves
to /etc/profiles/per-user/hermes/bin/hello.
Reverts the deprecation warning and docs changes from #17030, restores
extraPackages as the recommended way to give the agent extra tools.
Container mode is unaffected — extraPackages was always native-only
(the systemd path line is inside !cfg.container.enable).
* nix: clarify additive merge semantics for extraPackages user profile
---------
Co-authored-by: Siddharth Balyan <daimon@noreply.github.com>
extraPackages adds packages to the systemd service PATH, but the
terminal backend's login-shell snapshot rebuilds PATH from NixOS system
profiles, so tools added via extraPackages are invisible to terminal
commands, skills, and cron jobs — the entire use case.
Changes:
- Mark the option description as deprecated with explanation
- Emit a NixOS warning when extraPackages is non-empty, including a
ready-to-paste environment.systemPackages replacement
- Update docs: quick-reference table, plugin example, and options
reference all point to environment.systemPackages
The option still functions (non-breaking) so existing configs keep
working while users migrate.
* feat(nix): parameterize dependency-groups in python.nix
* refactor(nix): extract package to callPackage-able hermes-agent.nix
Makes the package overridable via .override{} and adds
extraPythonPackages parameter for PYTHONPATH injection.
Includes build-time collision check using PEP 503 name
canonicalization.
* feat(nix): add overlay for external NixOS consumption
External flakes can now add overlays = [ inputs.hermes-agent.overlays.default ]
to get pkgs.hermes-agent with full .override support.
* test(nix): add check for extraPythonPackages PYTHONPATH injection
Verifies wrapper has PYTHONPATH when extras provided, and
base package has no PYTHONPATH without extras.
* feat(nix): add extraPlugins option for directory-based plugins
Symlinks plugin packages into HERMES_HOME/plugins/ at activation time.
Validates plugin.yaml presence. Asserts unique plugin names at eval time.
Hermes discovers them automatically via its directory scan.
* feat(nix): add extraPythonPackages option for entry-point plugins
Overrides the hermes package with PYTHONPATH injection when
extraPythonPackages is non-empty. Plugin .dist-info directories
become visible to importlib.metadata for entry-point discovery.
Works in both native systemd and container modes.
* docs: add NixOS declarative plugin installation to nix-setup, plugins, and build-a-plugin guides
- nix-setup.md: new Plugins section with extraPlugins/extraPythonPackages
examples, overlay usage, collision checking note, options reference rows
- plugins.md: Nix row in discovery table, NixOS declarative plugins section
- build-a-hermes-plugin.md: Distribute for NixOS section after pip section
* fix: address review feedback — remove unrelated umask, fix fetchFromGitHub naming, simplify checks
- Remove accidentally introduced umask/migration changes (unrelated to plugins)
- Add pluginName helper, fix fetchFromGitHub producing name='source'
- Show name= in extraPlugins example docs
- Simplify checks.nix: use hermes-agent.override instead of re-callPackage
- Fix fragile grep shell logic in checks
* refactor: address simplify feedback — lib.getName, drop unused inputs', Python list for extras
- Use lib.getName instead of custom pluginName helper
- Drop unused inputs' from checks.nix perSystem args
- Pass extraPythonPackages as Python list literal instead of colon-split string
* fix: walk propagatedBuildInputs for plugin PYTHONPATH and collision check
Uses python312.pkgs.requiredPythonModules to resolve the full transitive
closure of extraPythonPackages. Without this, a plugin with third-party
deps (e.g. requests) would fail at runtime if those deps weren't already
in the sealed uv2nix venv. The collision check now also scans the full
closure, catching transitive conflicts.
* cleanup: fold plugins into subdir loop, use find for symlink cleanup, inline lib.getName
- Add 'plugins' to the existing cron/sessions/logs/memories subdir loop
instead of a separate mkdir/chown/chmod block
- Replace fragile for-glob with find -delete for stale symlink cleanup
- Inline lib.getName at both call sites, remove pluginName wrapper
Handle queued-title ValueError cleanup during session init, harden Discord message source building for test stubs, and fix the Dockerfile contract test syntax error. Also refresh the TUI lockfile and Nix build flags so nix ubuntu-latest no longer fails on npm lock/peer resolution drift.
* fix(nix): use --rebuild in fix-lockfiles to bypass cached FOD store paths
fix-lockfiles checked npm lockfile hashes by running
`nix build .#<attr>.npmDeps`, but fetchNpmDeps is a fixed-output
derivation — if the old store path exists locally, Nix returns it from
cache without re-fetching. This caused the script to report "ok" even
when hashes were stale, while CI (with no cache) failed with a hash
mismatch.
Adding --rebuild forces Nix to re-derive and verify the output hash
against the declared one, catching staleness regardless of local cache
state. Also updates the tui and web npm deps hashes that were stale.
* fix(nix): regenerate ui-tui lockfile to add missing @emnapi entries
npm ci was failing because @emnapi/core and @emnapi/runtime were
missing from ui-tui/package-lock.json despite being required as peer
deps by @napi-rs/wasm-runtime (via @rolldown/binding-wasm32-wasi).
Running npm install --package-lock-only adds the missing entries.
The npmDepsHash reverts to its previous value since fetchNpmDeps was
already fetching these packages as transitive dependencies.
The web dashboard (Vite/React frontend) is now built as a separate Nix
derivation and baked into the Hermes package. The build output is
installed to a standard location and exposed via the `HERMES_WEB_DIST`
environment variable, allowing the dashboard command to use pre-built
assets when available (e.g., in packaged releases) instead of rebuilding
on every invocation.
* Add setuptools build dep for legacy alibabacloud packages and updated
stale npm-deps hash
* Add HERMES_NODE env var to pin Node.js version
The TUI requires Node.js 20+ for regex `/v` flag support (used by
string-width). Instead of relying on PATH lookup, explicitly set
HERMES_NODE to the bundled Node 22 in the Nix wrapper, and add a
fallback check in the Python code to use HERMES_NODE if available.
Also upgrade container provisioning to Node 22 via NodeSource (Ubuntu
24.04 ships Node 18 which is EOL) and add a Nix check to verify the
wrapper and Node version at build time.
* feat(nix): container-aware CLI — auto-route all subcommands into managed container
When container.enable = true, the host `hermes` CLI transparently execs
every subcommand into the managed Docker/Podman container. A symlink
bridge (~/.hermes -> /var/lib/hermes/.hermes) unifies state between host
and container so sessions, config, and memories are shared.
CLI changes:
- Global routing before subcommand dispatch (all commands forwarded)
- docker exec with -u exec_user, env passthrough (TERM, COLORTERM,
LANG, LC_ALL), TTY-aware flags
- Retry with spinner on failure (TTY: 5s, non-TTY: 10s silent)
- Hard fail instead of silent fallback
- HERMES_DEV=1 env var bypasses routing for development
- No routing messages (invisible to user)
NixOS module changes:
- container.hostUsers option: lists users who get ~/.hermes symlink
and automatic hermes group membership
- Activation script creates symlink bridge (with backup of existing
~/.hermes dirs), writes exec_user to .container-mode
- Cleanup on disable: removes symlinks + .container-mode + stops service
- Warning when hostUsers set without addToSystemPackages
* fix: address review — reuse sudo var, add chown -h on symlink update
- hermes_cli/main.py: reuse the existing `sudo` variable instead of
redundant `shutil.which("sudo")` call that could return None
- nix/nixosModules.nix: add missing `chown -h` when updating an
existing symlink target so ownership stays consistent with the
fresh-create and backup-replace branches
* fix: address remaining review items from cursor bugbot
- hermes_cli/main.py: move container routing BEFORE parse_args() so
--help, unrecognised flags, and all subcommands are forwarded
transparently into the container instead of being intercepted by
argparse on the host (high severity)
- nix/nixosModules.nix: resolve home dirs via
config.users.users.${user}.home instead of hardcoding /home/${user},
supporting users with custom home directories (medium severity)
- nix/nixosModules.nix: gate hostUsers group membership on
container.enable so setting hostUsers without container mode doesn't
silently add users to the hermes group (low severity)
* fix: simplify container routing — execvp, no retries, let it crash
- Replace subprocess.run retry loop with os.execvp (no idle parent process)
- Extract _probe_container helper for sudo detection with 15s timeout
- Narrow exception handling: FileNotFoundError only in get_container_exec_info,
catch TimeoutExpired specifically, remove silent except Exception: pass
- Collapse needs_sudo + sudo into single sudo_path variable
- Simplify NixOS symlink creation from 4 branches to 2
- Gate NixOS sudoers hint with "On NixOS:" prefix
- Full test rewrite: 18 tests covering execvp, sudo probe, timeout, permissions
---------
Co-authored-by: Hermes Agent <hermes@nousresearch.com>