Commit Graph

185 Commits

Author SHA1 Message Date
ethernet
d0c91f47f1 fix: round-10 — PowerShell probe via -File; enable-grant test follows #64228; stage-hold fixture requests access
- test_source_build_env: Windows PowerShell 5.1 runs a multi-line -Command argument only up to
  the first line break and exits 0; the probe is a -File script now (the child never ran, hence
  the empty stamp).
- test_plugins_cmd_enable_disable_nested: `enable` no longer prompts for or writes an undeclared
  allow_tool_override grant (09bcf17801); the test asserts that and uses --no-allow-tool-override
  for the persisted False.
- test_stage_only: the directory hold opens with FILE_LIST_DIRECTORY, not access 0 — a
  zero-access handle does not oppose the rename the repin performs.
2026-09-21 07:31:22 -04:00
ethernet
9ee4397408 fix: round-9 Windows lane — retry argv keeps --force; mint fixture carries venv_sync; stamp probe reports the child
- desktop-update/windows.ps1: the legacy-install retry re-sends the identical request
  (--force included); the contract test compares both attempts.
- test_mint_launchers: the bootstrap imports hermes_cli.venv_sync/steward before
  prepare_launch can return early for a fixture repo; copy them into the tree.
- test_source_build_env: when the pwsh child writes no stamp, fail with the child's
  stdout/stderr instead of a bare FileNotFoundError (the Windows lane hides the cause).
2026-09-21 06:48:59 -04:00
ethernet
f7561667c7 fix: round-8 CI backlog (Windows-only lane)
- tests/conftest.py: `real_bash` fixture — the Windows runners resolve `bash` to System32's
  WSL launcher (UTF-16 "no installed distributions", exit 1); prefer Git for Windows'. Used
  by the setup-pin, install stage-frame and source-launcher shell tests.
- source-build-env.ps1: Test-Path before Remove-Item — under $ErrorActionPreference='Stop'
  a missing identity variable aborted the try block before the child ran (Windows PS 5.1
  raised where pwsh on Unix did not).
- desktop-update/windows.ps1: `--force` precedes the target arguments (the hand-off contract
  test reads argv in that order).
- test_install_ps1_desktop_stage: assert the current contract — the shared completion tail
  (source_completion.py --desktop) builds the products and -IncludeDesktop selects the desktop
  product inside `products` rather than adding a stage. The test predated the completion-tail
  refactor and had been red on the Windows lane since.
- test_windows_native_support: the restart watcher argv is `runtime_command` shaped
  ([python, -I, -c, bootstrap, pid, delay, ...]).
- test_mint_launchers: create the fixture repo's pm/ dir before copying pm/environments.py.
- test_browser_use_pm: console-script launchers report sys.argv[0] without `.exe`.
- test_update_stale_gateway_yield (from main): `_verify_fleet_after_update` has no
  `node_failures` here (PM owns node).
2026-09-21 06:03:05 -04:00
ethernet
1d602a04e3 fix: round-6 CI backlog (32 python, docker arm64, win32-arm64, windows lane, desktop lint)
- pm.environment: the `--no-install-package <name>` root read parses [project].name without
  tomllib on the pre-3.11 bootstrap python (Docker arm64 stage_runtime). Both parsers proven
  to agree on the real pyproject.
- windows-build-deps.ps1 / run_tests.sh: with DISTUTILS_USE_SDK, setuptools takes link.exe
  from PATH; under a bash-hosted step Git for Windows' coreutils `link` shadowed MSVC's.
  The MSVC linker directory now leads PATH (cl.exe was already found — this was the next
  failure in the ruamel-yaml-clib build).
- tests/install/e2e-assets/source-build-env.ps1: clear the identity variables through the
  env: drive — [Environment]::SetEnvironmentVariable(..., 'Process') on .NET/Unix does not
  reach spawned children, so the stamp child still saw GITHUB_SHA. Red→green under nix pwsh.
- old-updater surface: warm_agent_browser_npx_cache is a permanent def in both facade and
  sibling (the frozen surface names both); its compat pointer is retired, and the shims test's
  __module__ check holds.
- tests re-seamed / de-faked: import guard tests use the probe_root fixture (CI has no
  editable finder), the takeover child tree gets a hermes_constants stub, posix.sh hand-off
  test pins HERMES_HOME (our script honours the ambient one), the two Windows-layout
  PYTHONPATH tests are platforms("windows") (they fake `Lib/site-packages` on Linux; pm's
  site_packages() is host-correct), setup-pin test picks Git bash over System32's WSL stub,
  expose_cli's Windows test asserts the installer-convention convergence (the branch retired
  "windows-installer-owned"), plugin-manifest satisfied-dep fixture uses a core dep (pyyaml is
  gone), housekeeping test yaml imports go through hermes_yaml.
