2a8d2174173ab8d05d0b48a44580a8c0b2c8c19b
851 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bb4703c761 |
docs(auth): replace stale 'hermes login' references with 'hermes auth add'
'hermes login' was removed (the command now just prints a deprecation message and exits). The bundled hermes-agent SKILL.md, in-code error messages, the tip rotation, the proxy adapters, and the docs site still pointed agents and users at the dead command — so models loading the skill kept running 'hermes login --provider openai-codex' and getting a dead-end print. Replacements use the canonical 'hermes auth add <provider>' surface (or bare 'hermes auth' for the interactive manager). Files: - skills/autonomous-ai-agents/hermes-agent/SKILL.md (+ regenerated docs page) - hermes_cli/tips.py (tip rotation) - agent/google_oauth.py (gemini-cli error message) - agent/conversation_loop.py (nous re-auth troubleshooting line) - agent/credential_sources.py (docstring) - hermes_cli/proxy/cli.py + hermes_cli/proxy/adapters/nous_portal.py (proxy auth hints) - tests/hermes_cli/test_proxy.py (updated assertions) - website/docs/reference/faq.md, website/docs/user-guide/features/subscription-proxy.md - zh-Hans i18n mirrors for the above 'hermes logout' is still a live command and is left untouched. The 'hermes login' stub in hermes_cli/auth.py:login_command() and the cli-commands.md 'Deprecated' rows are intentionally kept as the discoverable deprecation surface. |
||
|
|
8b69ec03af |
feat(mcp): Nous-approved MCP catalog with interactive picker (#30870)
* feat(mcp): Nous-approved MCP catalog with interactive picker
Adds an optional-mcps/ directory mirroring optional-skills/: curated,
Nous-approved MCP servers shipped with the repo but disabled by default.
Presence in optional-mcps/ = approval. No community tier, no trust signals.
Entries are added by merging a PR.
New surface:
hermes mcp Interactive catalog picker (default)
hermes mcp catalog Plain-text list, scriptable
hermes mcp install <name> Install a catalog entry
Picker behavior:
not installed -> install (clone/bootstrap if needed, prompt for creds)
installed/off -> enable
installed/on -> menu (disable / uninstall / reinstall)
Manifest schema (manifest_version: 1) supports:
- transport: stdio (command/args, ${INSTALL_DIR} substitution) or http (url)
- install: optional git clone + bootstrap commands (for repos that need
local venv setup, like the n8n bridge); omit for npx/uvx servers
- auth: api_key (prompts -> ~/.hermes/.env), oauth (provider-mediated
or native MCP), or none
Catalog entries are never auto-updated. Users re-run `hermes mcp install`
to refresh. Credentials always go to ~/.hermes/.env (the .env-is-for-secrets
rule), never to per-server env blocks.
Ships n8n as the reference manifest (https://github.com/CyberSamuraiX/hermes-n8n-mcp).
Tests: 19 catalog tests + E2E install/uninstall round-trip via the shipped
manifest.
* feat(mcp): tool-selection checklist + Linear catalog entry
Adds install-time tool selection so users only enable the MCP tools they
actually want, and ships Linear as a second reference catalog entry to
demonstrate the http+oauth path alongside n8n's stdio+api_key+git-bootstrap.
Tool selection flow:
install (clone/auth/credentials) ->
probe server for available tools ->
curses checklist with pre-checked rows ->
write mcp_servers.<name>.tools.include
Pre-check priority:
1. user's prior tools.include (reinstall preserves selection)
2. manifest's tools.default_enabled (curated subset)
3. all probed tools (default)
Probe-failure fallback (server unreachable, OAuth not yet complete,
backing service offline):
- manifest declared default_enabled -> applied directly
- no default declared -> no filter written (all-on when reachable)
- both cases point user at hermes mcp configure <name>
Manifest schema additions:
tools:
default_enabled: [list, of, tool, names] # optional
Updates:
- optional-mcps/linear/manifest.yaml -- new reference entry (http+oauth)
- optional-mcps/n8n/manifest.yaml -- tools.default_enabled set to the
8 read-mostly tools; mutating tools (activate/deactivate, container_logs)
pruned by default
- docs: new 'Tool selection at install time' section in features/mcp.md
Tests: 7 new tests in TestToolSelection covering probe-success / probe-fail
matrix, manifest-default filtering, reinstall-preserves-selection, and
invalid-default-enabled rejection. 26 catalog tests + 32 existing
mcp_config tests passing.
* feat(mcp): polish — picker unification, include-mode convergence, hardening
Addresses review findings on PR #30870. Lands all improvements that
belong in this PR before merge; defers separate cleanup (consolidating
two probe implementations, change-detector tests) to follow-ups.
Picker UX (mcp_picker.py)
- Unifies catalog + custom (user-added) MCPs in one view with distinct
status badges (available / enabled / installed (disabled) /
custom — enabled / custom — disabled)
- Adds 'Configure tools (probe server + re-pick)' action to both the
catalog-installed and custom-row submenus — the existing
hermes mcp configure flow was previously unreachable from the picker
- Loops until ESC/q so the user can manage several entries in one
session instead of having to re-launch
- Uninstall message now mentions .env credentials are preserved with a
pointer to clean them up manually if no longer needed
- Surfaces a 'requires a newer Hermes' warning per future-manifest
entry instead of silently hiding it
Catalog (mcp_catalog.py)
- catalog_diagnostics() exposes which manifests were skipped and why
(future_manifest vs invalid) so UIs can give actionable feedback
- _do_git_install detects SHA-shaped refs (regex /[0-9a-f]{7,40}/)
and skips the doomed 'git clone --branch <sha>' attempt — clone --branch
only accepts branches/tags, so SHAs always failed noisily before
falling back to the full-clone path
- Probe-success all-tools-enabled message now mentions that new tools
the server adds later will be auto-enabled (no-filter mode)
Convergence (tools_config.py)
- _configure_mcp_tools_interactive now writes tools.include (whitelist)
instead of tools.exclude (blacklist), matching the catalog flow and
hermes mcp configure. The on-disk config shape no longer depends on
which UI the user touched last
- Two existing tests updated to assert the new include-mode contract
Discoverability
- Setup wizard final step now prints 'Browse curated MCPs: hermes mcp'
- Three tip-corpus entries pointing at the new catalog
- Docs updated with: trust model (manifests run code locally, gated by
PR review, but read before installing), runtime ${ENV_VAR} substitution
semantics, and the manifest_version forward-compat behavior
Tests
- 7 new tests covering future-manifest diagnostics, custom MCP picker
rows, SHA-ref git-install path, branch-ref git-install path, and the
tools_config include-mode write contract
- 80 MCP-related tests passing across test_mcp_catalog.py,
test_mcp_config.py, test_mcp_tools_config.py
* fix(mcp): drop setup-wizard catalog hint to satisfy supply-chain scanner
The wizard line 'Browse curated MCPs: hermes mcp' triggered the
CI supply-chain scanner because it pattern-matches on edits to any
file named hermes_cli/setup.py — that filename matches the Python
'install-hook file' heuristic even though this setup.py is the
user-facing 'hermes setup' wizard, not a packaging install hook.
The catalog is already surfaced via three tip-corpus entries in
hermes_cli/tips.py (which the scanner doesn't flag), so dropping the
wizard mention loses no discoverability. Worth revisiting after a
scanner allowlist for this specific file lands.
|
||
|
|
cea87d9139 |
fix(skills-hub): show every catalog source on /docs/skills (skills.sh, ClawHub, browse.sh, OpenAI, …) (#32336)
The Skills Hub page was stuck on a stale Feb 25 snapshot, showing only Built-in
+ Optional + Anthropic + LobeHub. The unified index already has 2078 skills
from skills.sh / ClawHub / LobeHub / GitHub taps / Claude Marketplace, and
BrowseShSource adds another ~330 — none of it was reaching the page.
Changes:
- website/scripts/extract-skills.py: read website/static/api/skills-index.json
(the unified multi-source catalog, rebuilt twice daily) as the canonical
external source. Keep the legacy skills/index-cache/ fallback for offline
builds. Add friendly per-source labels (skills.sh, ClawHub, browse.sh,
OpenAI, HuggingFace, Anthropic, LobeHub, etc.) and per-entry installCmd.
- website/src/pages/skills/index.tsx: add source pills + ordering for the 11
new sources; render installCmd from the index entry.
- website/scripts/prebuild.mjs: when no local skills-index.json exists, fetch
the live one from hermes-agent.nousresearch.com so local 'npm run build'
matches production without burning GitHub API quota.
- scripts/build_skills_index.py: crawl BrowseShSource so browse.sh entries
land in the unified index. Adjust source_order.
- tools/skills_hub.py: GitHubSource.DEFAULT_TAPS — openai/skills moved its
skills into skills/.curated/ and skills/.system/, so add both as explicit
taps (the listing code skips dotted dirs by design). Drop
VoltAgent/awesome-agent-skills (README-only, no SKILL.md files) and
MiniMax-AI/cli (singular skill, not a tap directory). Net effect: github
source jumps from 83 → 143 skills, with OpenAI properly included.
- .github/workflows/deploy-site.yml: build the unified index BEFORE running
extract-skills.py — previous order meant extract-skills always fell back
to the legacy cache. Drop the 'skip if file exists' guard; the file is
gitignored and must be rebuilt every deploy.
- .github/workflows/skills-index.yml: drop the broken 'deploy-with-index'
job (it cp'd 'landingpage/\*' which no longer exists, failing every cron
run since the landingpage move). Replace it with a workflow_dispatch
trigger of deploy-site.yml so the index refresh still reaches production
on schedule.
- website/docs/user-guide/features/skills.md: drop VoltAgent from the
default-taps doc list to match the code.
Before: 695 skills (Built-in 90, Optional 84, Anthropic 16, LobeHub 505).
After: 2168 skills across 9 source pills, including the 1212 skills.sh
entries the user expected to see.
|
||
|
|
bbc8f2f961 |
chore(models): drop retired grok-4-1-fast from metadata, tests, docs
xAI retired grok-4-1-fast. hermes_cli/models.py already removed it from the static fallback in an earlier commit, but the context-length metadata, the tests pinning those values, and the provider doc still referenced the retired ID. Clean those up so retired model names stop appearing in user-facing output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
386f245d9d |
feat(skills): add optional openhands skill — closes #477
Adds an optional autonomous-ai-agents skill that delegates coding tasks to the OpenHands CLI (https://github.com/All-Hands-AI/OpenHands). Sits alongside claude-code / codex / opencode and is the model-agnostic option in that family — any LiteLLM-supported provider works. This is a ground-truth rewrite of #19325 by @xzessmedia (Tim Koepsel). The original PR's SKILL.md was drafted by the OpenHands agent itself and hallucinated several flags that don't exist in the real CLI (\`--model\`, \`--max-iterations\`, \`--workspace\`, \`--sandbox docker\`), pointed at the wrong PyPI package (\`openhands-ai\`, which is the legacy V0 SDK), and claimed native Windows support that the upstream docs explicitly disclaim. Rather than cherry-pick and rewrite half the lines under contributor authorship, the SKILL.md was rebuilt against a verified install (\`uv tool install openhands --python 3.12\`) and a real end-to-end \`--headless --json\` run against openrouter/openai/gpt-4o-mini. Authorship credited via the \`author:\` frontmatter field and an AUTHOR_MAP entry in scripts/release.py. Changes: - optional-skills/autonomous-ai-agents/openhands/SKILL.md (new) - website/docs/user-guide/skills/optional/autonomous-ai-agents/autonomous-ai-agents-openhands.md (auto-gen) - website/docs/reference/optional-skills-catalog.md (one new row) - website/sidebars.ts (one new entry under Optional → Autonomous AI Agents) - scripts/release.py (AUTHOR_MAP entry for xzessmedia) Pitfalls documented in the SKILL came from running the tool, not from the upstream README: LiteLLM bedrock/sagemaker stderr noise on every invocation, banner spam (\`OPENHANDS_SUPPRESS_BANNER=1\` required), \`--override-with-envs\` mandatory or the CLI ignores LLM_* env vars entirely, the dashed-vs-undashed Conversation ID footgun for \`--resume\`, LiteLLM model-slug double-prefix when going through OpenRouter. |
||
|
|
5671461c0c |
feat(skills): add code-wiki skill — closes #486 (#32240)
* feat(skills): add code-wiki skill — closes #486 Bundled skill at skills/software-development/code-wiki/ that generates comprehensive documentation for any codebase: project overview, architecture walkthrough with Mermaid flowchart, per-module deep-dives, class diagram, sequence diagrams, getting-started guide, and (when applicable) API reference. Output defaults to ~/.hermes/wikis/<repo-name>/ (external to repo, like Google CodeWiki); in-repo output supported when user explicitly requests it. Uses only existing Hermes tools (terminal, read_file, search_files, write_file) — no Docker, no external services, no extra dependencies. Works on local repos and GitHub URLs (shallow-clones to a temp dir). Bounded scope defaults (depth 3, cap 10 modules) keep token cost reasonable on large repos. * refactor(skills): move code-wiki to optional-skills Per the 'when in doubt, optional' rule — wiki generation is a 'I want this big thing right now' capability, not daily-driver behavior. Lines up with finance/research/blockchain skills as install-on-demand rather than always loaded. Install via: hermes skills install official/software-development/code-wiki |
||
|
|
bdf3696705 |
docs(mcp-oauth): document paste-back flow and SSH options for remote MCP OAuth (#32067)
Follow-up to #32053. The OAuth-over-SSH guide and the MCP feature page previously only covered xAI and Spotify. Now that MCP servers can complete OAuth via stdin paste-back on remote/headless hosts, document it. oauth-over-ssh.md: - Add MCP servers to the 'Which Providers Need This' table. - New 'MCP Servers' section covering: paste-back (no setup, works anywhere), SSH port forward (same pattern as xAI/Spotify), and the 30s config-auto-reload race pitfall (use 'hermes mcp login <server>' from a fresh terminal instead of editing config from inside a running session). mcp.md: - New 'OAuth-authenticated HTTP servers' section under HTTP servers, covering auth: oauth config, token cache path, paste-back vs SSH tunnel for headless hosts, and the same reload-race pitfall. - Cross-links to the OAuth-over-SSH guide anchor. |
||
|
|
d3ffbc6409 |
feat(stt): add stt.providers.<name> command-provider registry
Mirror of the TTS command-provider registry (PR #17843) for STT. Lets any shell-driven ASR engine — Doubao ASR, NVIDIA Parakeet, whisper.cpp builds, SenseVoice, curl pipelines — become an STT backend with zero Python. Complements the legacy HERMES_LOCAL_STT_COMMAND escape hatch (preserved untouched via the built-in local_command path) and the register_transcription_provider() Python plugin hook also shipped in this PR. Resolution order (mirrors TTS exactly): 1. Built-in (local, local_command, groq, openai, mistral, xai) → native handler. Always wins. 2. stt.providers.<name>: type: command → command-provider runner. 3. Plugin-registered TranscriptionProvider → plugin dispatch. 4. No match → 'No STT provider available'. Files ----- - tools/transcription_tools.py: BUILTIN_STT_PROVIDERS frozenset retained; added _resolve_command_stt_provider_config, _transcribe_command_stt, and local helpers for template rendering, shell-quote context, and process-tree termination. Helpers are documented as mirrors of their tts_tool.py counterparts (kept local to avoid cross-tool private import). Wire-in is one insertion point in transcribe_audio() after the xai elif and before the plugin dispatcher. Plugin dispatcher additionally defensively short-circuits when a same-name command config exists (command-wins-over-plugin invariant). - tests/tools/test_transcription_command_providers.py: 50 new tests covering resolution (builtin precedence, type/command gating, case-insensitive lookup, legacy stt.<name> back-compat), helpers (timeout fallback, format validation, iter, has-any), template rendering (shell-quote contexts, doubled-brace preservation), end-to-end via _transcribe_command_stt (output_path read, stdout fallback, timeout, nonzero exit envelope, model override, language precedence), and dispatcher integration via the real transcribe_audio() including command-wins-over-plugin and builtin-shadow-rejection. - tests/plugins/transcription/check_parity_vs_main.py: extended from 10 to 13 scenarios. New cases: command-provider-installed, command-vs-plugin-same-name (verifies command wins precedence), explicit-openai-with-command-shadow (verifies built-in wins). Adds command_provider dispatch_kind detection via transcript prefix (CMD: vs PLUGIN:) so command-provider scenarios can be distinguished from plugin scenarios even when sharing a provider name. - website/docs/user-guide/features/tts.md: new 'STT custom command providers' section symmetric to the TTS section — example config, placeholder grammar table (input_path / output_path / output_dir / format / language / model), transcript-read-back semantics (file first, then stdout fallback), optional keys table, behavior notes, security note. Updated 'Python plugin providers (STT)' to include the new 'When to pick which (STT)' decision table and updated resolution-order section (now 4 layers instead of 3). Verification ------------ 189/189 STT targeted tests + 50/50 new command-provider tests pass. Combined sweep: tests/tools/ 5576/5576, tests/agent/ + tests/hermes_cli/ 8623/8623 — zero regressions across 14,199 tests. Parity harness: 13 scenarios, 9 OK + 4 expected diffs (no_provider_error → plugin, plugin_unavailable, command_provider × 2). E2E live-verified in an isolated HERMES_HOME with a real .wav file: command: → dispatched to stt.providers.my-fake-cli plugin: → dispatched to registered TranscriptionProvider command-wins-over-plugin: → command provider beats same-name plugin builtin-wins-over-command: → built-in OpenAI handler fires; stt.providers.openai: type: command does NOT hijack it. |
||
|
|
2cd952e110 |
feat(stt): add register_transcription_provider() plugin hook
Add an opt-in Python plugin surface for speech-to-text backends,
mirroring the TTS hook pattern. New backends (OpenRouter, SenseAudio,
Gemini-STT, custom proprietary engines) can be implemented as plugins
without modifying tools/transcription_tools.py.
Built-ins always win
--------------------
The 6 built-in STT providers (local/faster-whisper, local_command,
groq, openai, mistral, xai) keep their native handlers. Plugins
attempting to register under a built-in name are rejected at
registration time with a warning and re-checked defensively at
dispatch.
Resolution order
----------------
1. stt.provider matches a built-in → built-in dispatch (unchanged)
2. stt.provider matches a registered plugin →
a. if plugin.is_available() returns False → unavailability envelope
identifying the plugin (not the generic "No STT provider"
message — the user explicitly opted into this plugin)
b. otherwise plugin.transcribe() with model + language forwarded
from stt.<provider>.{model,language} config
3. No match → legacy "No STT provider available" error (unchanged)
Per-provider config namespace
-----------------------------
Plugins read their config from stt.<provider> in config.yaml, mirroring
how built-ins read stt.openai.model / stt.mistral.model. The dispatcher
forwards `model` and `language` from this section. Caller's explicit
`model=` argument overrides the config-set model.
Files
-----
- agent/transcription_provider.py: TranscriptionProvider ABC
- agent/transcription_registry.py: register/get/list providers,
built-in shadow guard, _reset_for_tests
- hermes_cli/plugins.py: register_transcription_provider() on
PluginContext
- tools/transcription_tools.py: BUILTIN_STT_PROVIDERS frozenset,
_dispatch_to_plugin_provider() with availability gate, wire-in
after xai branch and before "No STT provider" error
- tests/agent/test_transcription_registry.py: 27 tests
- tests/hermes_cli/test_plugins_transcription_registration.py: 3 tests
- tests/tools/test_transcription_plugin_dispatch.py: 28 tests
(covering built-in short-circuit, plugin dispatch, exception
envelope, non-dict guard, availability gate, language forwarding)
- tests/plugins/transcription/check_parity_vs_main.py: 10-scenario
subprocess-pinned parity harness vs origin/main
- website/docs/user-guide/features/{tts,plugins}.md: docs
Behavior parity
---------------
10 scenarios, 8 OK + 2 expected DIFFs:
no_provider_error → plugin (plugin-installed scenario)
no_provider_error → plugin_unavailable (plugin-installed-unavailable
scenario; PR returns cleaner envelope)
Zero behavior change for users not opting into a plugin.
Issue follow-up to #30398.
|
||
|
|
d7c5d5dee5 | fix: avoid persisting borrowed credential secrets (#31416) | ||
|
|
3b839f4369 | fix(context): align guidance with 64k minimum | ||
|
|
1d5deac346 |
fix(website): cross-locale doc links + drop empty ko locale (#31895)
The locale switcher appeared broken because hardcoded markdown links (`](/docs/X)`) got double-prefixed by Docusaurus to `/docs/<locale>/docs/X` (404) in non-English locales, and the MDX hero `<a href>` on the index page escaped locale routing entirely. Changes: - Rewrite 922 `](/docs/X)` -> `](/X)` across 166 docs files (strip trailing .md too). Docusaurus prepends locale + baseUrl itself. - docs/index.md -> index.mdx; hero "Get Started" anchor -> Docusaurus <Link> so it stays inside the active locale. - Drop `ko` locale entirely from docusaurus.config.ts + delete i18n/ko/ (4 stale auto-translated kanban pages, <2% coverage, misleading). Verified `npm run build` succeeds for both en and zh-Hans; `build/zh-Hans/ index.html` has no /docs/zh-Hans/docs/... double-prefixed paths. PR2 will translate the 335 English docs into i18n/zh-Hans/. |
||
|
|
7e165e843d |
Merge pull request #31760 from NousResearch/hermes/hermes-bf5898da
feat(docker)!: s6-overlay container supervision (salvage of #30136) |
||
|
|
a3abeb5954 |
Merge pull request #31775 from NousResearch/extending-docker-docs
docs(docker): add 'Installing more tools in the container' section |
||
|
|
6840ca2d1e |
docs(docker): add 'Installing more tools in the container' section
Documents five approaches for adding tools beyond what the official image ships with: npx/uvx for npm/Python tools, ad-hoc apt installs that Hermes remembers, derived images for durability, sidecar containers for multi-service stacks, and upstreaming via issue/PR for broadly useful additions. |
||
|
|
af144cd60d |
fix(model): include Premium+ in xAI OAuth label
X Premium+ also grants Grok OAuth access — the 'SuperGrok Subscription' wording suggested SuperGrok was the only entitlement path. Updated to 'SuperGrok / Premium+' across the picker label, setup wizard, auth flows, and docs so Premium+ subscribers know the row applies to them too. |
||
|
|
a1a53a5d6e |
docs(docker): dashboard IS supervised — update note that contradicted the PR
PR #30136 review caught that website/docs/user-guide/docker.md still said "The dashboard side-process is **not supervised** — if it crashes, it stays down until the container restarts." That was true under tini but is the opposite of the s6 behavior this PR ships and `test_dashboard_restarts_after_crash` proves. Replace with a description of what users actually see now: automatic restart by s6-overlay, new PID after a short backoff, logs via `docker logs`. The standalone-container caveat carries forward unchanged. |
||
|
|
4b4c36cb61 |
feat(docker): remove gosu from bundled image; s6-setuidgid handles privilege drop
The s6-overlay migration replaced every runtime use of gosu with s6-setuidgid (in stage2-hook.sh, main-wrapper.sh, per-service run scripts, and cont-init.d hooks), but the gosu binary itself was still being copied into the image from tianon/gosu, and several comments across the repo still pointed to it. Image changes: - Drop the FROM tianon/gosu:1.19-trixie AS gosu_source stage - Drop the COPY --from=gosu_source /gosu /usr/local/bin/ layer - Net: one fewer base-image pull, ~12-15 MB layer eliminated Documentation/comment refresh (no behavior change): - Dockerfile: update root-user rationale comment + cont-init.d comment - docker/main-wrapper.sh: drop "pre-s6 contract (gosu drop)" reference - docker-compose.yml: update UID/GID remap comment - .hadolint.yaml: update DL3002 ignore rationale - website/docs/user-guide/docker.md: privilege-drop helper is s6-setuidgid now - hermes_cli/config.py: docker_run_as_host_user docstring tools/environments/docker.py runs *arbitrary user images* via the terminal backend, not the bundled Hermes image. It still needs SETUID/ SETGID caps so user images that use gosu/su/s6-setuidgid all work. Renamed the cap-list constant _GOSU_CAP_ARGS → _PRIVDROP_CAP_ARGS and updated comments to list s6-setuidgid alongside the others as examples. The matching test (test_security_args_include_setuid_setgid_for_gosu_drop → test_security_args_include_setuid_setgid_for_privdrop) was renamed and its docstring updated; behavior is unchanged. Verification: - hadolint clean against .hadolint.yaml - shellcheck clean against all docker/ shell scripts - Image rebuilt successfully (sha 1a090924ccea) - Docker harness: 19 passed in 41.87s (every Phase 0 test + Phase 4 per-profile-gateway lifecycle + container-restart reconciliation) - tests/tools/test_docker_environment.py: 23 passed (rename did not break test discovery; pre-existing unrelated mock warning) The plan document (docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md) intentionally retains its historical references to gosu — it describes the pre-s6 entrypoint as background for understanding the migration. |
||
|
|
a36221ed91 |
docs(s6): document container supervision; doctor + skill + user-guide updates
Phase 5 of the s6-overlay supervision plan. Documentation + small
diagnostic cleanups; no behavior changes.
website/docs/user-guide/docker.md:
- Replace the old 'entrypoint script does the bootstrap' section
with the s6-overlay boot flow (cont-init.d/01-hermes-setup,
cont-init.d/02-reconcile-profiles, static main-hermes + dashboard
services, ENTRYPOINT-as-main-program pattern).
- Add a 'Per-profile gateway supervision' subsection covering the
new lifecycle commands, restart semantics, log persistence, and
'Manager: s6 (container supervisor)' status reporting.
- Add 'Breaking change vs. pre-s6 images' callout naming the
/init ENTRYPOINT and pointing affected wrappers at the pin
workaround.
website/docs/user-guide/profiles.md:
- Add a note under 'Persistent services' pointing container users
at the docker.md section explaining s6 supervision inside the
image. Host-side systemd/launchd documentation is unchanged.
skills/software-development/hermes-s6-container-supervision/SKILL.md:
- New maintainer skill covering the supervision-tree map, file
layout, the Architecture B rationale (cont-init.d args + halt
exit-code propagation), quick recipes, and the 8 pitfalls we hit
while implementing the plan (PATH-without-/command, root-owned
profile dirs, SOUL.md as marker, the '143' anti-pattern, etc.).
hermes_cli/doctor.py:
- _check_gateway_service_linger skips on s6 (the linger concept
doesn't apply inside the container).
- New _check_s6_supervision section reports main-hermes/dashboard
state and per-profile-gateway count (registered vs supervised
up), only inside the s6 container. Host doctor output unchanged.
- External Tools / Docker check no longer emits a 'docker not
found' warning inside the container; prints an explanatory
info line instead. Still respects an explicit TERMINAL_ENV=docker
(in case the user mounted /var/run/docker.sock).
hermes_cli/gateway.py:
- Document _container_systemd_operational more precisely: it's
NOT for our Hermes Docker image (s6-overlay handles that via
detect_service_manager() == 's6'). It still covers
systemd-nspawn / k8s-with-systemd-init cases, so leaving it in
place is correct; the docstring just makes that explicit.
Test harness (verification, no test changes in this commit):
19 passed, 0 xfailed. 66 service-manager / container-boot /
profiles-s6-hooks / gateway-s6-dispatch unit tests still green.
61 doctor tests still green. Hadolint + shellcheck clean.
Refs: docs/plans/2026-05-07-s6-overlay-dynamic-subagent-gateways.md
|
||
|
|
00ec0b617c |
feat(tts): add register_tts_provider() plugin hook (closes #30398)
Adds a `TTSProvider(ABC)` + `register_tts_provider()` extension point to the plugin context API, **alongside** the existing config-driven `tts.providers.<name>: type: command` registry from PR #17843. This is additive — the command-provider surface stays as the primary way to add a TTS backend. The hook covers cases the shell-template grammar can't reasonably express: - Native Python SDKs without a CLI (Cartesia, Fish Audio, etc.) - Streaming synthesis (chunked Opus → voice-bubble delivery) - Voice metadata API for the `hermes tools` picker - OAuth-refreshing auth flows None of the 10 inline built-in providers (`edge`, `openai`, `elevenlabs`, `minimax`, `gemini`, `mistral`, `xai`, `piper`, `kittentts`, `neutts`) are migrated to plugins. They stay inline. The hook is for *new* engines that aren't built-in. ## Resolution order The dispatcher's resolution order is the load-bearing invariant: 1. `tts.provider` is a built-in name → built-in dispatch. **Always wins.** 2. `tts.provider` matches `tts.providers.<name>` with `command:` set → command-provider dispatch (PR #17843). 3. `tts.provider` matches a plugin-registered `TTSProvider` → plugin dispatch (new). 4. No match → falls through to Edge TTS default (legacy behavior). Built-ins-always-win is enforced at THREE layers: - Registry: `register_provider()` rejects shadowing names with a warning. - Dispatcher: `_dispatch_to_plugin_provider()` short-circuits built-in names defensively before consulting the registry. - Picker: `_plugin_tts_providers()` filters built-in shadows out of the `hermes tools` row list defensively. Command-providers-win-over-plugins is enforced at TWO layers: - The caller in `text_to_speech_tool` checks `_resolve_command_provider_config` first. - `_dispatch_to_plugin_provider` re-checks for a same-name command config defensively so a refactor of the caller can't silently break the invariant. ## New files - `agent/tts_provider.py` — `TTSProvider(ABC)` with `synthesize()` (required), `list_voices()`, `list_models()`, `get_setup_schema()`, `stream()`, `voice_compatible` (all optional with sane defaults). Mirrors `agent/image_gen_provider.py` shape. - `agent/tts_registry.py` — `register_provider`/`get_provider`/`list_providers` with `_BUILTIN_NAMES` reject-shadowing invariant. Mirrors `agent/image_gen_registry.py` shape. - `plugins/tts/...` directory ready for community plugins (none shipped). ## Modified files - `hermes_cli/plugins.py` — `register_tts_provider()` method on `PluginContext`. Matches the gating shape of `register_image_gen_provider()` / `register_browser_provider()`. - `tools/tts_tool.py` — `_dispatch_to_plugin_provider()` + `_plugin_provider_is_voice_compatible()` + walrus-elif wiring into the main dispatcher. Built-in elif chain untouched. - `hermes_cli/tools_config.py` — `_plugin_tts_providers()` injects plugin rows into the Text-to-Speech picker category alongside the 10 hardcoded built-in rows. ## Tests - `tests/agent/test_tts_registry.py` — 47 tests covering registration, lookup, ABC contract, helpers, AND a `TestBuiltinSync` regression test that fails if `agent.tts_registry._BUILTIN_NAMES` drifts from `tools.tts_tool.BUILTIN_TTS_PROVIDERS` (kept duplicated due to circular import constraints). - `tests/tools/test_tts_plugin_dispatch.py` — 35 tests covering built-in-always-wins, command-wins-over-plugin, plugin dispatch, exception passthrough, voice_compatible helper. - `tests/hermes_cli/test_tts_picker.py` — 10 tests covering the picker surface, builtin shadowing defense, integration with `_visible_providers`. - `tests/hermes_cli/test_plugins_tts_registration.py` — 3 end-to-end tests via `PluginManager.discover_and_load()`. - `tests/plugins/tts/check_parity_vs_main.py` — 9-scenario subprocess parity harness vs `origin/main`. The only intentional diff is `fallback_edge → plugin` for the `plugin-installed` scenario. ## Verification - 95/95 new tests pass. - 170/170 pre-existing TTS tests (test_tts_command_providers, test_tts_max_text_length, test_tts_speed, etc.) pass unchanged. - Parity harness against `origin/main`: 8 OK + 1 expected DIFF. - E2E smoke: a registered plugin's `synthesize()` is called via `text_to_speech_tool` with the standard JSON envelope returned. - Ruff clean on all touched files. ## Docs - `website/docs/user-guide/features/tts.md` — new "Python plugin providers" section with a decision table (command-provider vs plugin), minimal plugin example, and the optional-hook reference. - `website/docs/user-guide/features/plugins.md` — TTS row updated to mention both surfaces (command-provider primary, plugin for SDK/streaming). Closes #30398 |
||
|
|
8e68426981 |
fix(cli): add inline --yes/now skip for destructive slash commands (#30768)
Issue #30768 reports that on native Windows PowerShell the destructive-slash confirmation modal renders but never registers keypresses, leaving the user unable to confirm or cancel /reset, /new, /clear, or /undo. The modal works on macOS, Linux, and WSL; PR #23907 (merged May 11) replaced the daemon-thread input() pattern with a prompt_toolkit-native keybinding modal but the win32 input pipeline apparently doesn't dispatch keys to the filter-conditioned handlers. The modal investigation is ongoing. This change ships the immediate escape hatch: append `now`, `--yes`, or `-y` to any destructive slash command to bypass the modal and run the action immediately. Works on every platform without touching the broken Windows code path. /reset now -> reset, no modal /new --yes my-session -> new session titled "my-session", no modal /clear -y -> clear, no modal /undo -y -> undo, no modal The default behavior (modal prompts when approvals.destructive_slash_confirm is True) is unchanged for users who don't pass a skip token. Implementation: - New classmethod HermesCLI._split_destructive_skip(text) -> (remainder, skip) parses a destructive-slash command string, strips the leading "/cmd" word and any recognized skip tokens (case-insensitive exact match, not substring), and reports whether a skip was requested. - HermesCLI._confirm_destructive_slash gains an optional cmd_original= arg. When the arg contains a skip token, it returns "once" immediately — before the gate check and before any modal rendering. - The /clear, /new, /undo handlers in process_command pass cmd_original through. /new additionally uses _split_destructive_skip to strip skip tokens from the remaining text before deriving the session title, so "/new now My Session" yields title="My Session" (not "now My Session"). Tests: - 7 new unit tests in tests/cli/test_destructive_slash_confirm.py covering the helper (recognized tokens, command-word stripping, case-insensitive exact match, None/empty input) and the modal bypass (now and --yes both skip; no-skip-token still consults the modal). - 3 new integration tests in tests/cli/test_destructive_slash_inline_skip_e2e.py driving HermesCLI.process_command end-to-end and asserting (a) new_session is invoked, (b) the modal is never reached, (c) the skip token does not leak into the session title, and (d) the no-skip-token path still reaches the modal as a sanity check that we haven't accidentally short-circuited the normal flow. All 31 tests across the destructive-slash test surface pass. Docs: - website/docs/reference/slash-commands.md documents the new flags both in the destructive-commands table and the dedicated approval section, with a link back to issue #30768 explaining why the escape hatch exists. |
||
|
|
7ab1677362 |
feat(security): on-demand supply-chain audit via OSV.dev (#31460)
Adds 'hermes security audit' — a one-shot vulnerability scan against
OSV.dev covering three surfaces a Hermes user actually controls:
1. The running Python's installed PyPI dists (importlib.metadata)
2. Plugin requirements.txt / pyproject.toml pins under ~/.hermes/plugins/
3. Pinned npx/uvx MCP servers in config.yaml
Zero new dependencies (stdlib urllib + importlib.metadata + tomllib +
concurrent.futures). No auth required for OSV's public batch API.
Flags: --json, --fail-on {low,moderate,high,critical} (default: critical),
--skip-venv, --skip-plugins, --skip-mcp
Output groups findings by source, sorts by severity descending, surfaces
fixed-versions inline. Exit 1 when any finding meets the --fail-on tier.
Deliberately out of scope: globally-installed pip/npm, editor/browser
extensions, daily background scans, auto-blocking of installs. The audit
is on-demand by design — daily scans become noise the user trains
themselves to ignore.
|
||
|
|
f378f00bfb |
fix(feishu): validate verification token before reflecting url_verification challenge
When FEISHU_VERIFICATION_TOKEN is configured, an unauthenticated remote could previously prove endpoint control by sending a url_verification payload with any attacker-controlled challenge string — the handler reflected the challenge BEFORE running the token check. Move the verification_token check ahead of the url_verification echo so the challenge response is gated on a valid token. Add a regression test covering the wrong-token case. Also fix the stale test_connect_webhook_mode_starts_local_server fixture to set FEISHU_VERIFICATION_TOKEN (post #30746 webhook mode requires a secret). Salvaged from PR #29663 by @m0n3r0 — kept the url_verification reorder and its regression test; dropped the host-conditional weakening of the #30746 secret guard (we want webhook secrets required regardless of bind host, not only on 0.0.0.0/::). Docs updated to call out the gating. Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com> |
||
|
|
be27bfed01 | security: harden API server key placeholder handling (#30738) | ||
|
|
2df2f9190b | fix(docker): keep dashboard side-process loopback by default (#30740) | ||
|
|
bc3f1f4f34 |
feat(secrets/bitwarden): EU Cloud + self-hosted server URL support (#31378)
Closes #31370. bws defaults to the US identity endpoint, so EU Cloud and self-hosted machine-account tokens fail with [400 Bad Request] {"error":"invalid_client"} during 'hermes secrets bitwarden setup'. The token is valid — it's just being checked against the wrong region. Add a Bitwarden region step to the wizard between the access-token and project-list steps: Step 1 Install bws Step 2 Provide access token Step 3 Pick region <-- new (US / EU / self-hosted-custom-URL) Step 4 Pick project (now talks to the right endpoint) Step 5 Test fetch Region is stored in config.yaml as secrets.bitwarden.server_url and plumbed into every bws subprocess as BWS_SERVER_URL (project list, secret list, test fetch, and the env_loader startup pull). Also: - Non-interactive: 'hermes secrets bitwarden setup --server-url ...' - Pre-existing BWS_SERVER_URL in the shell is detected and reused - Cache key includes server_url so EU/US fetches don't collide - 'hermes secrets bitwarden status' shows the configured region - 'invalid_client' / '400 Bad Request' from bws now triggers a hint pointing at the region setting instead of looking like a bad token |
||
|
|
2666009ccc |
docs: dedicated Nous Portal integration page and setup guide (#31296)
If Nous Portal is the recommended way to run Hermes Agent, it deserves more than a sub-section buried under `## Inference Providers`. Add two new pages and shrink the existing providers.md section to a stub that points at them. New pages: - `website/docs/integrations/nous-portal.md` — landing page. What's in the subscription (300+ model catalog table, Tool Gateway breakdown, Nous Chat, cross-platform parity, no-dotfile-credentials). Hermes 4 recommendation note. Setup paths (fresh install, existing install, headless / SSH, profiles). Day-to-day usage (portal status / portal tools / portal open, switching models, mixing gateway with own backends, subscription management). Configuration reference. Token handling. Troubleshooting. Cross-links. Sidebar-position 1 — first entry under Integrations. - `website/docs/guides/run-hermes-with-nous-portal.md` — task script. Eight numbered steps: subscribe → setup --portal → verify with portal status → first chat → switch models → customize gateway routing → voice mode → cron/always-on. Per-step troubleshooting. 'What this gets you in plain numbers' comparison table. Sidebar position 1 — first entry under Guides & Tutorials. Existing providers.md: - Replace the 80-line `### Nous Portal` deep-dive with a 13-line stub that summarizes the value prop, lists the three CLI commands, and links to the new pages. Saves ~6KB. Other provider sections and callouts (Codex Note, Two Commands, Tool Gateway tip) preserved. Sidebar: - `integrations/nous-portal` inserted right after `integrations/index`, before `integrations/providers`. - `guides/run-hermes-with-nous-portal` inserted first in Guides & Tutorials. |
||
|
|
a7b622effc |
docs(providers): move Nous Portal first, Google Gemini OAuth last (#31287)
Reorder the per-provider subsections under '## Inference Providers' so Nous Portal — the recommended setup — leads the list, and Google Gemini via OAuth (which carries a policy-risk warning) drops to last position right before the '## Custom & Self-Hosted LLM Providers' section. All other provider sections keep their relative order. Pure section move; no content changes. |
||
|
|
72ff3e909c |
docs(providers): rewrite Nous Portal section as primary recommended path (#31230)
The old section sold Nous Portal as access to Hermes-4 models, which is backwards — Hermes 4 is a chat/reasoning family that's NOT recommended for Hermes Agent (per portal.nousresearch.com/info itself). The actual value prop is the 300+ frontier agentic models (Claude, GPT, Gemini, DeepSeek, etc.) plus the Tool Gateway plus Nous Chat under one subscription. Rewrite to lead with that, position the portal as the recommended way to run Hermes Agent, demote Hermes 4 to a 'note' explaining why it's not the right pick for agent workloads, and link to the manage-subscription page from setup. |
||
|
|
e42fcc5625 |
fix(provider): make config.yaml model.provider the single source of truth (#31222)
Policy: if it ain't a secret it goes in config.yaml. HERMES_INFERENCE_PROVIDER was leaking behavioral config into the .env surface, including from the gateway, which bypassed config.yaml entirely. Behavior: - gateway/run.py: drop HERMES_INFERENCE_PROVIDER read in _resolve_runtime_agent_kwargs. Gateway now flows through resolve_runtime_provider() with no `requested` override, which reads model.provider from config.yaml first. Docs/UX (strip env var from user-facing surface): - --provider help text no longer mentions the env var - cli-config.yaml.example same - reference/environment-variables.md: remove HERMES_INFERENCE_PROVIDER row and the cross-reference from HERMES_INFERENCE_MODEL - reference/cli-commands.md: blank the env-var column for --provider - guides/xai-grok-oauth.md, guides/minimax-oauth.md: replace HERMES_INFERENCE_PROVIDER=x hermes invocations with config.yaml / --provider - developer-guide/adding-providers.md, model-provider-plugin.md: reframe Internal mechanism (kept as-is): - hermes_cli/main.py writes HERMES_INFERENCE_PROVIDER into the TUI subprocess env - tui_gateway/server.py reads it on TUI startup - resolve_requested_provider() / oneshot.py / cli.py still fall through to the env var as a last-resort behind config.yaml, which is what makes the TUI parent->child handoff work This stays. We just stop documenting it as a user knob. Tests: tests/gateway/test_auth_fallback.py — simplify mock to fail on first call, succeed on second; drop monkeypatch.setenv lines that no longer matter. Supersedes #31064 (closed with credit to @novax635 who surfaced the underlying issue but proposed aligning gateway *to* the env var rather than removing it). |
||
|
|
514a4eff36 |
docs(simplex): remove broken Docker install command (#26974) (#26975)
* docs(simplex): remove broken Docker install command (#26974) The "Or Docker" snippet pointed at `simplexchat/simplex-chat`, which is not a published Docker Hub image. Users following the docs hit: docker: Error response from daemon: pull access denied for simplexchat/simplex-chat, repository does not exist or may require 'docker login'. The SimpleX Chat project only publishes Docker images for its server components (smp-server, xftp-server) — the chat CLI is distributed as a binary release. Drop the broken `docker run` line and keep the verified binary-download path, with a note pointing users to the upstream Dockerfile if they want to build a container themselves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(simplex): drop misleading "Dockerfile" link text Copilot review flagged that the link text claimed "Dockerfile in the upstream repo" but the URL pointed at the repository root, not a specific Dockerfile path. Reword to "build from source from the simplex-chat repository" so the link text and target match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3b096d6f6d |
ntfy: tighten robustness, dedupe auth/truncation, add docs
Robustness: - Surface 401/404 stream failures via _set_fatal_error() so the gateway's runtime status reflects 'fatal: ntfy_unauthorized' / 'ntfy_topic_not_found' instead of staying 'connected' when the reconnect loop halts. Matches the pattern in whatsapp / telegram / sms adapters. - Strip whitespace from auth tokens so pasted tokens with trailing newlines don't produce malformed Authorization headers. Simplicity: - Extract _build_auth_header() and _truncate_body() to module-level helpers, used by both NtfyAdapter and _standalone_send. Removes the duplicated auth/truncation logic between the two paths. Docs: - website/docs/user-guide/messaging/ntfy.md — full setup guide, identity-model warning, self-hosting, cron usage, troubleshooting. - website/docs/reference/environment-variables.md — all 9 NTFY_* vars. - website/docs/user-guide/messaging/index.md — platform comparison row. - website/sidebars.ts — sidebar entry between simplex and open-webui. Tests: 78/78 (+ 10 new robustness tests covering token hygiene, fatal error propagation for 401/404, and the _truncate_body helper). |
||
|
|
ad11327db0 |
feat(kanban): warn users that scratch workspaces are deleted on completion (#30949)
First scratch workspace creation on an install now emits a one-shot warning log + a 'tip_scratch_workspace' event on the task. Sentinel file at ~/.hermes/kanban/.scratch_tip_shown silences subsequent creations across the whole install. Behavior unchanged — scratch is still ephemeral by design. This just makes the design visible to new users (reported in user community: 'progress files vanished, no warning anywhere'). Docs (en + ko) updated to spell out 'Deleted when the task completes' on the scratch bullet and 'Preserved on completion' on worktree/dir. |
||
|
|
64b3eb0dd7 |
docs: surface Nous Portal on pages where it solves a real problem the page describes (#30874)
Follow-up to #30869. Adds Portal mentions on user-facing pages that naturally call for an LLM + tool credentials but didn't previously acknowledge Portal as a one-stop option. - getting-started/installation.md: tip after the 'after install' block pointing at 'hermes setup --portal' for users who want everything wired at once instead of piecewise via 'hermes model' + 'hermes tools'. - user-guide/configuring-models.md: small tip near the top — the page is literally about provider/model choice and previously had zero Portal mention. - user-guide/features/voice-mode.md: Prerequisites need both an LLM and TTS — a Portal subscription is the single setup that covers both. - user-guide/features/batch-processing.md: highlights Portal as a predictable-cost option for parallel agent runs that hit many APIs. - user-guide/features/api-server.md: backend needs models + tools; one Portal sub gives a fully-equipped OpenAI-compatible endpoint. - user-guide/windows-native.md: early-beta users on Windows benefit most from skipping per-tool Windows-key-juggling. - integrations/providers.md: updates the existing Tool Gateway tip and the Nous Portal section to mention the new commands. - user-guide/features/fallback-providers.md: Nous row in the provider table now lists 'hermes setup --portal' as the fresh-install path. Tone discipline: one Portal mention per page, concrete CLI commands (no marketing copy), always solving a problem the page itself sets up. |
||
|
|
f3fb7899d0 |
docs: surface 'hermes setup --portal' and 'hermes portal' across user-facing pages (#30869)
PR #30860 added a one-shot Portal setup command and a small portal CLI surface. Update the docs so the new commands are discoverable without upgrading the tone of existing Portal mentions. - getting-started/quickstart.md: small tip near Choose a Provider pointing at 'hermes setup --portal' as the easiest fresh-install path. - user-guide/features/tool-gateway.md: lead the Get-Started section with 'hermes setup --portal' for fresh installs, keep 'hermes model' for already-configured users, and add 'hermes portal status / tools' to the activity-check commands. - user-guide/features/{web-search,image-generation,tts,browser}.md: the existing 'Nous Subscribers' tip blocks now name the one-shot command for new installs, keeping the existing 'hermes tools' path for users who only want to swap a single backend. - reference/cli-commands.md: register 'hermes portal' in the top-level command table, add a 'hermes portal' section with subcommands, and add '--portal' to the 'hermes setup' options table. Tone: each page already had a Portal mention. This PR keeps the per-page count to one and uses concrete CLI commands rather than promotional copy. Tool Gateway page is the one exception (the whole doc is about Portal). |
||
|
|
6a2df9f451 |
docs(env): clarify HERMES_ENABLE_PROJECT_PLUGINS contract (#29156)
The reference entry now documents the truthy set (``1`` / ``true`` / ``yes`` / ``on``) explicitly, matches the falsy half (``0`` / ``false`` / ``no`` / ``off`` / empty string) that the GHSA-5qr3-c538-wm9j fix re-aligned both the agent loader and the dashboard web server around, and points readers at the defence-in-depth rule that project plugins never have their Python ``api`` file auto-imported by the dashboard regardless of the env var. |
||
|
|
a7cd254c29 |
feat(tui): mouse_tracking DEC mode presets (salvage of #26681) (#30084)
* feat(tui): make display.mouse_tracking pick which DEC modes to enable
Previously the boolean flag was all-or-nothing across modes 1000+1002+1003+1006.
Inside tmux, mode 1003 (any-motion) makes every mouse cross of the prompt row
fire a clipboard probe that surfaces as "No image in clipboard" — sometimes
dozens in a row. Disabling tracking entirely killed scroll-wheel scrolling too,
since tmux's own scrollback is preempted by the alt-screen TUI.
`display.mouse_tracking` (and `/mouse <preset>`) now accepts `off | wheel |
buttons | all` in addition to the legacy booleans. `wheel` is 1000+1006:
scroll wheel + click only, no drag, no hover — the tmux-friendly subset.
`buttons` adds 1002 for drag-to-select. `all` (= legacy `true`) keeps the
hover-driven UI (scrollbar paginate-on-hover, link mouseenter, etc.).
* fix(tui): repaint + sync mouse mode when display.mouse_tracking changes
Two interacting bugs left the TUI blank when `display.mouse_tracking`
switched at runtime (config edit, /mouse <preset>):
1. AlternateScreen's effect re-runs on every `mouseTracking` change,
tearing down and re-entering the alt screen. After re-entry, ink's
frame buffers are reset by `resetFramesForAltScreen()` but nothing
schedules the follow-up render — the alt screen sits blank until
some other state change happens to trigger one. Add a
`scheduleRender()` in `setAltScreenActive`'s active=true branch so
the freshly-entered alt screen gets a full repaint immediately.
2. `setAltScreenActive` early-returns when `active` hasn't changed,
which silently drops a `mouseTracking` change if the cleanup→setup
pair somehow leaves `altScreenActive` already true. Call
`setAltScreenMouseTracking` explicitly from the AlternateScreen
effect so the in-memory mode and terminal DECSET sequence stay in
sync regardless of how `setAltScreenActive` resolved (the call is a
no-op when the mode is unchanged).
* fix(tui): address copilot review #4341269705
- tui_gateway/server.py: drop the never-referenced _MOUSE_TRACKING_MODES
frozenset (comment #3284802434). _MOUSE_TRACKING_ALIASES already
centralizes the canonical preset set via its values; the separate
constant added no behavior.
- tests/test_tui_gateway_server.py: update the existing
test_config_mouse_uses_documented_key_with_legacy_fallback to assert
the new preset strings ('all'/'off' instead of 'on'/'off',
display.mouse_tracking persisted as 'all' instead of True) and add
test_config_mouse_accepts_preset_strings_and_aliases covering /mouse
set with wheel/click/unknown (comment #3284802453). The on/off legacy
config.set return shape was an implementation detail of the boolean
flag, not a stable API — the slash command, gateway help text, and
docs all advertise the preset values now.
- ui-tui/packages/hermes-ink/src/ink/ink.tsx: schedule a render at the
end of reenterAltScreen() (comment #3284802461). Mirrors the same fix
in setAltScreenActive() from
|
||
|
|
552e9c7881 |
feat(secrets): Bitwarden Secrets Manager integration with lazy bws install (#30035)
* feat(secrets): Bitwarden Secrets Manager integration with lazy bws install
Pull API keys from Bitwarden Secrets Manager at process startup
instead of storing them all in plaintext in ~/.hermes/.env. One
bootstrap token (BWS_ACCESS_TOKEN) replaces N per-provider keys, and
rotating a credential becomes a single change in the Bitwarden web
app.
Bitwarden defaults to source of truth: secrets pulled from BSM
overwrite any matching env vars on startup so rotations actually
take effect. Set secrets.bitwarden.override_existing: false in
config.yaml to invert.
The bws binary is auto-downloaded into ~/.hermes/bin/bws on first
use (pinned to v2.0.0, SHA-256 verified against the GitHub release
checksum file). No apt, brew, or sudo required.
New surfaces:
hermes secrets bitwarden setup — interactive wizard
hermes secrets bitwarden status — config + binary + token state
hermes secrets bitwarden sync — dry-run fetch / --apply exports
hermes secrets bitwarden disable — flip enabled: false
hermes secrets bitwarden install — just download the binary
Failures (missing binary, bad token, no network) never block Hermes
startup — they emit a one-line warning to stderr and continue with
whatever credentials .env already had.
Docs: website/docs/user-guide/secrets/{index,bitwarden}.md
Tests: tests/test_bitwarden_secrets.py (26 tests, hermetic — bws
subprocess and HTTP downloads fully mocked)
* chore(infographic): add bitwarden-secrets-manager bento-grid retro-pop-grid
Generated for PR #30035 — Bitwarden Secrets Manager integration.
Style picked via pick_pr_infographic_style.py rotation:
layout: bento-grid
style: retro-pop-grid
aspect: 1:1 square
Saved at infographic/bitwarden-secrets-manager/infographic.png
|
||
|
|
ba9964ff0d |
fix(custom): pass custom provider extra body
Allow custom OpenAI-compatible providers declared under `custom_providers:`
to set provider-specific `extra_body` fields and have Hermes merge them into
chat-completions requests when the matching custom endpoint is active.
This is a manual per-provider override rather than a model-name heuristic.
OpenAI-compatible Gemma thinking support is real, but the on-wire payload
shape is backend-specific: some servers want top-level `enable_thinking`,
while vLLM Gemma and NIM-style endpoints expect `chat_template_kwargs`.
A per-provider override is safer than picking one assumed payload.
Example config:
```yaml
custom_providers:
- name: gemma-local
base_url: http://localhost:8080/v1
model: google/gemma-4-31b-it
extra_body:
enable_thinking: true
reasoning_effort: high
```
For vLLM Gemma or NIM-style endpoints, use the nested shape those servers
expect:
```yaml
extra_body:
chat_template_kwargs:
enable_thinking: true
```
Changes:
- `hermes_cli/config.py`: preserve `extra_body` in normalized
`custom_providers:` entries and allow it in the validated field set.
- `hermes_cli/runtime_provider.py`: propagate custom-provider `extra_body`
as `request_overrides.extra_body` for named custom runtime resolution,
including credential-pool paths.
- `agent/agent_init.py`: at agent init, locate the matching custom-provider
entry by `base_url` (+ optional model) and merge its `extra_body` into
`AIAgent.request_overrides`, with caller-provided overrides winning on
conflicting top-level keys.
- `plugins/model-providers/custom/__init__.py`: keep existing CustomProfile
behavior (Ollama `num_ctx`, `think=False` when reasoning disabled);
user-configured `extra_body` flows through `request_overrides`.
- `website/docs/integrations/providers.md`: document the explicit
`extra_body` override and the vLLM/Gemma `chat_template_kwargs` variant.
- Tests cover config normalization, runtime propagation, model matching,
trailing-slash equivalence, fallback when no `model` field is set, and
caller-override merging precedence.
Verified end-to-end against `CustomProfile` via `ChatCompletionsTransport`:
configured `extra_body` reaches `kwargs.extra_body` on the wire request,
and coexists with profile-generated entries (Ollama `num_ctx`, `think=False`)
without clobber.
Salvaged from #29022 onto current `main`. Cosmetic typing edit in
`plugins/model-providers/custom/__init__.py` and a stale-base docs revert
in `providers.md` were dropped during cherry-pick.
Closes #29022
|
||
|
|
a9db0e2c74 | Observe unmentioned Telegram group messages | ||
|
|
2a352f96ee |
fix(x_search): surface degraded results + validate dates
The xAI Responses API for x_search returns 200 OK with a
synthesized fluff answer in two failure modes that callers currently
cannot distinguish from a real, citation-backed result:
1. Any narrowing filter (allowed_x_handles, excluded_x_handles,
from_date, to_date) was active, but the X index returned no
matching posts. The model then answers from training data.
2. The date range is malformed, inverted, or pure-future (e.g.
from_date=2030-01-01). The API call burns quota and Grok
responds with a generic answer.
Mitigations, both client-side:
* Validate from_date / to_date before the HTTP call:
- Strict YYYY-MM-DD.
- from_date <= to_date when both set.
- from_date <= today UTC (no posts in a window that hasn't
started). to_date in the future remains allowed so callers
can request 'from yesterday to tomorrow'.
* Add 'degraded' + 'degraded_reason' to successful responses.
degraded=True iff any narrowing filter was active AND both the
top-level 'citations' array and inline 'url_citation'
annotations came back empty. A broad query with no filters that
returns no citations is *not* flagged degraded — that case is
just an unsourced answer, not a filter miss.
Tests cover all four validation paths plus six degraded-flag
scenarios (each filter type, inline vs top-level citation
recovery, broad query baseline). All existing tests continue to
pass; the additions are purely additive on the success-path
response shape.
Discovered while testing the x_search toolset end-to-end:
queries scoped to @Teknium1 returned confident-sounding generic
text about Nous Research with zero citations, and from_date in
2030 produced sassy non-answers. Both are now detectable by the
caller.
|
||
|
|
33a3cf5322 | docs(sessions): state.db is canonical for gateway messages | ||
|
|
4d2df86281 | docs(skills): clarify external dir mutations | ||
|
|
697d38a3f4 |
feat: auto-launch Chromium-family browser for CDP
Add browser CDP launch candidates for Chrome, Chromium, Brave, and Edge while preserving Chrome-first selection. Retry candidate launch failures instead of giving up after the first executable. Update /browser CLI and TUI messaging, docs, and tool descriptions from Chrome-only wording to Chromium-family browser support. Add regression coverage for Brave/Edge paths, Chrome-first precedence, fallback launches, and CDP endpoint probing. |
||
|
|
340d2b6de0 |
docs(xai-oauth): note X Premium+ also unlocks Grok OAuth (#29055)
The xAI Grok OAuth page only mentioned SuperGrok subscribers. An X Premium+ subscription on the X account you sign in with also unlocks Grok access via accounts.x.ai (xAI links the X subscription status to the xAI session automatically — see https://docs.x.ai/grok/faq). Updates the OAuth page title, prereqs, and overview table, plus the provider/configuration/x-search docs that reference the OAuth flow. |
||
|
|
43c7a1b262 |
docs(web-search): document xAI Web Search backend (#29052)
Follow-up to #29042 (xAI Web Search provider plugin). Adds xAI to the canonical user-facing and developer-facing docs, with the search-only caveat and the LLM-in-a-trench-coat trust model carried over from the class docstring. - user-guide/features/web-search.md - Backends table: new xAI row + extended search-only note - New 'xAI (Grok)' setup section with config knobs and trust-model caution admonition - Single-backend yaml comment now lists 'xai' - Auto-detection table: explicitly note that xAI is NOT auto-detected (XAI_API_KEY is shared with inference/TTS/image-gen so we don't silently take over web for users who only set it for chat) - developer-guide/web-search-provider-plugin.md - Added plugins/web/xai/ to the 'study these next' reference list - reference/environment-variables.md - XAI_API_KEY description now also mentions web search |
||
|
|
3f552568c1 |
docs(skills): document browse.sh source (#28939)
Add browse.sh (browse-sh) to the supported-sources table and integrated-hubs section in user-guide/features/skills.md, and to the --source notes in reference/cli-commands.md. Companion to the BrowseShSource adapter merged in #28936. |
||
|
|
2a7308b7c4 |
fix(update): quarantine hermes.exe vs concurrent Windows instance (#26670) (#26677)
* fix(update): detect concurrent hermes.exe on Windows; retry + restart-defer quarantine Closes #26670. When 'hermes update' runs on Windows with another hermes.exe alive (most commonly the Hermes Desktop Electron app's spawned backend) _quarantine_running_hermes_exe() fails to rename the venv shim with [WinError 32]. uv pip install -e . then exits 2, the git-pull fast path is silently abandoned, and the ZIP fallback runs (and fails the same way) before eventually succeeding. This change implements three of the five proposed fixes from the issue: 1. Concurrent-instance detection (preferred fix). _detect_concurrent_hermes_instances() uses psutil to enumerate processes whose .exe is one of our venv shims (hermes.exe / hermes-gateway.exe), excluding the caller's PID. When any match exists, cmd_update prints an actionable message naming the blocking PIDs and exits 2 BEFORE any destructive work. New --force flag bypasses the gate. 2. Retry + restart-deferred fallback. _quarantine_running_hermes_exe() now retries the rename up to 4 times with 100/250/500/1000 ms backoff (covers the transient AV-scanner-handle case). If all retries fail, it schedules the replacement via MoveFileExW with the OS deferred-rename flag so the new shim can land at the original path and the update completes; the old image is fully unloaded after the user's next system restart. 3. Actionable warning text. The old 'Could not quarantine: [WinError 32]' warning is replaced with one that names the likely culprits (Hermes Desktop, REPLs, gateway, AV) and points to the new --force flag. Tests: - 13 new tests in tests/hermes_cli/test_update_concurrent_quarantine.py covering: psutil-based enumeration, self-pid exclusion, case-insensitive matching of .EXE, no-psutil graceful degradation, off-Windows no-op, helpful warning formatting, retry-then-succeed, restart-deferred fallback, cmd_update abort + exit code 2, and --force bypass. - New autouse fixture in tests/hermes_cli/conftest.py defaults _detect_concurrent_hermes_instances to [] so the rest of the suite isn't tripped by the developer's own running hermes.exe. Opt-out marker 'real_concurrent_gate' registered in pyproject.toml. - Updating docs page (website/docs/getting-started/updating.md) gains a short section explaining the new Windows error and remediation. * chore: refresh uv.lock to match pyproject.toml exact pins aiohttp 3.13.4 -> 3.13.3 (matches pyproject pin: aiohttp==3.13.3) anthropic 0.87.0 -> 0.86.0 (matches pyproject pin: anthropic==0.86.0) hermes-agent 0.13.0 -> 0.14.0 (matches pyproject version) CI's uv lock --check was failing on the merged state because main drifted: pyproject.toml uses exact == pins for those two deps and the hermes-agent version was bumped to 0.14.0 but the lockfile still had 0.13.0. |
||
|
|
7bcdced6c1 |
fix(kanban): respawn guard defers blocker_auth instead of auto-blocking (#28683)
Follow-up to #28455. The respawn guard's blocker_auth rule (last error matched a quota/auth/429 pattern) was auto-blocking the task on first occurrence. That's too aggressive: transient rate limits typically clear in seconds to minutes, but the auto-block puts the task in 'blocked' status which requires manual unblock. Now treats blocker_auth the same as recent_success and active_pr: defer the spawn this tick, leave the task in 'ready', let the next tick try again. If the auth error genuinely persists, the existing consecutive_failures counter trips the auto-block circuit breaker after failure_limit failures via the normal path — so a persistent 401/403/quota-exhausted still ends up blocked, just not on first hit. Also documents the respawn_guarded event in kanban.md's events table with the three guard reasons. Updated test_dispatch_respawn_guard_auto_blocks_auth_error → renamed to test_dispatch_respawn_guard_defers_auth_error_without_auto_block; asserts task stays in 'ready' and the guard reason is recorded. |
||
|
|
88ee58f7d2 |
fix(kanban): stale reclaim must not tick failure counter (#28680)
Follow-up to #28452. detect_stale_running() was calling _record_task_failure() on every reclaim, which ticked the consecutive_failures counter. With the default failure_limit=2, two legitimately long-running tasks (>4 h without explicit heartbeat) would auto-block via the spawn-failure circuit breaker — even though no worker actually failed. Stale reclaim is dispatcher-side absence-of-heartbeat detection, not a worker fault. Removed the _record_task_failure() call; the 'stale' event in task_events is still the audit surface, but the failure counter is now reserved for spawn_failed / timed_out / crashed (real failures). Also documents the heartbeat requirement: - KANBAN_GUIDANCE in agent/prompt_builder.py now states the rule ('call kanban_heartbeat at least once an hour for tasks running longer than 1 hour') so workers learn the contract. - kanban.md adds the stale event row to the events table and flags the heartbeat requirement in the worker lifecycle list. New regression test: test_detect_stale_does_not_tick_failure_counter locks in the new behaviour. |