120 Commits
Author SHA1 Message Date
unsupportedpastels 0502fe0597 feat(plugins): one published list of what plugins may import
The public list `hermes plugins validate` checks plugin imports against
was a hardcoded frozenset inside hermes_cli/plugin_validate_private_imports.py,
with no page that showed it to plugin authors.

Move the list, unchanged (89 entries), into hermes_cli/data/plugin_api.json
(already shipped as package data via `data/*.json`) and load it once in the
validator. scripts/gen_plugin_api_docs.py renders
website/docs/developer-guide/plugins/public-api.md from the same file, and a
test fails when the page is stale. Validate output is unchanged for the same
inputs.
2026-11-05 18:47:10 -05:00
unsupportedpastels bce3109469 docs(plugins): get_session_env falls back to the environment when unbound
The plugin guide example said get_session_env returns "" outside a session. When the
session ContextVar was never bound it reads os.environ first and only then returns the
default, so the comment now says that.
2026-11-05 18:29:36 -05:00
unsupportedpastels d982caf654 fix(plugins): show the get_session_env import that validate accepts
The plugin guide told authors to read session identity with
`gateway.session_context.get_session_env(...)`, written as a dotted module
call with no import line. `hermes plugins validate` treats only
`from gateway.session_context import get_session_env` as public, so an author
who wrote `import gateway.session_context` to match that spelling got a
private-import warning.

