The compat scanner derived a directory from manifest.path by splitting at the
first ':' and falling back to .parent when the result was not a dir. An
entry-point plugin (`vendor_plugin:register`) and a Windows path
(`C:\Users\...`) both collapsed to '.', so a stray .py in the launch directory
was attributed to the plugin and the installed package was never scanned.
After the removal date that disables the wrong plugin. `_scan_root()` now
takes directory manifests verbatim and resolves entry points via
importlib.util.find_spec to the installed package dir; anything unresolvable
scans nothing.
`plugins.allow_deprecated_imports` used bool(), so the YAML string "false"
opened the post-removal bypass. It now requires the literal boolean True, and
summary_lines() says "force-loaded" instead of "DISABLED" when the override is
what kept the plugins running.
Reported-by: ayushnangia (PR #102117 review)
The compat sweep dropped the auth-facade re-export of _mark_qwen_oauth_active
while hermes_cli/auth_commands.py still reached it through the auth_mod module
alias, so every 'hermes auth add qwen' died with AttributeError on this branch.
Import it from hermes_cli/auth_qwen where it now lives. Module-alias attribute
reads dodge import-time checks; the sweep in the PR thread found this to be the
only production hit of that class.
Reported-by: yoniebans (PR #102117 review)
Root AGENTS.md 100,797 → 29,295 chars: what applies everywhere (invariants, rubric, footprint ladder, layout + shape rules, commit/PR, testing) plus a routing table. Area rules move to agent/, hermes_cli/, gateway/, tools/, plugins/, tui_gateway/, web/, skills/, cron/, apps/desktop/src/ AGENTS.md (3–9k each; ceiling is now 32k after d61cff60e3, target ~8k). Long-form process-identity and skin key tables go to website/docs/developer-guide/cli-internals.md. Zero rule loss; map in /tmp/rf/agents_md_zero_loss.md. Stale Bot Mode test paths corrected to apps/desktop/src/plugins/hermes-bots/*.test.ts.
Live Desktop E2E (real Electron, worktree backend, demo plugin on old paths) found the modal never fired:
compat_report() was only called from the CLI banner / doctor / update / plugins-compat surfaces, none of
which run inside the Desktop's `serve` backend, so .plugin-compat-report.json was never written.
PluginManager.discover_and_load now refreshes the report from the manifests it just discovered (fail-open).
E2E after the fix, all five acceptance steps green on the real seat: report written and names the plugin;
native dialog 'Plugins need an update' with plugin, date, and `hermes plugins compat`; OK persists the
dismissal; relaunch with the same userData shows nothing and logs no 'compat notice shown'; fixing the
plugin's imports deletes the report and shows nothing.
Live PTY check: the banner block named the plugin correctly but was preceded by one stderr
HermesPluginCompatWarning per moved name, duplicating it without the plugin name. warn_once now also
logs at WARNING (agent.log/gateway.log keep the record); cli.main() appends an ignore filter for the
category before plugin discovery. Appended, not overriding: -W error::...HermesPluginCompatWarning
(tests, plugin authors' CI) still wins, verified with the strict test run.
hermes_cli/plugin_compat.py is now the single source of truth for the compat window:
COMPAT_REMOVAL_DATE = 2026-09-14; scan_plugin() statically finds `from F import n`, `import F` + `F.n`,
alias forms and string targets against compat_manifest.json; compat_report() aggregates over the user's
ENABLED external (non-bundled) plugins; disable_reason() decides the loader's skip.
Surfaces (all read from that one report):
* CLI: yellow block under the banner naming plugins + date + `hermes plugins compat` (red + DISABLED after)
* `hermes plugins compat [--json] [path]`: file:line, old -> new per hit; exit 1 while anything remains;
`path` lets a plugin author scan their own checkout
* `hermes doctor`: "Plugin import paths (removed Sep 14, 2026)" section next to the xAI retirement check
* `hermes update`: post-update notice alongside the FTS/curator notices
* Desktop: compat_report() writes HERMES_HOME/.plugin-compat-report.json (deleted when clean); Electron
shows ONE warning dialog per distinct report after the backend is up and persists the dismissal in
userData/plugin-compat-dismissed.json. A new affected plugin, or the date passing, is a new report.
From the date, PluginManager skips a hitting external plugin before importing it, with the reason in
LoadedPlugin.error ("uses N import path(s) removed on 2026-09-14; run `hermes plugins compat` ...") — the
same path a plugin with a broken register() takes, so nothing else is affected. Escape hatch:
plugins.allow_deprecated_imports: true (config_defaults), which only helps until the compat commit is
actually reverted.
Docs: COMPAT_MANIFEST.md (removal date, what-happens table, author instructions), plugin dev guide section.
Tests: tests/test_plugin_compat_notice.py (scanner forms, report scope, date gate + escape hatch, summary
text, report file lifecycle, loader skip via a real PluginManager), electron/plugin-compat-notice.test.ts
(show once, re-show on a different set or on the date passing, malformed file ignored).
Live A/B on this box with a demo plugin on old paths: before the date it loads and the banner/doctor/report
name it; with today=2026-09-14 it is skipped with the reason and the banner turns red; with the escape
hatch it loads again.
33 read sites across 25 files used BOM-intolerant encoding='utf-8'.
Windows tooling BOMs files it touches; json.load on a BOM'd file fails
with 'Expecting value'. Reads now use utf-8-sig (writes unchanged).
check-windows-footguns.py --all: 33 → 0.
Upstream's linux_desktop_entry.py (806 lines) supersedes the pm-era
227-line version: hicolor icon install with PNG resize, launch-context
independent Exec, running-interpreter venv-semantic resolution. Take it
wholesale; _repo_pythonpath_entry had no external callers.
test_linux_desktop_entry: use upstream's icon-fallback test + keep the
quote-aware Exec assertion (Windows paths trigger _quote_exec_arg).
test_gui_command: restore pre-upstream-merge version — our main.py is
the pm-era one, so upstream's newer tests expect behavior we don't have;
2 remaining failures are pre-existing Windows-host npm-seam issues.
Every PLUGIN-COMPAT __getattr__ now calls hermes_cli.plugin_compat.warn_once(facade, name, target) before
resolving, emitting a HermesPluginCompatWarning (FutureWarning) once per process per name: old path, new
path, removal target. Importing a facade for its live API stays silent; only resolving a moved name warns.
COMPAT_MANIFEST.md documents the warning and how to silence it during migration.
Verified the runtime never routes through a pointer: every entry point (run_agent, cli, hermes_cli.main,
gateway.run, tui_gateway.server, web_server, model_tools + tool discovery, hermes_state, cron.scheduler,
browser_tool, mcp_tool, kanban, auth) imports clean and `hermes doctor` runs end to end with the warning
promoted to an error.
Also restores the check_compat_pointers CI step to .github/workflows/lint.yml, which a0be177aac dropped
when the compat layer was regenerated (the lint script itself was present; the workflow step was not).
hermes_cli/plugin_compat.py, tests/test_plugin_compat_warning.py and the two-line insert per facade are
part of the compat layer and go away with it.
Upstream's 43e67d872f (feat: local models) reintroduced the old bespoke
ranged-parallel machinery (download_file/_probe_range_support/
_DOWNLOAD_CONNECTIONS) into the local-models router and deleted the
pm.downloader-based _download_job plus the pause/resume routes.
Re-swap all 3 call sites (model download, runtime-install leg, browsed
download) onto pm.downloader.Download via _download_job: one resumable
8-way parallel job per plan, progress via the shared tick callback,
partials in the managed partials root. Restore /download/pause +
/download/resume routes with the _RUNNING handle registry (dl handle
kept off the JSON job dict; resume re-runs the job body).
Restore the pre-clobber route tests (FakeRangeOpener stands in for
pm.downloader._OPENER with honest Range support) incl. the pause /
resume / finished-job-releases-handles coverage.
- desktop.md: dist:win is MSIX-only, not NSIS+MSI
- BUILDING.md: sign-nested-chromium is LIVE (after-pack.mjs wires it), not dead
- pyproject: lazy_deps.py comment -> pm
- photon docs: drop dead PHOTON_NODE_BIN rows (adapter is pm-store-first);
restore PHOTON_MENTION_PATTERNS row my earlier edit wrongly removed
- urllib_security/models docstrings: SSL_CERT_FILE/certifi fallback ->
platform trust store (post truststore port)
_stamp_version_info raises RuntimeError when install-stamp.json has a
missing/illegal updateMechanism or a mispackaged 'light' payload. Every
uncached caller of get_version_info died on that. Wrap the call so a
malformed stamp falls through to git/unknown provenance; the authoring
build lane's own tests still surface the bad stamp.
Cherry-pick of 03b45db777 from ethie/bundles-local-models: TLS trust was
five hand-rolled ladders (agent/ssl_verify, hermes_cli/auth,
agent/model_metadata, hermes_cli/urllib_security, gateway/run's SSL_CERT_FILE
mutation) all ultimately pointing at certifi's frozen list. Trust now comes
from the platform verifier via truststore: CryptoAPI on Windows,
Security.framework on macOS, OpenSSL's store on Linux; install_truststore()
patches ssl.SSLContext process-wide. agent/ssl_verify.py is the one
authority; agent/ssl_guard.py deleted.
Also closes the session-flagged coverage gap: hermes_cli/main.py (CLI
entrypoint) and tui_gateway/entry.py now call install_truststore() so
subcommands/help that never construct an AIAgent still get OS-store trust.
Review findings on #102117 (independent reviewer + itsflownium):
* hermes_cli.kanban_db.connect / connect_closing pointed at hermes_cli.projects_db (different DB, no
board= parameter). The compat generator ranked candidate homes by path proximity when a name is
defined in several modules. Now it requires shape compatibility with the BASE definition (same
literal for constants, superset of parameter names for defs) and prefers the facade's own
<stem>_* sibling. Same class fixed for tools.tts_tool.DEFAULT_XAI_BASE_URL (-> tts_tool_providers),
and 17 constants/defs that had been pointed at same-named strangers (Matrix MAX_MESSAGE_LENGTH ->
Signal's 8000, tts MAX_TEXT_LENGTH -> BlueBubbles', honcho/retaindb/supermemory *_SCHEMA -> another
plugin's schema, ...) are now restored from BASE verbatim instead.
* send_yuanbao_direct (restored-def): body called adapter._outbound.send_direct, which HEAD moved to
the sender; rewritten to adapter._outbound.sender.send_direct.
* COMPAT_MANIFEST.md states the scope explicitly: public top-level names only; private names and
test monkeypatch seams are not preserved.
* scripts/check_subprocess_stdin.py: _splat_carries_stdin looked 30 lines ahead in the file text
and was satisfied by an unrelated later stdin=; it now finds the splatted name's definition via AST
and requires stdin inside that expression/body.
Tests: tests/test_compat_manifest_targets.py (pointer identity vs the facade's sibling; kanban
connect(board=) opens a Kanban DB, not projects.db; both FAIL on the previous layer),
test_subprocess_stdin_guard gains the false-negative probe, and the MoA -Q quiet-output contract
tests are back (tests/agent/test_moa_quiet_reference_output.py) against build_moa_facade.
Tasks 3-5 of the gateway-as-MSIX-service plan (settled: user-context,
1903 floor, config-only demand-start):
- hermes gateway service on|off|status (new subcommand group): 'on' =
SCM automatic-at-logon + StartService + gateway.service config
persisted; 'off' = demand-start + graceful StopService (the
frontend's planned-stop-marker path); 'status' = SCM state + the
config key, with a running-but-unpersisted warning. Windows-only
guard; friendly not-installed answers (source installs).
- Task 3 audit resolved: the gateway's DEFAULT restart_drain_timeout
is 0 (opt-out, clamped at runtime) and the frontend caps its stop
deadline at 25s < the SCM's 30s WaitToKillServiceTimeout — every
reachable config lands inside the window; the cap IS the contract
(already unit-tested in the Task 1 suite).
- Updater SCM branch (Task 4): _prepare_profile_gateway_update_restart
consults _try_scm_service_restart FIRST — sealed MSIX install +
HermesGateway service RUNNING → Restart-Service (graceful drain →
SCM respawn, no detached watcher, no process scan: the first
concrete piece of #92091's world on windows, arrived via the SCM).
Every other machine shape falls through to the ordinary paths,
byte-identical behavior. Never fails the update over a service
lookup.
- gateway.service config key: DEFAULT_CONFIG entry (False — the
config-only posture), deep-merge, no version bump.
- docs: website gateway-service.md (what it is, on/off/status, the
graceful-stop story, update handoff, uninstall = service removal,
platform notes) + sidebar.
tests: 8 hermetic verb/updater tests (status-not-installed, on
configures-starts-persists, off demand-stops-persists, unknown
action, updater gated on sealed+running, not-sealed no-touch,
stopped-service fall-through, posix guard) — the pm-ensure
function-binding trap hit AGAIN and is patched via importlib. Full
sweep: 89 passed 0 failed.
Task 1 of the gateway-as-MSIX-service plan (D6b windows half; settled
2026-09-03, ethie: user-context, 1903 floor, config-only):
gateway/windows_service.py — the service IS the payload launcher, not
a rust shim (the ethie catch: the distlib-minted hermes.exe already
exists in every bundle, so the manifest names it and the SCM-frontend
mode runs inside the payload python — pywin32 is already a core win32
dep; no new binary, no new build step, extending round 21's
no-rust-anywhere trajectory):
- SCM START → report Running fast (stdlib+pywin32 imports only; the
heavy hermes import happens in the CHILD it spawns: plain
`hermes gateway run`, no --service — every gateway code path stays
the daily one).
- SCM STOP → write the EXISTING planned-stop marker
(write_planned_stop_marker, the #33778 windows-graceful-stop path
`hermes gateway stop` uses) — the child's own watcher drains it.
The frontend's stop deadline sits BELOW the SCM's
WaitToKillServiceTimeout with margin (Task 3 contract); overflow
force-kills the child only (never a #85265-style tree taskkill).
- No pywin32 present → friendly stderr guidance, never a hang.
--service flag: gateway run parser + dispatch (windows-only guard).
tests: 7 hermetic contract tests (name, drain-window cap + env
override, marker-is-the-planned-stop-path, terminate fallback,
hand-run error shape).
The Sep 2026 decomposition (PR #102117) makes internal import paths a non-API: names now live in
the focused modules that define them. This commit is the ONLY thing keeping the old paths alive,
so external plugins have time to update. It is deliberately a single, unsquashed commit:
git revert <this sha>
removes every shim, stub and manifest at once on the announced date. Nothing in-tree may depend on
these pointers: scripts/check_compat_pointers.py (wired into lint.yml) fails CI if it does.
What it adds (see COMPAT_MANIFEST.md, compat_manifest.json):
- 332 facade modules get one delimited `PLUGIN-COMPAT` block appended at the end of the file
- 1,172 moved names resolved lazily via a module `__getattr__` (PEP 562) — never a top-level import,
so no import cycles; facades that already had `__getattr__` get a chained one
- 592 third-party/stdlib names the old modules used to expose, with their original import statements
- 266 public definitions that had been deleted as unused, restored byte-for-byte from the pre-decomposition
tree (+40 private helpers and 16 imports pulled in only because a restored definition needs them)
- 3 deleted modules recreated as re-export stubs (gateway/startup_watchdog, hermes_cli/observability/
relay_runtime, tools/environments/modal_utils)
- private names (`_x`) get no pointer: they were never API (3,792 skipped)
Verified: all 335 touched modules import under a fresh HERMES_HOME and every manifest name resolves;
the lint reports zero in-tree uses; ruff clean; targeted suites unchanged.
Consolidates on top of the concurrent browser_tool compat-removal commits (de60f789a7, 912a497ce2, da7ee6353e, 69eb1feb3e): the facade no longer re-exports any browser_tool_* sibling name; hermes_cli/update_cmd_deps.py's internal import (not its exported surface) and tests/hermes_cli/test_cmd_update.py patch targets now point at tools.browser_tool_install.
No runtime consumer read the proxy (terminal_tool/environments call is_interrupted()/set_interrupt()
directly); its only users were tests patching tools.interrupt._interrupt_event, which had no effect on
the code under test. tools/terminal_tool.py's own re-export of the name is owned by another worker.
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
hermes_state.py: delete every '# noqa: F401 (re-exported...)' import block (hermes_state_common/errors/guard/
readpool/sessions/fts/dbfile/wal/repair/registry + agent.context_compressor _DB_PERSISTED_MARKER_KEY); keep
only the names hermes_state.py itself uses, without noqa.
hermes_state_registry.py: drop get_shared_session_db/release_shared_session_db/close_shared_session_dbs
aliases; every caller (gateway/, tools/, tui_gateway/, cron/, mcp_serve, run_agent, tests) now imports
acquire/release/close_all/release_or_close from hermes_state_registry.
hermes_state_titles.py: drop set_auto_title_if_empty shim (title_generator keeps its getattr fallback).
Re-remove shim-only names restored by 34abf954bd: latest_user_message_row_id (tests call
latest_message_row_id(key, role='user'); role-targeting assertions kept) and get_session_activity (tests
build the snapshot via agent.session_activity.build_activity_snapshot over db.get_session(sid)).
hermes_state_wal._log_once resolves its dedupe sets as module globals instead of via hermes_state;
hermes_state_repair helpers call module globals directly (tests patch hermes_state_repair.<name>).
Frozen updater surface untouched (update_cmd_maint imports only SessionDB from hermes_state).
cron/scheduler.py no longer re-exports the split modules (scheduler_delivery /
_script / _prompt / _preflight); it imports only the 19 names it calls itself
(bottom-of-file, E402 kept for the import cycle). Dropped the shim-only
`import shutil` and the F401 note on windows_hide_flags (still used by
scheduler.py). Split modules now call same-module helpers directly, reach
sibling split modules via late-bound module refs (_delivery/_script/_preflight)
next to _sched, and import windows_hide_flags themselves; origin-resident names
(load_config, Path, _SCRIPT_TIMEOUT, heartbeat_run_claim, ...) still go through
_sched. Callers/tests import + patch the defining module.
run_agent.py: delete the `# noqa: F401` re-export block (agent.process_bootstrap
OpenAI/_SafeWriter/_get_proxy_*, model_tools get_tool_definitions/
handle_function_call/check_toolset_requirements, FailoverReason,
_qwen_portal_headers/_routermint_headers, session_persistence names,
estimate_request_tokens_rough, ContextCompressor + friends, jittered_backoff,
prompt_builder names, message_sanitization names, tool_dispatch_helpers
names) — 41 names run_agent never used itself — and the `_STREAM_DIAG_HEADERS`
back-compat class alias (no in-tree reader). run_agent now imports only what
it uses (get_toolset_for_tool, is_local_endpoint, coalesce/uniquify tool-call
ids, cleanup_vm/get_active_env from terminal_tool_lifecycle).
agent/*: `_ra().X` late-binds that only reached a re-export now import the
defining module directly (agent_runtime_helpers -> process_bootstrap.OpenAI,
model_tools.handle_function_call, session_persistence._safe_session_filename_component;
agent_init -> model_tools.get_tool_definitions/check_toolset_requirements,
_lazy_headers("agent.client_lifecycle", ...) for qwen/routermint;
system_prompt -> agent.prompt_builder / model_tools directly, dropping its
own _ra() shim and the `_r` parameter threading). `_ra()` stays for
run_agent-resident names (logger, AIAgent, _hermes_home, _set_interrupt, ...).
toolsets.py: remove resolve_multiple_toolsets (shim-only, restored by
34abf954bd); tests/test_toolsets.py pins the same union behavior via
resolve_toolset over each name.
providers/__init__.py: drop the OMIT_TEMPERATURE re-export (no callers via the
package); ProviderProfile stays because __init__ uses it for annotations —
2 tests repointed to providers.base.
agent/iteration_budget.py: drop the "run_agent re-exports the class"
docstring pointer; 4 tests import IterationBudget from its home.
model_tools.py (arg_coercion names), agent/tool_executor.py, and
hermes_cli/cli_session_mixin.py repoints landed via a sibling commit on this
shared worktree.
Callers repointed: gateway/run.py, hermes_cli/cli_chat_turn_mixin.py,
hermes_cli/cli_tui_mixin.py, tui_gateway/session_workdir.py,
agent/transports/codex.py (one-line imports) + comment pointers in
tools/file_state.py, tools/schema_sanitizer.py, scripts/tool_search_livetest.py.
Tests: patch("run_agent.X") / monkeypatch.setattr(run_agent, "X") /
`from run_agent import X` -> defining module across 99 test files.
Re-applies the gateway compat removal byte-for-byte; see 92d0bd0d73 for the
full inventory (30 re-exports/aliases + 2 shim modules dropped, 3 shim-only
names re-removed, 24 callers + 34 test files repointed). No new changes.