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.
This commit is contained in:
ethernet
2026-09-08 00:24:51 -04:00
parent 6590ecdc2d
commit 712734436e
130 changed files with 2997 additions and 2716 deletions

View File

@@ -0,0 +1,328 @@
---
title: "Package Management"
description: "PM tool pins, Python environments, optional dependencies, and installation ownership"
---
# Package management
`hermes pm` manages Hermes tool binaries and Python dependency environments.
It is not the application updater. Use the installation's
[update method](/getting-started/updating) to update Hermes itself.
## Pins, installed state, and runtime selection
Each file has a separate role:
| File | Role |
|---|---|
| `pm/lock.json` | Exact managed-tool versions, target-specific URLs, and SHA-256 hashes. |
| `pyproject.toml` and `uv.lock` | Python requirements, extras, platform markers, and the committed Python resolution. |
| Tool-store `facts.json` | Installed tool entries, their identities, environment exports, and realized-file digests. |
| Per-install `facts.json` | The selected Python environment, its input stamp, and enabled extras. |
| Payload `manifest.json` | Relative payload layout and the completed launch contract from the bundle builder. |
| `install-stamp.json` | Build provenance and the declared distribution/update owner. |
A lockfile entry does not prove that a package is installed. `hermes pm doctor`
compares the installed state with the lock and checks the realized bytes.
Startup uses a cheaper check. It does not query upstream versions on every launch.
## Source installs and packaged builds
Source installers provision the required tools plus Python. They select the
`all` Python extra. Named optional tools install when requested.
Native desktop bundles stage the supported tool set and all target-compatible
Python extras before packaging. `--extra all` and `--all-extras` are not
synonyms. Platform markers still exclude dependencies that cannot run on a target.
A packaged application's base payload is immutable. Hermes runs its backend
from that payload, rather than copying a source checkout on first launch.
The bundle builder checks its files and writes the launch paths into the desktop
build stamp. Electron uses those paths without probing or repairing the payload.
Additional pinned tools can use the writable tool store. Python additions use
a complete writable environment outside the signed package.
Termux uses a separate bionic build and a sealed APT package. Docker bakes its
runtime into the image and disables on-demand dependency installation. Nix
provides its runtime through derivations. See the
[Termux](/getting-started/termux), [Docker](/user-guide/docker), and
[Nix](/getting-started/nix-setup) guides for their limits.
## Writable state
The platform default data root is `~/.hermes` on POSIX and
`%LOCALAPPDATA%\hermes` on Windows. `HERMES_HOME` and profiles can change
which data root a process uses.
| State | Default location |
|---|---|
| Shared writable tool entries | `tools/` under the resolved default Hermes root. |
| Resumable downloads | `cache/partials/` under that root, not inside a signed payload. |
| Per-install selection and journal | `installs/INSTALL_KEY/` under the dependency-state root. |
| Python generations | `installs/INSTALL_KEY/environments/`. |
| Sync and update receipts | `logs/update_receipts/` under the active home. |
The install key derives from the canonical source or payload-repository path.
Separate checkouts therefore have separate Python selections. Profiles that
share an installation can contribute dependencies to the same environment.
Their configuration and credentials remain profile-scoped.
Do not edit facts or generation paths manually. Launchers resolve the selected
environment before third-party imports. Processes retain their existing imports
until they restart. Garbage collection preserves selected generations and
lease-managed generations with live readers.
## Optional Python dependencies and plugins
A built-in feature requests a project extra through `pm.ensure_import`.
Directory plugins declare Python requirements in `pyproject.toml`, or through
legacy `pip_dependencies` or `python_dependencies` lists in `plugin.yaml`.
PM prepares core requirements, enabled extras, and enabled plugin requirements
together. It seeds resolution from the existing lock. Compatible transitive
versions can change, but declared constraints and exact pins remain binding.
The generated workspace and extended lock remain outside shipped source.
A failed candidate does not replace the selected environment or silently
disable other plugins. If preparation succeeds, a restart can still be required
to activate the new environment in a running Hermes process.
Ordinary Hermes application updates preserve user plugin directories. Explicit
plugin updates can change the selected plugin's files. A wrapper with no Python
dependency declaration does not join the shared environment. Its external
sidecar remains separately owned. See the
[plugin guide](/developer-guide/plugins).
### Lazy-install policy
`security.allow_lazy_installs` controls on-demand installation. Already installed
dependencies remain usable when this setting is false.
```bash
hermes config set security.allow_lazy_installs false
```
Explicit install commands are distinct from on-demand installation. However,
a bundle's frozen feature list still restricts requested Python extra names
when lazy installs are disabled. Explicit plugin admission is a separate
operation, not an on-demand feature request. Do not treat this setting as a
sandbox or a blanket prohibition on manual package installation.
Docker additionally sets the internal lazy-install disable flag in the image.
PM is a dependency manager, not a sandbox for plugin code. Installing a plugin
requires trust in that plugin and its dependencies.
## Developer workflow {#developer-workflow}
PM prepares the toolchain for a source checkout. Activation makes that installed
toolchain available in a shell. Neither operation selects your editor's Python
interpreter or redirects an installed desktop app to this checkout.
### Prepare a checkout
Use an ordinary terminal outside the packaged Hermes app. Leave any existing
Python virtual environment first. On Windows, use native PowerShell with Git
and the target architecture's C++ build tools available. Dependencies without
wheels can also require Rust/Cargo and native libraries. PM does not install
those compiler toolchains. POSIX source builds have native build requirements too.
Clone the repository and select your branch before preparing dependencies:
```bash
git clone https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
```
For isolated development, select a separate data home before the first PM
command. Keep the same values when returning to this checkout.
Bash, from the repository root:
```bash
export HERMES_HOME="$HOME/hermes-dev-data"
export HERMES_RUNTIME_DIR="$HERMES_HOME/tools"
bash setup-hermes.sh
source ./activate
```
PowerShell, from the repository root:
```powershell
$env:HERMES_HOME = Join-Path $HOME 'hermes-dev-data'
$env:HERMES_RUNTIME_DIR = Join-Path $env:HERMES_HOME 'tools'
.\setup-hermes.ps1
. .\activate.ps1
```
`HERMES_RUNTIME_DIR` in these examples is a process-local development override.
It makes the bootstrap and PM use the same writable store. Do not persist a
path into an installed MSIX or macOS bundle. The setup scripts provision tools
and the `all` Python extra. They do not select `dev` or install JS workspaces.
The bootstrap uses uv to install and locate Python, then waits for uv to exit.
That Python runs PM directly. PM can then replace its uv entry without a running
bootstrap process holding the old executable. PM writes failure receipts without
PyYAML, including when dependency installation fails.
### Activate an existing installation
In each new shell, restore your development-home values and enter the checkout.
Then activate it without running installation again:
| Shell | Enter | Leave |
|---|---|---|
| Bash | `source ./activate` | `deactivate` |
| PowerShell | `. .\activate.ps1` | `deactivate` |
The leading dot and space in PowerShell are required. Executing
`.\activate.ps1` without dot-sourcing does not provide the same session scope.
The POSIX script uses Bash syntax. Use Bash for this recipe rather than `sh`,
fish, or assuming that a Zsh startup file has Bash semantics.
Activation prepends installed PM tools to `PATH`. It sets `PYTHONPATH` to this
checkout and its selected dependency tree. It does not download packages,
change an OS-wide PATH, or activate a conventional venv prompt.
Start in a clean shell rather than nesting this inside another venv.
`deactivate` restores the environment values captured by the activation script.
It does not uninstall packages or stop processes that you started.
Verify the interpreter and source before doing work:
```bash
python -c "import sys, pm; print(sys.executable); print(pm.__file__)"
python -c "import httpx; print(httpx.__file__)"
node --version
npm --version
python hermes --version
```
`python` must resolve to the PM store interpreter. `pm.__file__` must point
into this checkout. Dependencies come from the selected environment, which can
live outside the repository. A missing import means setup or selection needs
attention, even if `source ./activate` itself returned successfully.
### Work on this source tree
Use checkout-qualified commands so a global `hermes` command or MSIX alias
cannot run a different installation:
```bash
python hermes setup
python hermes
python hermes --tui
python -m pm.cli status
```
These commands use the selected development home. A source-file edit is visible
to the next process. Restart the affected CLI, gateway, or backend after edits.
Reinstalling every dependency is unnecessary for a Python-only source change.
For the JavaScript workspaces, run `npm ci` once at the repository root, then
run the relevant workspace command. For example:
```bash
npm run build --workspace ui-tui
npm run dev --workspace apps/desktop
```
The website is separate: `npm ci --prefix website`, then
`npm run build:fast --prefix website`. PM activation supplies tools, not these
`node_modules` directories or built assets. Native desktop builds have additional
requirements in the [desktop build guide](https://github.com/NousResearch/hermes-agent/blob/main/apps/desktop/BUILDING.md).
### Refresh dependencies without changing branches
After a branch or lockfile change, prepare dependencies with this checkout's PM:
```bash
python -m pm.cli install
```
Then leave and reactivate the environment, and restart affected processes.
Use `python -m pm.cli doctor` for tool diagnostics and `python -m pm.cli status`
for the latest sync receipt. Do not run `hermes update` just to refresh a
feature branch: it is an application update and can change the source branch.
Managed tool names and Python extra names are different interfaces:
```bash
python -m pm.cli install chromium
python -c "from pm import sync_venv; sync_venv(['dev'], explicit=True)"
```
The first command installs a tool. The second adds the declared `dev` extra
to this installation's existing Python selection. Extras accumulate through PM
sync. `pm install dev` is not a supported command: `dev` is an extra, not a tool.
After changing extras, reactivate before starting another Python process.
For a new project dependency, edit `pyproject.toml` and regenerate `uv.lock`
with `uv lock`. For JS dependencies, update the owning package manifest and
lock. Do not edit PM facts or generated workspaces. Unrecorded pip installs
are not durable and can disappear when PM selects a new environment.
### Test and editor environments
PM's `dev` extra does not make a bare store Python suitable for the canonical
test runner. The runner clears `PYTHONPATH` and needs an interpreter with pytest
installed in its own environment. Use the contributor guide's
[independent test environment](/developer-guide/contributing#manual-development-and-test-environment),
then run `scripts/run_tests.sh` (through Bash on Windows).
The runner checks repository `.venv`, repository `venv`, and the standard
source-install venv before using `HERMES_PYTHON` as a fallback. Read its startup
message to confirm which interpreter it selected. A worktree without a local
venv can use the independent test interpreter through that variable.
For editor debugging, select that independent interpreter, set the working
directory to this checkout, and launch `hermes` as the script. Keep its
`HERMES_HOME` separate from production. Terminal activation does not configure
an editor that was already running. Do not point an editor at a transient PM
generation or a signed application's Python executable.
## Commands
```bash
hermes pm --help
hermes pm doctor
hermes pm status
hermes pm install
hermes pm install chromium
```
| Command | Effect |
|---|---|
| `pm install [names...]` | Install named packages. With no names, provision required tools plus Python and sync the `all` extra. |
| `pm env [names...]` | Print the composed environment of installed packages as JSON. It does not install missing packages. |
| `pm doctor` | Check installed tool identities, files, and digests against the lock. |
| `pm status` | Print the latest sync/update receipt as JSON, or report that no receipt exists. |
| `pm gc` | Remove unreferenced tool-store entries, eligible download partials, and unused lease-managed Python generations. |
`pm env` can include inherited environment values. Do not publish its output
without removing credentials.
### Maintainer commands
These commands change dependency inputs or stage build artifacts. They are
not substitutes for an installed application's update mechanism.
| Command | Effect |
|---|---|
| `pm lock --bump NAME VERSION` | Resolve and hash supported target artifacts, then write the tool pin. |
| `pm update [names...]` | Query upstream versions, change tool pins, and install changed tools. |
| `pm update --check` | Query without writing. Exit 1 can mean updates exist; inspect output to distinguish an error. |
| `pm update --target TARGET` | Resolve versions for the specified target. |
| `pm update --uv` / `--npm` | Also refresh the Python or npm dependency resolution. |
| `pm install --target TARGET NAME...` | Stage explicit cross-target packages without recording them as the host's installed runtime. |
| `pm bundle --out DIR [--ref REF]` | Stage a source snapshot, native tools, facts, and Python dependencies. It does not produce a signed desktop installer. |
The complete desktop builder also builds the JavaScript surfaces, generates
launchers, and invokes native packaging. Maintainers can read
[Building the Desktop Installers](https://github.com/NousResearch/hermes-agent/blob/main/apps/desktop/BUILDING.md).
## Diagnostics
- **Missing or outdated tool:** read `hermes pm doctor`, then use an explicit PM install on a writable installation.
- **New environment requires restart:** restart the affected Hermes process. Do not add a second site-packages tree to its live imports.
- **Dependency conflict:** read `hermes pm status`. Correct the plugin requirements before retrying admission.
- **Damaged packaged base:** repair or reinstall through the package owner. Do not alter signed files to suppress the diagnostic.
- **Unknown package or extra:** use the declared name. `pm install` takes package names, not Python extra names or pip specifications.