The Bots roster highlight and the Routines (Cronjobs) tile were keyed off
host.state.profile — the gateway socket's home. Tab/tile focus moves without
swapping the socket, so opening one bot's chat while the socket was homed on
another highlighted the wrong bot and showed the wrong bot's cronjobs
(community report: Newsanalyst chat open, Hermes highlighted).
- sdk: new host.state.focusedSessionProfile — owner profile of the focused
chat, resolved from the focused stored session's row stamp via
rememberedSessionProfile() (same ladder as remembered navigation and the
HUD), with the gateway profile as the draft/uncached fallback.
- hermes-bots: $focusedBotProfile = focusedSessionProfile || profile
(feature-detected; older desktops keep prior behavior). BotRow highlight,
RoutinesPane scope, and the $selectedBot tracker use it. Turn-busy 'work'
mood stays keyed to the socket-home profile (only it can be mid-turn).
- tests: SDK atom behavior (vitest) + plugin source-shape suite; prewarm
harness stubs gain the new atom.
- docs: SDK page + hermes-agent skill reference list the new atom.
The deeplink-driven plugin install flow shipped in #89464 (salvage of
#82735 by @serefyarar) had no docs. Adds:
- user-guide/features/plugins.md: "One-click install links (Desktop)"
section under Managing plugins — link forms (repo/enable/force), the
confirm-first dialog contract (never auto-installs, same install-time
security scanning as the CLI), hybrid-repo behavior, legacy
plugin-agent/plugin-desktop routing, hermes-dev:// in dev builds, and
the no-SDK anchor example. Cross-links the MCP "Add to Hermes link"
equivalent.
- developer-guide/desktop-plugin-sdk.md: "Distributing with an install
link" section so plugin authors find the link form next to the
packaging docs.
Extends ctx.os.notify (the curated plugin OS door from #78685) with icon,
action buttons, and a serializable `activate` target. Body/action clicks
focus the window and navigate to the plugin's screen; activation paths
share one resolver (hermes-open-target.ts) with hermes:// OS deep links,
so `hermes://index-network/intent/1`, `/index-network/intent/1`, and
{ path, params } all land on the same hash-router route. Approval
notifications keep their existing session-scoped channel.
Salvaged from PR #84192 by @serefyarar (net diff of the PR branch applied
onto current main; branch carried merge commits so a single authored
commit preserves attribution).
Update the desktop docs for five just-merged desktop changes:
- Settings → Gateway + Settings → Connections are now one "Gateways" page:
retitle every reference, describe the Add-connection flow's four kinds
(Local / Hermes Cloud / Remote gateway / SSH) and the save-time duplicate
rules (one local; URL-normalized dedupe across remote/cloud; user@host:port
+ remote profile for SSH), and describe the "Per-profile overrides"
subsection that replaced the page-level Applies to chip row.
- Document the shared "Applies to" profile scope on the config-backed
settings pages (Model, Workspace, Safety, Memory & Context, Voice, Chat,
Advanced, Tools & Keys) and the Messaging overlay.
- Agent plugins section: bundled built-ins are hidden (user/git/project/
pip/portable installs only), Example Plugin is gone, and the section has
its own Applies to selector backed by plugins.manage's optional profile
param.
- Bot Mode: group chats are standalone Discord-style roster rows and open
in the main chat window (older builds fall back to the in-panel view).
- Desktop Plugin SDK: document the new host.openWorkspace(id, { render,
title, minWidth, onClose }) door, its refresh/re-front semantics, and
the feature-detection fallback pattern.
Also retitles the Settings → Gateway references in the web-dashboard guide.
No new pages; sidebars.ts unchanged. `npx docusaurus build` passes.
Covers the precise pinned-session resolver added in PR #88690: request
param shape, the preferred_session response field, hidden-row and
compression-lineage resolution, and older-gateway behavior.
An inline ::preview widget could render and be clicked, but the click went
nowhere: the sandbox has no channel to the agent, so an interactive chart
was a dead end. Now the frame injects a second script beside the measurer
that gives the page one voice:
window.hermes.send('get-price eth')
<button data-hermes-send="get-price eth">ETH</button> (zero-script form)
The prompt rides postMessage up tagged with the mount token, then goes
through the composer's own send path (requestComposerSubmit -> prompt.submit)
flagged display_kind=hidden — the same row-typing auto-continue and internal
notifications already use. The agent wakes and takes a real turn; the
durable row persists (context, resume, DB audit); but NO bubble renders,
live or on reload. The user clicks ETH and the chart just changes — the
off-screen loop is click -> hidden turn -> agent rewrites the widget file ->
frame hot-swaps.
Trust boundary matches size reports and is tighter where it matters: mount
token required (frames can't forge each other's intents), string-only,
trimmed, capped at 500 chars, throttled to one intent per second per frame.
The gateway whitelists display_kind to "hidden" — the RPC can't mint
arbitrary row types — and the flag threads through both turn paths (inline
and compute-host isolation) so isolated sessions don't resurrect bubbles on
resume.
The desktop platform hint teaches the model to wire interactive widgets
with data-hermes-send and to answer clicks by updating the widget's file
rather than with prose; the SDK doc documents the contract.
The SDK doc still described the v1 frame (fixed height attribute, rail card
under the frame) — chrome that no longer exists. And plugin_storage's usage
example used `with plugin_db(...)`, which reads as auto-close but sqlite3's
context manager only scopes transactions; the example now closes explicitly.
The first cut of the core ::preview consumer rendered the classic
preview-attachment card — a button into the right rail we already had, which
made the directive indistinguishable from an ordinary preview link. Now the
directive shows the thing itself: the workspace HTML file renders in a
sandboxed srcdoc iframe inline in the assistant message (opaque origin,
allow-scripts only — no reach into the app, its storage, or the bridge),
with an optional height attribute clamped to 120-1200px and the classic
card kept below as the rail escape hatch.
The frame waits for turn settle before reading the file (mid-stream it is
often mid-write), resolves relative paths against the session's own cwd,
and falls back to the plain card for non-HTML targets and remote gateways
(no local file door there).
Plugins that persist state have been writing into their own install tree
(<hermes home>/plugins/<name>/), which `hermes plugins update` git-pulls and
`hermes plugins remove` deletes — user data dies with the code that wrote it.
plugins/plugin_storage.py is the sanctioned home: plugin_data_dir(name) gives
one data root per plugin under <hermes home>/plugin-data/<name>/ (profile-
aware, created on first use, names validated against traversal), and
plugin_db(name) opens a WAL-mode SQLite database inside it. Secrets stay on
the existing secret-scope path — this is state, not credentials.
hermes-achievements, the in-tree offender, converts with a legacy-file
migration on first read.
The transcript becomes a contribution area (transcript.directives). A plugin
registers a named directive and the model addresses it by emitting
::name{key="value"} as its own paragraph; that leaf renders as the plugin's
component, wrapped in the contribution error boundary. Unclaimed or malformed
directives stay plain prose, so nothing changes for text that merely looks
like a directive (std::vector) or for users with the plugin disabled.
Core ships ::preview{file="..."} as the reference consumer (the existing
preview-attachment card), the desktop platform hint teaches the model the
syntax, and the SDK exports the area + types so runtime plugin.js files get
the surface through the normal plugins API.
- hermes_cli/providers.host_mandated_api_mode: add exact-hostname clause for
api.meta.ai → codex_responses (measured 0% cache on /chat/completions vs
93-99% on /responses with retention); update docstring.
- hermes_cli/runtime_provider._detect_api_mode_for_url: mirror clause for
api.meta.ai (exact hostname, #32243) to keep runtime resolver in lockstep.
- agent/agent_init: call host_mandated_api_mode early in api_mode cascade
(after explicit api_mode wins, before provider-name specials) via lazy
import; single source of truth, preserves user override.
- agent/transports/codex._default_prompt_cache_retention_for_request: return
24h for api.meta.ai unconditionally; build_kwargs setdefault preserves
override; Bedrock branch untouched.
- cli-config.yaml.example: add commented providers.meta example (api_mode
auto-detected).
- website/docs/developer-guide/adding-providers.md: list Meta alongside
Codex/xAI as codex_responses native provider with retention note.
- tests: add hermetic behavior-contract suites for mandate, retention,
content-addressed prompt_cache_key, reasoning passthrough, AIAgent init,
usage cache reporting, model-switch override, and config roundtrip; extend
test_model_switch_openai_api_mode with meta cases.
Independent second-pass review found three gaps:
- focusedUsage is null | UsageStats, not Partial — ClientSessionState.usage
is the full type (app/types.ts) and its only write site seeds the four
required fields before merging (gateway-event.ts). The earlier Partial
annotation traced the wrong type (SessionRuntimeInfo, an RPC payload).
Comment now names the genuinely optional fields instead.
- The tile-focus contract test never seeded $sessionTiles/$sessionStates,
so it proved focusedStoredSessionId follows a tile but could not
distinguish focusedSessionId/focusedUsage working from broken. Seed a
bound runtime with distinct usage and assert both readout atoms move.
- The two public host.state references (website docs + bundled skill
reference) enumerated the old six atoms; plugin authors would never
discover the new ones. Both lists updated.
tsc/eslint/vitest green (4/4).
Gate composer submit and plugin host busy on the target session slice, not a leftover foreground busyRef. Staff can keep typing while a worker session is running.
Includes the follow-up test that submit uses the target session busy flag.
Follow-up to the salvaged #87558 commit: the PR's docs promised the flags
follow "the focused chat", but PRIMARY_SESSION_VIEW is the primary
workspace tab only — a focused session TILE would read the wrong chat.
Wire host.state.busy / host.state.awaitingResponse through the focused
slice ($focusedStoredSessionId / $focusedSessionState), same semantics
as the statusbar busy pulse, with the primary view (and its draft
fallback) while the workspace holds focus.
Adds a tile-focus vitest case and corrects the docs wording.
Plugins can now read host.state.busy and host.state.awaitingResponse
for the focused chat. These follow the same session slice the chat pane
uses, so a draft falls back to the global flags and a background turn
does not leak.
#87326 shipped the lean-compaction capability on the compressor; this adds
the config.yaml surface (compression.tail_mode: legacy|lean, default
legacy), DEFAULT_CONFIG entry, and docs on both the dev-guide compression
page and the user-guide configuration page, with zh-Hans parity.
Closing a pane contributed by a plugin used to disable the entire plugin,
unloading every one of its contributions. For a plugin that owns several
independent panes (e.g. Bot Mode's Cronjobs pane alongside its Bots roster
and composer middleware), closing one pane silently killed the rest.
Now: closing one pane of a multi-pane plugin dismisses only that pane; the
plugin stays enabled and its other panes/commands/middleware keep working.
Reset layout restores dismissed contributed panes. A single-pane plugin
keeps the existing symmetric behavior (Close disables the plugin, with
Settings -> Plugins as the recovery path).
Adds regression coverage for both cases.
Follow-ups on salvaged #85287:
- discover_entrypoint_manifests() now carries BOTH the import-free kind
classification (from #85527) and capability declarations — the two
contracts compose in one function instead of the capability rewrite
dropping classification.
- Per-entry exception isolation: one malformed distribution no longer
blanks every other plugin's manifest (same contract as
providers/__init__.py entry-point scan).
- Documented the hermes_agent.plugin_capabilities group in the plugin
developer guide (pyproject example).
Builds on the three salvaged commits: adds the sources and integration points
they leave out, so a pip-installed memory provider is not a second-class
citizen next to a directory install.
Discovery
- Project-local providers (./.hermes/plugins/<name>/), gated on
HERMES_ENABLE_PROJECT_PLUGINS exactly as PluginManager gates its own project
scan. Completes the four sources CONTRIBUTING.md and AGENTS.md already
promised; memory was the only discovery system missing two of them.
- find_provider_dir() now resolves a package entry point to its directory.
This is load-bearing: config_schema.py (the dashboard panel) and cli.py (the
`hermes <provider>` subcommands) are read from disk rather than imported, so
without a directory a pip-installed provider silently lost both.
- list_memory_provider_names() includes entry-point providers, so they appear
in the dashboard's memory.provider dropdown.
Resolution stays import-free. hermes_cli.plugins.resolve_module_origin() is
extracted from _resolve_module_source() (added by the salvaged #76567) and
shared, so discovery walks a module's file layout instead of importing it.
find_provider_dir() is called from the dashboard and from argparse setup, long
before the operator has chosen a provider — importing every installed candidate
would execute third-party code on the strength of a package being present.
A test asserts the resolution leaves no side effects and no sys.modules entry.
Registration
- PluginContext gains register_memory_provider(). Memory was the only provider
category without one; context engine, image gen, video gen, web search,
browser, TTS, transcription, secret source, dashboard auth and platform all
have one.
- _ProviderCollector delegates unknown register_* calls to a real
PluginContext instead of carrying three hand-written no-ops. It silently
dropped register_tool/register_hook, and had no register_auxiliary_task at
all — despite PluginContext.register_auxiliary_task documenting a memory
provider (hindsight's pre-retain dedup) as its worked example. It can no
longer drift behind PluginContext.
- A raise after register_memory_provider() no longer costs the provider. The
loader caught it into a debug log, discarded the registered instance, and
fell through to "instantiate any MemoryProvider subclass" — returning a
different, unconfigured provider. A silent downgrade that looked like
success, and the exact outcome of calling register_auxiliary_task.
Activation is unchanged: still gated on memory.provider naming the plugin, and
covered by a test so the real PluginContext cannot start requiring
plugins.enabled — that would break every existing user-installed provider.
Verified end to end against a real third-party provider (kainappsinc/elephant)
installed by pip alone, with no directory copy: it appears in the dropdown,
resolves its directory, loads with its tools, and renders its dashboard panel.
Closes#40101.
Follow-ups on salvaged #81419:
- Honor the plugins.enabled allow-list / plugins.disabled deny-list (same
opt-in contract as the general PluginManager) — installed != loaded.
- Skip callables that require arguments: general plugins share the
hermes_agent.plugins group with register(ctx) targets; invoking them
zero-arg would TypeError-spam every startup.
- Fix test docstring (entry points are discovered FIRST, lowest precedence)
and docs mechanism wording; document the config gate.
- New tests: opt-in gate, deny-list, register(ctx) never invoked.
E2E-verified with a real pip-built package against a temp HERMES_HOME.
Model-provider discovery was filesystem-only (bundled dir, $HERMES_HOME,
legacy providers/*.py). The general PluginManager scans the
hermes_agent.plugins entry-point group but deliberately does NOT import
kind=model-provider manifests (providers/ owns their lifecycle), so a
pip-installed provider was recorded yet never called register_provider() —
it never appeared in the picker, contradicting the 'Distribute via pip' docs.
Add a _discover_entry_point_providers() step that scans the
hermes_agent.plugins group and imports each entry, supporting both a
module:func callable target and a bare self-registering module target.
- Runs BEFORE filesystem plugins (lowest precedence): last-writer-wins means
bundled/$HERMES_HOME profiles always override a pip provider of the same
name, so a third-party package cannot hijack a first-party provider id.
- Per-entry failures are isolated (logged + skipped), so one broken package
can't break discovery.
- Docs updated to describe the real mechanism; tests cover callable + module
targets, failure isolation, and first-party precedence.
Adds POST /v1/runs/{run_id}/steer and bridges Browser-Extension/WebUI
session chat streams into the active run registry so live runs on those
surfaces are steerable too.
- steer accepted only while run status is exactly 'running'; stop/stopping/
terminal states return 409 run_not_accepting_steer even while cooperative
shutdown retains the agent reference
- session SSE disconnect/cancellation interrupts and drains the executor-
backed run instead of cancelling only the async wrapper; control refs stay
registered until the turn actually exits
- undelivered steer text (accepted after the final response) is preserved as
pending_steer on the terminal run.completed event/status so clients can
replay it as the next user turn instead of losing it
- docs for the endpoint, next-tool-boundary delivery, acceptance-vs-delivery
semantics
Salvaged from PR #54466 by @abundantbeing.
Root `npm install` no longer installs agent-browser (it's not a root
package.json dependency anymore, see #43564) -- update docs that told
users to run it for that purpose, or that credited it with installing
"browser tools".
- browser.md: agent-browser resolves automatically via npx; a global
npm install -g is now presented as an optional way to skip the
one-time npx fetch, not a required step.
- browser-provider-plugin.md: fix stale comment claiming post_setup
"agent_browser" installs the npm dep -- it only ensures Chromium now.
- CONTRIBUTING.md: relabel the two optional `npm install` steps as
docs-site/workspace dependencies rather than "browser tools".
- termux.md: drop the now-pointless `npm install` from the manual
Node-dependencies step; Node.js itself is the only prerequisite,
agent-browser resolves lazily via npx same as everywhere else.
Final-diff simplify/review pass findings on #83785:
- Consent gate (confirm_truncate -> 4029) now checked BEFORE target
resolution, restoring the pre-PR precedence: an unconfirmed submit
carrying truncation params refuses without paying the durable-transcript
read or heal-stamping live history dicts, and an unconfirmed out-of-range
ordinal returns 4029 (not 4018). Malformed params still refuse first
with 4004. Regression test added (spy DB asserts zero reads pre-consent;
mutation-checked against the previous commit).
- _coerce_truncate_ordinal generalized to _coerce_truncate_int(param_name):
the row_id branch was inlining the exact bool-guard + int() -> 4004
pattern the helper had just extracted.
- Deleted the dead user_indices re-read after _resolve_truncate_row_id
(heal mutates dicts in place; the filter output is identical) and the
duplicate range check that had deadened the pre-existing guard.
- Desktop: exported isVisibleUserMessage from use-prompt-actions/utils and
used it in visibleUserOrdinal / visibleUserIndexAtOrdinal /
rebindSurvivorRowIds — one predicate for the ordinal parity all three
depend on instead of three verbatim copies.
- Docs: programmatic-integration.md documents survivor_user_row_ids.
Address rewinds/edits via SQLite messages.id (truncate_before_row_id)
instead of shifting user ordinals. Resolve against in-memory stamps,
then durable session history when live turns drop _row_id; refuse
unknown durable targets with 4018 (no ordinal fallback) and 4030 on
ordinal/row_id mismatch. Stamp _row_id on insert, load row ids on
resume paths, send rowId from Desktop, filter renderer-synthetic ids,
and stop silently resending failed targeted edits without truncation.
Add production-shaped SessionDB tests for resolve and fail-closed paths.
Fixes#82959
Desktop plugins reach the backend exclusively through the generic ws
JSON-RPC door (host.request), but profile enumeration/creation only
existed on the dashboard REST router, which plugins cannot reach — so
anything 'one chat per agent profile'-shaped (bot rosters, profile
pickers, team panes) was impossible to build as a plugin.
- tui_gateway/methods_profiles.py: new @method handlers
* profiles.list — profiles + optional last_session preview per profile
(mirrors session.list's kanban/tool deny-list; best-effort per-profile
state.db probe degrades to null instead of failing the call)
* profiles.create — ws twin of POST /api/profiles (clone_from/clone_all/
no_skills/description), plus optional SOUL.md content and a best-effort
model+provider pin; mirrors the CLI flow (seed skills, safe alias)
Both run on the RPC pool, not the WS reader thread (list_profiles walks
skill trees; create copies bundles).
- SDK: host.openSession(id, { profile, intent }) — open a stored session
the way core surfaces do, soft-swapping to the owning profile's backend
first (ensureGatewayProfile), and host.newChat(profile) — fresh draft in
a named profile (same door as the sidebar's per-profile '+').
- Docs: desktop-plugin-sdk.md gains both surfaces.
First consumer: a Grok Bot-style 'Bots' roster plugin (one persistent
chat per agent profile with a New Agent dialog) built on exactly these
four doors.
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.
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.
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>
Route plugin target parsing, validation, and host-driven delivery through PlatformEntry across CLI and cron while preserving the host-only send_message policy.
Per review, docs/plugins.md was a parallel guide outside the published
navigation. Its genuinely unique material moves into
website/docs/developer-guide/plugins/index.md: the middleware
registration surface (all four VALID_MIDDLEWARE kinds with contracts and
chaining rules), the per-API-call request hooks, and the
allow_tool_override operator grant (which also fixes pre-existing text
implying override=True alone suffices for non-bundled plugins). Every
migrated claim was re-verified against current main; stale material
(deep-copy claim, approval-surface coverage, a hook that is not in
VALID_HOOKS on main) was corrected or dropped rather than copied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
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.
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.
Review follow-ups on the composite salvage (whole-bug-class sweep):
- session.branch and _persist_branch_seed copied parent history without
display_kind/display_metadata, so a tagged timeline marker (personality
pivot, model switch, auto-continue) re-entered the branched session as a
bare role=user row after a restart — re-planting the phantom-ordinal
class this PR fixes. Both projection dicts now carry the tags; regression
asserts added to both branch tests (mutation-checked: fail without the
fix).
- ui-tui renderer learns display_kind=personality_switch (was falling
through to an opaque user bubble; desktop got the case in commit 1).
- programmatic-integration docs: document the two new 4004 refusals
(boolean ordinal, bare confirm_truncate).
- hermes_state comment: archived rows are searchable only with
include_inactive=True, not by default search — align comment with the
actual FTS filter.
- strip stray trailing blank line in test_tui_gateway_server.py
Explain that schema.description is model-facing while register_tool(description=...) only populates ToolEntry metadata, and remove the duplicated hello-world description in English and zh-Hans docs.
Refs #60735
Co-authored-by: Shiki <132348332+songshikang0111@users.noreply.github.com>
Agent Plugins v1 packages with 'streamable-http' mcp.json entries now load
through Hermes' existing URL-based MCP client instead of being reported and
skipped. The stdio-only limitation was the agreed follow-up slice from
PR #81196.
Boundary rules from the v1 spec (§7.2.1) are enforced:
- URL must be absolute http(s), no user information, no fragment; plain
HTTP only for localhost/loopback hosts.
- Configured package headers are never forwarded across a cross-origin
redirect: translation marks entries strict_redirect_headers, and the
redirect hook in the native runtime strips those headers (plus
Authorization) whenever a redirect leaves the original origin. On mcp <
1.24.0, where the client cannot hook redirects, such servers fail closed
with an actionable upgrade message.
- Legacy 'sse' entries remain reported and skipped.
The redirect hook is extracted into a testable module-level factory
(_make_redirect_header_stripper); default behavior for native config
servers is unchanged (Authorization-only stripping).