Commit Graph

136 Commits

Author SHA1 Message Date
teknium1 ea5757a8fe fix(nix): container mode does not linger the host service user; document the cron/linger dependency
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.
2026-09-15 04:03:25 -07:00
Zaar Hai ec30eefec9 fix(nix): linger the service uid so cron can create its worker scope
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`.
2026-09-15 04:03:25 -07:00
yoniebans d19038e76b Merge upstream main (b81383ec21) into the install-e2e suite branch
Conflicts, three, resolved:
- scripts/desktop-update.ps1: upstream's side taken whole. Upstream moved
  the hand-off to scripts/desktop-update/windows.ps1 (this file is now a
  one-line compat forwarder) and the new implementation already drains
  both pipes asynchronously with bounded abandonment, which supersedes
  this branch's stderr-drain fix for the same deadlock.
- apps/desktop/e2e/fixtures.ts: kept upstream's resolveElectronBinary
  import alongside this branch's consolidated mock-server path.
- tests-js/scripts/mock-server.ts: kept upstream's task-panel trigger
  addition inside the consolidated file; rewired the five upstream specs
  still importing './mock-server' to the consolidated path (export sets
  verified identical) and dropped the superseded apps/desktop/e2e copy.
2026-09-01 19:33:13 +02:00
ethernet 76f6ba3706 feat(nix): give Home Manager a programs module and the desktop app
Home Manager separates an installation from a daemon. This module put
both under `services.hermes-agent`, and `installPackage` added a program
to the PATH from a service module.

`programs.hermes-agent` now installs the command line application and
the desktop application. `services.hermes-agent` keeps the state, the
configuration and the daemons, and stays the authority: the new module
reads `hermesHome` and the backend address from it. A person can enable
one without the other, which is a machine with an application and no
gateway, or a headless gateway with no display.

The desktop application needs this split to work correctly. A launcher
that starts from the desktop menu reads no shell profile, thus the
HERMES_HOME that `home.sessionVariables` exports reaches an interactive
shell only. Home Manager writes `systemd.user.sessionVariables` to
environment.d, and this module puts no HERMES_HOME there, because that
file applies to each user unit. The application then opens ~/.hermes
while the services use `hermesHome`, and the person sees no sessions and
no keys. Thus the launcher carries the value itself, through a new
`extraEnv` argument on the desktop package.

The application also gets the Nix agent package, with
HERMES_DESKTOP_HERMES. The usual distribution of the Electron
application carries its own Hermes runtime and downloads more at the
first start. `hermesDesktop` is a passthru of the agent and pins
`finalAttrs.finalPackage`, so an override of `extraPythonPackages` or
`extraDependencyGroups` reaches both. One machine thus has one runtime.

`backend.sessionTokenFile` connects the application to the backend of
the service. Without it the module runs `hermes serve` and the
application starts a backend of its own, which gives two backends on one
HERMES_HOME. The backend reads the file into
HERMES_DASHBOARD_SESSION_TOKEN. The launcher reads the same file into
HERMES_DESKTOP_REMOTE_TOKEN, beside a HERMES_DESKTOP_REMOTE_URL that
names the address of the service.

Measurements against a live `hermes serve` on loopback show why that
shape is the correct one:

- `_resolve_session_token()` reads HERMES_DASHBOARD_SESSION_TOKEN, and
  `_has_valid_session_token` accepts that value as a Bearer credential.
  A request without it gets 401, and a request with the wrong value
  gets 401.
- The /api/ws socket accepts a query parameter only. A header gets 403,
  and `?token=` connects. Hermes Desktop builds exactly that URL, in
  `apps/desktop/electron/connection-config.ts`. Thus a test of the HTTP
  leg alone is a false positive.
- `resolveDesktopRemoteRoute` throws when the URL is set and the token
  is not. Thus the two variables travel together or not at all.

The token enters no Nix store path. `makeWrapper --set` and a systemd
`Environment=` value both write a literal into the store, which all
users can read. Thus each side reads the file at start time. The
launcher does it through a new `extraRun` argument on the desktop
package, and the backend through the launcher script that
`backend.waitFor` already uses. launchd has no EnvironmentFile, so a
script is the one shape that works on Linux and on Darwin.
`backendArgv` gives the plain argv only when nothing must run before
the backend.

`services.hermes-agent.installPackage` is removed. It defaulted to true,
so a person who never named it still got the command line. A silent
removal thus gives them a machine with no `hermes` and no message. The
module refuses a configuration that sets it, and the text names the
exact replacement for the value they gave.

Checks:

- the launcher carries HERMES_HOME
- the launcher reports HERMES_MANAGED only when the services own the
  configuration, because no activation writes a marker without them
- the launcher pins the agent package that `programs.enable` installs
- the launcher names the backend of the service, and gives a token
  beside the URL
- the backend reads the session token
- each side reads the file at start time, and the token is no `--set`
  value
- `programs.enable` alone starts no service
- `installPackage` is refused, with a message that names the
  replacement, and its absence evaluates

Each check reads the wrapper of the real package, and not an option
value. Each one was tested with a mutation that breaks the behavior it
asserts.
2026-08-21 17:00:30 -04:00
ethernet fd3a783a3e feat(nix): wait for the backend bind target before it starts
The backend binds to `backend.host` immediately. The bind fails when the
target is not ready, because uvicorn cannot bind a name that does not
resolve, or an address that no interface holds. A unit that starts at boot
loses this race against the daemon that supplies the target, such as
tailscaled.

A bind to a Tailscale MagicDNS name shows the problem. The name is the
correct bind target, because the dashboard refuses each request with a Host
header that is different from the address that the server bound to, and a
shared machine has a different address in each tailnet. But the name does
not resolve until tailscaled is up, so the unit fails at each boot until
`Restart=on-failure` finds the moment when the name works.

A systemd user unit cannot order itself after a system unit. `After=` and
`Requires=` are silent no-ops across that boundary. Thus the wait is a poll,
and not a dependency.

This change adds three options to `services.hermes-agent.backend` on both
the NixOS module and the Home Manager module:

- `waitFor` — `null` (the default, unchanged behavior), `"hostname"`, or
  `"interface"`
- `interfaceName` — the interface to take the address from
- `waitTimeout` — the time in seconds before the unit stops

With `waitFor`, ExecStart becomes a launcher that polls for the target and
then execs hermes. `exec` keeps hermes as the MainPID, so the restart logic
of systemd sees the real process. A timeout stops the unit with an error. It
does not bind a fallback address, because a fallback can expose the backend
more widely than the user intends.

The default is not changed. Without `waitFor`, ExecStart is the same
command line as before.
2026-08-21 14:45:58 -04:00
Teknium e69b8e561d feat: consolidate 'hermes version' into 'hermes --version', remove the subcommand
'hermes --version' (and -V) now prints the full version report — banner
version line with upstream SHA, install directory, authoritative install
method, Python and OpenAI SDK versions, and update status — making the
separate 'hermes version' subcommand redundant. The subcommand is removed.

- _startup_fast.print_fast_version_info() is now THE canonical version
  printer: static lines print instantly from stdlib probes, then the
  banner label, install-method resolver, and update check lazy-import
  after the first line is on screen (each degrades gracefully).
- main.py _print_version_info() delegates to it (used by /version in the
  CLI chat surface and the --version flag path); the old duplicate
  implementation is deleted.
- hermes_cli/subcommands/version.py removed; parser wiring, subcommand
  sets, console-engine extraction entry, and tests updated. Hermes
  Console keeps a 'version' command wired to the shared printer.
- Termux fast paths now include update status too (previously
  check_updates=False).
- Docs/i18n, CONTRIBUTING, SECURITY, and nix checks updated to
  'hermes --version'.
2026-08-20 03:05:46 -07:00
ethernet d5a9c2ba6c feat(nix): home-manager module, shared with the NixOS module
Hermes is an agent for one person. The credentials, the memory, the
sessions and the cron jobs all belong to that person. But the only
declarative path was a NixOS system service. Issue #9056 asks for the
user-level equivalent. 25 public Nix configurations already write one by
hand, and several of them copy nix/nixosModules.nix and edit the systemd
part.

This module is not a second copy of that file. The code that both modules
share moves into nix/moduleCommon.nix:

  - the options
  - the renderers for config.yaml, .env and the documents
  - the activation body
  - the command lines of the processes

nixosModules.nix keeps only the parts that need root. Those parts are the
service user, stateDir, addToSystemPackages, container mode and tmpfiles.
The file goes from 1008 lines to 666.

`services.hermes-agent` is now the same option set on both modules. A
NixOS example works on Home Manager without a change, and an option added
one time appears on both.

The Home Manager module is different only where it must be. It uses
systemd.user.services on Linux and launchd.agents on Darwin. It uses
home.activation and not system.activationScripts. It sets HERMES_HOME
directly, with the default ~/.hermes, so an existing directory continues
to work. It uses the modes 0600 and 0700, because the state has one user
and does not need the group-shared umask of the NixOS module. It does not
support container mode, which needs root and the Docker socket.

The change also makes four corrections that apply to both modules:

- backend.mode runs `hermes serve` or `hermes dashboard`. Both modules
  had only the gateway. But Hermes Desktop and the web dashboard connect
  to a different process, so six of the configurations in public repos
  add a second unit by hand. serve and dashboard are one entry point with
  one flag of difference, and you can run only one of them. Thus the
  option is an enum. The NixOS module asserts against container mode with
  a backend, and does not make a unit that cannot start.

- hermesHomeFiles installs files into HERMES_HOME. The `documents` option
  installs into the working directory, which is correct for AGENTS.md but
  wrong for SOUL.md and memories/. Hermes reads those files from
  HERMES_HOME, in agent/prompt_builder.py:2095. A SOUL.md in `documents`
  made a workspace file that Hermes never loaded as the identity. The
  documentation said this in prose, but two directory diagrams showed the
  opposite. This change corrects both. A key in either option can now
  contain subdirectories.

- `documents` needs an explicit `workingDirectory`. The default of that
  option is bad on both modules. It is the home directory of the user on
  Home Manager, and ${stateDir}/workspace on NixOS. A user who declares
  workspace files without a directory therefore gets a place that the
  user did not select. The place is also different on each module. The
  modules now refuse that combination.

  The test is on the priority of the option and not on its value. An
  option that nothing sets keeps the priority of its own default, and
  each definition from a user is stronger. Thus a directory with the same
  text as the default still counts as a selection, and so does a
  mkDefault. A comparison of values detects neither case.

- Each activation writes .env again from a base in the Nix store, and
  does not add to the file that exists. Thus a second activation cannot
  put the same secret in the file two times, and a removed
  environmentFile goes away. environmentFiles keeps the type `listOf
  str` and not `path`, so Nix cannot copy a sops-nix or agenix path into
  the Nix store, which all users can read.

- HERMES_MANAGED and the .managed marker now hold the name of the system
  that manages the install. Thus a refusal says "managed by home-manager"
  and not "managed by NixOS", and `hermes update` gives the Nix guidance
  for both shapes. The CLI does not print a rebuild command for each
  system. It names the owner, and the user knows their own tool. A bare
  `true` and an empty marker still mean NixOS, so this does not change an
  existing install.

Verification. Six new checks, all built:

  nixos-module           evaluates the module with evalModules and the
                         NixOS module list. It asserts both units, one
                         HERMES_HOME, and that the module refuses
                         container mode with a backend.
  home-manager-module    evaluates the module with the
                         homeManagerConfiguration function of
                         home-manager. The process assertions run against
                         systemd units on Linux and launchd agents on
                         Darwin.
  module-option-parity   asserts that each shared option is on both
                         modules, and that the two exclusion lists name
                         only options that exist.
  env-file-assembly      runs the real .env script and checks the
                         contents, the mode, that a second run gives the
                         same bytes, and that a removed file goes away.
  workspace-files-need-a-directory
                         checks that the module refuses `documents`
                         without a directory, and accepts a directory
                         that has the same text as the default.
  service-argv           runs each command line that the modules build
                         through the real parser of the CLI, with one
                         sentinel flag added, and requires that argparse
                         refuses only the sentinel.

`nix flake check` passes, with 21 checks in total.

The CLI branches that treat an install as a Nix install move to one
helper, is_nix_install_method. Four call sites in main.py, web_server.py,
update_cmd.py and doctor.py tested the literal set {"nix", "nixos"}, and
each one missed home-manager. recommended_update_command asks the managed
state before the code-scoped stamp again, because a managed install can
carry a stale stamp that names an update path the managed guard refuses.
The metrics contract gets a home-manager bucket, so a Home Manager
install does not report as unknown.

Each check was mutation-probed. 22 faults were injected, and the checks
caught all 22:

  - a lost --no-open
  - a backend that runs the gateway
  - an overwritten config.yaml
  - documents in the wrong directory
  - a different HERMES_HOME on the two processes
  - a lost HERMES_HOME export
  - a missing backend unit
  - a removed assertion
  - an .env file that grows at each activation
  - an install that reports NixOS
  - an empty .managed marker
  - an option on the NixOS module only
  - a stale entry in an exclusion list
  - a renamed subcommand
  - an unknown flag
  - the workspace-files assertion always passes
  - the assertion compares values instead of priorities
  - an off-by-one that lets an untouched default through
  - the assertion also fires for hermesHomeFiles
  - a mkDefault no longer counts as a selection
  - the Home Manager module stops wiring the assertion
  - the NixOS module stops wiring the assertion

The 16 Python tests in tests/hermes_cli/test_managed_install_shapes.py
were probed the same way. 8 faults were injected and 8 were caught.

These tests fail on this tree. They fail in the same way on the stashed
HEAD, and they have no relation to Nix:

  - test_git_probe_tree_kill.py (2 tests)
  - test_update_import_guard.py (1 test)
  - test_telegram_media_read_timeout.py (2 tests)
  - test_teams.py (a collection error)

Closes #9056

# Conflicts:
#	hermes_cli/main.py
#	hermes_cli/update_cmd.py
#	hermes_cli/web_server.py
2026-08-18 20:42:06 -04:00
ethernet 6680afba4a fix(nix): exclude test js files from src 2026-08-18 00:59:00 -04:00
alt-glitch 0490b00e74 fix(nix): set HERMES_BIN default in wrapped binaries
The TUI resolves the CLI via process.env.HERMES_BIN (externalCli.ts) and
falls back to a bare 'hermes', which is not on PATH for nix run / nix
profile installs that only expose the wrapped binaries. Set a
--set-default so the wrapper advertises its own hermes while an explicit
operator override (documented in kanban_db.py) still wins.
2026-08-12 04:09:39 -04:00
ethernet ea4cd375f8 ci(install-e2e): retire the bubblewrap sandbox - git redirect everywhere, macos legs live
The fake Internet (bubblewrap + slirp4netns + MITM proxy +
upload-pack shim, 883 lines across dev-sandbox.sh, stage2-run.sh,
proxy.py, ssh-shim.sh, openssl.cnf, install-update-e2e.sh) existed to
isolate install.sh's network. The GIT_CONFIG_GLOBAL insteadOf redirect
the windows driver introduced does the same job with a gitconfig file
and works on any OS, so:

* install-e2e-run.yml now runs tests/install/installer-script-e2e.sh
  directly on the bare runner - no sandbox deps, no userns sysctls -
  and takes a runner input;
* the macos matrix calls the SAME workflow on macos-latest, deleting
  install-e2e-macos-run.yml: installer-script -> installer-script /
  hermes-update flip from grey to live, app-update pairs stay TODO
  inside the shared gate;
* install.sh is no longer curl'd through a fake CA - each leg runs
  the copy from the ref a user of that version actually executed;
* scripts/dev-sandbox.sh becomes the minimal isolation sandbox from
  ab6b9492f (separate HERMES_HOME / Electron userData / app name,
  same CLI surface: --persistent, --from, --delete), keeping its
  .hermes-sandbox dir name so gitignore and docs hold;
* nix/sandbox.nix drops the bwrap/proxy closure and keeps only the
  Electron runtime LD_LIBRARY_PATH the desktop app needs.

Verified: nix build .#sandbox + smoke run (isolated HERMES_HOME
created, ephemeral cleanup), shellcheck/bash -n on both scripts,
actionlint on all three workflows, and the new driver ran the full
v0.20.2 -> HEAD hermes-update pass locally before this commit.
2026-08-11 21:20:42 -04:00
ethernet 7112fbcbcf fix(nix): update electron sha 2026-08-05 17:50:18 -04:00
ethernet acb590fc4a fix(nix): fix electron headers sha 2026-08-05 11:47:05 -04:00
ethernet b27cdc3824 feat(nix): desktop app icon 2026-08-05 11:47:05 -04:00
ethernet 84874c58a5 feat(dev-sandbox): support fake installer / fake main / git clones
allow you to simulate the whole official curl | bash installer,
and subsequent hermes updates.

Run development commands in a bubblewrap filesystem and network sandbox
with a local HTTPS MITM fixture server and a fake github
git-upload-pack transport.
Package the sandbox command and expose it from the nix devShell.

Stage the local installer at its canonical fake HTTPS URL and add a
persistent installation/update test path. Route root installs through
sandbox-owned filesystem locations and snapshot dirty source worktrees
into temporary fake commits so update tests can fast-forward without
changing the real checkout.

Includes a --install-ref sandbox installer mode that fetches any commit
(--from-main is a nice shorthand for local development) outside the
sealed sandbox, installs from that snapshot, and then promotes the fake
remote to the current worktree so update flows can be exercised with FF.

Notes on non-root sandboxes:
Giving a non-root sandbox a network is tricky.
slirp4netns joins the target userns and setuids to root before configuring the
netns, so the userns must map a uid 0; bwrap's --unshare-user maps exactly ONE
uid, so --uid 1000 leaves no root to become and slirp diedswith
`setns(CLONE_NEWNET): Operation not permitted`. Stage 1 builds the user+net
namespaces with `unshare` and two one-id ranges:

    inner 0    -> a subuid, unused by the payload, present only so slirp can
                  become root
    inner 1000 -> our real host uid

Mapping the payload to the *host* uid (not a second subuid) keeps everything the
sandbox writes owned by us, so `rm -rf` on a persistent sandbox still needs no
privileges. Stage 2 execs bwrap WITHOUT --unshare-user -- it only adds mount/pid
-- sidestepping bwrap's refusal to accept --uid outside a userns it created.
Costs a /etc/subuid range for the invoking user (we error with the exact line to
add) and util-linux `unshare`; `--root` needs neither.
2026-08-04 17:36:26 -04:00
ethernet 7eefb09314 fix(nix): tie devShell's HERMES_PYTHON to the venv actually on PATH
`nix/devShell.nix` collected `devShellHook` by scanning every package:

    nonNpmHooks = map (p: p.passthru.devShellHook or "") packages;

But `minimal` and `messaging` are `.override` variants of `default`, so
each carries its own `devShellHook` exporting its own HERMES_PYTHON. The
scan therefore concatenated three conflicting exports and forced Nix to
evaluate and realise three separate uv2nix editable venvs on every
`nix develop`.

`attrValues` is alphabetical, so the last hook won (`minimal`) while
`python`/VIRTUAL_ENV came from `default`'s devDeps:

    HERMES_PYTHON        = ...dimim2... (minimal — no optional deps)
    python / VIRTUAL_ENV = ...85r28... (full)

Inside the shell `$HERMES_PYTHON -c "import anthropic"` failed while
`python -c "import anthropic"` succeeded. Worse, `scripts/run_tests.sh`
prefers HERMES_PYTHON, so the suite ran against the minimal venv. Its
guard did not catch this: it only checks that HERMES_PYTHON has pytest,
and minimal's venv does (pytest is in the `dev` group), so the wrong
interpreter was silently accepted.

Tying the hook to `packages.default` — the same package whose `devDeps`
are installed — keeps HERMES_PYTHON, `python`, and VIRTUAL_ENV pointing
at one venv by construction.

    editable venvs referenced   3      -> 1
    their combined closure      421 MB -> 140 MB
    test failures               85     -> 32

The venv mismatch was masking 53 failures; e.g. test_web_tools_config.py
goes 2-failed -> 38-passed. Full suite is now 25369 passed / 32 failed,
and those 32 reproduce identically on a pristine HEAD worktree with no
nix/ changes under the same interpreter (mostly NixOS artifacts — tests
spawning bare `python3` in a scrubbed env exit 127).
2026-08-03 13:32:08 -04:00
ethernet 713a983e4a 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.
2026-08-01 21:17:51 -04:00
ethernet f88ed6c717 fix: fix @nousresearch/ui version, update to npm 12 2026-08-01 17:05:42 -04:00
ethernet d5e135a513 fix(nix): update electron headers sha 2026-08-01 12:53:24 -04:00
ethernet f976284245 nix: update nixpkgs, update nodejs to 26
brings npm 12 :)
2026-07-31 13:42:03 -04:00
Teknium c770515e2b modernize re-added Vercel integrations: SDK 0.7.2, telemetry off, sibling-site wiring
- Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock
- Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1
  set before import, user-overridable) per the no-opt-out-telemetry policy
- Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file
  decomposition landed after the removal)
- Widen post-removal backend sets that vercel_sandbox missed: terminal_tool
  container_backend + _CONTAINER_BACKENDS, file_tools fallback set,
  env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards,
  prompt_builder probe container_config
- Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP
- Re-add vercel dependency group to nix #full variant (reverts #33773 workaround)
- Update restored tests to current contracts: upload-only credential sync-back
  (bcfc7458fa), registry-derived provider env list, parametrized backend fixture,
  drop tests superseded on main (slack wizard move #41112, nous status format)
2026-07-29 19:48:37 -07:00
ethernet a4bc1ca502 fix(timeline): persist typed display events (#69771)
* fix(desktop): hide persisted agent-only history scaffolding

Filter verification-stop nudges and context-compaction handoffs at the
stored-history mapper boundary. Preserve a real reply when a compaction
handoff shares its stored message.

* test(desktop): build persisted E2E sessions through the real agent

Drive tui_gateway.entry over its stdio JSON-RPC transport against the mock
provider, wait for real completion events, and persist normal session history
through AIAgent and SessionDB. Migrate resume and hidden-history coverage,
including real compression and live verify-on-stop scaffolding, then remove
the unused direct SessionDB import scripts.

* fix(desktop): use the provisioned Python for real-session E2Es

Run the stdio gateway through uv's synced project environment outside the
Nix dev shell, while retaining the fully provisioned Nix Python when the
shell advertises HERMES_PYTHON_SRC_ROOT.

* fix(nix): expose the provisioned Python environment to uv

Mark the Nix-built Python environment active in the dev shell so the shared
E2E session builder can always run through `uv run --active --no-sync`.

* fix(timeline): persist typed display events

* fix(timeline): strip display-only fields from provider payloads, preserve through rewrites, fix /resume display history

Three review findings from PR #69771:

1. Provider payload leak: display_kind and display_metadata were forwarded
   to the provider API as unknown message fields. Strict OpenAI-compatible
   backends can reject the next request after a model switch or resumed
   typed event. Strip both from the per-request api_msg copy in
   conversation_loop alongside the existing api_content pop.

2. Rewrite/import data loss: _insert_message_rows preserved display_kind
   but silently dropped display_metadata. After replace_messages,
   archive_and_compact, or session import, async-delegation completion
   events lost their task counts and fell back to generic display text.
   Add display_metadata to the INSERT columns and bind tuple.

3. CLI /resume stale recap: startup --resume A set _resume_display_history
   from A's lineage. A subsequent in-session /resume B loaded B only into
   conversation_history via get_messages_as_conversation, leaving the stale
   A display projection. _display_resumed_history preferentially read the
   stale attribute, showing A's recap for B. Switch /resume to
   get_resume_conversations and update _resume_display_history alongside
   conversation_history.

Tests: 890 Python (5 files), 35 desktop TS — all green.

* feat(tui): render typed display events as ◈ markers in the Ink TUI

The TUI was not handling display_kind at all — model switch markers and
async delegation completions rendered as opaque user messages with the
full [System: ...] text, and hidden compaction handoffs were visible.

Wire display_kind through the full TUI chain:

- _history_to_messages (tui_gateway/server.py) forwards display_kind
  and display_metadata to the gateway transcript payload.
- GatewayTranscriptMessage (gatewayTypes.ts) gains both fields.
- Msg.kind (types.ts) gains 'event' value.
- toTranscriptMessages (domain/messages.ts) maps:
  - hidden → skip entirely
  - model_switch → event "model changed"
  - async_delegation_complete → event "N background agents finished"
    (or "background agent work finished" without metadata)
- messageGroup (blockLayout.ts) routes event to its own group, with
  SELF_SPACED + PAINTS_TRAILING_GAP so it owns its margins.
- messageLine.tsx renders event-kind as a dim ◈ marker with no gutter,
  matching the CLI's ◈ event rendering.
- 4 new TUI tests for hidden/model_switch/async_delegation mapping.

TUI typecheck: clean. TUI lint: 0 errors (2 pre-existing warnings).
TUI tests: 9 passed (1 pre-existing failure on main, unrelated).
2026-07-23 14:46:24 -04:00
ethernet 2244be2282 feat(devshell): add terminal UI capture tools (#69629) 2026-07-22 17:28:21 -04:00
ethernet d84e11af4d rip out brew + pip/PyPI wheel support (#68217)
Removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while
preserving the supported source, Docker, and Nix workflows.

Changes:
- Removes the Homebrew formula, PyPI publish workflow, sdist manifest
  (MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
- Keeps setuptools metadata and entry points required by editable installs
  and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist
  builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
- Removes pip/Homebrew install detection, PyPI update checks, the pip
  self-update path, the deprecation-banner state, the postinstall subcommand,
  wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py,
  and the ACP Registry manifest/version-lockstep release logic.
- Adds /nix/store/ path detection so `nix run` / `nix profile install`
  installs (which don't set HERMES_MANAGED) are correctly identified as
  "nix" rather than falling through to "git"/"unknown".
- Retired install-method values ("pip", "homebrew") in existing
  .install_method stamps (both code-scoped and home-scoped) are ignored by
  the allowlist reader and fall through to "unknown" instead of resurrecting
  a retired enum value.
- Updates Nix packaging to ship bare runtime data (locales, optional-mcps)
  through store symlinks and wrapper env vars instead of wheel data-files.
- Removes the ACP Registry manifest/icon and their version-lockstep tests.
- Deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry
  tests; adds parametrized coverage for the packaging build guard covering
  BOTH sdist and wheel paths (the guards live in separate cmdclass entries
  — a passing sdist test proves nothing about the wheel path).
- Updates installation/platform documentation and related user-facing copy.
- Adjusts the supply-chain scan so deleted install-hook files do not trigger
  a finding, while additions or modifications still require the existing
  ci-reviewed label gate.

Supported installation paths (unchanged):
- git installer (install.sh)
- Docker
- Nix/NixOS
- editable development installs (uv sync, uv pip install -e ., pip install -e .)
2026-07-22 16:51:01 -04:00
ethernet 6b54582438 fix: tool_calls double-encoding on import (#68856)
* nix: add `cage` to devShell

* test(desktop): add pre-filled sessions support

Exports createSandbox, writeMockProviderConfig, writeEnvFile,
buildAppEnv, findElectron, and launchDesktop from fixtures.ts so
specs can compose their own seeded-backend fixtures without duplicating
the sandbox/config/launch logic.

* test(desktop): auto-fail e2e tests on error banner

Adds a shared test fixture (e2e/test.ts) that wraps @playwright/test's
page with an error-banner guard. When any [role="alert"] element
(error notification toast) appears in the DOM during a test, the test
fails with the error message text.

The guard uses:
- A MutationObserver (injected via addInitScript) that watches for
  [role="alert"] elements appearing at any point during the test
- A final DOM scan in afterEach for alerts still visible at teardown
- Deduplication so the same error text only fires once

All existing e2e specs updated to import { test, expect } from './test'
instead of '@playwright/test'. No per-spec setup needed — the guard is
auto-installed on every page via the extended fixture.

This catches issues like the "resume failed" error banner that can
appear during session loading — previously the test would pass while
an error toast was silently visible on screen.

* fix(state): parse tool_calls JSON string before re-serializing

_insert_message_rows and append_message both do json.dumps(tool_calls)
to serialize the field for SQLite storage. But when tool_calls arrives
as a JSON string (from import_sessions / export_session, which store it
as TEXT), json.dumps double-encodes it — wrapping the already-serialized
string in quotes and escaping the inner quotes.

When _rows_to_conversation later does json.loads(row['tool_calls']),
the double-encoded string parses back to a plain string (not a list).
_history_to_messages then iterates this string character-by-character,
calling tc.get('function', {}) on each char — 'str' object has no
attribute 'get'.

This was a pre-existing bug (on main), but only triggered by the
import_sessions path (the live agent always passes tool_calls as a
Python list). The e2e error-banner guard caught it via the 'Resume
failed' notification toast.

Fix: in both append_message and _insert_message_rows, parse tool_calls
with json.loads first if it's a string, then re-serialize.

* fix(desktop): exempt boot-failure from error guard

- boot-failure: add allowErrorBanners() beforeEach — these tests
  deliberately trigger boot errors, so error toasts are expected
- test.ts: export allowErrorBanners() opt-out + reset flag in afterEach
2026-07-21 18:53:05 +00:00
ethernet 933c823ae8 nix: add cage to devDeps 2026-07-20 20:38:59 -04:00
ethernet c5111388c7 fix(desktop): minor type fixes and devShell cage dep
- Type gatewayState in session store
- Electron main.ts: force-show window for e2e test workers
- tsconfig: include e2e test types
- nix/devShell.nix: add cage for headless visual testing on tiling WMs
2026-07-20 11:44:40 -04:00
ethernet 1705a44074 fix(nix): include apps/shared as tui dep (#68109)
https://github.com/NousResearch/hermes-agent/pull/61067
added a @hermes/shared dep for `ui-tui`. added it to the nix deps to fix
builds
2026-07-20 11:23:34 -04:00
ethernet f8b6d381e2 fix(nix): dirty-tree wrapper bug + filtered rebuild scope + overlay alias (#65237)
* fix(nix): fold makeWrapper line continuations into optionalStrings

When rev == null (any dirty-tree build), the empty optionalString
expansion left the previous line's trailing backslash dangling onto a
blank line, ending the makeWrapper command early and running
`--suffix PYTHONPATH ...` as its own shell command (`--suffix: command
not found`, exit 127). Clean trees passed CI; dirty trees with
extraPythonPackages failed — exactly the path the NixOS module
exercises.

The continuation now lives inside each optionalString (" \\\n  --set
..."), so the makeWrapper chain stays intact whether or not the
optional flags expand.

Verified by building with rev = null + extraPythonPackages =
[ pyfiglet ]: wrapper builds, PYTHONPATH suffix lands inside the
makeWrapper call, wrapped `hermes --version` runs, and the collision
check still executes (certifi correctly rejected).

* perf(nix): filter derivation sources to shrink rebuild scope

Every derivation previously saw the whole repo, so any file change
rebuilt everything. Each derivation now gets a filtered src with only
the files it consumes:

- lib.nix: derive npm workspace topology from the root package.json
  `workspaces` globs (single source of truth — a new workspace member
  is picked up with zero nix edits). pythonSrc (cleanSourceWith)
  excludes the JS workspace trees, docs/website, docker/.github,
  tests, nix/, flake.nix/flake.lock, root docs, and skills/ +
  optional-skills/. importNpmLock reads from a fileset-filtered
  npmRoot (root manifests + member package.jsons only).
- mkNpmPassthru takes `dirs` — the workspace dirs the package
  contains — and builds a per-package fileset src from them. web and
  desktop include apps/shared (file: dep). One shared
  `nix run .#update-npm-lockfile` replaces the per-package
  update_*_lockfile bins that only existed inside build sandboxes.