- apps/desktop main.ts: three imports restored in round 4 whose users main removed.
2026-09-21 03:44:00 -04:00
ethernet
f3b1399211 fix: round-5 CI backlog after the 779-commit main merge
Merge fallout (my resolution errors, all caught by CI):
- hermes_cli/backup.py + gateway.py: `theirs` on those hunks re-imported clusters HEAD had
  already moved to backup_restore.py / kept in the facade. backup.py loses the 349-line
  duplicate (main's #110179 fix is ported into backup_restore._import_db_member); the
  systemd service-unit cluster returns to gateway.py (PM's _prepare_service_launcher /
  _pm_managed_node_dirs / _systemd_command have no home in main's extraction) with main's
  utf-8-sig read. gateway_service_unit.py is dropped.
- gateway/run.py: main's plugin-update chore is not profile-scoped (the housekeeping
  ordering test pins the scope/drain sequence).
- pyproject + 30 test files: `import yaml` -> `import hermes_yaml as yaml` (pm-clean has no
  pyyaml); gateway/config._bundled_platform_manifest_name reads through hermes_yaml.
- tests re-seamed onto pm-clean's shape: residency admission (installed_engine),
  supervisor child env (binary is a constructor argument), update import guard
  (update_cmd_deps is gone; our probe already scrubs PYTHONPATH — both #115032 invariants
  pass), shallow-count git responses (stash path asks `status --porcelain -z`); dropped
  tests for retired code (_run_node_bootstrap/_ensure_tui_node, Windows resume demotion).
- tests/tools/test_local_env_blocklist.py: restore the two helpers the suite-reduction
  commit dropped and the blocklist import.

Real fixes:
- pm: classify_uv_failure/ResolutionConflict move beside the uv runner (pm.environment,
  stdlib-only). pm.workspace imports tomllib at module level and cannot load on the 3.10
  bootstrap python that streams uv output in the Docker arm64 image.
- tools/browser_tool.warm_agent_browser_npx_cache: back as a permanent definition — it is on
  the frozen old-updater surface, and the revert-scheduled compat pointer does not count.
- hermes_cli/memory_setup: the dashboard's pip row uses pm.environments.
  running_from_selected_environment for installed vs restart_required.
- scripts/windows-build-deps.ps1: export DISTUTILS_USE_SDK/MSSdk so setuptools trusts the
  primed MSVC environment instead of asking vswhere (`env -i` test runner on win32-arm64
  compiling ruamel-yaml-clib); run_tests.sh forwards them.
- tests/pm/test_windows_build_deps.py: start the protocol test from a parent env without the
  toolchain variables the runner job already exports.
- tests/conftest.py scrubs HERMES_BUNDLED_PLUGINS (Nix-wrapped hermes on the dev host);
  tests/home_io_guard.py treats sys.path site-packages under the real home as the
  interpreter's installation (PM-activated developer shell).
- tests-js: four `curly` lint errors from main's new scripts.
2026-09-21 02:47:48 -04:00
ethernet
f130c79b9a ci: bash shebangs in two tests from main; regenerate the google-workspace skill doc 2026-09-21 01:13:59 -04:00
ethernet
9f2ba1b74d merge origin/main (779 commits) into ethie/pm-clean
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).

Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.

uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
2026-09-21 00:58:39 -04:00
teknium1
10c273813d feat(plugin-catalog): screenshots and readme entry fields for the plugin pages
Two optional, submitter-controlled fields on a catalog entry feed the
entry's own page at /docs/plugins/<name>:

- `screenshots:` — up to 6 https URLs on GitHub hosts (same host rule as
  `image`, so the site never fetches from third-party hosts and a raw URL
  pinned to the sha is as immutable as the code).
- `readme: true` — the docs build renders the README from the PINNED
  commit (raw.githubusercontent.com / gitlab.com raw at <sha>), never live
  content, so what a user reads is what the reviewer read.

