Commit Graph

10 Commits

Author SHA1 Message Date
Teknium ff190a6462 Inspired by Copilot CLI: plugin auto-update at session start + update --all
Copilot CLI v1.0.79 added an autoUpdate marketplace setting that refreshes
plugins at session start. Hermes adaptation:

- hermes plugins update --all: sweep every git-installed plugin; pinned
  plugins and non-git dirs are skipped with a note instead of aborting.
- hermes plugins autoupdate <name> [on|off]: per-plugin opt-in flag stored
  in the install metadata sidecar (pinned/non-git plugins are rejected).
- Startup sweep: opted-in plugins are git-pulled on the background
  plugin-discovery thread AFTER discovery completes, throttled to once per
  24h via a stamp file. The running session keeps the code it already
  imported; updates take effect next session (stale bytecode cleared),
  so the live registry and prompt cache are never touched.
- Non-interactive updates leave newly declared capabilities ungranted
  (fail closed), same as the existing update path.
- Docs + 20 new tests (real-git E2E for pull/revision/bytecode/throttle).
2026-08-13 20:12:39 -07:00
Teknium 46e20083d8 feat(plugins): plugin packs — declarative, shareable plugin sets (#64166)
Adds hermes-pack.yaml: a single YAML file pinning a set of plugins to
exact 40-char commit SHAs with optional non-secret plugins.entries
config seeds and a declared (not yet installed) skills list.

CLI:
- hermes plugins pack install <path|https-url> [--force]: mandatory
  review screen (plugins + refs + declared capabilities), one summary
  confirmation, then fan-out through the existing pinned install path.
  Per-plugin capability consent rides the standard #64228 flow — a pack
  never bulk-grants. Partial failures reported per plugin; non-zero
  exit when any fail. Interactive only (no --yes in v1).
- hermes plugins pack export [--enabled-only] [--name]: pack YAML on
  stdout from install metadata (repo + exact SHA); local-only plugins
  become warning comments; secrets/capability grants stripped.
- hermes plugins pack show <path|url>: dry-run view.

Supply chain: refs must be exact 40-char SHAs (tags/branches rejected
naming the entry, same rule as the community index); config seeds
reject secret-shaped, capability, and allow_* keys; bare names resolve
through the community index; https-only URL fetch with size cap.

Tests: tests/hermes_cli/test_plugin_packs.py (36) — parse/validate,
SHA enforcement, mocked install fan-out, consent-per-plugin assertion,
export round-trip + sanitization, partial-failure exit code, parser
wiring. No live network.

Docs: user-guide plugins.md packs section (notes packs build on the
manifest v2 fields per #64165) + cli-commands.md rows.

Closes #64166
2026-08-12 19:56:44 -07:00
hans 17030939db feat(plugins): inter-plugin event bus with declared emits/listens
Give plugins a first-class, namespaced pub/sub event bus so plugin↔plugin
interaction is a declared, testable contract instead of ad-hoc imports.
Closes #64164 (sub-issue 03/14 of the plugin-interface expansion epic #64182).
Additive-only: when no plugin calls emit/subscribe, behavior is unchanged.

Interface (on PluginContext):
- `ctx.emit(event, payload=None) -> int` publishes to subscribers and returns
  the count invoked. The namespace is FORCED to the plugin's own registry key
  (`manifest.key or manifest.name`): pass only the bare event name, delivered
  as `<key>:<event>`. Fail-closed — any name containing `:` (a `hermes:`
  reserved-core prefix, a foreign `other:` namespace, or an own-colon'd name)
  is rejected with a ValueError + logged warning naming the plugin.
- `ctx.subscribe(full_event, callback)` registers an ordered listener for a
  fully-qualified `<plugin>:<event>`. Subscribing is unrestricted (any plugin
  may listen to any published event); only emitting is namespace-gated.

Delivery mirrors invoke_hook: registration-order iteration, per-callback
try/except isolation (one raising subscriber never breaks delivery to the
rest), payload passed as `cb(**payload)`. A per-thread depth counter caps
re-entrant emits at 8 — mutually-emitting plugins terminate cleanly with one
logged warning, never an infinite loop or RecursionError.

Discoverability: optional advisory `emits:`/`listens:` manifest fields (no
manifest-v2 dependency; not enforced) are parsed and surfaced by a new
`hermes plugins show <name>` (alias `info`) command. `get_plugin_subscriptions()`
module accessor mirrors `get_plugin_auxiliary_tasks()`.

Tests (tests/hermes_cli/test_plugin_event_bus.py, 22): two-plugin delivery +
listener count; forced namespace (delivered as `b:ping`); spoof rejection
(parametrized `hermes:x` / foreign / own-colon'd / `:x` / `x:` / empty) with
no delivery; name-fallback when key empty; per-callback isolation; recursion
cap termination + warning; manifest emits/listens parsed (present/absent/from
yaml); module accessor; `plugins show` output. `pytest test_plugin_event_bus.py
test_plugin_auxiliary_tasks.py` → 37 passed. I independently re-verified the
namespace rejection and recursion-cap termination outside the test suite.

Note: the reserved-name gate rejects any `:`-containing input rather than a
bare-name denylist — a bare `core_event` is allowed and delivered under the
plugin's own namespace. Say the word if a reserved bare-name list is wanted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-12 18:57:51 -07:00
Teknium 2e0183169c feat(plugins): community plugin index + hermes plugins search (#64181)
Static machine-readable community plugin index with fuzzy search and
index-resolved installs, mirroring the Skills Hub catalog pattern
(fetch → HERMES_HOME/cache with 24h TTL → bundled seed fallback).

- hermes_cli/plugin_index.py: index fetch/cache/seed chain, fuzzy
  search (name/description/tags/author + typo tolerance), capability
  filter, bare-name resolution. Canonical URL overridable via
  plugins.index_url config key.
- hermes_cli/data/plugin_index.json: bundled seed (offline fallback +
  format reference) with 5 real ecosystem plugins, each pinned to an
  exact commit SHA.
- hermes plugins search [term] [--json] [--capability] [--refresh]:
  Rich table or JSON output, offline-safe, with an explicit
  'indexed ≠ audited' footer.
- hermes plugins install <name>: bare names (no slash, no URL scheme)
  resolve through the index to owner/repo[/subdir] @ pinned ref and
  hand off to the existing install path (ref wired through the #82029
  exact-ref support). Ambiguous names list candidates and exit;
  explicit owner/repo and Git URL installs are untouched, and an
  explicit --ref always beats the index pin.
- Docs: discovery section in user-guide plugins.md (format, submission
  workflow via PR to hermes-plugin-index, security framing) and
  reference/cli-commands.md rows.
- Tests: tests/hermes_cli/test_plugin_index_search.py (38 tests, no
  live network) covering parsing, search, remote→cache→seed fallback,
  TTL, install resolution/ambiguity/passthrough, and --json output.
2026-08-12 18:52:08 -07:00
Teknium b088535c78 feat(plugins): capability declarations + install/update consent flow (#64228)
Unify the scattered per-plugin trust gates into one declared, diffable
capability model with an install/update-time consent flow. Consent +
audit over host API surfaces — explicitly NOT a sandbox.

New module hermes_cli/plugin_capabilities.py:
- Canonical CAPABILITY_REGISTRY mapping each capability id 1:1 to an
  EXISTING enforcing gate (no capability minted without a surface):
    tools.override          -> allow_tool_override
    llm.provider_override   -> llm.allow_provider_override
    llm.model_override      -> llm.allow_model_override
    llm.agent_id_override   -> llm.allow_agent_id_override
    llm.profile_override    -> llm.allow_profile_override
    llm.task_override       -> llm.allow_task_override
- plugin_capability_granted(plugin_id, capability): canonical check —
  granted set OR deprecated legacy allow_* key; fail closed on unknown
  ids and any unreadable/corrupt consent state; emits checked_by audit
  log lines on every decision.
- record_consent() persists plugins.entries.<id>.granted_capabilities +
  capabilities_consent {hash, granted_at} and mirrors grants into the
  legacy keys so existing enforcement sites keep working unchanged.
- capability_set_hash / pending_capabilities / declared_set_changed
  power the update-time re-consent diff.

Wiring:
- plugin.yaml manifest field `capabilities:` parsed into
  PluginManifest.capabilities (unknown ids dropped with a warning).
- hermes plugins install: consent screen (one Y/n) when the manifest
  declares capabilities; non-interactive installs proceed with
  capabilities ungranted (fail closed).
- hermes plugins update: when the new version declares capabilities the
  granted set lacks (hash diff), the additions are surfaced and require
  re-consent — an update can never silently widen access.
- hermes plugins enable: consent screen replaces the standalone
  tool-override prompt for capability-declaring plugins.
- hermes plugins capabilities [<id>]: declared vs granted per plugin,
  flags grants held via deprecated legacy keys.
- PluginContext.has_capability() probing API so plugins degrade
  gracefully; _tool_override_allowed migrated to the canonical
  plugin_capability_granted path (reference migration; legacy
  allow_tool_override still honored).

Tests: tests/hermes_cli/test_plugin_capabilities.py (38 tests) —
declaration parsing, consent grant/persist, update re-consent on added
capability, fail-closed on missing/corrupt state, legacy-gate backward
compat, consent CLI flow (grant / decline / non-interactive).

Docs: user-guide plugins.md consent section (with explicit not-a-sandbox
warning) + developer-guide plugin authoring capability note.

Salvages the intent of PR #37976 (@coygeek — require renewed review
before plugin updates), scoped to capability diffs.

Part of #64182.
2026-08-12 18:05:21 -07:00
Teknium 6601330e0a feat(plugins): install exact commit refs 2026-08-12 16:27:30 -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
GodsBoy ca78c6d7a6 feat(plugins): load portable agent components 2026-08-07 09:44:21 -07:00
teknium1 bff61f558f feat(plugins): enable-time consent prompt for tool_override grant
Builds on memosr's sink-level opt-in gate (#29249). Enabling a
non-bundled plugin now surfaces the privileged allow_tool_override
decision at `hermes plugins enable` time instead of leaving the
operator to discover the config key after a runtime rejection.

- `hermes plugins enable <name>` prompts for non-bundled plugins:
  'Allow this plugin to replace built-in tools?' Default is deny
  (blank Enter / non-interactive stdin / EOF all fail closed).
- --allow-tool-override / --no-allow-tool-override flags for
  non-interactive and scripted use (and a future desktop checkbox).
- Bundled plugins are trusted: never prompted, no entry written.
- Writes plugins.entries.<key>.allow_tool_override, the same key the
  sink gate reads (manifest.key == discovery key), so consent and
  enforcement compose end to end.
2026-06-30 04:00:42 -07:00
teknium1 1a626470ca refactor(cli): promote 9 closure handlers to top-level + extract their parsers (god-file Phase 2 follow-up)
Subcommands whose handler was a closure defined inside main() — memory, acp,
tools, insights, skills, pairing, plugins, mcp, claw — have their handler
promoted to a top-level function and their parser block extracted into
hermes_cli/subcommands/<name>.py (build_<name>_parser, injected handler).

These 9 had zero closure-over-main-locals, so promotion is a pure relocation.
acp/mcp parser blocks use the shared add_accept_hooks_flag helper.

main() 1798 -> 954 LOC (71% below the 3297 Phase-2 starting point);
add_parser calls in main.py 89 -> 28.

Deferred: sessions, computer-use, secrets handlers reference <name>_parser
(for a no-subcommand print_help fallback) — left in place to avoid the
_self_parser indirection; minority, low value.

Behavior-neutral: all 9 subcommands' --help (incl nested subactions) byte-
identical to pre-extraction (diff-verified). tests/hermes_cli/ 6519 passed /
0 failed; new test_subcommands_followup.py covers the 9 builders.
2026-06-07 22:56:23 -07:00