Commit Graph

27 Commits

Author SHA1 Message Date
Maciej Lisiewski
bda3aea496 ci: remove unused variables (linter) 2026-10-10 15:20:14 +02:00
unsupportedpastels
fe2b130b8b fix(pm): record the Docker image's baked extras and restore them on boot
The image bakes eight extras into /opt/hermes/.venv but never recorded
them as PM's dependency selection. The first generation a container built
(a lazy extra such as fal, a plugin's dependencies) started from an empty
selection, so it carried only what was requested and dropped aiohttp, the
messaging SDKs and mcp. After the next restart the API server, platform
adapters and MCP servers failed to start.

- pm/build_env.py, Dockerfile: --record-selection writes the frozen
  build's --extra list to the store facts as the venv selection, as a
  bundle build does. enabled-features.json is not written, so the frozen
  lazy-install policy stays out of Docker.
- pm/recovery.py: stage2's refresh restores extras the image ships that a
  volume's recorded selection lacks, through the normal explicit sync. On
  failure the image venv boots and the missing extras stay recorded for
  the next attempt. A malformed recorded fact is refused, as
  venv_is_current does.
- pm/install.py: share the PEP 685 extra-name key between
  _still_declared and the boot reconciliation.

Fixes #135329
2026-10-09 17:35:25 +00:00
kshitijk4poor
c3b06d1443 refactor(update): trust the npm closure through pm.ensure(verify=False)
The previous commit pre-checked the closure with a hand-rolled walk + is_installed before ensure, which repeated ensure's own check and left a window between them. pm.ensure already has the knob: verify=False trusts the recorded digest (as startup does) and an explicit ensure still installs a missing tool. Thread it through pm.client and source_build_env; the old comment claimed a verification that the interrupted-update finish path never ran.
2026-10-08 15:20:58 -07:00
alt-glitch
a1461d11ab fix(pm): spell store roots PM opens without Store like Store does
Some PM code reaches a store root from paths.store_root() or
writable_store_root() directly instead of through Store, so its file
calls kept the ordinary spelling and stay cut at MAX_PATH on Windows
with LongPathsEnabled=0. Worse, its comparisons against a Store root
(now \\?\-spelled) never matched.

Each such root now gets long_root() once, where the variable is born:
- Facts.refresh_digests digests whole payload tool trees;
- verified_tools walks and digests the source store's tool trees;
- the uv cache seed copies the payload's deep uv-cache tree out;
- _toolchain's writable store, so `location.root != writable` compares
  like with like and the copy store is built from the same spelling;
- _installed_location passes store.root to Facts.installed;
- ensure() picks the shipped facts by comparing two Store roots;
- the corrupt-facts guard compares both sides spelled long.

Returned and exported paths stay ordinary (previous commit).
2026-10-07 06:40:39 +05:30
alt-glitch
953a598860 fix(pm): give text that leaves PM the ordinary spelling of store paths
Store now spells its root \\?\C:\... on Windows so PM's own file calls
under it pass MAX_PATH. That spelling must not escape: the same folder
spelled two ways hashes and compares differently, and children such as
cmd.exe, gpg or uv refuse or record the prefix (uv would write it into
pyvenv.cfg).

native() now converts store-derived paths where their text leaves PM:
- process argv and cwd: uv via PythonEnvironment._run (its one exit),
  binary version probes, npm self-install, the PortableGit extractor,
  gpg provenance checks, the plugin-eviction interpreter probe;
- environment values: compose_env (PATH and package env diffs), the
  activation PATH dirs, HERMES_RUNTIME_DIR;
- hashed identities: the PM runtime inputs and isolated-environment
  identity, so existing records still match and nothing rebuilds;
- API results callers execute or compare: installed_package,
  uv_launcher, verified_tools entries;
- user-facing verification messages.

Facts records already store {STORE}-templated env, so no absolute path
reaches them. Tests pin that identities are spelling-independent and that
records, composed env and uv invocations from a verbatim root carry no
prefix (both fail on e8c97320ac).
2026-10-07 06:40:39 +05:30
Brooklyn Nicholson
2d66d6e2f8 fix(pm): wait out Windows scanner holds in the archive hoist and entry swaps
flatten_single_dir renames a tree extracted seconds earlier, while
Defender or another scanner still holds it, and ffmpeg/agent-browser
installs died on the package/bin rename with [WinError 5] (#131884).
The entry displace/restore renames in install.py had the same bare call.

pm.filesystem.retry_held retries EACCES/EPERM/EBUSY for about 3s and
raises anything else immediately; the hoist, merge_tree and the entry
swaps use it.

Co-authored-by: bighamx <10262931+bighamx@users.noreply.github.com>
Co-authored-by: lzmuhioin <1814826+lzmuhioin@users.noreply.github.com>
2026-10-05 17:08:32 -05:00
kshitijk4poor
7159021d1e fix(pm): carry the libatomic remedy as InstallError's remedy
The hook glued the remedy into the reason, so the error ended with two
fixes, the second ("retry, or run hermes pm doctor") wrong for a missing
host library. Return (reason, remedy) and pass the remedy through. When the
package installs but Node still cannot load it, say that instead of telling
the user to run the command that just succeeded.
2026-10-04 19:19:41 +05:30
JoaoMarcos44
ad6760a416 fix(pm): repair Node libatomic at install boundary
(cherry picked from commit 2ac66739aa5c50fd352155e2c629271227a9697b)
2026-10-04 19:19:41 +05:30
teknium1
3a744c7d66 fix(pm): clear read-only bits with a pm-local helper, not hermes_cli
The salvaged commit imported hermes_cli.fs_utils.rmtree_force inside
_remove_entry. PM installs run before the rest of the tree exists: the
Docker toolchain stage copies only pm/, so every image build died with
"No module named 'hermes_cli.fs_utils'" on its first fetch cleanup.

pm/filesystem.py is the stdlib-only home for exactly this kind of
primitive. hermes_cli/fs_utils.py stays as is; it is part of the frozen
old-updater surface.
2026-10-03 01:11:10 -07:00
teknium1
d00d57b70f test(pm): pin read-only file removal on a real Windows runner
A read-only file (PortableGit's etc/hosts) inside a replaced entry made
_remove_entry fail every attempt. Since the held-tree fix the install no
longer aborts, but the tree is set aside and gc can never reclaim it, so
every replaced git entry leaked. The live Windows case requires the
replaced tree to be gone outright after a reinstall. Also trims the
salvaged comment to its why.
2026-10-03 01:11:10 -07:00
blennerhassett_btye
be431788dd fix(pm): clear read-only bits when removing a replaced tool entry
`_remove_entry` used a bare `shutil.rmtree`. Git for Windows' PortableGit
archive ships `etc/hosts` read-only, and Windows refuses to unlink a
read-only file, so removing the rotated `.previous-git-*` entry raised
WinError 5 on every attempt. The retry loop cannot help here: a read-only
attribute is not a transient handle, so all 5 retries failed and the
whole update aborted with:

    Source update preparation failed: git: install failed:
    [WinError 5] Access is denied:
    '...\.previous-git-2.53.0+3-win32-x64\etc\hosts'

The abort happened before the checkout was reset, so `hermes update` made
no progress at all and the install stayed pinned indefinitely.

Use the existing `hermes_cli.fs_utils.rmtree_force`, which clears the
read-only bit before deleting and whose docstring already describes this
exact scenario. It is imported lazily to match how the rest of `pm`
reaches into `hermes_cli`.
2026-10-03 01:11:10 -07:00
teknium1
bd75d30917 fix(pm): set a held replaced tree aside instead of failing the install
Every cleanup that runs after a publish or restore has finished deletes
garbage: the displaced .previous-* entry, a restored .displaced-* tree,
the fetch-* archives. A running Hermes process keeps the replaced
interpreter's DLLs mapped, so on Windows that delete can fail far past
the retry window and turned a completed update into "install failed:
[WinError 5]" (#124807).

One helper now owns that cleanup for all four sites. When the delete
fails it renames the tree to .reclaim-<uuid> (Windows permits renaming a
tree with mapped images): the old slot is free for the next publish of
the same entry, no restore can ever pick the half-deleted tree up, and
the next install or `hermes pm gc` deletes it once the hold is gone.
This replaces the sidecar marker for restored displacements (#128955)
and the warn-and-keep for previous entries from the salvaged commit:
a kept .previous-<entry> blocked the next publish of that entry, could
be restored half-deleted, and was unreachable by gc after a version bump.
2026-10-02 16:45:39 -07:00
janviernine
62761ec991 pm: keep locked stale previous entries instead of failing install
Assisted-by: Hermes Agent
2026-10-02 16:45:39 -07:00
Gille
98cceaef2b fix(pm): keep restored runtimes usable after locked displaced cleanup (#128955)
* fix(pm): tolerate locked restored displacement cleanup
2026-10-02 18:08:16 -04:00
teknium1
92ce6f4760 fix(pm): heal agent-browser entries staged without the exec bit
28edf7c75a made stage() chmod the native agent-browser binary, but
entries staged before it (lazy installs since the pm store landed) stay
0644 forever: every browser_* call fails with PermissionError and the
store never restages because the entry verifies. Restore the bit in
place when the entry is looked up on its own target (modes are not part
of the pinned digest); an entry that cannot be chmodded counts as not
installed so ensure() restages it.
2026-09-28 10:11:33 -07:00
teknium1
e0fae5566a fix: Hermes's own lint, activate, tui and MCP launchers use PM's node/npx/uv, never the user's
Post-write .js/.ts lint on the local backend runs PM's node/npx instead of
the terminal shell's PATH (user dirs first). pm.activate always puts the
store dirs in front, also on drift, activating only packages whose whole
chain is installed. tui_gateway puts the store dirs ahead of ~/.local/bin.
Bare npx/npm/node/uv/uvx MCP commands resolve to PM's copies; the system
fallback tables are gone and an absolute command: stays the user's.
2026-09-27 22:30:55 -07:00
Matt Healey
ac3ebbfedc fix(pm): match recorded extras to declarations by normalized name
uv treats `foo_bar` and `foo-bar` as the same extra (PEP 685), so an
exact comparison would drop a recorded extra whose spelling differs from
its declaration. Compare normalized names; the recorded spelling is still
what reaches uv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:48:40 -04:00
Matt Healey
24e8c341c1 fix(pm): prune recorded extras the tree no longer declares
The venv ledger unions its recorded extras into every sync. When a source
update removes an extra (hindsight, 73c598e319/285768fdfb), installs that
recorded it pass `--extra hindsight` to `uv sync` forever and fail with
"Extra `hindsight` is not defined in any project's optional-dependencies
table". Every CLI launch then retries the failed source-update completion,
and gateways keep booting the previous generation's code.

Drop recorded extras the checkout's pyproject no longer declares, in both
the sync target and the currency probe. An explicitly requested unknown
extra still fails loudly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:48:40 -04:00
ethernet
cae75f0ead fix(pm): an unreadable secondary profile config cannot fail an update
Venv.apply refused the whole graph when any secondary profile's
config.yaml was unreadable, so one broken sibling config failed every
update. Update syncs now leave that profile's plugins out of the union,
report it (stderr + receipt warning), and build the rest; the profile's
plugins rejoin on the next sync once its config is fixed. Boot currency
already skips broken secondaries, so the result reads as current.
Ordinary syncs keep refusing to shrink the recorded graph.
2026-09-25 00:00:09 -04:00
ethernet
ccf631701a fix(pm): disable plugins that no longer fit instead of failing the update
An update resolves the enabled plugin union against the NEW core. A plugin
admitted against the old core can stop fitting when core moves (managed
Python 3.13 -> 3.14 vs a member's requires-python <3.14, a requires_hermes
upper bound, a bumped pin), and the whole update then died after the
source swap with a non-resolver InstallError whose 'retry' hint failed the
same way every time.

Update syncs now pass evict_incompatible_plugins=True (update completion,
historical takeover, launch-time completion, venv_sync, post-update
drift). PM screens statically first (requires-python vs the target
interpreter, manifest/requires_hermes), then, if the rest still fails,
builds core alone to prove the plugins are the cause and re-adds members
in config order, disabling each one that breaks the build. Misfits land in
plugins.disabled (memory.provider cleared) in every home that enables
them, published through the existing journaled change hook (the journal
now carries several configs), and are reported on stderr + receipt
warnings. Admission and ordinary syncs still refuse; only a core that
cannot build on its own fails an update.
2026-09-25 00:00:09 -04:00
ethernet
7581ef4866 fix(pm): keep the pre-sync tool gate out of the venv check
activate(allow_incomplete=True) discarded the venv verdict but still
computed it, and venv_is_current reads plugin selection through the
application config reader (ruamel). The update child runs the bare
bootstrap interpreter, so `hermes update` failed with "No module named
'ruamel'" once it published tools before the sync.
2026-09-24 17:56:48 -04:00
ethernet
581bb440fd refactor(pm): split _install into recovery, currency and copy helpers
Extract settling an interrupted publication, the facts/stage-marker
currency check, the verified bundled copy and the repair log out of
pm.install._install. Order of checks, locking and errors is unchanged;
CC drops from 40 to 25.
2026-09-24 14:08:21 -04:00
ethernet
9b4ed5f5ec refactor(pm): one plugin-input request for sync_venv and venv_is_current
sync_venv took four mutually exclusive plugin kwargs (plugin_dirs,
extra_plugin_dirs, selection, staged_plugin) and venv_is_current two, with
runtime ValueErrors guarding the combinations. Replace them with a single
`plugins=` argument typed as one of pm.plugin_inputs.Members, Candidates,
Selection or StagedUpdate, so a conflicting request cannot be expressed.
The module also owns the worker wire encoding that client.py and worker.py
each duplicated.

pm.install.sync_venv is split into cohesive helpers (feature policy,
install lock, publication snapshot, target selection, commit) with the
same ordering, receipts and recovery; its CC drops from 54 to 16.

All in-tree callers and tests move to the new argument.
2026-09-24 14:08:21 -04:00
ethernet
97b0ab42e8 fix(pm): flush a generation tree before its selection record names it
The selection record (the install's facts.json, a side environment's
active.json) is already written last, atomically and fsynced, so it is the
generation's completion marker. But nothing flushed the tree it vouches for
first: a power loss mid-publish could persist the record while the venv's
files were still in the page cache, and the next boot would select a
half-written environment whose pyvenv.cfg happened to exist.

Both generation publishers (Venv via sync_venv, and _ensure_generation for
side environments) now call pm.environments.flush_before_selecting() after
the tree is complete and before the record is written: one os.sync() rather
than an fsync per file, which would cost minutes for a venv. Windows has no
os.sync; there the atomic record is the only guarantee, documented in place.

Reusing the existing record instead of adding a new marker file means no
migration: generations published by earlier builds stay selectable, and no
reader needs a legacy rule. Every reader of the selection (selected_venv,
environment_python, and their callers) already requires the record.
2026-09-24 11:50:30 -04:00
ethernet
dd9efff98b fix(pm): skip the tool re-hash during shell activation
source ./activate and . .\activate.ps1 always run setup, and setup always
runs `python -m pm.cli install`. An explicit install re-hashes every
published tool entry so it can repair a corrupted one. On this machine
that hash reads about 550 MiB and takes 6.0 of the 6.6 seconds, even
when nothing changed.

Shell activation now passes --trust-recorded and trusts the digest the
install recorded, the same check startup already uses. A missing tool
is still installed and a stale venv is still rebuilt. `hermes pm
install` and `hermes update` keep the byte check, and the flag refuses
names, --extra, and --target so it cannot narrow an install someone
asked for by name.

Measured on this machine, already up to date: the activation path drops
from 6.6 s to 0.9 s. A bare `python -m pm.cli install` stays at 7.2 s.
The remaining 0.9 s is process startup and imports, not hashing.
2026-09-23 13:17:55 -04:00
ethernet
9c11bb96bd fix(pm): put tools on PATH before the venv sync
Windows PowerShell 5.1 returns every match from Get-Command. .Source on
that array joins the paths with a space, and the call operator then treats
the joined string as one program name. Git for Windows ships git.exe in
cmd\ and bin\, so setup died with CommandNotFoundException before pm
install ran.

setup-hermes.ps1 now installs the tool closure first (`pm install
--tools-only`), then prepares the ARM64 compiler environment, then syncs
the venv. The sync inherits that compiler environment. A bare `pm install`
and the update takeover path publish tools and put them on PATH before
uv sync. A missing tool stops the sync. A missing venv does not.

Verified: scripts/run_tests.sh on test_install_default_closure.py,
test_install_extra.py, and test_windows_build_deps.py — 13 passed.
2026-09-22 12:33:02 -04:00
ethernet
6a5a6a05d2 refactor(pm): rename the pm.ensure submodule to pm.install; lazy_deps back to the shim
`import pm.ensure` bound the submodule onto the package, shadowing the facade's
`pm.ensure()` function for every later caller in the process (photon's sidecar
start hit `'module' object is not callable`). The module is pm.install now; the
function keeps its name. The facade resolves through `__import__` rather than
`importlib.import_module` so a test that patches import_module globally does not
break attribute access on pm.

tools/lazy_deps.py returns to the 16-line stop_for_relaunch shim the branch wrote
(an origin/main merge had replaced it with main's 775-line implementation); the
project-metadata tests follow. update_cmd re-exports the four old_updater_deps
names the shim tests resolve through hermes_cli.update_cmd.
2026-09-18 23:27:05 -04:00