Commit Graph

287 Commits

Author SHA1 Message Date
alt-glitch
1eb77a2154 fix(pm): the long-paths live test runs the real bundled-copy install (review)
test_long_paths_windows_live calls _install(copy_from=...) with facts.json inside each store instead of copying and publishing by hand.
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
alt-glitch
af60718b03 test(pm): live Windows proof for PM with LongPathsEnabled=0 (#130242)
Drives a real Store through scratch, the bundled-copy copytree, tree_digest,
publish and removal over a tree with files and a directory beyond MAX_PATH,
then checks the facts record and composed child env carry no \\?\ prefix.
The test first asserts RtlAreLongPathsEnabled() is false so it cannot pass
as a false proof. The wine2e lane sets LongPathsEnabled=0 in a separate,
final step and selects the test with -m 'platforms and integration'.
2026-10-07 06:40:39 +05:30
Teknium
911da42e9e refactor(security): remove the bundled tirith scanner from core
tirith ran `tirith check` on every terminal command, default-on with fail-open, as a
pinned PM binary auto-downloaded at startup. Measured against the shipped 0.4.2 binary
it caught 14/16 attack commands but flagged 12/24 everyday agent commands
(`echo $((6*7))`, `"$VAR/x.sh"`, `gh api | python3`, LAN curls, `.dev` hosts,
`pip install` via analysis_incomplete), and in cron/-q deny mode each of those is a
hard block. It only ever saw terminal strings (execute_code, writes and the browser
bypass it), fail-open meant no protection wherever the binary could not install
(Windows, Termux, x64 musl), and it produced a steady issue/PR stream (fail_open
semantics four times, install loops filling disks, a SIGSEGV hang, session-approval
scoping, third-party promo text on approval cards).

Removed: tools/tirith_security.py, the tirith branches of the command guard (the
is_tirith tuple, session-only "always" downgrade, import-error/fail-open paths,
unattended tirith pass), the CLI and gateway startup installers, the PM package and
lock pins, the nix runtime dep, security.tirith_* defaults, the CLI warning string
and three tirith tips (gaps refilled from the tail so translations survive), plus
the gateway's "no automated assessor" warning that only existed to point at tirith.

Config v50 drops security.tirith_* and enables nothing in its place. Hermes ships
no tirith plugin; if tirith upstream publishes one, it goes through normal catalog
admission under their name.
2026-10-06 14:22:13 -07:00
Teknium
3dadeb9246 fix(pm): cold updates on Python < 3.13 trust a stored intermediate CA
An install still on its legacy 3.11 venv runs its first `hermes update`
from that interpreter, which downloads uv and Python 3.14 with plain
urllib before PM's worker (and truststore) exists. Windows can cache Let's
Encrypt "Root YR" in its intermediate store without ISRG Root X1; GitHub's
asset CDN now chains through Root YR, so 3.11 rejected every download with
"unable to get issuer certificate" while PowerShell and 3.14 accepted it.

Python 3.13+ sets VERIFY_X509_PARTIAL_CHAIN by default. Set it on the PM
downloader's opener so the legacy interpreter applies the same rule;
hostname, expiry and pinned sha256 checks are unchanged.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>
2026-10-05 23:37:02 -07:00
Teknium
de06a61c51 fix(pm): record a partial pm update as failed in the pm receipt
Follow-up to the salvaged per-package pin isolation (#125400). Once a
failing pin no longer aborts the run, the surviving pins still run the
venv sync, whose receipt finalizes `ok` as latest.json. `hermes pm
status` and the desktop sync card read latest.json, so a run that left a
package on its old pin and exited 1 was recorded as a clean success.

Write a kind=update `failed` receipt after the sync, with one failed step
per pin, whenever any pin failed (also when every pin failed, which
previously wrote no receipt at all). Trim the tests to the two
invariants and assert the receipt in the mixed case.
2026-10-05 15:34:40 -07:00
Yuan Li
7c8d474617 test(pm): cover per-package pin isolation in _apply_pins
Three regression tests: a failing pin reports and does not block the
healthy ones (pinned row reaches the lockfile, broken row keeps its old
version), a run where every pin fails leaves the lockfile byte-identical,
and a clean run keeps exit 0 with no failure summary.
2026-10-05 15:34:40 -07:00
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
af90026aa0 fix(update): run the libatomic pre-install spawn only on Linux
The extra interpreter spawn ran on every platform before run_completion's
Popen. On Windows it went through subprocess.run first, which the taskkill
interrupt tests patch to assert only the completion child exists, so all
three failed. install_before_lock is a no-op off Linux anyway; skip the
spawn there.
2026-10-04 19:19:41 +05:30
kshitijk4poor
47e6c2ebb5 fix(update): keep the completion parent stdlib-only around the libatomic pre-install
run_completion runs in the OLD process after the checkout swap, and its
contract is never to import application helpers from the replacement tree.
Importing pm.libatomic there crashed the update when the new tree lacked
the module (downgrade or older pin) and on architectures where
current_target() raises. Run the pre-install as its own process from the
new tree, ignore its outcome, and let the in-lock repair report.

Also: gate on an openable /dev/tty first (cheapest check, and the detached
child exits immediately), treat an unsupported architecture as "skip",
bound the sudo -n probe, and drop the timeout on the interactive install
so a slow prompt or mirror is never killed mid-dpkg (it runs outside PM's
lock). Found in the final-head review of #132664.
2026-10-04 19:19:41 +05:30
kshitijk4poor
c4d4ac569f fix(pm): install libatomic before the detached update child, not a sudo ticket
hermes update's completion runs PM in a start_new_session child with no
controlling terminal, so the in-child sudo -v warm-up could never read a
password and the cold password-sudo update from #124926 still failed. A
cached ticket was also the wrong currency: under sudo's default per-tty
timestamps a ticket minted in one session is not visible to another.

Install the package itself, interactively, at the last point that still
owns the terminal: run_completion() before it detaches, plus the existing
ensure_tools_for_sync() entry for the other update paths. Gate on an
openable /dev/tty instead of isatty() (a detached child inherits a tty
stdin and a forwarded-isatty stdout). Inside PM's lock the repair is
unchanged: sudo -n, stdin closed. Reported by @JoaoMarcos44 on #132664.
2026-10-04 19:19:41 +05:30
kshitijk4poor
847acaf1fb fix(install): never prompt for sudo under --non-interactive
The libatomic sudo warm-up checked only for a terminal, so
--non-interactive, -NonInteractive and --skip-setup still reached
`sudo -v </dev/tty` inside an SSH session or any PTY-allocating runner.
Gate it on NON_INTERACTIVE like setup and gateway. Reported by
@JoaoMarcos44 on #132664.
2026-10-04 19:19:41 +05:30
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
kshitijk4poor
776dcba637 test(pm): keep the two libatomic invariant tests
Install-time repair re-probes after installing the host package, and the
automatic attempt never reads a TTY (sudo -n, stdin closed) while PM holds
its install lock. The remaining cases pinned helper internals; one also
failed whenever the pytest tmp path contained the word libatomic.
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
emozilla
5059f20e1b Merge remote-tracking branch 'origin/main' into ethie/llamacpp-0.5.0-linux-cuda-main
# Conflicts:
#	hermes_cli/local_runtime/bootstrap.py
#	hermes_cli/main.py
2026-10-03 22:53:47 -04:00
emozilla
361963828d feat(pm): llama.cpp CUDA on Linux arm64; bundle libgomp for Linux engines
b11146 ships ubuntu-cuda-13.4-arm64 (+ cudart), so llamacpp-cuda gains
linux-arm64, auto resolves NVIDIA arm64 Linux to CUDA, and the desktop's
localModels flag ships on Linux arm64 too.

Every upstream Linux llama.cpp build (CPU included) links the system
libgomp, which minimal hosts (WSL, containers) lack, and a normal install
never touches the system package manager. Each Linux engine now pins
Ubuntu 24.04's libgomp1 .deb (glibc 2.38, the floor upstream already sets)
and stages libgomp.so.1 beside llama-server, where the $ORIGIN RUNPATH
finds it; the artifact mirror covers the pool URL after 24.04's EOL.
DebPackage's ar+tar unpack becomes pm.package.unpack_deb and reads zstd
data.tar (Ubuntu's default) with the stdlib codec PM's 3.14 runtime has.

Pre-PM engines on Linux carry only llama.cpp's own archives, so adoption
maps their manifests to those and records them as outdated (usable now,
update offered) instead of failing to match.

Verified on an RTX Spark N1X (WSL, Ubuntu 26.04, no system libgomp):
ensure_engine() for cuda, cpu and vulkan through PM's real download/stage/
verify path; ldd resolves libgomp from the entry; llama-server serves
completions (CUDA0 at 445 tok/s). linux-x64 cpu/vulkan staged through the
same path with no NEEDED library missing beyond glibc/libstdc++/OpenSSL 3.
2026-10-03 22:32:13 -04:00
teknium1
eb7e862032 fix(pm): confine the read-only retry to Windows unlink/rmdir failures
Review of #131847: the retry ran for every rmtree failure on every OS.
On POSIX an unreadable directory hands the callback os.open, and
func(path) raised TypeError (missing flags), which escaped the OSError
set-aside path and failed an install that had already published. And
os.chmod followed a symlink inside an unwritable directory, rewriting
the mode of a file outside the tree to 0200.

The retry now only fires for a PermissionError from os.unlink/os.rmdir
on Windows, and clears the link's own attribute where chmod can follow
links; everything else raises as plain rmtree does.
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
teknium1
0dacc67e5a test(pm): pin held-tree cleanup with two invariants and a live Windows hold
Folds the four salvaged regression tests into two invariants (publish arm,
restore arm with gc selectivity) and adds a windows-only test that maps a
real DLL from inside the live entry and reinstalls over it.
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
dalebetz
ea93303f47 fix(windows): pin vcpkg to the Visual Studio that has the arm64 toolset
scripts/windows-build-deps.ps1 discovers the ARM64-capable Visual Studio
(vswhere -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64) and
initializes VsDevCmd against it, then builds OpenSSL through vcpkg. But
vcpkg resolves Visual Studio on its own and takes the newest instance. On a
host with a second BuildTools install that lacks the native arm64 toolset,
the arm64-windows triplet fails:

  error: in triplet arm64-windows: Unable to find a valid toolchain for
  requested target architecture arm64.
  The selected Visual Studio instance is at: ...\Microsoft Visual Studio\18\BuildTools
  The available toolchain combinations are: x86, amd64, x86_amd64,
  amd64_x86, arm64_x86, arm64_amd64

...in the same run that logs "-> ARM64 C++ build tools found: ...\2022\BuildTools".
The install fails, `hermes update` exits 1, the retry fails identically, and
the desktop relaunches on the previous build ("running with the previous
dependencies — run `hermes update` to finish it"), so it never updates.

Export the discovered instance as VCPKG_VISUAL_STUDIO_PATH, the documented
lever vcpkg's own error points at, without clobbering a caller's explicit pin
(same respect-if-set shape as CARGO_HOME/RUSTUP_HOME above).

Extends the initializer fixture: the discovered instance must be pinned when
unset, and a caller pin must survive the call. Needs a Windows runner — the
suite is platforms("windows") and the initializer refuses non-Windows hosts.
2026-10-02 19:37:27 -04:00
ethernet
d6bb42e8ba feat(activate): add activate.fish for the fish shell
`activate` is bash syntax, so fish users had no way to take on the dev
environment. `activate.fish` does what `activate` does: snapshots the keys it
will change, applies the composed env, defines a `hermes` function that
refuses outside the worktree, adds the prompt prefix, and defines
`deactivate`. It shells out to the same bash library, so composition is not
reimplemented.

`pm.environments --format fish` emits the env as `set -gx` statements.
fish's read-only names (`PWD`, `SHLVL`, `_`, ...) are skipped because
`set -gx PWD` aborts the script, and values named `*PATH` stay lists.
`hermes_compose_env` takes the dialect (`sh` or `fish`) as its second argument.
2026-10-02 18:43:30 -04:00
ethernet
4ed093cb6b test(pm): copy the activation library into the real-setup activation fixture
`activate` now sources scripts/_activation.sh, and this fixture assembles its
own checkout from a fixed file list that did not include it, so `activate`
reported "setup failed" before running setup.
2026-10-02 18:15:08 -04:00
ethernet
b4d5092d3f refactor(activate): split interactive activation from running a command in the env
`activate` doubled as the thing scripts sourced non-interactively, so it
carried `--` argument guards and every caller had to tolerate its prompt and
`hermes` function. Composition (setup sync, bootstrap Python, pm env) now
lives in scripts/_activation.sh, and there are two consumers:

- `activate`: interactive, reversible (`deactivate`).
- new `scripts/run-in-hermes-env CMD...`: applies the env to one process and
  execs into it. Replaces `scripts/_hermes-python`; every Python script's
  shebang and `scripts/run_tests.sh` (which re-executes itself under it,
  however it was launched) use it.

`pm.environments --format sh` emits the env as shell exports, so the inline
JSON-to-export Python is gone from `activate`. An install that did not build
the test environment now reads as stale for every consumer.

Tests are split along the same seams: activation support fixtures, bash/ps1
activation, env-script quoting, and the runner's staleness.
2026-10-02 18:15:08 -04: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
Brooklyn Nicholson
8b4b440ef3 fix(desktop,pm): stop the boot parking on a failed update receipt and bound completion retries (#122206)
Three gaps stranded the app through a legitimately long or failed
source-update completion:

- update-gate: a fourth gate signal 'failed-receipt' (hasFailedReceipt on
  UpdateGateDeps, optional) reports a FINISHED, failed update even when a
  stale marker survives; waitForUpdateClearance gains abandonOn so the
  boot path stops parking the full 20-minute budget on a receipt that
  already says failed and boots the current build instead.
- backend-ready: while the backend prints venv_sync's source-completion
  banners the 90s port-announce deadline is re-armed (5-min grace,
  30-min total cap, real wall clock) so a healthy multi-minute repair is
  not killed and re-run on every boot.
- venv_sync: relaunch-driven completion-tail retries are bounded — an
  attempts record beside the pending marker keeps the first retry
  immediate (the historical flaky-tail case), backs off after 2
  consecutive failures (15-min window), and caps at 6 attempts, past
  which the launch keeps the marker, skips the tail, and points at
  'hermes update'; success clears both the marker and the record.
2026-09-29 20:57:51 -05:00
Siddharth Balyan
45079e6330 fix(security): the first tirith scan no longer waits on its download (#128150)
* fix(security): the first tirith scan no longer waits on its download

A process that never ran the CLI startup install (desktop serve, the
gateway, a newly routed profile) claimed the install on its first scan and
ran pm.ensure("tirith") inline. The first terminal call then waited on the
PM worker, the runtime toolchain and the tirith download before it ran.

The cold scan now starts the same background install as startup and scans
with the default path, so the call fails open at once like any scan during
a startup download.

* fix(security): a scan during the tirith download never trips the breaker

The previous commit let a cold scan run while the background install was
still downloading. That scan spawned the bare default "tirith", got
FileNotFoundError and counted it as a crash. Three terminal calls during
the download opened the circuit breaker, and every scan then returned
allow unscanned for 300 s, even after the download finished. With
tirith_fail_open: false the first three commands were blocked and the
breaker then let commands run unscanned.

A scan that finds the default binary missing while this home's install
thread is alive no longer spawns or counts a crash. Fail-open allows the
command at once ("tirith installing"). Fail-closed waits for the install,
as the inline install did before, and then scans with the real binary.
So only the fail-open case skips the scan during a download; the earlier
commit's "fails open at once" holds only for fail_open.

The PM consumer test now joins the background install before it reads
the recorded download requests.
2026-09-29 17:29:05 +00:00
kshitijk4poor
d57cf08f23 test(pm): the LSP node-runtime test opts into a trusted workspace
It checks PATH inheritance and env overrides for typescript-language-server.
With deny-by-default trust, a ServerContext built without a decision is
untrusted and the server skips without a Hermes-side TypeScript SDK.
2026-09-29 03:28:46 +05:30
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
JoaoMarcos44
0b105e0714 fix(pm): report unwritable install state once 2026-09-28 06:42:00 -07:00
Yuan Li
80e834b129 fix(pm): preserve transport knobs under no_config isolation, strip only index redirects
The isolation filter now drops only settings that redirect WHERE packages
resolve from (index URLs, find-links, strategy, index credentials); transport
knobs (UV_NATIVE_TLS / UV_INSECURE_HOST / UV_HTTP_TIMEOUT) survive — corporate
networks need them to reach the pinned wheel URLs at all.

Addresses the review note on the no_config scope in the #125071 triage.
2026-09-28 06:42:00 -07:00
Yuan Li
32aeb9ef7b fix(pm): honor no_config isolation for bridged index settings in uv runs
stage_runtime (the pinned pm/ self-repair builder) passes no_config=True,
but that only appended uv's --no-config, which removes config FILES. The
pip-mirror bridge (pm.index_config) reaches uv as env vars instead, so a
user's pip.conf index-url still rode into the runtime sync via
runtime_environment() -> _base_environment(env=None). Re-resolving the
official-index pm/uv.lock against a mirror changes the resolution, and
uv sync --locked refuses — breaking the top-level update / doctor /
pm-repair recovery paths on any machine with a pip mirror (#124418).

When no_config is set, strip forwarded index/transport knobs from the
child env: a fully-pinned graph never needs a mirror to resolve, and the
default path keeps the bridge so mirrored/air-gapped application installs
still work.
2026-09-28 06:42:00 -07:00
teknium1
dd3ba4b4b8 fix(pm): close the lease create→flock race on the writer side, no grace window
A time-based grace kept a just-abandoned lease 'held', so back-to-back `hermes pm repair` runs
never collected the generation the previous run execv'd out of (e2e test_generation_gc red).
Rename+replace is out (msvcrt byte locks refuse renames on Windows), so the writer now re-checks
its lease path after taking the flock and retakes when a peer's prune unlinked it in between; the
pruner only unlinks while holding the lock, so a visible path after the lock is always ours.
2026-09-28 04:44:29 -07:00
teknium1
6da0d5a887 fix(review): fail closed on unopenable or just-created runtime leases
Review majors on #126179 (runtime_state._prune_unlocked_leases):

1. The boot-time prune opened every sibling lease with O_RDWR and caught
   only FileNotFoundError, so one lease this user cannot open (a root-owned
   0o600 file from `sudo hermes` on the same checkout, the #125525 class)
   raised PermissionError inside activate_dependencies and hermes_bootstrap
   exited 1 with "run hermes pm repair" on every launch. Any other OSError
   now counts the lease as HELD: GC stays fail-closed, boot never aborts.

2. lease_directory creates (O_CREAT|O_EXCL) and flocks in two syscalls, and
   pm/worker.py, pm/launch.py and pm/environments.py call it outside
   runtime_lock, so a peer's prune in that window could take the
   non-blocking lock and unlink the file; the first process then held a
   lock on an unlinked inode and its pin was invisible for its lifetime
   (collect_generations could rmtree a running generation). A lease younger
   than LEASE_GRACE_SECONDS (60 s) is never a prune candidate and counts as
   held. Chosen over a temp-name + os.replace scheme because it is three
   lines, keeps a crash-between-create-and-flock leak collectable, and a
   60 s margin dwarfs a microsecond window.

test_next_reader_removes_lease_left_by_hard_exit ages the hard-exited
lease past the grace window before asserting the next reader removes it;
its invariant is unchanged.
2026-09-28 04:44:29 -07:00
chelsealong
42941c0a37 fix(pm): put the checkout launcher ahead of the venv's own hermes script (#124627, salvage #124828)
The runtime venv installs the core project editable against the PM
build snapshot (workspace/), so venv/bin/hermes always imports that
frozen copy. PM only rebuilds the venv on lock/extras/python-pin/plugin
changes, so a source-only update leaves the copy stale while the
gateway keeps running the checkout via activate_dependencies's
sys.path override. Any child that resolves `hermes` off PATH (terminal
tool, cron, kanban workers) still finds venv/bin first and runs the
stale snapshot instead — including `gateway install`/`restart`, which
then pins launchd/systemd to the snapshot's launcher path.

Prepend the checkout's own .hermes/bin ahead of venv/bin in
activate_dependencies so `hermes`/`hermes-acp` always resolve to the
launcher that puts the checkout first, regardless of what else lives
in the venv. This also fixes tools/environments/local.py's
_resolve_hermes_bin_dir(), whose shutil.which("hermes") now finds the
launcher dir instead of the venv's console script, so no separate guard
on PROJECT_ROOT in generate_launchd_plist / systemd unit generation is
needed: the snapshot's hermes_cli is never the running one.

Fixes #124627
2026-09-28 04:44:29 -07:00
Ahmett101
b23d4f4b32 fix(pm): collect abandoned runtime leases (#125609, salvage #125632)
lease_directory releases its per-invocation lease file only through atexit,
which os.execv (hermes_bootstrap's re-exec into the managed environment) and
os._exit (gateway shutdown_watchdog._hard_exit, hermes_cli/main.py past
finalization) never run: a 4s supervisor restart loop left ~860 lease files an
hour in the SELECTED generation, which generation GC never visits.

The kernel lock dies with the process even when atexit does not, so the file
itself is the only leak: every reader (the next lease_directory and
leases_held) now unlinks lease files nobody holds a lock on. Both run under
runtime_lock at boot / in the collector, so a lease between O_CREAT|O_EXCL and
its lock is never pruned from under a live process.
2026-09-28 04:44:29 -07:00
teknium1
a31e618e70 fix(pm): download npm-hosted tools through the user's npm registry
pm/lock.json and the Npm/AgentBrowser templates record registry.npmjs.org, and
pinned_source handed that URL straight to the downloader, so ~/.npmrc /
npm_config_registry were ignored and closed networks could not provision npm or
agent-browser. pinned_source now routes public-npm URLs through the configured
registry (npm's own precedence: npm_config_registry, then the user npmrc); the
lock's SHA256 still verifies the bytes and progress keeps the lockfile URL.
npm_dist_tags reads from the same registry.

The E2E cell test_npm_registry_mirror_serves_pm_npm_download drops its gate.

Co-authored-by: funky-xamarin <30426178+Wenfengcheng@users.noreply.github.com>
2026-09-28 02:00:41 -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
teknium1
c274b9e9c9 test: Windows PATH .cmd preference applies to non-PM tools only 2026-09-27 22:04:26 -07:00
teknium1
b63c138d78 fix: Hermes never falls back to the user's node/npm/npx/uv
Maintainer ruling: only Hermes and only its packaged package managers
(uv/pip/node/npm) are ever used; no PATH fallback when the managed tool is
missing, no "prefer the user's if new enough".

- hermes_constants.find_node_executable: node/npm/npx resolve to PM's
  installed copy or None. Every caller already pm.ensure()s on None, so a
  missing runtime is now provisioned instead of silently borrowing the
  user's Node (native-addon ABI / npm cache mismatches).
- agent/lsp/install._install_npm: pm.ensure('npm') when PM npm is absent,
  instead of failing over to whatever npm is on PATH.
- gateway._append_node_dir_for_service: stop baking the invoker's PATH node
  dir into generated systemd/launchd units.
- main_install_repair._resolve_node_runtime_npm: drop the PATH re-scan for
  another npm.
- source_build.source_product_current: run the freshness reader only with
  PM's node.
- doctor: Node/npm rows and npm audit use PM's copies (Termux APT distro
  keeps its system Node).
- install.sh ensure_uv / install.ps1 Get-Uv: always stage the pinned uv
  artifact; delete the "uv on PATH if new enough" developer shortcut.
2026-09-27 22:04:26 -07:00
teknium1
27062c3474 fix: install cua-driver and the Browser Use CLI by default again
Computer use and browser use are meant to work out of the box. The PM rewrite
(3d12e86ef1) and the MSIX installer rework (47f4ab3a17) dropped the
install-time cua-driver fetch (7060ac7bed) and the Browser Use CLI install
(baa6b2e34d). On a fresh install the computer_use check_fn therefore stayed
False, so the tool never reached the model and its lazy ensure could not fire,
and browser_exec quietly fell back to the built-in tools.

- cua-driver is a default PM package, so the installers, a bare
  `hermes pm install` and `hermes update` carry it on every target it builds
  for. Adds the missing Android gap (the lock has no bionic artifact).
- The shared default-tool step, which the installers (via source completion)
  and `hermes update` both run, provisions the Browser Use CLI for the default
  and explicit Browser Use backends. `--skip-browser` declines it along with
  agent-browser, and `off`/Camofox never use it.
- Installers regain --skip-computer-use / -SkipComputerUse (recorded as
  `--without cua-driver`).
- The update message stops calling every default "browser tools".
2026-09-27 19:18:58 -07:00
kshitijk4poor
084645fba4 fix(pm): PortableGit unpack raises InstallError and never pipes the extractor
- Refusal, non-zero exit and timeout now raise InstallError (pm/client.py
  catches only InstallError; TimeoutExpired escaped entirely). The off-Windows
  refusal names a real remedy instead of the generic 'retry'.
- stdin/stdout/stderr -> DEVNULL: with capture_output the stub's RunProgram
  children inherit the pipes and hold run() open past the stub's exit (and
  CPython re-communicates without a timeout after a kill). The stub prints
  nothing under -y, so the output tail was dead code.
- TemporaryDirectory(ignore_cleanup_errors) as in Npm.unpack; unpack empties
  its destination like extract(); drop redundant local imports.
- Tests: CompletedProcess over hand-rolled stubs, no raising=False, timeout
  case added, change-detector pin-suffix test removed.
2026-09-28 02:04:08 +05:30
finn763
7a40c28750 fix(pm): execute the pinned git SFX from a scratch copy, not the cache
CI run 36189416163 failed after "git: unpacking": executing the cached
fetch-<sha> PortableGit PE in place left it handle-held (Defender
on-execute scan / the stub's RunProgram child chain) past pm's ~2 s
_remove_entry retry, so download cleanup raised WinError 32. Review
(teknium1, 5 threads) adds the rest:

- Git.unpack now copies the artifact into a .sfx-* dir beside the
  staging tree and executes the copy; pm's .staging-* teardown
  (ignore_errors) owns that path, so any hold lands on a disposable
  path and the cache dir only ever holds read handles
- refuse off-Windows with the cross-host trade-off stated instead of a
  raw PermissionError; documented in package-management.md and the PR
- the extractor is a GUI-subsystem stub that is silent under -y: error
  messages now carry the exit code and the usual causes (disk full,
  path length, antivirus) instead of promising captured output; the
  docstring no longer claims "no GUI" and records that the stub shows
  an Extracting window and runs the vendor post-install
- install.ps1: WaitForExit(600000) + Kill() mirrors pm's timeout=600,
  Fail reports the exit code and the silence
- tests: the OS-refused-exec assertion and the not-in-text change
  detectors are replaced by subprocess-argv invariants (scratch copy
  location, exit-code message, off-Windows guard before any execution)

Related to #122512

(cherry picked from commit adaf76a286bebe30c89aee4174df27f1945b60c9)
2026-09-28 02:04:08 +05:30
finn763
6d9ae2da6f fix(install): stage pinned git without tar/bzip2 (PortableGit SFX)
Windows 10 boxes whose System32 tar.exe cannot run the bzip2 filter die
at stage=prerequisites with "unable to run program bzip2 -d" while
extracting the pinned Git-2.53.0.3 tar.bz2 (#122512). Repin git for
both win32 targets to git-for-windows' PortableGit self-extracting 7z,
which carries its own extractor and the bundled usr/bin/bash.exe:

- pm/lock.json: new artifact urls + sha256 (the pin authority)
- scripts/install.ps1: generated fragment regenerated; Get-PinnedGit
  downloads the SFX and waits on it explicitly (the stub is a
  GUI-subsystem exe, so PowerShell's & does not wait); the System32
  tar invocation and its msys symlink excludes go away
- pm/packages.py: Git.fetch_url/Git.unpack run the self-extractor
  after the sha256-verified download
- pm/store.py: drop the now-callerless git_msys branch of extract_tar
- tests: RED->GREEN test runs the real Get-PinnedGit against a
  bzip2-less System32 tar.exe stub with no bzip2 on PATH; the three
  obsolete tar-contract tests and the install.ps1/PM msys-links parity
  test are replaced by a no-external-decompressor contract test

Closes #122512

(cherry picked from commit 4915304213495d3207ec6cd659e57cd16ef08d60)
2026-09-28 02:04:08 +05:30
kshitijk4poor
7546680ff8 fix(pm): pm update pins only BtbN month-end ffmpeg builds
BtbN deletes daily autobuild tags after 14 days and keeps the last build
of each month for two years (util/prunetags.sh). btbn_index took the
newest daily, so every `pm update ffmpeg` wrote a pin that 404s two weeks
later (#125350, #122240). It now skips the current month's tags (still
dailies the next build displaces) and every tag but the last in each
finished month, so the tool only ever pins a retained build.
2026-09-28 00:51:32 +05:30
teknium1
5cbca02510 test(pm): keep FFmpeg's musl gaps when the re-pin test narrows its gap table
The test replaced FFmpeg's gaps with only the bionic entry, which now
turns the musl targets back on and asks BtbN for a glibc build that
FFmpeg.fetch_url refuses on musl. Extend the real table instead.
2026-09-27 03:24:39 -07:00
teknium1
f5c3c7d70a test: trim musl coverage to two invariants
Keep one PM invariant (a musl ELF userland resolves to linux-<arch>-musl
and the default closure is satisfiable with musl-native artifacts) and
one installer invariant (uv_bootstrap_target picks musl uv from a musl
ELF interpreter even when ldd says glibc). Drop the lock-content and
URL-shape change-detectors.
2026-09-27 03:24:39 -07:00
JoaoMarcos44
24487f3db6 fix(pm): keep musl installs on compatible runtime artifacts
Skip glibc-linked FFmpeg on musl in both default install and update roots.
Select musl uv during the standalone shell bootstrap, and let the native
userland resolve libc before bootstrap Python build metadata.

Add regression checks for the closure, target precedence, and installer pins.
2026-09-27 03:24:39 -07:00
JoaoMarcos44
8371dd17f6 fix(pm): select native musl artifacts on Linux 2026-09-27 03:24:39 -07:00