The developer guide and the user guide now name that import, and the
slash-command example uses it to read the platform and the chat id. A test
validates the guide's `get_session_env` example as a plugin and expects no
private-import warning.
2026-11-05 18:29:36 -05:00
Teknium 9ed1a7371b docs(hooks): what a pre_llm_call block stores, and that only a returned block blocks
The block sections now say the prompt is stored as the withheld placeholder
(later turns, compaction, session_search, memory providers and review read
only that; transcripts still show what was typed), that the hook runs before
turn-start compaction, and that a callback which raises or times out is
skipped, so the turn goes ahead. observer-hooks.md lists the block return.
2026-10-31 22:59:20 -07:00
Teknium ad9229c843 feat(hooks): pre_llm_call can block a turn before any inference request
A pre_llm_call callback (Python plugin or shell hook) may now return
{"action": "block", "message": "..."} to veto the turn: no model call is made,
the message is the reply the user sees and the transcript keeps a normal
user -> assistant pair, and the result carries
turn_exit_reason=blocked_by_plugin_pre_llm_call. This is the supported seam for
DLP / privacy / policy plugins that previously had to patch core (#57364).

The vetoed prompt also stays on the host for that turn: no session titling call,
no external-memory prefetch or sync, no background memory review. Its user row
gets an api_content placeholder for the main-loop replay. That alone does not
keep the prompt out of later turns (compaction, session_search and memory
providers read the row's content); the next commit stores the placeholder as
the content.

First valid block wins; a Python block without a message is ignored (same as
pre_tool_call). Shell hooks accept both the Hermes {"action": "block"} and the
{"decision": "block", "reason": ...} shapes, mirroring pre_tool_call.
2026-10-31 22:59:20 -07:00
Teknium 0040f02b5c fix(plugins): clear catalog sweep 1028 scanner false positives and close three validator gaps
Install scanner (tools/plugin_guard*, PLUGIN_SCANNER_VERSION bumped once):
- hardcoded_secret: a PostHog project key (phc_, write-only ingest key) joins the
  Google installed-app secret as a public client key -> high (caution), only
  when every matched literal on the line has a public prefix (telesec
  telemetry.py:27 was dangerous). phx_ personal keys and any other token stay
  critical.
- proc_access: reads of /proc/self|<pid>/{cgroup,mountinfo,stat,status,...}
  (container placement, pid start time) land at low instead of high
  (hermes-lcm-x cpu_capacity.py was caution). environ, cmdline, mem, maps, fd,
  root/cwd, ns, bare /proc/self, any unlisted leaf and any write keep high.
  Generalises #144304's cgroup/mountinfo allowance and keeps the finding
  visible instead of dropping it.
- bytecode_file: a committed .pyc/.pyo (git ls-files; plus any bytecode outside
  __pycache__) is a high structural finding, the binary_file tier (rule 17;
  hermes-emotion shipped tests/__pycache__/*.pyc and scanned safe). Untracked
  caches the validate run writes are not flagged.

Validator (hermes plugins validate, warnings only):
- private imports: gateway.session_context.get_session_env is the documented
  route (plugins/index.md) and a plugin's fallback import of its own install
  path (plugins.memory.openltm._db) is not a core import.
- new warning for private-method calls on core handles (SessionDB, AIAgent,
  GatewayRunner, PluginContext) the plugin does not define itself
  (opencode-sync db._execute_write, runner._evict_cached_agent).
- desktop surface review warning for raw window.hermesDesktop calls and app
  markup selectors ([data-slot], [data-tree-*], [data-pane-host]) in CSS text
  or closest() (squad-hub, novel-reader, search-sources).
2026-10-28 23:07:47 -07:00
Teknium e1be6fcfe0 fix(config): config_keys come from the loader's discovery, count only for active plugins, never widen a section
Review findings on the config_keys seam:

- Discovery: drop the glob scan. config_keys is parsed onto PluginManifest
  (plugin.yaml, and extensions.com.nousresearch.hermes in a portable
  plugin.json) and read from the loader's own collect_directory_manifests +
  entry points + resolve_manifest_winners, so skip rules, precedence and the
  project-plugin gate are the loader's.
- Only active plugins count: gate_manifest must load the plugin (disabled wins,
  plugins.enabled opt-in, requires_hermes); memory providers count when
  memory.provider selects them.
- Bounded: a declared path is at least two segments deep and is ignored when
  DEFAULT_CONFIG already defines it; runtime keys are consulted only after the
  schema walk refused a key, so a plugin declaring [agent] no longer hides
  agent.max_turnz.
- No lru_cache: a plugin installed or enabled in a long-running process counts
  on the next config get/set (the lookup runs only for refused keys).
- computer_use.backend (read by the plugins/computer_use selector) and
  dashboard.kanban move to a core table with their readers; the selection
  sections derive from read_selection's own _SELECTION_NAME_KEYS.
- Tests use fixture plugins (enabled/disabled, hostile [agent], selected memory
  provider, portable plugin.json, project opt-in).
2026-10-28 13:09:00 -07:00
Teknium e402afa783 fix(config): recognize runtime-read selection and plugin-declared keys in config get/set
`hermes config get browser.cloud_provider` / `browser.use_gateway` warned "not a recognized
config key" although the runtime selects the browser provider from them. The validator only
walks DEFAULT_CONFIG, and provider selections stay unseeded on purpose (a stored key is the
explicit pick), as do core-section keys plugins read.

- tools.tool_backend_helpers.selection_config_keys(): the keys read_selection() honours
  (<category>.use_gateway, browser.cloud_provider, stt/tts/image_gen/video_gen provider keys,
  web.backend/search_backend/extract_backend).
- plugin.yaml / dashboard manifest.json `config_keys`: a plugin names the core-section paths it
  reads; declared for retaindb, byterover, computer_use/cua, kanban dashboard.
- hermes_cli/config_runtime_keys.py consults both before the DEFAULT_CONFIG walk.

Found on Hermes Cloud staging.
2026-10-28 13:09:00 -07:00
Teknium 2cd20e5a5e docs: hold_stream fails closed, keeps its hooks through a reload, hides commentary in history 2026-10-25 06:18:48 -07:00
Teknium 905e54d248 docs: hold_stream for transform_llm_output (hooks reference + plugin guide) 2026-10-25 06:18:48 -07:00
Teknium 69fb5c10ed docs(plugin-catalog): password-manager plugins are listed only from their vendor
Admission rule 18 (README and the mirrored developer-guide copy): a plugin that registers a
browser_vault login backend is listed only at tier verified, from the password-manager
vendor's own repository, or at tier official when Nous maintains it, and states that Hermes
enforces the same rule at load. browser-vault-plugin.md gains "Who can register a backend".
2026-10-25 05:32:43 -07:00
Cleo a8db83446f feat(plugins): ctx.side_question asks the session's own agent after a turn
post_llm_call now carries a single-use side_question_token when a loaded
plugin holds the new llm.side_question capability. ctx.side_question(token,
prompt) redeems it: ONE text-only request on a cache-parity fork through
the turn loop's own request phases (assemble_api_request, build_api_request,
perform_api_call), never the tool loop. The parent's system prompt, tools[],
reasoning config and max_tokens are replayed verbatim, so the request reads
the warm prompt cache. Tool calls in the reply are dropped, nothing is
persisted, and usage lands on the session as side_question.

Lease rules: one attempt per turn shared by every plugin (consumed even on
failure), 60 s, invalid once the parent starts its next turn, prompt capped
at 4096 chars, timeout capped at 60 s. The lease holds its parent weakly and
runs under the turn's captured contextvars (profile and secret scope).
Refused with None: MoA, agent-as-provider runtimes, requests that carry
provider-executed tools.

has_capability moves with side_question into a PluginContext mixin
(hermes_cli/plugins_capability_ctx.py) so hermes_cli/plugins.py shrinks.

Fixes #143788
2026-10-24 19:56:21 -07:00
Teknium e3f1dced6b docs: plugin workspaces solve only for the installing platform 2026-10-24 19:27:03 -07:00
Teknium 7ae117eb4c fix(plugins): name portable plugin skills after the plugin, not a hashed namespace
A portable plugin's skills registered as agent-plugin-<slug>-<sha8>:<skill>
(agent-plugin-blender-5a80e4ba:blender) because one manifest field named both
its plugin-data directory and its skill namespace. The hash only matters on
disk. Skills now take the plugin's own name (blender:blender), the form every
native plugin skill uses; a clash with an already-registered qualified name is
skipped with a warning naming the owner, as before. The data directory keeps
its hashed name, so no plugin data moves, and skill_view still resolves names
saved in the old form.
2026-10-24 04:01:10 -07:00
Teknium 3ee9fcfbb5 docs(mcp): what the trust-gate and one-shot consent prompts offer 2026-10-23 16:16:43 -07:00
Teknium 7489b22af4 docs(plugins): portable packages lack requires_hermes, not a version field
plugin.json accepts `version`; what it has no field for is a Hermes floor. Review feedback on the portable-package wording.
2026-10-23 02:22:46 -07:00
Teknium 0255dedd1a feat(plugins): enforce the catalog's requires_hermes at install and update
The catalog entry's requires_hermes was display-only: `hermes plugins
install <name>` cloned and published a pin the catalog says needs a newer
Hermes, and `hermes plugins update` re-pinned an installed plugin onto it.
Only the plugin's own plugin.yaml floor was checked, and only at load.

- install_catalog_entry (the one seam every install by catalog name goes
  through: CLI, dashboard/Desktop/TUI plugins.manage, `hermes setup`'s
  importer step, the left-core and memory-provider auto-installs) refuses
  with "<plugin> needs Hermes <spec>; this is <version>. Update Hermes
  (`hermes update`) and install again." before any clone. The connector
  card's pre-check uses the same function.
- repin_catalog_plugin (every catalog update) keeps the installed pin and
  says the update is available after updating Hermes; `hermes plugins
  update` with no name lists such a plugin as held instead of applying it,
  and check-updates marks it.
- `hermes plugins search` / `info` still list incompatible entries,
  marked as needing a newer Hermes.

Portable packages (plugin.json) have no version field, so the catalog
entry's requires_hermes is their only floor; the gate is kind-agnostic and
covers them (tested), and the agent_plugins comment and docs say so.

The verdict is plugins_manifest.requires_hermes_error itself (same
running_hermes_version + version_satisfies), so a dev checkout, canary
build or tagless checkout is judged exactly as the plugin.yaml gate judges
it. No override flag.
2026-10-23 02:22:46 -07:00
Teknium 0b69853adf docs(browser): browser_exec is the only browser path; Camofox is a plugin
Rewrite the browser feature page, configuration, tools/toolsets/env-var/pm
references, docker, windows, bot-screen, termux/installation and the secondary
developer-guide/integration/feature pages (EN + zh-Hans): drop agent-browser,
the built-in browser_* tools, browser.backend and /browser use; document
browser.executable_path / browser.user_data_dir / browser.chromium_args and the
AGENT_BROWSER_* migration; point Camofox users to 'hermes plugins install camofox'.
2026-10-23 01:41:25 -07:00
Teknium 49520132f4 feat(plugins): plugin slash commands can ask the user to pick an option
PluginContext.ask_choice(question, choices) -> str | None lets a sync
slash-command handler ask the user who ran it to pick one option and get
the picked value back. Choices are strings or {value, label, description}.
Each surface binds an asker around the handler call
(hermes_cli/plugin_choices.py), and every asker is that surface's
existing clarify prompt. No surface gets a new card type or wire shape:

- messaging chats (gateway/plugin_command_choice.py): adapter.send_clarify
  plus the clarify tool's own delivery (_clarify_send_then_wait, text
  fallback, card retire). Telegram/Discord/Slack show buttons, other
  platforms a numbered list. The typed reply reaches the clarify
  text-intercept while the command still holds the chat.
- local sessions on the gateway (classic CLI, Ink TUI, Desktop): the
  `clarify` server request to the session's viewers. Two fixes make the
  answer arrive while slash.exec is still waiting for the handler:
  * tui_gateway/ws.py: a viewer's answer to an open server request (a
    response frame, `clarify.lock`) is settled at once instead of queuing
    behind that socket's running slash.exec.
  * hermes_cli/gateway_plugin_commands.py: the classic CLI answers the
    session's `clarify` requests with its clarify panel while it waits for
    slash.exec. No turn pump reads the session's frames between turns.
- in-process classic CLI and the legacy `hermes serve` backend: their
  clarify panel / `clarify` server request.

Returns None when nothing can ask (hooks, cron, async handlers on the
event loop), when the user cancels or answers with something that is not a
choice, or when the clarify timeout passes. The handler then prints its
text usage.
2026-10-22 14:37:10 -07:00
Teknium 919322f6c3 feat(delegation): pluggable delegation providers
delegate_task asks the active delegation provider (delegation.backend, a new
provider category registered with ctx.register_delegation_provider) for each
task's route: model/provider/endpoint/reasoning resolved by the existing
delegation credential resolver, or a Hermes profile the child runs as. A
provider may add enum-checked per-task schema parameters built once with the
tool definitions. Core keeps the whole lifecycle.

- agent/delegation_provider.py + agent/delegation_registry.py: ABC + registry;
  the built-in 'default' strategy is today's behaviour and the fallback for an
  unregistered/unavailable backend.
- tools/delegate_tool_routes.py: per-task routes, parameter validation, and the
  profile scope binding (home override + secret scope + multiplex semantics +
  terminal policy) around a profile child's construction and conversation.
- Host isolation: HOST_OBJECT_BASES row, so providers run under
  plugins.isolation: host.
- delegation.backend in DEFAULT_CONFIG; registrar table moved to
  hermes_cli/plugins_registrars.py to keep plugins.py under its cap.
- Default config: delegate_task schema and every hermes-cli tool definition
  are byte-identical to main.
2026-10-22 14:37:10 -07:00
Teknium 6661080238 fix(pm): a plugin whose pyproject installs nothing no longer rebuilds the dependency environment
Enabling or updating a plugin whose pyproject.toml declares `dependencies = []` (the official
agent-importers plugin) ran "Resolving / Installing Python dependencies" and built a new
environment generation although nothing gets installed, and every update of the plugin did it
again.

Workspace membership keyed on "has a pyproject", so such a plugin became a uv workspace member,
and members_stamp hashes a member's whole tree into the venv stamp: enabling it, or any change to
its files, made the recorded environment stale. A plugin is now a member only when it installs
packages (`PythonDeclaration.installs_packages`, the predicate dependency consent already uses
since the sibling fix for the empty consent step), so membership, requires-python checks and the
stamp all follow what the plugin actually puts in the environment.

Test fixtures that used a dep-less pyproject as a cheap stand-in for a member now declare a real
dependency (a local wheel through tool.uv.sources, resolvable offline, or a requirement where no
resolution runs); their assertions are unchanged. New: enabling a `dependencies = []` plugin
through the real admission path keeps the selected generation and recorded stamp, a later change
to its files keeps the venv current, and a plugin with a real dependency still joins.
2026-10-22 11:12:32 -07:00
Teknium 0983654319 fix(plugins): drop the prototype-patching rule from the Desktop surface lint
The rule could not tell a bundled library's own classes from patching a renderer global: it kept
failing legitimate bundles (three.js once minified to $-names) and still missed a bare $ receiver.
Prototype writes in a Desktop plugin are now read by the catalog reviewer instead; the policy in the
catalog docs stays, with a note that review checks it by hand. eval/new Function, non-SDK imports,
script injection and app-DOM reach are still linted.
2026-10-21 08:17:10 -07:00
Teknium ce18aafe01 fix(mcp): spend totals survive session rotation; each cron run has its own; review fixes
Independent review of #142470:
- Compression (CLI, TUI/Desktop) and /branch continue a chat under a new session id, which reset its
  spend total: a chat at its limit got a fresh budget. The continuation hook every rotation site already
  calls (transfer_session_yolo) now moves the total too.
- Every cron job in a process shared one tally (their approval session key is ""), so one job's spend
  refused the next and stayed spent until restart. The cron run scope binds its own run id.
- tool_costs keys the server does not offer (a typo, or Hermes' mcp_<server>_ prefix) are named in a
  warning at registration instead of silently carrying no budget.
- Untrusted over-limit prompts lead with the limit notice (the gateway text fallback keeps ~200 chars);
  "1 credit", not "1 credits"; the multiplex rekey moves the prompt slots with the totals.
- Tests: rotation + per-run cron tally + unknown-key warning (red on the previous head), and parallel
  over-limit prompts asked one at a time (red with the prompt slot made a no-op).
- Docs: where a total survives and resets, the counted-on-start wording, the same-name override note.
2026-10-21 04:12:11 -07:00
Teknium 2fe303419f docs(mcp): tool costs and per-chat spend limits for MCP servers and portable packages
Also keeps a package's declared costs on a same-name native override and logs
which keys were carried.
2026-10-21 04:12:11 -07:00
Teknium f8fc25a869 fix(plugins): Desktop lint stops flagging a bundled library's own classes
The "prototype patching" admission rule matched any `X.prototype.y =` line. A
bundled library setting fields on classes it declares itself trips it: three.js
alone hits it 29 times (`var Vector2 = class _Vector2 { constructor() {
_Vector2.prototype.isVector2 = true } }`, `KeyframeTrack.prototype.ValueTypeName
= ...`), so no Desktop plugin could ship a three.js viewer.

The write now fails only when the receiver is not a class/function the file
declares, or is a renderer/browser global (Storage, HTMLElement, Array, ...),
which keeps failing even when the file shadows it with a same-named class (a
regex cannot see scope). Every attack shape the rule exists for still fails:
`Storage.prototype.setItem = ...`, an alias of a global, a shadowed global.
2026-10-19 07:39:24 -07:00
Teknium 2d35f6184b feat(plugins): portable plugins get the Settings > Plugins form, live
A portable Agent Plugins v1 package (plugin.json) had no way into Settings >
Plugins: the schema reader only looked at plugin.yaml, and v1 rejects unknown
top-level fields. It now reads extensions.com.nousresearch.hermes.config_schema
(the vendor namespace that already carries server trust and timeouts) for the
form, the save, the loader's schema warnings and catalog admission.

Two gaps any plugin with settings hit once the form existed:

- The plugin's Desktop half had no way to learn a save happened. Every
  plugins.manage settings save now broadcasts plugin.<id>.settings.changed
  ({written}); a secret-only save still makes the (empty) settings call so the
  event fires for a new API key too.
- A secret the form stored never reached the plugin's running MCP server (its
  env ${VAR} was rendered at spawn). After a save, Hermes reconnects the plugin's
  servers whose resolved launch inputs changed (identity digest, rendered the way
  the connect path renders it); a toggle-only save leaves them alone.
2026-10-19 07:27:50 -07:00
Teknium be46373c5b fix(plugins): plugin OAuth grants are dropped on revoke and uninstall; sign-in needs a person
A revoked grant kept its dead access and refresh tokens on disk; it now keeps only the marker
and fingerprint. hermes plugins remove drops the plugin's grants, so a later plugin installed
under the same key does not inherit the sign-in, and hermes auth logout <plugin>:<id> still
removes a grant after the plugin is gone. hermes auth add refuses to run from the gateway, cron or
an agent session and says which hosts get the code, the client secret and the token first.
2026-10-19 07:09:56 -07:00
Teknium 9f188e6598 fix(plugins): plugin OAuth endpoints are https only and use the plugin's own variables
Maintainer requirement on #139985: authorize_url, token_url and hosts are https at manifest
validation and catalog admission, with no loopback http exception. client_id_env and
client_secret_env may not name a variable Hermes itself uses (a provider API key, a platform
token): Hermes POSTs those values to the manifest's token_url, so a manifest naming
OPENROUTER_API_KEY would have sent it there. A host outside the IdP's domains is a reviewer
warning, and the install notice names the sign-in host.
2026-10-19 07:09:56 -07:00
Ilya Balobanov 440e32ad85 docs(plugins): give only the Google client as an example of a plugin OAuth client
Microsoft and Notion were named without being tried against this route, and
Notion's token endpoint expects HTTP Basic client authentication, which the
broker does not send.

Refs #139077
2026-10-19 07:09:56 -07:00
Ilya Balobanov c3d3b15cc5 feat(plugins): a re-pin that moves an OAuth entry's authorize host asks first
The authorize URL is the page the user's browser opens to sign in. A catalog
re-pin that points it at another host now shows up in the consent delta, next
to the token host, hosts and scopes. A new path on the same host still only
changes the fingerprint, so the user signs in again.

Refs #139077
2026-10-19 07:09:56 -07:00
Ilya Balobanov b1b4effe51 feat(plugins): OAuth clients a plugin declares, signed in and refreshed by Hermes
A plugin that calls a third-party API with the user's own OAuth client
(Google, Microsoft, Notion) declares it under `oauth:` in plugin.yaml
instead of running the consent flow and keeping the refresh token itself.

- `hermes auth add|status|refresh|logout <plugin>:<id>` runs the PKCE
  login of auth_oauth_pkce_plugin and keeps the grant in the active
  profile's auth.json (section `plugin_oauth`, keyed by the plugin's key)
- `credentialed_provider_request(<id>, ...)` attaches the bearer for the
  entry's declared hosts only (https, port 443) and refreshes the grant
  under the auth-store lock; the calling plugin's own entry is resolved
  before `_PROVIDERS`, so a later core provider of the same name cannot
  take it over
- each grant stores a fingerprint of the entry it was made for: a plugin
  version that changes the token URL, hosts, scopes or client variables
  gets ProviderNotSignedIn until the user signs in again
- the client secret is read with get_secret right before each token POST
  (OAuthPKCEConfig.token_request) and never sits in the frozen config
- grant-dead codes retire the grant; invalid_client, unauthorized_client
  and invalid_scope surface as AuthError codes; a sign-in granted fewer
  scopes than declared saves nothing
- `hermes plugins validate` checks the block and the declared variables
  and warns when plugin code names the token URL; install/show notices
  and the catalog re-pin consent delta list the entries

Refs #139077
2026-10-19 07:09:56 -07:00
Teknium caa1ea8d92 fix(mcp): an unset ${VAR} env entry counts as absent; set values pass verbatim
Follow-up to #141379 from an independent post-merge review:
- the MCP SDK re-adds its inherited set (PATH, HOME, TERM, ...) at spawn,
  so popping an unset override never unset those keys. Treat the entry as
  absent instead: the key keeps its baseline value, one rule for every key,
  matching what the child actually receives.
- carry resolution provenance from interpolation (_RenderedStr) instead of
  guessing from the rendered text, so a set variable whose value is
  literally '${OTHER}' is passed through, not dropped.
2026-10-19 02:10:20 -07:00
TekniumandRodrigo Gomes 43c9e0ae52 fix(mcp): leave an unset ${VAR} env entry out of a stdio server's environment
A stdio MCP server env entry like "GHIDRA_INSTALL_DIR": "${GHIDRA_INSTALL_DIR}"
was passed to the child as the literal text "${GHIDRA_INSTALL_DIR}" when the
variable was unset. Servers that validate the value refuse to start (REA:
"GHIDRA_INSTALL_DIR must be absolute"), so a portable plugin could not forward
an optional variable, and Hermes' filtered MCP env means forwarding is the only
way a user's variable reaches the server.

_build_safe_env now drops an env entry whose whole value is still one
unresolved ${VAR}/${env:VAR} reference. Partial references, context vars and
url/headers keep their existing behaviour. Applies to config.yaml mcp_servers
and plugin mcp.json alike (same launch path).

Co-authored-by: Rodrigo Gomes <2362425+rodrigogs@users.noreply.github.com>
2026-10-18 21:16:12 -07:00
Teknium ed9fb3cf56 fix(plugins): fire post_compaction on every committed compaction path
The salvaged hook fired from ContextCompressor.compress(), which is not where
a compaction commits: compress_context can still refuse or roll back the
candidate (snapshot stale, commit fence, split failure), the gateway's
/compress runs its own compressor and commits elsewhere, and Codex-native,
micro-compaction and proactive-prune rewrites never call compress() at all.
session_id was also the compressor's bound id, which is stale after rotation.

Fire it from the compaction-record seam (agent/compaction_events.py) that
already sees exactly one committed record per rewrite on every agent path,
and from the gateway session.mutate compress commit. Payload: session_id (the
session holding the compacted transcript), old_session_id, kind, reason,
before/after token estimates, messages_removed. Bounded by
plugins.hook_callback_timeout, since it runs inside the commit window.

Tests trimmed to one invariant (fires once per committed compaction, never
for an aborted one, raising plugin isolated); docs in hooks.md and the
plugin guide.
2026-10-18 02:31:09 -07:00
TekniumandLucas Piera aab0dc72cf feat(plugins): on_session_delete covers Desktop deletes and deliberate archives
Finn's state-layer hook missed the paths Desktop actually uses: single
delete and archive go through the canonical mutation receipt
(mutate_runtime_session), not SessionDB.delete_session, so a plugin never
heard about either. Archive (the "this is done" signal #132639 asked for)
fired nowhere.

- mutate_runtime_session fires after its write commits for operation
  'delete' and for a real unarchived -> archived sidebar flip; an exact
  retry replays the receipt and stays silent.
- set_session_archived fires reason="archive" on a real flip, so TUI
  session.archive, `hermes sessions archive` and deferred TUI archives are
  covered from one place. Repeats, un-archives, the idle auto-archive
  sweep and profile adoption (notify=False) stay silent.
- Payload is now session_id + reason + profile on every path (profile
  from the store's home), deleted_ids when more than one row went;
  primary id is the one the caller asked for, not the alphabetically
  first.
- One fail-open seam (emit_session_deleted) calls lifecycle.invoke_hook
  directly; the extra notify_session_deleted wrapper is gone.
- Tests trimmed to two invariants against the real hook registry.

Co-authored-by: Lucas Piera <pieralukasz@gmail.com>
2026-10-18 02:11:06 -07:00
finn763 ee3ce9a4d9 fix(plugins): fire on_session_delete on every session-deletion path 2026-10-18 02:11:06 -07:00
Teknium 0975937593 feat(plugins): pass the calling session to plugin slash commands on every surface
Widen the contributor's opt-in `source=` to the calling-session context plugins
asked for (#140199, #91526, #123245): a `ctx.register_command` handler that
declares any of `session_id`, `session_key`, `platform`, `source` (or
`**kwargs`) receives them; one-argument handlers keep `handler(raw_args)`.

- One call boundary (hermes_cli/plugin_command_dispatch.py) used by the gateway
  (messaging platforms and local CLI/Desktop sessions via
  `_hm_run_plugin_handler`), the TUI/Desktop RPC (`_run_plugin_command`) and
  the classic CLI's off-gateway fallback. The first positional parameter
  always gets raw_args whatever its name.
- Messaging plugin commands look up the chat's current session entry (never
  create one), so HERMES_SESSION_ID / `session_id` are the id a later /new
  reports as old_session_id instead of "" (#123245).
- Fold the contributor's separate gateway adapter module back into
  `_hm_run_plugin_handler` so the local-session path shares it.
- Tests trimmed to two invariants, both red on origin/main.
2026-10-18 01:27:20 -07:00
ewtillman 06e8c5bc70 feat(plugins): expose gateway source to slash handlers 2026-10-18 01:27:20 -07:00
Teknium bb834a2cb3 docs(plugins): name locate_skill's real second parameter; decode SKILL.md like skill_view 2026-10-18 00:47:38 -07:00
Tekniumandnekwo 93fafc4d6f feat(plugins): public names for the skills tool's discovery and lookup helpers
Plugins that show the agent's skills (catalog views, "open this skill", drift checks against the
installed copy) need the same walk, order, search roots and lookup gate the skills_list / skill_view
tools use, plus the package hash the bundled-skill sync stores. All seven were private.

Rename, with every internal caller moved and no underscore aliases (same shape as the platform
helpers in e6cf2a9):
- tools/skills_tool.py: find_all_skills, sort_skills, skill_search_dirs, locate_skill,
  skill_lookup_path_error
- tools/skills_sync.py: dir_hash
- tools/skill_usage.py: read_skill_name

The names join _PUBLIC in the private-import lint and are documented under "Read the skill catalog"
in the plugin developer guide.

Builds on #129698 by @nekwo, which proposed the same names with legacy aliases kept.

Closes #129696

Co-authored-by: nekwo <beastant1@gmail.com>
2026-10-18 00:47:38 -07:00
Teknium f96587a477 docs: keep the catalog rules block identical to plugin-catalog/README.md
The rule-9 pointer to the platform helpers section broke the docs/README rules mirror test;
the section stays reachable from the platform adapter guide.
2026-10-17 14:51:46 -07:00
Teknium 14508f97f7 feat(plugins): document and allowlist the platform-plugin public surface
hermes plugins validate warned on every platform plugin's use of
OwnAccessPolicyMixin, tools.approval.resolve_gateway_approval, the clarify
button API, the SSRF client pieces and the setup-wizard helpers. Those names
join _PUBLIC and get a "Public helpers for platform plugins" section (plus
the two new PlatformEntry authz fields) in adding-platform-adapters.md.
2026-10-17 14:51:46 -07:00
unsupportedpastels 8af0911efc docs(plugins): slash command handlers run in the gateway process
A plugin `/command` typed in the classic CLI now runs in the gateway that
owns the session, so `os.getcwd()` there is the gateway's directory and
`print()` goes to the gateway's stdout. The plugin docs did not say so.

The user guide (features/plugins.md) and the build-a-plugin guide now say
to return text instead of printing, to take the session's directory from
`agent.runtime_cwd.resolve_agent_cwd()`, and to read session identity with
`gateway.session_context.get_session_env()`. The real-daemon test's plugin
gains a `/probe-cwd` command that checks both helpers return the session's
values while `os.getcwd()` is the gateway's.

Refs #134889 (CLI-13)
Refs #134889 (PLG-7)
Refs #139523
2026-10-16 16:11:56 +00:00
Teknium 0706401b80 fix(plugins): a newer server-declaration key no longer makes a plugin uninstallable
The portable loader raised on any key in extensions.com.nousresearch.hermes
.servers.<name> it did not know, so a plugin using a key added in a later
release could not be installed at all on an older one: REA's per-server
`timeout` broke every v0.22.2 install. Ignore unknown keys with a diagnostic
(logged at load, listed by `hermes plugins validate`), the same treatment
unknown top-level manifest fields already get. Known keys, `trust` included,
are still validated and applied.
2026-10-15 09:19:05 -07:00
Teknium f30020ed5e fix(mcp): preserve server deadlines through outer tool execution 2026-10-14 14:09:29 -07:00
Teknium fcc170b798 fix(plugins): honor portable per-server call deadlines 2026-10-14 14:09:29 -07:00
Teknium 562fda904c fix(pm): plugins may move transitive core packages again (revert #137110 overreach)
#137110 pinned every core-locked package to its exact locked version in
the generated plugin workspace (`_trust_core_lock`). That overrode PM's
deliberate rule that a plugin may move a transitive core package within
core's declared ranges (only core's exact pins are fixed), and #137110
rewrote PM's test for that rule to assert the opposite. Five catalog
plugins stopped installing: cognee, loreconvo, mnemosyne, omnivoice and
radio-dm-gateway.

Remove `_trust_core_lock` and restore
tests/pm/test_workspace_build_inputs.py byte-for-byte from before #137110.
What #137110/#137159 got right stays: generated workspaces carry no
release-age cutoff, so users' machines never check upload dates and
mirrors without them keep working; the 3-day check still runs at
`hermes pm lock` and in CI.
2026-10-12 16:26:19 -07:00
Teknium 0290081fa4 docs(plugins): catalog rule 9 notes validate warns on private core imports 2026-10-12 13:11:28 -07:00
Teknium 37725f24f9 docs(pm): 3-day release age checked at lock time; plugins trust core's lock 2026-10-12 04:55:36 -07:00
Teknium decf192176 plugin-catalog: delist tabro; add rule 17 (no unscannable executable payloads)
tabro bundles a prebuilt Windows .exe (relay-native-host-0.4.2.exe) in its
plugin tree. The install scanner cannot read machine code, and the SHA-256 the
plugin checks it against is committed in the same commit, so it only proves the
file was not changed after that commit, not where it came from. A reviewer
cannot tell it matches the in-repo C++ source. That is a precedent worth
closing before someone ships a binary that is not benign.

- Delists tabro (deletes plugin-catalog/tabro.yaml). Existing installs keep
  working; not added to removed.yaml — there is no evidence of malice, only no
  way to verify the binary. An attested build (rule 17) is the way back.
- Adds admission rule 17: no compiled binaries, committed Python bytecode, or
  archives hiding either, detected by file header not extension. A native
  component is allowed only with a GitHub build-provenance attestation tying it
  to the plugin repo's own workflow at the pinned commit; a setup-time download
  is allowed only from the tool's official source, pinned by checksum and
  disclosed. Mirrored into the docs submission guide (the mirror test enforces
  identical text).

A full-content scan of all 543 pinned entries found tabro is the only one with
a native executable. The enforcing gate ships in a follow-up PR.
2026-10-11 02:53:45 -07:00