- python.nix: release venv loads the uv2nix workspace from pythonSrc.
  The editable venv keeps an unfiltered ./.. root —
  mkEditablePyprojectOverlay calls lib.path.splitRoot, which rejects
  a cleanSourceWith set, and the editable install reads the live
  checkout anyway.
- hermes-agent.nix: skills ship exclusively via HERMES_BUNDLED_SKILLS
  / HERMES_OPTIONAL_SKILLS (same mechanism as Homebrew packaging;
  setup.py's _data_file_tree returns [] for missing dirs), so
  SKILL.md edits no longer rebuild the venv. optional-mcps stays in
  the wheel — pyproject.toml lists its manifests as explicit
  data-files. Bundled assets are symlinked instead of copied, making
  the wrapper drv near-instant when only an input changed.
  __pycache__ filtered from bundled skills.
- checks.nix: find -L through the new symlinks; assert
  optional-skills presence + HERMES_OPTIONAL_SKILLS in the wrapper.
- run_tests.sh: fall back to $HERMES_PYTHON when no local venv
  exists, guarded by an `import pytest` probe (HERMES_PYTHON from a
  wrapped hermes binary points at the release venv, which has no
  pytest — without the guard every test file dies with "No module
  named pytest" while the runner exits 0).

Verified: nix flake check exit 0; built .#default .#tui .#web
.#desktop; SKILL.md and flake.nix edits leave the venv drvPath
unchanged; .py edits leave the tui drvPath unchanged; .tsx edits
leave the venv drvPath unchanged (and do change the tui drv);
scripts/run_tests.sh runs 299 tests green through both the venv and
HERMES_PYTHON paths, and rejects a pytest-less HERMES_PYTHON.

* refactor(nix): overlay aliases the flake's own package instead of re-instantiating

The overlay previously re-called callPackage against the consumer's
nixpkgs (final), so pkgs.hermes-agent could be a different derivation
than nix build .#default and the NixOS module's default — an untested
build matrix against arbitrary consumer nixpkgs versions, for a
package whose Python side is uv2nix-locked anyway.

Now the overlay is a pure alias for the flake's own locked package:
one callPackage site (packages.nix), everything else references it.
.override { ... } still works — callPackage's makeOverridable travels
with the derivation.

Verified: direct drvPath == overlaid drvPath; .override produces a
distinct drv.

* fix(nix): dedupe extraPlugins assertions, replace MESSAGING_CWD with terminal.cwd

- Delete the duplicated extraPlugins duplicate-name assertions block
  (same assertion declared twice back to back).
- Stop setting the deprecated MESSAGING_CWD env var, which made the
  module trip hermes' own startup deprecation warning. The working
  directory is now injected as terminal.cwd into the generated
  config.yaml; cfg.settings wins via recursiveUpdate, and container
  mode maps to the in-container mount path.
2026-07-15 21:45:35 -04:00
ethernet 5e849942c3 feat(dev): add isolated sandbox script for local dev
scripts/desktop-sandbox.sh runs a Hermes desktop instance in an isolated
sandbox — separate HERMES_HOME, separate Electron userData, and a
distinct
app name (HERMES_DESKTOP_APP_NAME) so it doesn't compete with the main
desktop instance's single-instance lock.

Two modes:
- Ephemeral (default): temp dir, cleaned up on exit
- --persistent: stored under .hermes-sandbox/ in the worktree git root,
  survives restarts for repeat testing

In the Nix devShell the script is available as 'sandbox'.

Also makes APP_NAME overridable via HERMES_DESKTOP_APP_NAME in main.ts —
app.setName() runs before requestSingleInstanceLock(), so the overridden
name changes the lock key. collectRelaunchEnv already preserves
HERMES_DESKTOP_* vars through self-update relaunches; test updated to
cover the new env var.
2026-07-10 18:51:49 -04:00
ethernet 39d09453f9 feat(desktop): ts-ify everything 2026-07-08 16:24:16 -07:00
ethernet 4d7f8ade3e feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)

pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.

- hermes_cli/config.py: shared is_unsupported_install_method() /
  format_unsupported_install_warning() helpers so the wording and docs
  link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
  warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
  the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
  'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
  install_warning; applyRuntimeInfo + the live session.info event fire
  a snoozable warning toast via a new reportInstallMethodWarning(),
  mirroring the existing backend-contract-skew toast pattern. i18n
  strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
  Homebrew banner test, and two tui_gateway session_info tests
  (install_warning present for pip, absent for git).

* fix(nix): make `hermes` in developement environment actually work

install modules as editable overlay with uv

* feat: print install method when running --version

* fix: correct detect install method when running from a subtree
2026-07-07 21:13:19 -07:00
emozilla 2d206a3a42 fix(desktop): stop hermes desktop from clobbering tracked main.cjs (#52735)
`npm run build` ended with `bundle-electron-main.mjs`, which esbuild-bundled
electron/main.cjs and renamed the bundle on top of the tracked source file.
Because every `hermes desktop` runs `npm run build`, each launch rewrote a
checked-in source file (~7.5k-line source -> ~14.8k-line bundle), dirtying the
working tree with a build artifact that `git restore` couldn't keep (the next
launch re-clobbered it) and forcing autostash/restore conflicts on update.

The bundle only existed to inline `simple-git` so the packaged app.asar (which
ships no node_modules) wouldn't crash at launch with "Cannot find module
'simple-git'". Replace it with the mechanism the repo already uses for the
other hoisted runtime dep (node-pty): stage the dependency closure and resolve
it from process.resourcesPath at runtime.

- stage-native-deps.cjs: resolve simple-git's runtime closure (walking
  dependencies + optionalDependencies, so a version bump that adds a transitive
  dep can't silently reintroduce the crash) and stage it under
  build/native-deps/vendor/node_modules/. The `vendor/` nesting is load-bearing:
  electron-builder drops a node_modules dir at the ROOT of an extraResources
  copy but keeps a nested one.
- git-review-ops.cjs: fall back to the staged
  native-deps/vendor/node_modules/simple-git when the hoisted require() fails;
  dev runs resolve the hoisted copy and never hit the fallback.
- package.json: drop the bundler from the `build` script so main.cjs is never a
  build target again.
- nix/desktop.nix: drop the direct bundler call (the closure rides the existing
  `cp -rn native-deps` into $out) and patch process.resourcesPath in
  git-review-ops.cjs alongside main.cjs.
- delete scripts/bundle-electron-main.mjs.

Verified: electron-builder's own file filter keeps the full staged closure
(0 dropped), and a packaged win-unpacked build launches with the git-review
pane resolving simple-git from the staged vendor path.
2026-06-28 01:30:09 -04:00
ethernet 447f9e7c89 change(nix): simpler dev setup 2026-06-26 19:15:18 -07:00
ethernet 8ae793d3de change(nix): ship fat hermes agent by default 2026-06-26 19:15:18 -07:00
Brooklyn Nicholson b8d220f268 feat(desktop): wire project settings and shell chrome 2026-06-25 16:40:27 -05:00
Siddharth Balyan 3ca0ef7e3f fix(nix): hashless npm deps via importNpmLock (#48883)
The npm workspace pins a single npmDepsHash for fetchNpmDeps. Any change to
package-lock.json that doesn't also refresh that hash breaks the bundled
hermes-tui / hermes-desktop-renderer build for Nix flake consumers, and no
nix CI catches it — the workflow that ran fix-lockfiles was removed in
9eb0bcd6 ("change(ci): rip out nix ci for now").

Fetch the workspace deps with pkgs.importNpmLock instead. It resolves each
package from the lockfile's own integrity hashes, so package-lock.json is the
single source of truth and there is no separate hash to drift.

This also removes:

- the fix-lockfiles checker/refresher and its devShell wiring — it existed
  only to keep npmDepsHash in sync, so it is dead once the hash is gone, and
  its sole CI consumer was already removed in 9eb0bcd6;
- the patchPhase that normalized lockfile trailing newlines — importNpmLock's
  npmConfigHook overwrites the lockfile rather than diffing it, so the
  normalization is unnecessary.

npm-lockfile-fix is retained: importNpmLock requires an integrity-complete
lockfile, which that tool guarantees when the lockfile is regenerated.

Co-authored-by: ak2k <19240940+ak2k@users.noreply.github.com>
2026-06-19 13:57:12 +05:30
Siddharth Balyan 646cd1b43e fix(nix): refresh npmDepsHash after the Electron 40.10.2 pin (#47792) (#48457)
PR #47792 pinned Electron to an exact 40.10.2 and regenerated the root
package-lock.json (dropping @electron/get@5 + @electron-internal/extract-zip,
restoring @electron/get@2 + extract-zip@2 + yauzl), but did not refresh the
shared npmDepsHash in nix/lib.nix. The hash still described the previous
40.10.3 lockfile, so npmConfigHook fails on every Nix build with
"npmDepsHash is out of date" for hermes-tui / hermes-web / hermes-desktop.

Regenerate the single shared hash to match the current lockfile.

Verified with fetchNpmDeps (authoritative, not prefetch-npm-deps):
  nix build .#tui.npmDeps  -> builds clean
  nix build .#tui          -> Validating consistency -> Installing dependencies
                              -> Finished npmConfigHook (no hash error)
2026-06-18 15:00:08 +00:00
xxxigm 45e2f4fdcd nix: refresh npmDepsHash for the @assistant-ui/store pin
The store pin changed package-lock.json, so the workspace-wide
npmDepsHash in nix/lib.nix is stale and the Nix flake check fails on
the hash mismatch. Use the hash reported by the real fetchNpmDeps
build (the flake check's `got:`), which is authoritative — it differs
from prefetch-npm-deps' lockfile-contents hash, exactly the divergence
nix/lib.nix already documents.
2026-06-15 11:55:02 -04:00
Teknium 29c6985590 fix(nix): refresh npm deps hash 2026-06-15 06:18:27 -07:00
Brooklyn Nicholson acd4278c8a fix(nix): use fetchNpmDeps hash from flake check
prefetch-npm-deps returned a different digest than the actual
fetchNpmDeps build; use the CI-reported hash.
2026-06-13 02:34:25 -05:00
Brooklyn Nicholson be6713c536 fix(nix): refresh npm deps hash 2026-06-13 02:16:13 -05:00
Brooklyn Nicholson d62e9b7592 build(nix): refresh npmDepsHash for the remend dependency
Adding remend changed package-lock.json, so the flake's pinned npm deps
hash went stale and `nix flake check` failed. Bump it to match.
2026-06-12 21:17:22 -05:00
ethernet 906bee9cf7 fix(nix): natively compile and correctly stage node-pty for desktop app
- Add ELECTRON_SKIP_BINARY_DOWNLOAD=1 to nix/lib.nix to prevent offline download failures.
- Manually trigger native compilation of node-pty via npm rebuild --build-from-source in buildPhase.
- Run stage-native-deps.cjs to copy the natively compiled binary into build/native-deps.
- Flatten native-deps and install-stamp.json to the root of the output derivation in installPhase, matching electron-builder's extraResources behavior so main.cjs can find it at process.resourcesPath + '/native-deps/node-pty'.
- Add doCheck=true and a strict checkPhase to fail fast if the staged native binary is missing.
2026-06-12 03:55:09 -04:00
Austin Pickett e2145a5c9c fix(ui-tui): stabilize embedded dashboard chat gateway (#44528)
Cherry-picked from #39840 by @flyinhigh and rebased cleanly on main.

- Defer config fetch in createGatewayEventHandler until gateway.ready to
  avoid render-phase RPC that can mutate transcript state and trigger
  React error 301 in embedded dashboard PTYs.
- Use undici WebSocket fallback when globalThis.WebSocket is unavailable
  (Node attach mode and sidecar mirror sockets).
- Add regression tests for both fixes.

Co-authored-by: flyinhigh <flyinhigh@users.noreply.github.com>
2026-06-11 19:47:53 -04:00
ethernet 5e5308d34d fix(node): fix @types/node version
TODO lock to a specific node/npm version.
this is a fix for a diff between 10 and 11.
2026-06-11 15:36:39 -04:00
xxxigm 743c55efa3 fix(desktop): stop file tree throwing "Cannot have two HTML5 backends" on remount (#43541)
* fix(desktop): stop file tree throwing "two HTML5 backends" on remount

The Agent Workspace file tree (react-arborist) shows a permanent "TREE ERROR"
with `[error-boundary:file-tree] Cannot have two HTML5 backends at the same
time.` react-arborist mounts its own react-dnd DndProvider + HTML5Backend per
<Tree>. react-dnd v14 keeps that manager on a global, ref-counted singleton
context and nulls it when the count reaches 0. The tree is keyed on
`${cwd}:${collapseNonce}`, so changing folder / collapsing forces a fresh
<Tree>; during the remount the singleton can be torn down and recreated while
the previous HTML5Backend still owns `window.__isReactDndHtml5Backend`, so the
new backend's setup() throws. The error boundary then sticks, because "Try
again" just remounts into the same race.

Pass arborist a stable, app-lifetime `dndManager` (new getFileTreeDndManager
singleton) so it reuses one backend for the life of the app and never
double-claims the window flag. Drag/drop is already disabled on this tree;
this only changes how the (unused) dnd backend is provisioned.

Promotes dnd-core and react-dnd-html5-backend to explicit deps (already present
transitively via react-arborist's react-dnd 14.x line, so they dedupe to one
instance).

* fix(nix): bump npmDepsHash for desktop dnd deps

Adding dnd-core / react-dnd-html5-backend changed the workspace
package-lock.json, so the single workspace-root npmDepsHash in
nix/lib.nix was stale and the nix build failed. Regenerate it
(hash from the failing nix CI job's 'got:' value).

* fix(nix): update npmDepsHash for merged lockfile

After merging main, the workspace lockfile combined main's dep
changes with the desktop dnd additions, so the npmDepsHash needed
recomputing again. Hash from the nix lockfile-check job.

* fix(nix): use fetchNpmDeps hash for desktop dnd lockfile

prefetch-npm-deps reported sha256-lVnybH9RE/... but fetchNpmDeps
wants sha256-mYgKXE/FL4hnkrEvpVv+ULM/oeyIfO2AM9Ol8OrfWm0= for the
merged workspace lockfile. Use the nix build 'got:' hash so CI passes.

---------

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-06-11 11:47:34 -07:00
ethernet 90f4b3040d change(tooling): remove react-compiler eslint, update concurrently
concurrently 9 had a critical vuln dependency,
react-compiler eslint plugin is built into react-hooks eslint plugin as
of https://react.dev/blog/2025/10/07/react-compiler-1
2026-06-10 11:59:34 -04:00
ethernet 3bfbb3f2a0 change(tooling): typecheck in CI, update ts to 6
fix(ui-tui): fix ts 6 real type errors

change(tooling): use new node everywhere
2026-06-10 11:59:34 -04:00
ethernet a5d05cf30e fix(nix); don't run .#fix-lockfiles
its so slow
2026-06-09 16:55:33 -04:00
Siddharth Balyan 7230fcb7f2 revert(nix): drop the cp patchPhase workaround from #41867 (#42151)
#41867 replaced mkNpmPassthru's patchPhase with
`cp $npmDeps/package-lock.json package-lock.json`, on the theory that
prefetch-npm-deps strips advisory fields (engines/os/cpu) from the cache
lockfile. That diagnosis was wrong.

prefetch-npm-deps copies the lockfile into the cache *verbatim*
(prefetch-npm-deps/src/main.rs reads it and writes it unchanged). Building the
cache fresh from the current root lockfile yields exactly the pinned
npmDepsHash, and that cache's package-lock.json is byte-identical to the source
(740 "engines" blocks on each side). With the hash correct, npmConfigHook's
consistency check passes on its own — verified by building .#tui and .#default
green with this (original) patchPhase.

So the cp was unnecessary, and worse: it bypasses the consistency check
wholesale, silently masking a genuinely stale npmDepsHash (a lockfile that
changed without its hash being refreshed) instead of failing loudly. The
original patchPhase keeps the check meaningful while still handling the one real
cosmetic difference it was written for (trailing newlines); stale-hash drift is
caught by the npmDepsHash itself plus the auto-fix workflow.

Keeps the fix-lockfiles real-build verification and the nix-lockfile-fix.yml
file-path fix from #41867 — only the patchPhase cp is reverted.
2026-06-08 20:29:41 +05:30