Commit Graph

5 Commits

Author SHA1 Message Date
teknium1
358d50ca6d fix(plugins): plugin dependencies follow the plugin's own security policy
Hermes's 14-day `[tool.uv] exclude-newer` quarantine applies to Hermes's own
dependencies only (uv lock/sync, `hermes update`, LAZY_DEPS extras via
`ensure()`). A plugin's declared `python_dependencies` install under the
PLUGIN's policy: `install_specs(policy="plugin")` runs uv with `--no-config`
from any cwd, still inside the core constraints file.

Reverses item 3 of #118841, which ran the uv tier with cwd=<checkout> for
every install so the quarantine reached plugin deps from any cwd. That made
catalog re-pins floored on a <14-day release uninstallable (#120076:
"only hindsight-client<=0.9.2 is available"; #114530 held on the same gate).

Maintainer ruling (Teknium): "plugins dont have to abide by our 14 day rule
btw. They can have their own security policy on that. Only hermes'
dependencies themselves have to. We should recommend that they do this for
their plugins and we should give guidance to plugin devs that they should
though."

- tools/lazy_deps.py: INSTALL_POLICIES ("core" | "plugin"); `_uv_policy_args`
  replaces `_uv_policy_cwd`; `_venv_pip_install(policy=)` defaults to core
  (ensure/LAZY_DEPS), `install_specs(policy=)` defaults to plugin.
- hermes_cli/plugin_python_deps.py: `resolve()` passes policy="plugin".
- Docs: developer guide "Dependency security policy" section, catalog README
  admission rule 9, AGENTS.md pinning policy — plugin authors are responsible
  for their deps and strongly recommended to pin upper bounds, floor on the
  oldest API-compatible version and run their own release quarantine
  (`uv --exclude-newer` in their CI); operators can set UV_EXCLUDE_NEWER.
- Tests: the #118841 cwd test is replaced by two invariants — a plugin install
  carries `--no-config` and no checkout cwd (red on base), a core lazy install
  keeps the checkout cwd and no `--no-config`.
2026-09-23 10:07:51 -07:00
Victor Kyriazakos
4c342c05de feat(gateway): stop, start and restart one profile under the host multiplexer
Under the host multiplexer every installed named profile came online because
its directory existed, and the only way to stop one profile's bots was to stop
the host, which stopped everyone's. Both are fleet-operator blockers.

`hermes -p X gateway stop` on a profile served by the host now parks it: it
writes `profiles/X/gateway.parked` first (so the 30s reconcile cannot re-add X
between the verb and the marker) and sends the new `unserve-profile` control
verb, which tears down X's adapters, reconnects and cron inside X's own scope
(the teardown `_unserve_profile` already used for deleted profiles). `start`
removes the marker and sends `serve-profile`, which runs the same add-path the
reconcile loop uses. `restart` cycles both without parking. The default
profile keeps today's whole-host meaning.

`profiles_to_serve()` skips parked profiles, so adapters, cron, ingress
membership and the served record follow from one chokepoint; roster callers
that mean "every installed profile" (plugin deps, Windows update, dashboard
listing and topology, migration inventory) pass `include_parked=True`.
Provisioning may pre-create the marker: an installed profile stays offline
until an operator starts it. Host boot logs one INFO per parked profile;
`gateway status` shows `parked (hermes -p X gateway start)`.

Tests: profiles_to_serve contract, control verbs through the runner
(round-trip and every refusal), CLI marker-before-socket ordering, reconcile
honouring the marker both ways, migration inventory retaining parked
profiles, two-home E2E through the real loaders.
2026-09-23 08:25:28 -07:00
Victor Kyriazakos
0238c9d740 feat(gateway): gateway.standalone opts a named profile out of the host multiplexer
A named profile that authors `gateway.standalone: true` in its own config.yaml
runs its own gateway again, the pre-multiplex topology, while the default
gateway keeps serving every other profile. Topology becomes something the
operator authors per profile instead of something the box infers from boot
state, which is what a fleet running per-profile gateways lost when
`gateway.multiplex_profiles: false` was retired.

Changed
- hermes_cli/profiles.py: `profile_is_standalone(home)` reads the profile's
  own config.yaml (memo by file signature, tolerant of malformed yaml, always
  False for the default profile with one warning). `profiles_to_serve()`
  excludes standalone profiles; roster callers that mean "every installed
  profile" (plugin deps, Windows update, launch policy, dashboard listing and
  topology) pass `include_standalone=True`.
- gateway/host_attach.py: `standalone_attach_decision` starts a standalone
  profile's gateway beside the host multiplexer once every live gateway
  confirms it does not serve that profile; refuses with a rescan message while
  one still does. Used by the initial attach check and the lock-losing race.
- hermes_cli/gateway_multiplex_mode.py: a standalone launcher never becomes
  the host multiplexer (`STANDALONE_PROFILE_REASON`), including callers that
  supply an explicit GatewayConfig.
- hermes_cli/gateway.py, web_server_gateway.py: `hermes -p X gateway
  install/start/run` proceeds without --force for a standalone profile; the
  refusal text for other profiles points at the opt-out; status shows
  "standalone (gateway.standalone: true)" and the default lists skipped
  profiles.
- gateway/run_profile_reconcile.py: the host does not re-adopt a profile whose
  own gateway is live (removing the key while it runs no longer double-binds).
- hermes_cli/gateway_migrate.py: standalone profiles are neither blocker nor
  fold target; the plan lists them as "standalone by config".
- gateway/run.py: one INFO line per standalone profile at host boot.

Tests: two-home E2E through real loaders and resolve_multiplex_mode, decide()
with fake host records for both arms, lock-losing branch, reconcile guard,
migrate plan, refusal predicate both ways, topology, memo and malformed-yaml
contracts. All red on base.
2026-09-22 19:55:02 -07:00
teknium1
a08dee94c1 fix: read plugin-home config through the loader; register update export
The config-read guard forbids raw yaml loads of config.yaml outside owner
modules, and hermes_cli.main's frozen lazy-export surface must list every
update_cmd symbol it proxies.
2026-09-17 00:11:27 -07:00
teknium1
96e8a23222 feat(plugins): install declared Python dependencies and re-apply them after hermes update
A directory plugin's pyproject.toml [project].dependencies (or manifest
python_dependencies) are now installed into the Hermes venv on install/enable/
update, resolved under a constraints file built from Hermes' own pinned
dependencies together with every enabled peer's declarations. A candidate that
cannot resolve is refused before its tree is moved into place; nothing is
installed and no other plugin is touched. --no-deps opts a single install out.

hermes update rebuilds the venv from Hermes' lock and strips everything else, so
its git, zip and both repair paths now re-apply the union of every profile's
enabled plugins. When the union no longer resolves, each plugin is resolved on
its own so only culprits are dropped (never an alphabetical neighbour); a
plugin-vs-plugin conflict peels non-memory plugins first because a Hermes that
boots without memory reads as data loss. Dropped plugins are disabled through
the real config writer with a loud message naming the fix.

python_runtime: external lets sidecar-venv plugins (Mnemosyne's shape) opt out
of the union; hermes-agent self-dependencies and direct-URL requirements are
never installed (the latter are surfaced for the user to install by hand).
2026-09-17 00:11:27 -07:00