Commit Graph
112 Commits
Author SHA1 Message Date
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
teknium1 84a244e6d6 docs(plugin-catalog): rule 11 — credentials come through Hermes, never from files
Plugins read declared secrets with agent.secret_scope.get_secret and use a
subscription sign-in only through credentialed_provider_request
(requires_auth), so Hermes attaches the token. They never read .env,
Hermes's auth.json, another tool's login or any credential store directly,
and never refresh, rotate or write OAuth tokens. Missing route = ask Hermes
for one. The plugin developer guide's own examples switch from os.environ to
get_secret to match.
2026-10-10 13:26:51 -07:00
Teknium 4070f011a2 feat(plugins): core-made Codex requests so plugins never hold the token
Plugins that need the user's ChatGPT/Codex subscription (hermes-live-voice mints
an OpenAI Realtime client secret) vendored their own reader + refresher for
~/.codex/auth.json. Codex refresh tokens are single-use, so that second refresher
races Hermes's locked refresh and can revoke the user's whole token family.

hermes_cli.plugin_provider_requests.credentialed_provider_request() lets core make
the request instead: it resolves the credential through
resolve_codex_runtime_credentials (Hermes's locked refresh, profile-scoped),
attaches Authorization itself, sends only to chatgpt.com / api.openai.com, never
follows redirects, and returns status/headers/body. Callers are attributed by the
plugin directory on the call stack and must declare `requires_auth: [openai-codex]`
in plugin.yaml; the declaration shows at install, in `hermes plugins show`, and a
catalog re-pin that adds it asks for consent. A plain importable function so
dashboard plugin_api.py modules can use it.
2026-10-10 10:43:32 -07:00
teknium1 cc1500a071 fix(desktop): a linked plugin checkout drives its desktop half; doctor flags a stale copy
The Desktop loads a unified package's desktop half from the copy in
desktop-plugins/<name>/, not from the package. Editing the source shipped
nothing and nothing said so:

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

Electron now follows symlinked package dirs, marks their copy linked, and
re-syncs it on any byte difference. The renderer watches a linked copy's
source plugin.js and, on save, runs the reconcile and hot-reloads. Catalog
and git installs keep the mtime rule. `hermes plugins doctor` warns when
the copy differs from the source and says how to refresh it.
2026-10-08 04:07:30 -07:00
teknium1 82b9bfedd6 fix(plugins): enabling a plugin from Desktop/TUI/dashboard no longer strips the core tools
With no saved platform_toolsets, _toggle_plugin_toolset seeded
platform_toolsets.cli: [<plugin_ts>]. A list naming only plugin keys
replaces the hermes-cli composite, so the model lost every built-in tool
(26 -> 1, no terminal). The seed is dropped: an unknown plugin toolset is
already on by default when no list is saved.

plugins remove now clears the plugin's toolset from saved lists even when
plugin.yaml has no provides_tools (the toggle already no-ops without a
toolset); before, hermes config check warned about an unknown toolset.

Docs: say what provides_tools actually drives.
2026-10-08 03:40:04 -07:00
witcheer dde8800ed9 docs(plugins): list the built-in author tools at the top of the plugin guide 2026-10-08 01:02:23 -07:00
Teknium 1e347ccf2a feat(plugins): a portable package can ask Hermes to gate its MCP server
Agent Plugins v1 has no trust field, and Hermes's portable mcp.json reader
accepts only type/url/headers for a remote server, so a package server always
ran at the default trust: full. A package whose tools trade, spend or send
(senpi resolving trade approvals on a user's Hyperliquid account) could only
ask the model to check with the user; Hermes itself never prompted.

plugin.json's Hermes extension now accepts
  extensions.com.nousresearch.hermes.servers.<name>.trust: untrusted
which is applied to that mcp.json server exactly like a config.yaml
'trust: untrusted': every write-capable call asks first and fails closed
when unattended. 'full' (the default) is accepted and changes nothing, any
other value refuses the package, so a package can narrow access but never
widen it, and a config.yaml server with the same name still replaces the
package's entry. 'hermes plugins validate' lists the gated servers.
2026-10-07 04:51:23 -07:00
Siddharth Balyan 3aaae8c537 Plugins: apps are found wherever they are installed on Windows, macOS and Linux (#134050)
* feat(platform): app declarations list every place an app may be installed

An app block's location may now be one path or an ordered list. A list
item is a path, where a `*` segment stands for a versioned folder and the
highest version is tried first, or a mapping naming a location kind:
command (PATH), uninstall_registry (InstallLocation plus a file, Windows),
app_bundle (/Applications, ~/Applications), flatpak (app id) and snap.
The directories behind each kind live in known_dirs.

Each kind's OS, key, presence and locator are one LOCATION_KINDS entry in
resolver/app.py. The declaration parser validates against that table, so
a new kind is one entry instead of three coordinated edits in two modules.

With requires.min_version, availability reads the version of every
present copy and returns the first that qualifies, so an old copy found
first does not hide a newer one; if none qualifies it reports the highest
readable version. A Linux block may omit a version source (Linux has
none) and is then gated on presence only; Windows and macOS blocks still
need one, and a Linux-only declaration cannot set a minimum.

Brought over from #129246. The Blender 0.2.0 catalog pin stays out: older
clients reject a list location, so it follows a release that ships this.

* fix(platform): bad app declarations and vanished registry entries no longer break app lookup (review)

A location with a non-string kind raises DeclarationError instead of TypeError.
A location's file and key value are checked after stripping, so a leading / or \ or a drive cannot escape the install folder.
An uninstall registry entry that cannot be opened or read is skipped instead of ending the scan of its root.
2026-10-07 00:58:22 +05:30
Teknium a928a959e3 feat(plugins): let plugins register Automation Blueprints via ctx
Plugins can now call ctx.register_automation_blueprint(key, *, title,
description, schedule_template, prompt_template, category, slots,
deliver_default, skills, tags). The fields mirror the built-in
AutomationBlueprint, and slots are BlueprintSlot field dicts, so the call
also crosses the plugin-host boundary.

- One lookup, cron.blueprint_catalog.list_blueprints() / get_blueprint(),
  merges the built-ins with the active profile's plugin blueprints at call
  time. /blueprint (CLI/TUI/messengers), /api/cron/blueprints and the
  instantiate route all use it. Each dashboard route resolves inside the
  ?profile= scope, off the event loop.
- Keys are namespaced <plugin>:<key>, so a plugin cannot shadow a built-in.
  A ':' in the key, a duplicate key, an unknown placeholder, a bad slot type
  or a schedule that does not parse is warned and ignored at registration.
- Catalog entries carry source/plugin. Desktop fetches the catalog for the
  profile it creates jobs in, keyed per profile, and shows the plugin name
  on the blueprint row and in the "Start from" select. The dashboard shows
  a plugin badge.
- A created job is an ordinary cron job with the prompt and schedule already
  filled in, so it keeps running if the plugin is later disabled.
- The ctx content registrars (register_skill, now joined by
  register_automation_blueprint) move to hermes_cli/plugins_content.py.
  The dashboard's blueprint types move to web/src/lib/automation-blueprints.ts.

Closes #132373. Requested by @enckequity.
2026-10-05 17:34:36 -07:00