Validator rejects malformed values (admission), the loader parses and
drops off-host screenshots with a warning (client), and the extractor
emits `screenshots`, `readme`, `readmeUrl` and a `maintainerSlug` for the
author pages. Tests on all three.
2026-09-20 20:43:13 -07:00
liuhao1024
04845f5f3e fix(desktop): skip the local gateway restart on update when the Desktop is remote-served
A Desktop whose active connection is remote (SSH/remote/cloud, including the
registry primary) owns no local messaging gateway, yet the update hand-off
always ran `hermes update --gateway`. On hosts where launchd/service recovery
fails, the updater falls back to a detached local `gateway run --replace`;
with the same Telegram bot token as the remote VPS gateway, the two processes
compete for getUpdates and Telegram rejects one consumer, taking the
production bot offline (#117529).

Pass the ownership down the hand-off: globalRemoteActive() now adds
--no-gateway (posix) / -NoGateway (windows) when the Desktop is remote-served,
and both orchestrators drop --gateway from every update invocation (initial +
retry). The local-ownership default keeps --gateway exactly as before.
2026-09-20 19:30:16 -07:00
fangliquan
6d92f105fe test(install): preserve bash path in probe regression 2026-09-20 15:22:22 -07:00
fangliquan
075158134c test(install): run dependency probe regression on Linux 2026-09-20 15:22:22 -07:00
fangliquan
8c5a5deb79 fix(install): probe the command link directory for dependencies 2026-09-20 15:22:22 -07:00
teknium1
11975e61db test: trim --files-from coverage to two invariants
Keep the two tests that pin the user-facing contract (a file-backed
list bypasses discovery; `-` reads stdin). The mutual-exclusion and
unreadable-path error tests exercised argparse plumbing already covered
by the runner's flag-routing contracts and pushed the suite past the
salvage bar of two invariant tests per fix.
2026-09-20 10:51:40 -07:00
liuhao1024
8828e356f7 fix(tests): let run_tests_parallel take the explicit file list from a file
--files carries the whole list as one argv element, and Linux caps a
single argument at MAX_ARG_STRLEN (128 KiB) - a much smaller limit than
ARG_MAX. The whole-suite list (~210 KB) dies with E2BIG in execve before
the runner's first line runs, so 'run the whole suite except one file'
cannot be expressed through --files at all.

Add --files-from PATH (or '-' for stdin), one path per line, mutually
exclusive with --files. A bare '-' after --files-from is normalized to
the '='-joined form because argparse treats '-' as a positional.
2026-09-20 10:51:40 -07:00
ethernet
f99d780006 test: finish the CI python-tests backlog against a CI-shaped environment
Verified with a build_environment test venv (no editable finder, no committed
PM selection) plus real uv on PATH — the shape CI runs.

- test_old_updater_takeover: the fake NEW checkout carries update_handoff.py;
  the test never relied on the source being importable from an editable venv.
- test_update_launch_completion: record the completion-tail child instead of
  running the checkout's (nonexistent) source_completion.py.
- test_update_missing_configured_deps: source_launch already stubs
  hermes_cli/; link the remaining modules beside the stub.
- test_worker: the injected runtime_lock stub accepts the timeout kwarg the
  worker now passes (it raised TypeError before reaching the lock).
- test_plugin_guard / test_plugin_install_ref: PM publishes plugins only under
  the active home's plugins/ and does so in the worker — install into the
  sandboxed home, and fail the metadata write inside the worker.
- test_pm_build_consumers: setup_toolchain deliberately puts uv on PATH after
  the interpreter (c8bbac5c6e); assert that ordering instead of its absence.
- test_icon_flavors: tile_color reads the most chromatic lower-half pixel (the
  inward contrasting border and LANCZOS smear defeat a fixed coordinate); the
  SHA glyph readback skips cells the portrait covers (da9f6d2dcd renders her in
  front of the badge) and requires a majority of each glyph.
- test_desktop_update_target: the fixture CLI reports the checkout as its
  install directory again (parents[1] inside the string was bumped with the
  file move in cb7c688171).
- test_termux_python_linkage: skip when the host has no shared libpython or no
  patchelf (relocatable python-build-standalone in CI).
- test_source_build_env: smoke-env.mjs is dependency-free (main removed the
  Playwright entry it imported through); pm/_fixtures.stage_host_python copies
  the stdlib beside the interpreter for payload tests.

test_api_server_runs::test_events_stream_forwards_interim_commentary is an
upstream flake (identical file on origin/main fails 2/3 locally).
2026-09-20 12:40:38 -04:00
ethernet
925c08ceca fix: CI python-tests backlog — no import-time dependency syncs, CI-shaped test fixtures
Production:
- agent/bedrock_adapter.py, agent/vertex_adapter.py: pm.ensure_import ran at
  module import. In any process that imports these modules without a committed
  PM selection (CI's build_environment test venv, a fresh checkout) that sync
  rebuilt the dependency environment mid-process and replaced sys.path with a
  generation missing the caller's own packages (anthropic, aiohttp vanished).
  The extra is now ensured at first client build / credential request.
- plugins/platforms/matrix/adapter.py: a complete install needs no
  ensure_and_bind round trip; only a partial one syncs.
- tools/browser_tool.py: drop the facade's duplicate warm_agent_browser_npx_cache
  shim; the compat pointer already resolves to browser_tool_install.

Test harness:
- tests/home_io_guard.py: PATH-entry probes (shutil.which) and the running
  interpreter's own installation (stdlib reads, realpath ancestry, fixture
  symlinks into it) are not Hermes state; a patched Path.expanduser must not
  crash the guard. run_tests.sh no longer filters PATH — the guard owns it.
- tests/tui_gateway/conftest.py: import hermes_bootstrap before any file opens
  a MagicMock hermes_constants window (6 files exited the process at boot).
- tests/hermes_cli/conftest.py probe_root: scratch checkouts the import guard
  probes need hermes_bootstrap.py (the launcher imports it).
- tests/pm/_fixtures.py stage_host_python: a copied relocatable python needs
  its stdlib beside it (No module named 'encodings' on CI).
- tests/install/e2e-assets/smoke-env.mjs: dependency-free env shaping so the
  source-build-env probe runs under bare node (main deleted the Playwright
  entry it was imported through).
- adapt main's new tests to branch seams (model_metadata_http, launch
  completion tail, CI toolchain exports uv after python, source_launch
  hermes_cli stub, systemd_notify single marker).
