26 Commits
Author SHA1 Message Date
Teknium fa78e686fa fix(plugins): doctor also refuses links inside a plugin that lead to its temp dir
The root containment check missed a symlink (or Windows junction) inside
the plugin pointing at TMPDIR: copytree follows directory links, so the
copy still walked into hermes-plugin-doctor-*/plugins/<name> and read its
own output. Every directory entry is now resolved during the copy and
refused when it overlaps the staging destination; links elsewhere still
stage as before.
2026-10-30 04:39:18 -07:00
Tekniumandgyeyeop e1e956f1df fix(plugins): doctor refuses to copy a plugin into its own temp dir
When TMPDIR sits inside the plugin being checked (directly, through a
symlink, or inside the one member of a category), Doctor's staging copy
wrote into a descendant of its own source and walked its own output
until ENAMETOOLONG or a full disk. The home-directory fix (#146761) only
stopped Doctor from picking the wrong folder; a valid plugin could still
contain the temp dir.

Both staging branches now go through _stage_copy, which resolves source
and destination (so symlink aliases cannot hide the overlap) and refuses
a destination inside the source before copytree runs.

Containment guard from #146617.

Co-authored-by: gyeyeop <226306458+gyeyeop@users.noreply.github.com>
2026-10-30 04:39:18 -07:00
kshitijk4poor 903230b1e2 fix(plugins): doctor stages only the plugin it validates
Follow-up to the cherry-picked fix from #146591. That fix stops Doctor
resolving a home that holds one plugin checkout beside other directories,
but a resolved category was still copied wholesale:

- A folder holding one plugin plus loose files (a Downloads folder with
  disk images) still resolved, and the whole folder was copied into the
  temporary HERMES_HOME. Doctor then reported OK.
- A category of several plugins was copied in full and only then failed
  with "Expected one plugin manifest".

Doctor now copies only the category's single plugin directory, under the
category name so discovery still keys it category/plugin, and refuses a
category of several plugins before copying anything. A model-provider
plugin inside a category (the plugins/model-providers/NAME layout) is now
loaded from its own directory; the provider loader used to get the
category directory and reported that no ProviderProfile registered.

The category check also skips the foreign-harness manifest dirs that
discovery skips (_FOREIGN_HARNESS_MANIFEST_DIRS), through a new
plugins_discovery.is_skipped_plugin_dir() that scan_directory uses too
(its two debug lines for skipped dirs become one).
The staging copy and the category check share one skipped-dir list.

A directory refused because plugins sit beside other directories (a repo
root with myplugin/ next to tests/) now names those plugins in the error,
leaving out dirs that discovery would skip.
2026-10-30 11:27:59 +05:30
xxxigm 831637fe8d fix(plugins): do not copy a home that merely contains a plugin checkout
A category directory's children are plugins. One checkout next to other
directories made plugins doctor resolve that parent and copytree it.
2026-10-30 11:27:59 +05:30
Teknium ca8e43aea7 fix(desktop-plugins): copy root-layout desktop halves; no false desktop success
A mixed package with plugin.yaml + __init__.py and a ROOT plugin.js is
reported by the install probe as carrying both halves, so a browser tab
(and native Desktop on a local backend) installs the agent half alone and
leaves the desktop half to the reconcile pass. Both reconcilers (the host's
desktop_plugin_halves.py and Electron's desktop-plugins-root.ts) only read
desktop/plugin.js, so nothing was copied while the dialog still said
"Desktop plugin <name> installed".

- One entry rule for every reader: desktop_source / desktopHalfSource (root
  plugin.js wins, else desktop/plugin.js), used by both reconcilers, the
  Webapp installer, findDesktopEntry, plugins.manage list's has_desktop_half
  and the plugin doctor's stale-copy check.
- A root-layout half publishes the package root as the installers do,
  without .git and __pycache__, held to the installer's plain-tree rule
  (no links, 64 MB cap); a refused tree is logged and not copied. The
  marker's source is the package root, so the prune follows it.
- The install dialog reports the desktop half only when the reconcile
  copied it or <root>/<name>/plugin.js is already there; an agent install
  that left no desktop half reports "Desktop plugin install failed".
2026-10-28 17:21:32 -07:00
Maciej Lisiewski 2ff58849f4 ci: ruff UP037 fixes 2026-10-09 17:34:38 +02:00
teknium1 cc1500a071 fix(desktop): a linked plugin checkout drives its desktop half; doctor flags a stale copy
The Desktop loads a unified package's desktop half from the copy in
desktop-plugins/<name>/, not from the package. Editing the source shipped
nothing and nothing said so:

- a package symlinked into plugins/ (the in-place dev layout the Python
  loader already follows) was never copied at all — Dirent.isDirectory()
  is false for a symlink;
- an installed package edited in place stayed stale until Rescan/restart.

Electron now follows symlinked package dirs, marks their copy linked, and
re-syncs it on any byte difference. The renderer watches a linked copy's
source plugin.js and, on save, runs the reconcile and hot-reloads. Catalog
and git installs keep the mtime rule. `hermes plugins doctor` warns when
the copy differs from the source and says how to refresh it.
2026-10-08 04:07:30 -07:00
ethernet c13287c915 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/electron/main.ts
#	hermes_cli/backup.py
#	hermes_cli/config.py
#	hermes_cli/plugin_catalog.py
#	hermes_cli/plugins_cmd.py
#	hermes_cli/plugins_cmd_catalog.py
#	hermes_cli/plugins_discovery.py
#	hermes_cli/profiles.py
#	hermes_cli/update_cmd_deps.py
#	pyproject.toml
#	tests/gateway/test_dm_topics.py
#	tests/hermes_cli/test_config.py
#	tests/hermes_cli/test_plugins_cmd.py
#	tests/hermes_cli/test_update_autostash.py
#	tests/tools/test_lazy_deps.py
#	tools/lazy_deps.py
#	tools/skill_ledger.py
#	utils.py
#	website/docs/user-guide/security.md
2026-09-22 05:16:50 -04:00
bsdnn 024f1b07bd fix(plugins): unload the doctor's temp plugin before removing its home
`plugins doctor` loads a plugin under a temporary HERMES_HOME via a throwaway
PluginManager, restores the global registries, and closes the temp dir — but
never calls manager.unload(), so host-owned ctx.on_unload(...) callbacks never
run. A context-engine plugin that opened SQLite under the temp home left the DB
handle open, and TemporaryDirectory removal then failed with WinError 32 on
Windows (#99918).

Call manager.unload() at the top of the teardown finally, while the temporary
home still exists, so registration disposal and on_unload callbacks (e.g.
closing the SQLite handle) run before the directory is removed. Best-effort: the
existing snapshot restore remains the authoritative registry cleanup, so an
unload hiccup never masks it or the original exception.

Reported by @supplefrog.
2026-09-22 00:06:29 -07:00
ethernet c9bd7459c2 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	scripts/run_tests_parallel.py
#	tui_gateway/model_switch.py
2026-09-22 00:28:30 -04:00
teknium1 eeb220d40c fix: model-provider plugins resolve per bound profile home (#88143)
Symptom: a model-provider plugin installed with `hermes plugins install`
(e.g. claude-subscription-directsdk) worked from a terminal but the Desktop
app failed every session build with "Unknown provider '<name>'".

Cause: `providers/__init__.py` scanned `$HERMES_HOME/plugins` exactly once
per process, under whichever profile home happened to be bound at the first
lookup, and the registry was global. The Desktop backend and the multiplex
gateway serve several profiles from one process, so any profile other than
the first-discovered one never saw its own plugins, and a plugin installed
while the process ran was invisible until a restart.

Change: bundled, pip and legacy providers stay process-wide; `$HERMES_HOME`
plugins load into a per-home layer keyed by `hermes_home_key()` at lookup
time (`get_provider_profile`, `list_providers`, `provider_source`). The
layer rescans when the plugin directories' mtimes change, so a fresh install
is found on the next lookup. The registration target is a ContextVar so two
turn threads scanning two homes cannot cross-register, and no lock is held
across plugin imports (a lock there could deadlock against a thread
mid-`import hermes_cli.auth`). Per-home module names let two profiles carry
the same plugin. `plugin_dev` reuses the module-name helper.

Live repro (tui_gateway `session.create` on a secondary profile whose config
selects a plugin installed only there): base -> agent_error "Unknown
provider 'fakeprov-b'"; fixed -> provider resolves and the build proceeds to
the plugin's own runtime check.
2026-09-21 18:38:36 -07: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
teknium1andOliver Mee 78004e8463 fix(plugins): hermes plugins doctor understands kind: model-provider
Doctor pushed every plugin through PluginManager._load_plugin, which demands register(ctx);
model-provider plugins register a ProviderProfile at import through providers/ discovery (the
manager skips the kind on purpose), so every valid provider plugin failed with
'no register() function' while 'hermes plugins validate' passed it. Route the kind through
providers._import_plugin_dir, report the registered profiles, and restore the live registry
and module state afterwards.

Same diagnosis and shape as PR #86160 by @oliver-mee (Aug 14), which reached it first; this
version imports through providers._import_plugin_dir (the real discovery loader) instead of
the PluginManager namespace. Credit: #86160.

Co-authored-by: Oliver Mee <102673257+oliver-mee@users.noreply.github.com>
2026-09-20 14:29:39 -07:00
ethernet fc4086c4c5 fix(cli): route dependency hints through pm
Replace direct Hermes-environment pip advice with PM repair, existing
setup commands, or explicit extra sync. Keep Termux package guidance.
Plugin discovery reports missing dependencies without installing them.

Targeted Nix runner, HERMES_TEST_FILE_RETRIES=0:
- Seven focused files: 153 passed.
- Voice CLI integration: 31 passed on the follow-up run.
- Dashboard follow-up: 33 passed before one failure with --maxfail=1.
  test_post_memory_provider_setup_routes_pip_through_pm receives failed
  instead of restart_required from the memory setup endpoint.

Doctor launcher and runtime-detection gaps are reported separately.
No doctor checks are disabled. The full suite was not run.
2026-09-11 18:13:53 -04:00
Teknium 7a33369e81 simplify(compat): interrupt — drop _ThreadAwareEventProxy/_interrupt_event legacy alias, repoint 2 test files
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.
2026-09-03 14:00:59 -07:00
Teknium c93ace77c2 simplify(compat): config/runtime_provider/plugins/commands/secrets_cli/kanban — drop 96 re-exports (incl. PEP 562 facades) + 3 aliases (get_pre_tool_call_directive/_block_message, get_telegram_handler_factories), repoint 56 callers + 50 test files 2026-09-03 14:00:17 -07:00
Teknium 6849bacd1a refactor(hermes_cli): unify _sub_dict into plugin_capabilities._child_dict; merge requires_env normaliser 2026-09-02 21:40:39 -07:00
Teknium 761686347c refactor(hermes_cli): hug closing brackets in plugin CLI modules (AST-identical) 2026-09-02 21:10:03 -07:00
Teknium 4cdf9550ce refactor(hermes_cli): merge split f-string literals, squeeze post-import blanks (AST-identical) 2026-09-02 20:41:00 -07:00
Teknium b10a148053 refactor(hermes_cli): AST-neutral statement packing across plugin CLI modules 2026-09-02 20:37:05 -07:00
Teknium 7f457609b3 refactor(hermes_cli): compact plugin_dev, plugin_index, plugin_capabilities 2026-09-02 20:31:19 -07:00
Teknium cfa327e5dc refactor(hclib): remaining hermes_cli library modules — dead code, unified helpers, flattened branches 2026-09-02 15:03:42 -07:00
Teknium ca9952cbb3 fix(plugins): doctor temp home can no longer be stranded by a failed staging copy
Follow-up to the salvaged manifest guard (#90859): the doctor's temporary
HERMES_HOME now enters the ExitStack before the staging copytree, so
ENOSPC / KeyboardInterrupt / any exception during staging deterministically
removes the hermes-plugin-doctor-* directory instead of relying on the
TemporaryDirectory GC finalizer (which cannot run while the traceback pins
the frame). Regression test proven via sabotage run against the old code.
2026-08-31 08:41:15 -07:00
DevKJ a00ae1d519 fix(cli): stop plugins doctor from copying a non-plugin directory
`hermes plugins doctor` defaults its target to `.`, and
resolve_plugin_path accepted any directory that existed. Doctor then
copytree'd the resolved path into a temporary HERMES_HOME *before* the
runtime got to reject it, so running the command from a directory that
is not a plugin copied that whole tree.

Run from $HOME on macOS this copies the home directory, and because
`Library/CloudStorage` is not excluded it also materializes every
cloud-only Google Drive/iCloud placeholder. Observed locally: 461 GB
written to /private/var/folders and still growing when the process was
killed, on a machine with 49 GB free.

Resolve now requires a manifest before returning a path, mirroring
PluginManager._scan_directory: `plugin.yaml`/`plugin.yml`/`plugin.json`
in the directory itself, or in one immediate subdirectory for the
category layout. Plugin-id candidates are only tried for values that can
be an id, since joining `.` onto a plugins root resolves to the root and
would hand Doctor every installed plugin at once.

Non-plugin targets now fail with a clear message and no copy.
2026-08-31 08:41:15 -07:00
Teknium bd6dcd4bd5 feat(plugins): manifest v2 — schema version, api_version, inter-plugin deps, pip-dependency declaration seam, config schema (#64165)
Additive plugin.yaml v2 fields (all optional; v1 manifests unchanged forever):

- manifest_version: manifest FILE-FORMAT version (absent = 1). Deliberately
  split from api_version per the round-2 design correction. Newer-than-
  supported versions load with a warning, unknown fields ignored.
- api_version: runtime plugin API generation the plugin targets (integer).
- requires_plugins: advisory inter-plugin deps ({id, version_range?}).
  Missing dep = warn + still load (ctx.has_plugin() runtime probe added).
  Load ORDER is dependency-respecting: graphlib topological sort, stable
  alphabetical tiebreak; cycles warn and fall back to alphabetical.
- python_dependencies: declared pip requirements — VALIDATED AND SURFACED
  ONLY (loader warning + install-time printout + doctor checks with a pip
  install hint). Never auto-installed: the isolation design for the install
  seam (#15220) is an explicitly deferred follow-up per the round-2 review.
- config_schema: JSON-schema-ish description of plugins.entries.<id>.settings
  keys; validated at load, mismatches are actionable warnings naming the key
  and expected type — never load failures.
- Formalized metadata: license, homepage, tags.
- Unknown manifest fields warn-don't-fail (debug-level for v1 manifests).
- hermes plugins doctor gains v2 checks: future manifest_version, invalid
  api_version, dep declarations, unpinned/missing python_dependencies,
  unknown config_schema types.
- Docs: manifest v2 reference table in the developer-guide plugins index,
  including the explicit pip-seam isolation deferral and the note that
  #64166 packs build on these fields.
- Tests: tests/hermes_cli/test_plugin_manifest_v2.py (19 tests) covering v1
  regression, v2 parse, unknown-field warn, dep order, cycle fallback,
  config_schema warnings, and the surfaced-not-installed pip seam.
2026-08-12 18:39:22 -07:00
Teknium 7a5062fbcd feat(plugins): add runtime-backed plugin Doctor
Validate plugin manifests, imports, hook signatures, and runtime registrations through the real plugin loader in an isolated temporary home.
2026-08-12 16:27:07 -07:00