Commit Graph

129 Commits

Author SHA1 Message Date
Teknium 3b7c940208 feat(gateway): more normalized gateway_platform_event types (#64176)
Extend the normalized-envelope pipeline shipped in #82063 with new event
types, each with its own versioned, event-local payload contract:

- Telegram: message_edited (edited_message updates; editor-identity auth
  extraction, forum topic thread_id, bounded text/caption, ISO edited_at)
- Discord: message_edited, message_deleted, thread_created, thread_renamed
  (on_message_edit/delete, on_thread_create/update fire-sites with has_hook
  no-subscriber fast-paths, bot-authored events dropped, rename-only
  filtering on thread updates)

All events flow through the same gateway-owned post-auth boundary; malformed
or unauthorized events drop, fail closed. Raw SDK payload access is
deliberately NOT shipped (round-2 correction: needs its own
gateway.raw_events capability and design).

The Discord fire-site machinery (no-subscriber fast-path, observer isolation,
connect-time wiring) adapts the observer-hook design from PR #62584
(@paoloantinori) onto the normalized-envelope contract; PR #36875's raw
telegram update hook is superseded by the same correction.

Docs: hooks.md gains per-event payload contract tables.

Co-authored-by: Paolo Antinori <pantinor@redhat.com>
2026-08-12 20:10:51 -07:00
Bartok9 8be9c76f8c fix(plugins): hook delivery parity + symmetric force-reload (#64178)
Salvaged from PR #64188 (@Bartok9), re-reviewed against the #64229
ownership ledger (landed in #84923).

Delivery parity (survived):
- Module-level invoke_hook/invoke_middleware/has_hook/has_middleware
  lazily run plugin discovery via _delivery_manager(), so surfaces that
  never import model_tools (dashboards, TUI slash workers, query mode,
  cron, gateway platform events) deliver plugin callbacks instead of
  silently dropping them (#50776, #67597, #67890, #50937).
- _delivery_manager() joins any in-flight background discovery first and
  tolerates test doubles that monkeypatch get_plugin_manager().

Symmetric force-reload (survived):
- agent/shell_hooks.py gains re_register_config_hooks(); the force branch
  of discover_and_load() calls it after a successful sweep, restoring
  config.yaml shell hooks that the ledger-driven unload wiped but cannot
  restore (they are config-owned, not plugin-owned) (#60036).
- unload(plugin=None) now sweeps pre-ledger _plugin_tool_names entries
  out of the process-global tools.registry, mirroring the platform-name
  sweep that already existed, so zombie tools cannot survive a force
  reload in long-lived pre-ledger processes (#60050).

Superseded by the ownership ledger (dropped from #64188):
- _unload_global_plugin_registrations() bulk tool/platform teardown —
  the ledger's reverse-order handle disposal with previous-entry
  restoration covers it more precisely.
- tools.registry/platform_registry displaced-entry LIFO restore stacks —
  the ledger's restore_registration() identity-checked previous-entry
  restoration made them redundant.
- Discovery serialization lock + double-checked singleton — main already
  has _discovery_lock on every discover/unload path and a keyed,
  lock-guarded per-home manager cache (#24714 concern is covered).

Fixes tracked under #64178 (#50776, #60036, #60050, #24714, #67798,
#50937, #67597, #67890, #31480#31480 already handled on main by
_parse_hooks_block warn+suggest).
2026-08-12 19:40:59 -07:00
Victor Kyriazakos 15959d8259 fix(observability): forward Hermes session id on approval hooks
Approval marks were emitted under a synthetic 'default' relay session:
the hook payload carried only turn_id/tool_call_id, so the observability
plugin's _session_id() fell back to 'default', parenting approval marks
to a session scope that never closes — and close-time exporters never
shipped them. The audit board's approval tables stayed empty while
approvals were demonstrably firing (staging 2026-08-10).

Bind session_id in set_current_observability_context at both dispatch
sites (model_tools tool dispatch, plugins pre-tool-call approval gate)
and forward it on every approval hook. Explicit session_id in a hook
payload still wins; unbound contexts omit it (legacy behavior).
2026-08-12 19:20:03 -07:00
Teknium 11310068c6 feat(plugins): pre_command observer hook + capability-gated ctx.call_mcp (#64204)
Part A — pre_command observer hook (observer-first per #64182 ground rule 3):
- New VALID_HOOKS event `pre_command`: fires when a recognized slash command
  is about to be dispatched, BEFORE the handler runs, on both surfaces:
  - CLI: cli.py process_command (right after alias resolution)
  - Gateway: gateway/run.py _handle_message cold-path canonical dispatch
- Payload: surface ('cli'|'gateway'), command (canonical), alias_used,
  args_raw, session_key, platform. Return values IGNORED in v1; a plugin
  returning a directive-shaped dict gets a debug log so future
  block/rewrite adopters are discoverable (#64231 taxonomy).
- Deliberately NOT fired on the gateway running-agent intercept path
  (/stop, /approve, busy_policy dispatch during an active run): those are
  control-plane escape hatches on an in-flight run and must stay outside
  plugin observation/veto reach.
- fire_pre_command_hook() helper never raises, so broken plugin infra can
  never break command dispatch.

Part B — ctx.call_mcp (capability-gated, default-off, ground rule 4):
- PluginContext.call_mcp(server, tool, arguments, timeout=30): synchronous,
  callable from plugin hooks/tools, routes through the EXISTING native MCP
  client machinery (tools.mcp_tool._make_tool_handler: background loop,
  trust-tier gates, circuit breaker, reconnect) — never a parallel client.
- Gate: plugins.entries.<id>.mcp_allowlist (list of server names).
  Absent key / unreadable config / non-list value => default-deny.
  Unlisted server raises PermissionError naming the exact config key.
  TODO seam left for the #64228 declared-capability model.
- Bounded: timeout clamped to 1-600s and forwarded to the MCP loop call;
  results capped at 64KB with truncation marker; stable
  {ok, result|error, structuredContent?, truncated?} envelope.

Tests (transport mocked, no live MCP servers):
- tests/hermes_cli/test_pre_command_hook.py: both surfaces fire, canonical
  alias reporting (/exit->quit, /q->queue), hook-before-handler ordering,
  control-plane exclusion, hook failure non-fatal, observer-only directive
  handling.
- tests/hermes_cli/test_plugin_call_mcp.py: default-deny (absent entry,
  unreadable config, non-list, '*'), allowlist enforced per-server,
  denied calls never touch transport, timeout forwarding/clamping,
  result truncation, error/structuredContent envelopes.

Docs: hooks.md shipped-catalog row for pre_command; plugins.md
"Calling MCP servers from plugins" section with the security note.

Closes #64204
2026-08-12 19:16:59 -07:00
Teknium 4be8bd0816 ci: retrigger — previous pull_request run failed with zero jobs (transient workflow materialization) 2026-08-12 19:13:32 -07:00
Teknium b9542f8e1f fix(plugins): preserve force-path platform sweep + scoped plugin-source listing after rebase
Rebase over the capability-model merge dropped two behaviors the tests
pin: (1) unload_all must still unregister every _plugin_platform_names
entry from the global platform registry (pre-ledger state has no
handles); (2) list_plugin_sources() must see profile-scoped
registrations — scoped entries are plugin-registered by definition.
2026-08-12 19:13:32 -07:00
Teknium 2219747990 feat(plugins): widen ownership ledger to all registration surfaces
Extends the salvaged #64229 ledger (PR #76490) to cover the registries
added on main since the PR was cut, and lands the remaining Phase 0
lifecycle pieces:

- register_system_prompt_section and register_approval_transport now
  record ownership handles, so unload/force-reload removes plugin
  system prompt sections and approval transports too
- ctx.on_unload(callback): plugin cleanup callbacks run through the
  reverse-order ledger walk, exception-isolated
- ctx.spawn_task(coro): supervised background asyncio tasks tracked in
  the ledger and cancelled on unload
- document the #65593 multi-profile constraint on the ledger (keyed per
  manager/(hermes_home, plugin_id); identity-conditional restores) with
  a TODO(#64178) for full profile keying of remaining global slots

Part of #64229; prerequisite for #64178.
2026-08-12 19:13:32 -07:00
doncazper 85020f2238 fix(plugins): isolate ownership by profile 2026-08-12 19:13:32 -07:00
terry197913 4e1b2e436c fix: scope plugin manager by resolved hermes home (keyed cache)
fix: remove .codegraph artifacts from commit
2026-08-12 19:13:32 -07:00
doncazper 22af80bcfd feat(plugins): add ownership ledger unload lifecycle 2026-08-12 19:13:32 -07:00
Hans 52eb8eb533 feat(plugins): add pre_transcription hook and STT prompt threading
Adds a pre_transcription transform hook (prompt/language/model mutable,
file_path read-only, last-writer-wins per the transform_* convention)
fired before any STT backend, threads prompt to faster-whisper
(initial_prompt) and OpenAI/Groq/Mistral/DeepInfra (prompt), adds an
optional stt.prompt config key on the same plumbing, and keeps the
no-hook dispatch path byte-identical. Fixes #64168.

Documents the new surface for users: a "Transcription prompt
(vocabulary hints)" subsection in the configuration guide (composition
order, per-provider support matrix, length contract, privacy warning),
a pre_transcription entry in the hooks reference, and the mirrored row
in the plugins hook table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AG6LyYMvHC2o6HbVUozmVR
2026-08-12 19:01:30 -07:00
Hans 67168a391f fix(plugins): bound event delivery and own subscriptions 2026-08-12 18:57:51 -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
webdevtodayjason fdd45323bf feat(plugins): redaction pattern registry — vendor token formats as plugins
Every new vendor token format has required a core PR appending to
_PREFIX_PATTERNS in agent/redact.py (fw_, retaindb_, hsk-, mem0_, brv_
all landed that way; #58466/#58501 are the latest of the class). This
adds an additive-only registry so provider plugins own their format:

- agent/redact.py: register_redaction_patterns(patterns, source) —
  validates each pattern (must compile; must start with >=2 literal
  characters so the pre-screen substring gate keeps working and
  redact-everything patterns like `.*` are structurally impossible),
  dedupes against built-ins and prior registrations, then atomically
  rebuilds _PREFIX_RE and _PREFIX_SUBSTRINGS. Registered patterns get
  identical treatment to built-ins everywhere: same head/tail masking,
  same non-reusable «redacted:label…» sentinel on file_read, same
  security.redact_secrets operator opt-out. Additive-only by design —
  a plugin can extend masking, never weaken it. Includes a
  test/teardown reset helper.
- hermes_cli/plugins.py: PluginContext.register_redaction_patterns()
  delegating with per-plugin attribution; warns and returns 0 on any
  failure so a broken plugin can never break startup.
- Bundled reference plugin `nvapi-redaction` (opt-in): masks NVIDIA
  API keys (nvapi-, used by NIM / build.nvidia.com) — a real format
  missing from core, shipped as the one-liner plugin that previously
  would have been a one-line core PR.

13 new tests: baseline gap, masking + built-ins unaffected, invalid
regex / no-literal-prefix / dedupe / non-string rejection, file_read
sentinel labeling, reset semantics, PluginContext wiring incl.
exception isolation, and a no-mocks end-to-end through the demo
plugin. Existing redaction suites (tests/agent/test_redact.py,
tests/tools/test_kanban_redaction.py) pass untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWMcB7RPSYUpsXDfBgwjzM
2026-08-12 18:55:14 -07:00
zccyman b85e5bb4ba feat(plugins): allow plugins to register custom @-prefix context references
Closes #26193

Adds ContextReferenceProvider ABC so plugins can register custom
@-prefixes (e.g. @issue:ENG-123) with autocomplete and expansion.
Plugin output flows through existing token-limit guards. Zero
breaking changes.
2026-08-12 18:41:59 -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
Dineth Hettiarachchi 00f4da01ec feat(plugins): add streaming output observer hooks
Salvage of PR #64317 (@deaneeth) onto current main, implementing #64161:
observer-only on_stream_start / on_stream_delta / on_stream_end /
on_interim_message plugin hooks dispatched through a host-owned bounded
queue (one worker per callback) so plugin callbacks never run inline on
the token path. Reasoning deltas are opt-in via
plugins.stream_reasoning_deltas.
2026-08-12 18:38:25 -07:00
GodsBoy f46c600a54 feat(gateway): allow plugins to inject session messages 2026-08-12 18:25:33 -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 9994bc9ec9 fix(gateway): enforce post-auth normalized reaction observer
Builds on Paolo Antinori's #68431 salvage for #64176. Move plugin dispatch behind the profile-scoped runner authorization boundary, fail closed on malformed reaction identities, preserve observer registration across Telegram app rebuilds, and document the deliberately observer-only contract.

Co-authored-by: Paolo Antinori <pantinor@redhat.com>
2026-08-12 16:42:28 -07:00
Paolo Antinori c0a4535a26 fix(plugins): address #64176 review on gateway_platform_event (#68431)
Response to teknium1's hermes-sweeper review (keep_open, salvageability=medium).

1. Post-auth gate. The group-99 catch-all fired gateway_platform_event before
   the authorization boundary. Extract _is_source_authorized(source) from
   _is_user_authorized_from_message and add _source_from_reaction_for_auth;
   reactions whose actor the intake would reject no longer reach plugins.
   Fails closed if source extraction raises, so a future non-reaction event
   type cannot silently bypass auth before its own extraction is wired.

2. Shared registration. Extract _register_handlers(app) from connect() so the
   gateway_platform_event observer (group 99) is re-registered alongside the
   core handlers on any rebuild path.

3. Trim inert hook surface. Drop the three reserved gateway_* names from
   VALID_HOOKS (keep only gateway_platform_event). The others land with their
   real contracts and fire-sites when #64231 is finalized.

Tests: unauthorized/authorized/open reaction gating, fail-closed for a future
non-reaction event type, and _register_handlers re-registration.

Ran /simplify and /code-review (high) before pushing.
2026-08-12 16:42:28 -07:00
Paolo Antinori 929be4d1aa feat(plugins): gateway_platform_event observer hook (normalized envelopes)
First slice of #64176's observer-hook half — a normalized-envelope inbound
event hook, replacing raw-SDK handler args with a stable contract (per #64176's
"normalized versioned envelopes only; raw SDK gated behind a capability" rule).

- VALID_HOOKS: register the four gateway_* names from #64176
  (gateway_platform_event fires today; gateway_session_titled /
  gateway_message_delivered / gateway_thread_created reserved pending #64176's
  fire-sites).
- BasePlatformAdapter._fire_gateway_hook: reusable, has_hook-guarded,
  per-call-isolated fire helper (the no-subscriber common case short-circuits).
- TelegramAdapter: a group-99 catch-all TypeHandler normalizes inbound updates
  into gateway_platform_event envelopes. message_reaction -> {platform,
  event_type:"reaction", payload{emojis, custom_emoji_ids, chat_id, message_id,
  thread_id}} (custom-emoji reactions captured via custom_emoji_id; standard via
  .emoji — no None in consumer-facing lists). Other update types return None
  pending #64176's taxonomy (#64231). Normalization is wrapped so a malformed
  update can't raise into PTB dispatch.

Observer-only — zero behavioral change to core dispatch. Supersedes the raw
inbound half of #62584 (telegram:update -> normalized gateway_platform_event).

Tests: VALID_HOOKS registration; _fire_gateway_hook routing/has_hook/isolation;
_normalize for standard, custom-emoji, and mixed reactions + non-reaction;
_on_platform_update firing + normalize-error isolation.

Ran code-review (high) + simplify before pushing.
2026-08-12 16:42:28 -07:00
SeoYeonKim 9acf0db889 feat(plugins): add cache-safe system prompt sections
Salvage the plugin-owned static prompt idea from PR #51589 into the constrained #64167 contract: stable IDs, deterministic placement, bounded fail-open rendering, and full-prompt resume recovery without new session columns.

Co-authored-by: Topher Ross <biz@topherross.com>
2026-08-12 16:34:58 -07:00
Teknium d409f67485 feat(platforms): add typed plugin send paths
Route plugin target parsing, validation, and host-driven delivery through PlatformEntry across CLI and cron while preserving the host-only send_message policy.
2026-08-12 16:27:19 -07:00
Kevin Anderson 274214d3c9 fix(send_message): avoid shared schema mutation and support sync enricher handlers 2026-08-12 16:27:19 -07:00
Kevin Anderson 482682db78 send_message: plugin enricher registry for custom platforms 2026-08-12 16:27:19 -07:00
Teknium de56e49a7c feat(plugins): add approval transport interface 2026-08-12 16:26:55 -07:00
Teknium 6bf93c0e38 feat(plugins): add namespaced config and durable state bridge 2026-08-12 16:26:43 -07:00
Teknium 729b8a7169 test(plugins): enforce behavior compatibility contract 2026-08-12 16:25:29 -07:00
hans 1176222b7c feat(plugins): route ctx.llm.complete(task=) through registered aux slots
Wire an optional `task=<key>` kwarg through the PluginLlm facade so a
plugin can route an LLM call through an auxiliary model slot it
registered via `ctx.register_auxiliary_task`. Registration already
existed; this adds the missing consumption half. Closes #44673.
Sub-issue 08/14 of the plugin-interface expansion tracking issue #64182.

- New optional `task:` kwarg on complete/acomplete/complete_structured/
  acomplete_structured. Unset or "auto" keeps today's main-model path
  byte-for-byte (task=None reaches call_llm exactly as before), so no
  prompt-cache or default-behavior change.
- A set task resolves provider/model through `auxiliary.<task>` via the
  existing auxiliary_client path, identical to built-in aux tasks.
- Trust gate (per the round-2 design correction): a plugin may only pass
  a key it registered itself; a built-in key additionally requires
  `plugins.entries.<id>.llm.allow_task_override: true`. A foreign or
  unknown key is rejected with a PluginLlmTrustError and a logged warning
  naming the offending plugin and key -- fail loud, NOT a silent fallback
  to auto (which would mask misconfiguration and could route to the main
  model the user steered elsewhere).
- The plugin_llm audit dict and audit-log line gain a `task` field.
- register_auxiliary_task now stores the plugin's canonical id
  (`key or name`, the same id ctx.llm is bound to) as the slot owner, so
  the trust gate matches ownership even when a manifest sets a distinct
  key. For the common no-key case this equals the name (unchanged).

Tests (tests/agent/test_plugin_llm_task_routing.py, 24): _check_task
resolution incl. own/foreign/unknown/built-in-gated keys and loud
rejection; end-to-end routing sync+async+structured; production-path
forwarding into call_llm/async_call_llm (covers the task=None->task line);
and ownership resolution against the real plugin registry incl. the
name/key reconciliation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-12 16:25:20 -07:00
Teknium e0bb71cb73 fix: track secret source registration origin 2026-08-12 16:25:10 -07:00
Bartok9 2e29de2296 fix(plugins): delegate secret-source enablement to is_enabled contract (#64177)
Address teknium1 review on #64189:
- Re-pull gate now delegates to each source's is_enabled(cfg) via the
  registry contract, so a plugin source with custom activation logic is
  honored (previously only secrets.<name>.enabled was checked).
- Add BUILTIN_SOURCE_NAMES to the registry so plugin-vs-bundled is a
  single source of truth instead of a hard-coded set at the call site.
- Reconcile docs: rewrite the timing :::note to describe both the
  post-discovery re-pull and the remaining import-time limitation, and
  cross-link the first-process bootstrap section.
- Tests: real SecretSource subclasses, custom is_enabled activation
  (positive + negative), is_enabled-raises skip, builtin-only no-op,
  and a discovery-registration end-to-end re-pull check.
2026-08-12 16:25:10 -07:00
Bartok9 7a7e73d310 fix(plugins): re-pull plugin secret sources after discovery (#64177)
After plugins register SecretSource backends, reset the env-loader cache
and re-run load_hermes_dotenv when an enabled plugin secret source is
configured. Closes the first-process bootstrap gap where import-time env
load stale-outs plugin vaults (tommck / Community ask). Fail-open, no-op
without plugin sources.

Docs: first-process bootstrap timing on secret-source plugin guide.
Tests: unit coverage for noop / enabled re-pull / discover hook.
Part of #64182 plugin-interface expansion.
2026-08-12 16:25:10 -07:00
Teknium 55f9e472a0 perf(cli): sub-400ms warm startup — probe-mode check_fns, lazy MCP SDK, banner snapshot, parallel worktree add
Cold CLI time-to-banner was ~1.8s (hermes) / ~2.8s (hermes -w). The banner
path was paying for work the session doesn't need before first input:

- aux availability probes built REAL OpenAI/httpx clients (openai import
  ~0.3s + SSL context) just to answer check_fns. New aux_probe_mode()
  returns a cache-excluded stub; resolution policy unchanged.
- tools/mcp_tool imported the mcp SDK (~260ms, mcp.types pydantic model
  construction) at module import even with zero MCP servers configured.
  SDK import is now lazy behind _ensure_mcp_sdk(); _MCP_AVAILABLE is a
  find_spec probe so every existing gate/test keeps its semantics.
- banner blocked 500ms on the update-check prefetch; now waits 50ms and
  defers the warning line to a daemon thread (prints above the prompt).
- banner recomputed get_tool_definitions + skills scan + git state every
  launch; now snapshotted to ~/.hermes/cache/banner_snapshot.json keyed on
  (config.yaml, .env, checkout rev, toolsets) and replayed on warm launches
  with a background refresh. Agent tool list is still computed fresh.
- _resolve_active_context_length probed the Nous portal /models (~200ms
  network) per launch; the tool-search gate now prefers the on-disk
  context cache when present.
- schema reconciliation re-executed SCHEMA_SQL in a scratch SQLite DB
  (~85ms) per SessionDB(); the reference parse is now disk-memoized by
  DDL hash (live-DB diffing still runs every startup).
- bundled-skills sync (~120-170ms rglob/hash) moved off the startup path
  to a daemon thread; plugin discovery starts in the background and every
  synchronous consumer joins via discover_plugins().
- hermes_cli.auth imported httpx eagerly (~30ms); now a lazy proxy that
  test monkeypatching still reaches (setattr forwards to the real module).
- fast chat launch: unambiguous 'hermes'/'hermes chat' invocations skip
  building all ~40 subcommand parsers (bails to full dispatch on anything
  else, incl. container mode).
- -w path: git worktree add runs with checkout.workers=8 (0.6s→0.2s) and
  overlaps HermesCLI construction; --skills preload runs in the background
  and is folded in at agent init (finalize_preloaded_skills, same
  fail-loud contract for fully-unknown skill lists); stale-worktree prune
  moved off the banner path.

Warm results (PTY time-to-banner, 5-run): hermes 1.80s → 0.38-0.40s;
hermes -w -s hermes-agent-dev --yolo 2.82s → 0.57-0.69s.
2026-08-10 10:40:19 -07:00
GodsBoy 8cb066404e fix(plugins): address portable MCP review feedback 2026-08-07 09:44:21 -07:00
GodsBoy e288d93fc1 fix(review): harden portable plugin boundaries 2026-08-07 09:44:21 -07:00
GodsBoy ca78c6d7a6 feat(plugins): load portable agent components 2026-08-07 09:44:21 -07:00
kshitij 0d32607c62 fix(gateway): split check_fn (passive probe) from ensure_deps_fn (active installer)
PlatformEntry.check_fn served three contradictory roles: adapter-creation
gate, config auto-enablement gate, and status display. Plugins had to pick
one function for all three:

- Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/
  feishu): every status display could pip-install SDKs as a side effect
  (the desktop 94% boot-loop class).
- Passive probe as check_fn (teams, wecom_callback): create_adapter()
  returned None before connect() could lazy-install, so the SDK never
  installed (#79812 deadlock; wecom_callback's platform.wecom_callback
  LAZY_DEPS entry was dead code).

The split makes both call sites correct by construction:

- check_fn is now contractually PASSIVE (probe only, never installs).
- New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer;
  create_adapter() runs it exactly when check_fn is False — the platform
  is enabled+configured and the gateway is about to connect it.
- Config enablement keeps a configured platform whose deps are missing
  but installable; the install itself is deferred to create_adapter().
- Status surfaces (_platform_status, hermes status) read only the
  passive probe and can never trigger pip.

Migrated all lazy-installable platform plugins to the split; platforms
with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged
— no ensure_deps_fn means a False check_fn stays a hard block.
wecom_callback gains a working installer for the first time.

Builds on @xxxigm's #79812 (both commits cherry-picked with authorship
preserved), reworking the check_fn swap into the two-field split so the
Teams fix doesn't reintroduce install-on-status.
2026-08-07 13:28:43 +05:30
Alex Fournier f1fd678e44 feat(observability): add Relay skill metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:34 -07:00
Alex Fournier 8b0c3da8c0 feat(observability): aggregate bounded tool metrics 2026-07-29 11:25:55 -07:00
Alex Fournier 14bed44c8c Reapply "feat(observability): integrate NeMo Relay runtime and shared metrics"
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 21:10:51 -07:00
Jeffrey Quesnelle 841a5a744a
Revert "feat(observability): integrate NeMo Relay runtime and shared metrics" 2026-07-27 22:28:08 -04:00
Alex Fournier 4fe4b0dca7 Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-27 08:12:37 -07:00
Tony Simons f60abd6e37 fix subagent lifecycle ownership invariants 2026-07-26 23:27:30 -07:00
Tony Simons 1865fb5fcd feat(plugins): add public subagent lifecycle API 2026-07-26 23:27:30 -07:00
Alex Fournier 45580cc93a Merge origin/main into feat/hermes-relay-shared-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-26 09:21:19 -07:00
teknium1 75e0d52034 fix(windows): sweep remaining bare read_text/write_text sites + linter rule
AST-driven pass over every Path.read_text()/write_text() without an
explicit encoding= across non-test code: 71 sites in 34 files
(skills_hub, hermes_cli/main+profiles+service_manager+container_boot,
mem0/hindsight/honcho plugins, achievements dashboard, release/CI
scripts, productivity+comfyui skill helpers, agent/*). Verified zero
positional-encoding collisions before insertion; per-file compile()
check after.

Adds a check-windows-footguns rule flagging bare single-line
read_text/write_text (multi-line forms stay covered by the AST guard
test from #38985). Together with the salvaged contributor commits this
retires the ~169-site bare file-I/O class (#37423's long tail).
2026-07-24 17:10:39 -07:00
Alex Fournier 4dedaa4237 refactor(runtime): consolidate Relay lifecycle ownership
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:59:06 -04:00
Alex Fournier 64faff6768 refactor(observability): move Relay runtime ownership into core
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-19 08:54:49 -04:00
hellno b03c94dbed fix(approval): emit observer hooks for smart verdicts 2026-07-13 02:00:09 -07:00