2026-09-20 11:47:06 -04:00
ethernet
e1576d06a6 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim
salvage + its test and workflow steps dropped), the shim re-exec stays retired,
package.json carries no electron-builder block (afterExtract identity stamp wired
into electron-builder.config.cjs instead; after-pack.mjs keeps signing only),
Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap
(export_scratch_tmp_env) is taken and re-run after profile resolution.
2026-09-19 22:57:07 -04:00
teknium1
3999096d18 ci: forbid literal /tmp paths outside a burn-down baseline
scripts/check_no_tmp_literals.py flags /tmp path tokens in production code, skills,
docs and prompt strings (tests, CI workflows, Dockerfiles, lockfiles, i18n mirror,
code comments and docstrings exempt; ${TMPDIR:-/tmp} idiom exempt). Opt out one line
with 'no-tmp: ok — <why>' on the line or the line above. _BASELINE lists pre-existing
hits per file: growth fails, burn-down is advisory (--strict-baseline / --print-baseline
to refresh). Wired into lint.yml next to check_compat_pointers.
2026-09-19 10:44:26 -07:00
ethernet
339a6ac7b6 test(install): products and desktop stages share one completion call
dbeebaadfc folded the desktop build into the products stage; the manifest
is one ladder and --include-desktop only adds --desktop to the
source_completion hand-off.
2026-09-19 04:41:58 -04:00
ethernet
06da1225e6 test: pin merged-from-main tests to this branch's contracts
hermes_yaml (ruamel, YAML 1.2) raises its own YAMLError and needs a quoted
URL in flow mappings; the pre-argparse interface probe in main.py is an
allowlisted raw config read; the compat-pointer walker forwards onerror;
tools/lazy_deps.py is a retirement shim with no uv call site; _self stays on
the frozen old-updater surface; the tzdata marker also carries the Python floor.
2026-09-19 04:15:04 -04:00
ethernet
2eec0d9b64 fix(install): the shared completion tail runs from a source slice
- build_update_products builds only the frontends the checkout carries; a
  python-only slice (the installer's acceptance fixture) publishes commands
  and runs maintenance without asking PM for node.
- stderr_timestamp.py is a launcher boot file copied into published
  commands; it inlines the EX_CONFIG code instead of importing gateway.restart.
- Tests: the stamp-writer slice gains hermes_cli/release_channels.py and
  pm/paths.py (the modules update_channel now imports); the source-launch
  fixture records the source_completion hand-off (--finish-update) instead
  of building products; the stdlib recovery probe blocks PM's engine
  modules, not the pm.environments boot leaf; the memory-provider restart
  test selects a generation the running interpreter has not activated;
  the warm-path installer stage is `products`.
2026-09-19 02:53:58 -04:00
ethernet
5e63231e2d fix(icons): the renderer carries its own canary rule; hermes_cli is absent in its venv
The icon generator runs in an isolated icon-build environment (Nix, Docker,
PM) that has no application package, so importing the canonical regex from
hermes_cli.update_channel raised ModuleNotFoundError and broke the nix flake
check, the docker image build and the desktop pack. Keep a local copy and pin
it to the canonical rule with a behavioural test over every tag shape.
2026-09-19 01:38:09 -04:00
ethernet
d70feca03d Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	hermes_cli/update_cmd.py
#	tests/hermes_cli/test_cmd_update.py
2026-09-19 01:14:17 -04:00
teknium1
c07708671d fix(gateway): every adapter session key goes through one seam (+ lint)
A secondary-owned Yuanbao bot keyed its per-group dispatch queue and RecallGuard
entries with the free `build_session_key(source)` — no profile, so `agent:main:` —
while `handle_message` popped under `agent:<owner>:`. Two derivations of one
identity: the group queue was shared across bots and the RecallGuard entries
leaked. Weixin, Telegram's photo batch, Slack's thread key and Raft's wake key
each carried their own copy of the call as well.

Every adapter-side key now comes from `BasePlatformAdapter._source_session_key`
/ `_event_session_key` (owner namespace, runner-seeded isolation flags, and —
after the RoutingIdentity PR — the pinned identity). Weixin's `_text_batch_key`
override is deleted (the base does the same). Slack's thread key reads the
isolation flags from the adapter config the runner seeds, not the store's.

Lint: pattern P32 in `scripts/ci/profile_scope_patterns.json` flags
`build_session_key(` / `SessionSource(` under `gateway/platforms/**` and
`plugins/platforms/**` except `platforms/base.py`; the checker gains an optional
`path_regex` per pattern. Advisory, like every other pattern.

Phase 2 of #88715.
2026-09-18 22:04:43 -07: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
teknium1
0ddba07ad7 fix(ci): report an interpreter crash as CRASHED, not "no tests ran"
When a per-file pytest subprocess dies by signal (the sqlite cross-thread
close in #113186 was a SIGSEGV after every test had passed), faulthandler
prints "Fatal Python error: Segmentation fault" and no summary line, so
every count parses to 0. The runner filed that under "1 file where no
tests ran (collection/import error, ...)" beneath a summary that read
"0 failed" and exited 1 — two wrong diagnoses for one real bug, and it
was misread as a runner problem twice on main.

The runner now detects a signal death or a "Fatal Python error:" banner,
prefixes the captured output with the diagnosis (same convention as the
timeout path), marks the progress line CRASHED, counts "N files CRASHED"
on the summary line, lists the file in its own failure bucket, and no
longer trips the "NO TESTS RAN" guard for a crash that ran tests. The
flake retry already covers crashes (any non-zero rc), so nothing changes
there.
2026-09-18 19:41:51 -07:00
ethernet
cb7c688171 test: mirror the source tree for 27 misfiled root tests; one PM home fixture
Root-level tests/ is for root-level modules; 19 files testing scripts/, 3 testing
pm/ and 5 testing hermes_cli/ move to the mirrored directory (workflow file lists
and cross-imports updated; path arithmetic bumped one level).

tests/pm gains a conftest with the isolated_machine_home fixture that seven
modules had copy-pasted verbatim.
2026-09-18 20:13:26 -04:00
ethernet
bbec973514 refactor(pm): pm owns the dependency-environment layout and interpreter paths
hermes_cli.runtime_paths (venv generations, selection, activation) moves to
pm.environments, and gains venv_bin_dir / venv_python / project_python. Every
in-tree caller asks pm for an interpreter now; pm no longer reaches back into
hermes_cli for its own environment layout (pm.packages, pm.extras, pm.ensure,
pm.paths imported hermes_cli.runtime_paths). The three open-coded
"Scripts/python.exe or bin/python" ladders in pm collapse onto venv_python.

hermes_constants.venv_python_path / venv_bin_dir and hermes_cli.runtime_paths
stay as frozen-updater-surface shims only (tests/compat/old_updater_surface.json).

To keep the boot path light, pm/__init__ resolves its facade lazily (PEP 562)
and pm.registry loads the built-in package definitions on first read instead of
at import: `import hermes_bootstrap` now loads pm + pm.environments only (25ms,
was 37ms with the eager facade dragging in the downloader). The stripped-payload
fixtures that ship only pre-import files keep working for the same reason.

Also restores two frozen-surface re-exports the F401 sweep dropped
(banner._github_compare_behind, cua_backend.resolve_cua_driver_cmd).
2026-09-18 20:02:36 -04:00
ethernet
071ccfbcdf test: restore platforms() host gating that merges reverted to the legacy trio
Four origin/main merges brought back `linux_only` / `macos_only` /
`windows_only` marks in 41 test files, along with the pre-platforms()
versions of scripts/ci/list_os_marked_tests.py and check_os_marker_fakes.py.
Because the legacy names are no longer registered, pytest treated them as
unknown marks — a warning — so every Windows- or macOS-only test RAN on
Linux (test_local_runtime_recovery.py tripped the live-system kill guard).

Rewrite the marks, restore the platforms()-aware CI scripts (keeping main's
os.walk fix for vanishing __pycache__ dirs), drop the stale _BASELINE entries,
and make the conftest reject the retired marks outright so the next merge
cannot resurrect them silently.
2026-09-18 19:06:52 -04:00
ethernet
a6ae6ace51 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/js-tests.yml
#	agent/model_metadata.py
#	apps/desktop/electron/main.ts
#	apps/desktop/scripts/bundle-electron-main.mjs
#	apps/desktop/src/app/settings/about-settings.tsx
#	apps/desktop/src/app/settings/gateway-settings.test.tsx
#	apps/desktop/src/app/settings/gateway-settings.tsx
#	apps/desktop/src/app/updates-overlay.tsx
#	gateway/shutdown_flush.py
#	hermes_bootstrap.py
#	hermes_cli/local_runtime/binaries.py
#	hermes_cli/main.py
#	hermes_cli/managed_uv.py
#	hermes_cli/update_cmd.py
#	hermes_cli/update_cmd_deps.py
#	hermes_cli/update_cmd_fleet.py
#	hermes_cli/update_cmd_maint.py
#	hermes_cli/update_receipt.py
#	hermes_cli/update_serve_obligations.py
#	hermes_constants.py
#	tests/hermes_cli/test_doctor.py
#	tests/hermes_cli/test_managed_uv.py
#	tests/hermes_cli/test_pending_supervisor_recovery.py
#	tests/hermes_cli/test_startup_fast_guards.py
#	tests/hermes_cli/test_update_desktop_stale_warning.py
#	tests/hermes_cli/test_update_fleet_restart_pending.py
#	tests/hermes_state/test_hermes_state.py
#	tests/tools/test_tirith_security.py
#	tools/bot_relay.py
#	tools/checkpoint_manager.py
#	tools/write_approval.py
#	website/docs/getting-started/updating.md
#	website/docs/reference/environment-variables.md
2026-09-18 17:26:10 -04:00
teknium1
49397cf2b4 fix(tests): parallel runner reports a known flag's missing value as usage, not per file
The bare-flag check asked pytest's parser which tokens it does not know,
but wrapped parse_known_args in the same blanket except that guards
parser construction. A known flag with a missing value (`--tb` alone)
raises pytest.UsageError there, which the except turned into "nothing
unknown", so discovery ran and every per-file pytest died with
"argument --tb: expected one argument".

Keep the fallback around building the parser only; let parse_known_args
run outside it and surface UsageError (and the unknown-token list) as
this runner's own usage error before discovery. One invariant test.
2026-09-18 10:23:21 -07:00
teknium1
d7bedcee1e fix(tests): parallel runner rejects unknown bare flags with usage instead of sweeping
A bare token this runner does not own used to be forwarded to every per-file
pytest, so a typo (`--jbs`, or `--help` before #114065's fix) discovered the
whole suite and each file died with "unrecognized arguments" — hours to learn
about a typo. Validate the bare passthrough tokens against pytest's own
argparse parser (installed plugins loaded), and fail once with this runner's
usage (exit 2) before discovery. argparse handles the attached-short-value
(`-rA`), combined-flag (`-xvs`) and `-k expr` forms, so real pytest flags keep
passing through; tokens after a literal `--` are the caller's explicit choice
and are never validated. If pytest's parser cannot be built, the check is
skipped and behaviour is unchanged.

Follow-up to KoNit-K's `-h`/`--help` interception (#114065). Fixes #114059.
2026-09-18 10:23:21 -07:00
KoNit-K
41332e7851 fix(tests): handle parallel runner help flags 2026-09-18 10:23:21 -07:00
ethernet
498725ae59 test(install-e2e): a migration retiring a key is not user-state loss
An install from v2026.3.12 ships `.env` with `LLM_MODEL` set, because that
release's template wrote it. The upgrade runs config migration 12 -> 13, which
clears that dead var -- and the user-state verifier reported the change as the
upgrade modifying the user's own state, failing the leg.

The verifier exists to catch an upgrade taking state away; a var the CURRENT
tree retires is not that. The retired set is parsed out of
`hermes_cli/config_migrations.py` (the `for dead in (...): save_env_value(dead,
"")` shape) rather than restated here, so it cannot drift from the tree, and an
unreadable source retires nothing -- every .env change stays fatal.

Tolerance is deliberately narrow: only keys the tree retires, only when the
upgrade EMPTIED them, and only when no key was added or removed. Clearing a
live key, or deleting a retired one outright, still fails.
2026-09-18 02:57:38 -04:00
ethernet
633ff8801e fix(install-e2e): judge every sqlite database by its rows, not its bytes
state.db was already handled that way; cron/executions.db was not, so the cron
ticker writing rows mid-window would have failed the leg as a byte modification of
user state. Any *.db is now row-judged: byte churn is tolerated, and a table that
LOSES rows still fails (_rows_shrank compares the counted tables, so genuine loss
is caught, as the new test pins against cron_jobs in cron/executions.db).

tests/scripts/test_verify_user_state.py: 23/23.
2026-09-17 22:17:55 -04:00
ethernet
9974dad62b fix(install-e2e): a vanished sqlite sidecar is not deleted user state
The last red leg failed with "2 deleted": cron/executions.db-shm and
cron/executions.db-wal -- SQLite's own sidecars, which exist only while a
connection is open. A snapshot taken with one live records them, they disappear
when it closes, and the report read that as the upgrade deleting user state.

-wal/-shm/-journal are tolerated deletions now and reported as such, while the
databases themselves stay judged (state.db by row counts), pinned by a test that
tolerates the sidecars vanishing AND still fails when executions.db goes.
tests/scripts/test_verify_user_state.py: 22/22.
2026-09-17 22:13:24 -04:00
ethernet
3f12c497c8 chore(install-e2e): explain a .env modification the key diff cannot name
desktop->desktop reported "0 deleted, 1 modified" with MODIFIED .env and NO
`variables ...` line: the per-key digests deliberately ignore comments, blank lines
and ordering, so a rewrite in exactly those parts is invisible to them. The snapshot
now records the file's line/comment/blank counts and its key order, and the report
prints that comparison when no key differs -- the next failure names the shape that
changed instead of leaving a bare "1 modified". Counts and names only, never
content, pinned by a new case (tests/scripts/test_verify_user_state.py 21/21).
2026-09-17 20:04:08 -04:00
ethernet
a9d12fabb4 fix(install-e2e): a live cron ticker's stamps are not user state
cron/ticker_heartbeat and cron/ticker_last_success came back as fatal
modifications: the ticker writes them on its own schedule, inside the verified
window or not. On a cold home they land as ordinary additions (tolerated); once
the ticker exists, the same file moves -- and the harness's own background process
failed the user's upgrade outward. Both names are now tolerated under any cron/
directory, while the user's own cron definitions (cron/jobs.json) still fail if
they move, which the new test pins.
2026-09-17 19:18:52 -04:00
ethernet
009ba6bb6e chore(install-e2e): name the .env variables an upgrade rewrote
An app-driven upgrade rewrote $HERMES_HOME/.env to a different sha256 at an
IDENTICAL byte count, so the leg reported "0 deleted, 1 modified" and two hashes
of a secrets file with no lead. The snapshot now records per-key VALUE digests
for .env-shaped files and the report names the variables added/removed/changed --
names and equality only, never content, which is also asserted by the new test.
2026-09-17 18:05:31 -04:00
ethernet
a91a8b0f62 fix(install-e2e): judge bundled skills as advisory inside profiles too
The verifier's judged walk pruned only plugins/**, so a bundled skill under
profiles/<name>/skills/** was judged while the identical tree at the home root
was advisory. Every leg that owns a second profile therefore failed
USER-STATE PRESERVATION on the update's own skills sync (21 modified entries,
all "advisory modified (skills sync)" in the same report).

_walk_root now carries the per-ENTRY classification instead of the per-root
flag, consulting _is_bundled_skill at both roots -- the profiles branch of that
predicate was unreachable before. skills/.archive/** stays judged at both
roots, since the curator's archive holds restorable user skills.
2026-09-17 15:26:05 -04:00
ethernet
b4a294fff9 Merge origin/main; keep PM as plugin dependency owner
Reconcile plugin declarations and validation through PM's atomic generation publication; preserve external runtimes, target markers, and conflict refusal. Keep one source-update completion owner and port upstream lifecycle changes to the PM desktop/runtime paths.
2026-09-17 13:52:05 -04:00
ethernet
1ec55baea8 refactor(dev): drop the activation demo; keep only the invariant tests
The demo was scaffolding, not repo content: nothing referenced it but a
docstring.

The tests were 12 against the repo's 1-2 invariant bar. Keep the two that fail
silently when inverted -- the sentinel reaching an exec'd child (the unexported
variable this change fixes) and the prologue's staleness gate, which inverted
either way costs a re-sync per run or a stale environment that looks fine. The
per-OS command pair stays because neither host can observe the other's string.
Dropped the exit-code/message restatement, the no-sentinel cold path, and the
demo-driven harness.
2026-09-16 23:45:47 -04:00
teknium1
ba153d6969 fix(install): installers keep the root lockfile when a workspace manifest is dirty
`scripts/install.sh::discard_update_lockfile_churn` and `scripts/install.ps1::Discard-LockfileChurn`
run the same per-directory predicate as `hermes update` did before the previous commit, so an
installer-driven update of a managed checkout (Desktop / bootstrap) reverted the root
`package-lock.json` whenever only `apps/desktop/package.json` was dirty, leaving spec and lock
out of sync for the next `npm ci`. Port the same ownership model: the root lock is kept when the
root manifest or any manifest matching a root `workspaces` glob is dirty; nested lockfiles are
still kept only with their sibling manifest; a manifest outside the graph still does not
protect the root lock.

install.sh reads the globs with sed/grep (no jq dependency) and matches with `case`; install.ps1
uses ConvertFrom-Json and `-like`. Bash side live-A/B'd in a throwaway repo (red on main, green
after; controls unchanged); the PowerShell side is the same shape and could not be executed on
this Linux host (no pwsh).
2026-09-16 17:44:36 -07:00
KoNit-K
b6cc751e5f fix(update): support uv default venv in desktop updates 2026-09-16 17:13:56 -07:00
fangliquan
9e2962e0d1 test(compat): preserve nested first-party directories 2026-09-16 16:58:02 -07:00
fangliquan
1655dcd35d fix(compat): prune dependency trees from pointer scan 2026-09-16 16:58:02 -07:00
ethernet
53d757fe85 feat(dev): self-activating scripts with a stale-aware activation sentinel
Repo scripts assume the PM-activated environment, so running one without
activation fails much later with a confusing ImportError. Add the two halves
covering both invocation paths:

- scripts/_activation.py: require_activation() exits immediately, naming the
  exact command for the caller's shell (source ./activate on POSIX,
  . .\activate.ps1 on a native Windows host), before any heavy import.
- scripts/_hermes-python: the POSIX shebang target. `#!/usr/bin/env -S bash -c
  '...'` hands itself the target path through bash -c's $0, sources activate,
  then execs the interpreter on the same file -- so tracebacks and __file__
  still point at the real script and ./scripts/foo.py works from any cwd with
  no manual source.

__HERMES_ACTIVATED changes from a bare "1" to the installed-state file the
environment was composed against, so one value carries activation, which
checkout activated it, and a staleness stamp. The prologue compares that file
against uv.lock / pyproject.toml / pm/lock.json with the `-nt` builtin -- no
process spawn -- and re-activates once when the inherited environment predates
its inputs. pm rewrites that file only on a real sync, so the check settles
back to current rather than re-syncing on every run.

A legacy "1" keeps working: require_activation() tests non-emptiness, and the
prologue's [ -e ] fails on it, so it activates once and upgrades.
2026-09-16 19:32:07 -04:00
ethernet
bf499370bd test(install-e2e): assert the user's own state survives an upgrade
The install/update legs asserted plenty about the code -- the checkout
landed, the version bumped, the desktop artifact exists -- and nothing
about the user's own state. An upgrade that ate auth.json or truncated
state.db would have passed every leg.

Adds a read-only, stdlib-only verifier (snapshot/verify) plus the hooks
that drive it around the real upgrade, on the POSIX and Windows drivers.
The state it defends is produced through the ordinary CLI
(hermes chat -q / auth add / profile create), never seeded by the
harness, and each action asserts it actually landed so a leg cannot
'pass' while testing nothing.

Judged: config.yaml, .env, auth.json, state.db, gateway_state.json and
the user's trees. state.db is compared by row counts, not bytes -- a
live SQLite file moves for benign reasons. The bundled skills/ tree is
recorded but never judged (the product re-syncs it), and plugins/** is
left to verify-plugin-preservation.py.
2026-09-16 18:08:50 -04:00
teknium1
034313e7cd feat: plugin catalog entries carry an optional version label and card image
The 40-hex sha stays the release, but nobody reads one. Entries may now add
`version: "1.4.0"` (free-form, <=32 chars, never parsed) and `image:` (an https
URL on raw.githubusercontent.com / github.com / *.githubusercontent.com).

Why GitHub-only: the Desktop catalog browser deliberately never fetches from
third-party hosts, and a raw URL pinned to the entry commit is as immutable as
the sha it decorates.

Readers updated together: PluginCatalogEntry + entry_from_mapping (drop with a
warning, entry survives), validate_plugin_catalog.py (admission error), the
site extractor (drop, never fatal), the /docs/plugins card (banner + version
pill + "1.4.0 @ abcd1234" pin), the CLI table/info (pin_label), the TUI-gateway
plugin row (catalog_version -> Desktop "Update to 1.4.0"), and the Desktop
catalog detail header (image).
2026-09-16 14:18:39 -07:00
ethernet
4fbec9c442 feat(pm): repair retired termux pool pins from pm update --termux
The termux-main pool deletes a package's previous archive when it rebuilds, so
the runtime-lib pin table and the bionic lock rows rot without warning. The last
rotation broke a build on eight rows at once, and the stager's concurrent
downloads only surfaced whichever 404 won the race.

pm now owns the pin table it repairs: scripts/termux/runtime_libs.json moves to
pm/termux_runtime_libs.json, so pins live in pm/ and scripts consume them — the
direction scripts/ci/archive_inputs.py already reads pm/lock.json in.

`hermes pm update --termux` repins exactly the rows whose archive the pool has
replaced, hashing each replacement against the index SHA256 before writing
url/version/hash together. `--check` reports without writing and exits 1, so a
retired pin can fail a cheap preflight instead of a payload build.

It is a repair, not an update: an alive pin is never moved, because a repin can
land a rebuilt library under a moved soname and the table is the payload's
recursive DT_NEEDED closure. A pin whose package the pool has dropped outright
is reported and left alone. `--termux` runs alone — names/--target/--uv/--npm
are ignored, since repairing foreign-target pins is not a version resolution.

Verified: `pm update --termux --check` against the live pool reports 89 rows
served; a table deliberately pinned to the retired libiconv 1.18-1 repins to
1.19 with the pool's hash through the real network path; 19 new tests; the
tests/pm, tests/ci and tests/scripts suites have the same failure set as the
base commit (91 pre-existing Windows environment failures, none new).
2026-09-16 11:46:30 -04:00