main
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
298df0820d |
fix(nix): fresh declarative installs no longer warn their config predates version 12 (#126022)
* 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). |
||
|
|
3aa215fdc9 |
build: remove leftover references to the dropped hindsight extra
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
|
||
|
|
712734436e |
fix(pm): make bootstrap and bundle ownership explicit
Finish bootstrap uv before PM replaces its store entry. Keep failure receipts stdlib-only and align the cryptography requirement and override with the locked version. Let bundle builders declare launch paths and update ownership. Remove payload discovery, Store probing, and the unused develop command. Derive Nix Python from the PM lock and share its provenance stamp. Document setup, activation, optional dependencies, and distribution ownership. Targeted Windows tests, relocated runtime launches, Electron bundling, and bilingual docs builds pass. Native Nix and signed-package acceptance remain CI gates. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |