feat(telegram): inline keyboard UI for /resume command#3
feat(telegram): inline keyboard UI for /resume command#3indigokarasu wants to merge 173 commits into
Conversation
…earch#21203) detect_audio_environment() unconditionally added a hard warning when running inside a container, blocking /voice on even when the host audio socket was correctly forwarded (PulseAudio or PipeWire) and sounddevice could enumerate devices. Mirror the existing WSL/PulseAudio handling: if PULSE_SERVER or PIPEWIRE_REMOTE is set, downgrade to a notice and let the audio backend decide. When neither is set, keep the block but extend the message with the exact -v / -e flags users need. Closes NousResearch#21203
Keep local Hermes Docker runtime data, NotebookLM auth/cache, and personal compose overrides out of Git and Docker build contexts. This protects tokens, OAuth state, sessions, logs, and caches while preserving the source tree. Constraint: Only .gitignore and .dockerignore are in scope for this commit. Tested: git diff --cached --name-only and git diff --cached --stat Co-authored-by: OmX <omx@oh-my-codex.dev>
`hermes update` has always hard-coded its target to `main`. Add --branch
so callers can update against a non-default channel while preserving every
existing behavior at the default:
- `hermes update` still pulls main (no behavior change)
- `hermes update --branch X` pulls origin/X, auto-stashing and switching
local HEAD to X first if needed
- `hermes update --check --branch X` reports behindness against
origin/X (and skips the upstream/X probe,
since forks don't have upstream copies of
their own feature branches)
- Branch absent locally → retry as `checkout -B X origin/X` (track)
- Branch absent everywhere → exit 1 with a clear error, after restoring
the user's prior stash so we don't strand
them in a weird state
The fork-upstream sync logic was already guarded on `branch == 'main'`,
so non-main updates correctly skip the upstream trampling without
further changes.
5 new tests cover: explicit --branch, default-to-main, switch-from-other,
track-from-origin, and the fail-cleanly case. Full test_cmd_update.py
suite (15 tests) passes on main.
Three follow-up fixes — all the same shape: silently doing the wrong thing instead of either honoring --branch or refusing. 1) --check --branch <missing> raised CalledProcessError from 'git rev-list ... --count' (check=True) when the branch didn't exist on origin. 'git fetch origin' succeeds without a refspec (it just fetches what's there), so the bad-branch case wasn't caught at the fetch step. Now verify the compare ref with 'git rev-parse --verify --quiet' before rev-list and emit a friendly error. 2) _update_via_zip (Windows fallback for broken git file I/O) hard-coded branch = 'main', so on the ZIP path --branch=foo silently downloaded main.zip and told the user it worked. Refuse in that case instead — silently lying about which branch got installed is exactly what --branch was added to prevent. 3) _cmd_update_check PyPI path returned before looking at branch, so PyPI users running 'hermes update --check --branch=x' got a generic PyPI version check with no indication --branch was dropped. Now prints a one-line warning when --branch was explicit and non-main. Also pull the '(getattr(args, branch, None) or main).strip() or main' expression into _resolve_update_branch(args) — three callsites agree on the same parsing. Tests: 5 new tests for the --check + --branch matrix (named branch, missing branch, default-main upstream-first, PyPI warning) and the ZIP refusal. test_cmd_update.py is 20/20 green, broader hermes_cli/ suite (4952 tests) unchanged.
Docker containers often run in isolated networks without access to PyPI. The lazy-install mechanism fails silently in these environments, causing ImportError when users try to use Anthropic, Bedrock, or Azure providers. Add --extra anthropic, --extra bedrock, and --extra azure-identity to the Dockerfile's uv sync command so these provider packages are pre-installed in the published image. Fixes NousResearch#30394
… CMD s6-overlay's /init scrubs the environment before invoking both /etc/cont-init.d/* scripts and the container's CMD wrapper. As a result, ENV directives from the Dockerfile (HERMES_HOME=/opt/data, HERMES_WEB_DIST, …) and compose-time `environment:` entries (HERMES_UID, HERMES_GID) never reached the scripts that actually use them. Three concrete failures observed on macOS Docker Desktop with `~/.hermes:/opt/data`: * stage2-hook.sh ran with HERMES_UID unset → no UID remap, hermes user stayed at UID 10000 instead of the host user's UID. * skills_sync.py (invoked from stage2-hook) ran with HERMES_HOME unset → get_hermes_home() fell back to Path.home()/.hermes, populating a shadow $HERMES_HOME/.hermes/skills tree on the mounted volume (visible on the host as ~/.hermes/.hermes/skills). * The main `hermes gateway run` process inherited HOME=/root from the /init context (s6-setuidgid doesn't update HOME), so libraries resolving XDG_STATE_HOME via $HOME tried to write to /root/.local/state/hermes/gateway-locks/ and failed with EACCES, preventing the Discord adapter from acquiring its bot-token lock. Three surgical changes restore correct env flow: 1. The auto-generated /etc/cont-init.d/01-hermes-setup wrapper now uses `#!/command/with-contenv sh`, matching the pattern already used by docker/cont-init.d/02-reconcile-profiles. The container env (Dockerfile ENV + compose `environment:`) now reaches stage2-hook.sh and the skills_sync.py subprocess it spawns. 2. docker/main-wrapper.sh also switches to `#!/command/with-contenv sh`. The container CMD (`gateway run`, `chat`, `setup`, …) now sees HERMES_HOME and the other container-level env vars. 3. docker/main-wrapper.sh exports HOME=/opt/data before `s6-setuidgid hermes`. with-contenv populates HOME from the /init context (/root); s6-setuidgid drops privileges but does not update HOME. The hermes user's home per /etc/passwd is /opt/data, so the explicit override matches passwd. No behavior change for the non-buggy paths: the s6-supervised services already used with-contenv, and HOME=/opt/data only affects processes that resolved $HOME-based paths to /root (silently broken).
…earch#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 NousResearch#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.
…ts (NousResearch#32809) Updates curated picker lists for both the OpenRouter fallback snapshot (`OPENROUTER_MODELS`) and the Nous Portal list (`_PROVIDER_MODELS['nous']`). Regenerates website/static/api/model-catalog.json via `scripts/build_model_catalog.py` to keep the docs-hosted manifest in sync (drift guard in `test_in_repo_lists_match_manifest`). tests/hermes_cli/test_models.py fixtures updated — they pinned the old model id as their live-fetch sample.
Grok models (and other LLMs) sometimes omit the schedule parameter when calling the cronjob tool with action=create because the schema only listed 'action' in required[] and the schedule description did not explicitly state it was mandatory (issue NousResearch#32427). Fix: update schema descriptions to clearly state schedule is REQUIRED for action=create, making this explicit for models that rely on description text for parameter compliance. Fixes NousResearch#32427
When the gateway processes /reload-mcp, it reconnects MCP servers and updates the global _servers registry, but cached AIAgent instances in _agent_cache keep the tools list they were built with. The user had to also run /new (discarding conversation history) before the agent could see the new tools — even though /reload-mcp had succeeded. This patch refreshes each cached agent's .tools and .valid_tool_names in _execute_mcp_reload after discovery returns, so existing sessions pick up new MCP tools on their next turn. The slash-confirm gate in _handle_reload_mcp_command already obtains user consent for the implied prompt-cache invalidation before this code runs. Mirrors the equivalent behaviour the CLI already does in cli.py _reload_mcp. Per-agent enabled_toolsets and disabled_toolsets are preserved so an agent that was scoped to a subset of toolsets does not silently gain disabled tools after the reload. Original diagnosis + initial implementation in NousResearch#23812 from @fujinice. The auto-reload watcher half of that PR is intentionally dropped — users want /reload-mcp to remain explicit. Co-authored-by: fujinice <45688690+fujinice@users.noreply.github.com>
… 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.
Added AUTHOR_MAP entry for the cherry-picked fix in the preceding commit so the release contributor audit can resolve Carlton's noreply email.
…-local-runtime-files Ignore local Hermes runtime files
…ousResearch#33005) Pre-stages the AUTHOR_MAP entry so the contributor-check workflow passes when Will Falcon's image-gen SSE fix lands.
…ocker-desktop feat(docker): add Windows Docker Desktop compatible compose file
…opagation fix(docker): propagate env through s6 to cont-init and main CMD
…-audio-bridge-32009 docs: add Docker audio bridge notes
…ocker-home-29108 docs: clarify xurl auth HOME in Docker
qwen3.7-max on OpenCode Go rejects the OpenAI-compatible (oa-compat) format with HTTP 401 but works correctly via the Anthropic Messages endpoint (/v1/messages with x-api-key auth). Route it the same way MiniMax models are routed: anthropic_messages api_mode. Changes: - hermes_cli/models.py: add qwen3.7-max routing + curated list - hermes_cli/setup.py: add to setup wizard model list - hermes_cli/auth.py: update provider comment - tests: add assertions for qwen3.7-max api_mode routing
Add a first-class active-session orchestrator for the Ink TUI: - list, activate, close, and launch live process-local TUI sessions - hydrate committed and in-flight output when switching sessions - dispatch a new prompt session from the +new row with session-scoped model picks - expose a clickable live-session count in the status chrome - preserve stable row order while initially focusing the current session - support mouse hit-testing for floating orchestrator overlays - add backend and frontend regression coverage for the lifecycle and UI helpers
…mode-docker-respect-pulse-pipewire fix(voice): honor PULSE_SERVER/PIPEWIRE_REMOTE inside Docker (NousResearch#21203)
…#27507) build-essential is a Debian metapackage (libc6-dev + gcc + g++ + make + dpkg-dev). The Dockerfile already installs gcc + python3-dev + libffi-dev explicitly, which covers the C-ext compile cases lazy_deps may hit at first boot. g++/make/dpkg-dev aren't reached by the resolved [all]+[messaging] tree on current main — verified via uv sync --dry-run on cp313-linux. Co-authored-by: Monty Taylor <mordred@inaugust.com>
Two pre-existing test failures on main, both pointing at code that was hardened recently — not behaviour bugs, test expectations that fell out of date. 1. tests/tools/test_kanban_tools.py::test_worker_complete_rejects_stale_run_id c002668 ("fix(kanban): add grace period to detect_crashed_workers") gates each running task behind a launch-window grace period so freshly-spawned workers whose PID isn't yet visible on /proc don't get reclaimed. The test creates a worker_env fixture moments before asserting reclamation, so the default 30s grace skips the liveness check and detect_crashed_workers returns []. Fix: set HERMES_KANBAN_CRASH_GRACE_SECONDS=0 in the test so we get the immediate-reclaim semantics the assertion expects. 2. tests/tools/test_windows_native_support.py:: TestKanbanWaitpidWindowsGuard::test_source_gates_waitpid_loop ffdc937 ("fix(kanban): hoist zombie reaper out of dispatch_once") reshaped reap_worker_zombies to use an early-return Windows guard (\`if os.name == "nt": return []\`) instead of an inverted gate (\`if os.name != "nt":\`). Both correctly keep the waitpid loop off Windows — the early-return form is stronger because the rest of the function never runs. Fix: accept either gate pattern in the source scan. Both failures reproduce verbatim on \`origin/main\` in a clean env; neither relates to in-flight work on NousResearch#33564 (the FD-leak fix). Filing this as a separate fix-it PR per green-CI-policy so the kanban CI shard stays green for downstream PRs.
…6 image
Pre-s6, `docker run nousresearch/hermes-agent gateway run` was the
standard invocation: gateway ran as the container's main process,
tini reaped zombies, container exit code matched gateway exit code,
no supervision. With s6-overlay as PID 1, the same invocation now
auto-upgrades to supervised semantics — auto-restart on crash,
dashboard supervised alongside (when HERMES_DASHBOARD=1 is set),
multiple profile gateways under the same /init.
Users get the new behavior with zero changes to their docker run
command. A loud one-line breadcrumb on stderr explains the upgrade
and points at the opt-out for users who genuinely want pre-s6
foreground semantics.
How it works:
1. `_gateway_command_inner` (the `gateway run` handler) checks if
we're inside a container with s6 as PID 1.
2. If yes, dispatches `start` to the s6 service manager (registers
and starts gateway-default), then `exec sleep infinity` to keep
the CMD process alive without binding container lifetime to
gateway PID lifetime. The supervised gateway can flap freely;
`docker stop` still tears everything down via /init stage 3.
3. If no, falls through to the existing foreground code path
unchanged. Host runs of `hermes gateway run` are unaffected.
Three gates make the redirect inert outside the intended scope:
* `detect_service_manager() != "s6"` — host/non-s6-container runs.
* `HERMES_S6_SUPERVISED_CHILD=1` env var (recursion guard) —
exported by `S6ServiceManager._render_run_script` for the
s6-supervised invocation itself. Without this guard, the
supervised `gateway run --replace` would re-enter the redirect
and recurse (run → start → run → start → ...) infinitely.
* `--no-supervise` CLI flag OR `HERMES_GATEWAY_NO_SUPERVISE=1` env
var — explicit user opt-out for CI smoke tests, debugging the
foreground startup path, or any case wanting "CMD exit =
container exit" semantics. Strict truthiness (1/true/yes,
case-insensitive); typos like `=0` do NOT silently opt out.
Tests:
* Unit tests in tests/hermes_cli/test_gateway_s6_dispatch.py
cover all five paths (host no-op, supervised fire, sentinel
recursion guard, CLI flag, env var truthy + falsy). The two
load-bearing gates (sentinel + opt-out) were mutation-tested
by removing each gate in isolation and confirming the dedicated
test fails with the expected error.
* Docker harness tests in tests/docker/test_gateway_run_supervised.py
cover the round trips end-to-end against a built image: redirect
fires (sleep-infinity heartbeat + supervised gateway-default
slot + breadcrumb), --no-supervise opt-out (foreground gateway,
no want-up on the slot), HERMES_GATEWAY_NO_SUPERVISE env var
works identically, recursion is impossible (≤1 supervised
python gateway-run + exactly 1 sleep-infinity parented to the
CMD wrapper), and HERMES_DASHBOARD=1 produces both supervised
gateway and supervised dashboard.
Docs:
* Added a `:::tip Gateway runs supervised` admonition near the
main docker.md example explaining the upgrade and pointing at
the opt-out. Pre-s6 (tini-based) images still run gateway run
as the foreground main process, so the note is scoped to the
s6 image only.
Trade-off documented in the helper docstring: container exit code
under the redirect is sleep's exit code (always 0 on SIGTERM), not
the gateway's. That was an explicit design call — the supervised
gateway is allowed to flap without taking the container with it,
which is what "supervision" means. CI users who want exit-code
forwarding can pass --no-supervise.
…NousResearch#31213) * fix(tui): suppress mouse-residue leaks during Python launcher startup `hermes --tui …` spends ~100–300ms inside the Python launcher (lazy imports, arg parsing, session resolution) before exec'ing the Node TUI binary. During that window stdin is still in cooked + echo mode. If a prior session left DEC mouse tracking asserted (or the user spammed mouse movement while the previous session was opening), the terminal keeps emitting `\\x1b[<…M` SGR motion reports that get echoed straight back into the user's shell scrollback as literal `^[[<…M` text and sit there above the TUI banner until the next clear. The Node side already calls `resetTerminalModes()` in `entry.tsx`, but by then the race is already lost — the bytes echoed during the Python warmup window were committed to the scrollback before Node started. Fix: write the mouse-tracking disable sequence at the very top of `hermes_cli.main`, before every heavy import. The terminal stops emitting motion events as soon as the bytes hit the wire (one TTY round-trip), shrinking the race window from hundreds of milliseconds to a few. `HERMES_TUI_NO_EARLY_DISABLE=1` opts out for diagnostics. * test(tui): drop dead _reload_main, hoist import out of patch context Addresses Copilot review on PR NousResearch#31213. The tests used to import `hermes_cli.main` inside the `patch("os.write")` context, which Copilot pointed out is order-dependent: if the module is already loaded (e.g. imported by a prior test in the same process), the import is a no-op and the patch only sees the explicit `_suppress_mouse_residue_early()` call. Either way the assertion can flake when run alongside other tests. Move the import to module scope — every subprocess gets a fresh `hermes_cli.main`, whose module-level invocation is a no-op under pytest argv. Tests then exercise `_suppress_mouse_residue_early()` directly inside their own patch context. Also drop the unused `_reload_main` helper. * fix(tui): skip early mouse-disable when stdout is not a TTY Addresses Copilot review on PR NousResearch#31213. `hermes --tui … >log` or CI capture pipes fd 1 away from the terminal. The disable bytes can't reach the terminal in that case but would still get written into the log file as raw CSI sequences. Guard with `os.isatty(1)` inside the existing `try/except OSError` block so the 'never break startup' contract holds. * docs(tui): rephrase 'raw cooked mode' as 'cooked + echo mode' Copilot review nit on PR NousResearch#31213 — the original wording was self- contradictory. Pre-TUI stdin state is cooked + echo (kernel TTY discipline still owns the line buffer and echoes input back). The TUI switches it to raw mode later when Ink mounts.
Follow-up to NousResearch#33583 (the gateway-run-supervised redirect). Before this fix, the supervised gateway's stdout (most visibly the "Hermes Gateway Starting…" rich-console banner) was swallowed by `s6-log` into the rotated file at `${HERMES_HOME}/logs/gateways/<profile>/current` and never reached `docker logs`. Operational signal lived in two places: * **docker logs** — saw stderr (Python `logging` defaults to stderr), so warnings/errors were visible. * **the rotated file** — saw stdout (rich banners, `print()` output, third-party libs that wrote to fd 1). This was surprising for users coming from the pre-s6 image, where `docker run … gateway run` produced a single unified stream in `docker logs`. They'd see partial output, conclude something was broken, and dig around for the missing pieces. Fix: add the `1` s6-log action directive before the file destination so each line is forwarded to s6-log's stdout — which propagates up the s6-supervise pipeline to /init's stdout = container stdout = `docker logs`. The file destination is preserved as a second destination, so the rotated log (with ISO 8601 timestamps) still exists for `hermes logs` and for survival across container restarts. Trade-off considered: timestamps. Putting `T` between `1` and the file destination (not before `1`) means: * docker logs sees raw lines — Python's logging formatter has its own timestamps, and `docker logs --timestamps` adds another layer when desired. No double-stamping in the common reading path. * The persisted file gets s6-log's ISO 8601 timestamp so even output that lacked a Python-logger timestamp (rich banners, third-party raw prints) is correlatable in `current`. Verification: * New unit-test assertion in `test_service_manager.py` locks the `s6-log 1` directive into the rendered run-script. Mutation- tested by reverting to the pre-fix script (no `1`); the assert catches it cleanly. * New docker-harness test `test_supervised_gateway_stdout_reaches_docker_logs` builds the image, runs `docker run … gateway run`, and asserts the unique `⚕` banner glyph reaches `docker logs`. Also verifies the rotated file still contains the banner (no regression on the existing file destination). Mutation-tested end-to-end: built a deliberately-broken image without the `1` directive and the test failed exactly as designed, citing the banner present in `current` but absent from `docker logs`. * `website/docs/user-guide/docker.md` gains a new `:::note Where gateway logs go` admonition documenting both destinations and the audit-log file at `${HERMES_HOME}/logs/container-boot.log`. Existing functionality preserved: every other docker-harness test still passes against the new image. Unit-test sweep across `tests/hermes_cli/` (5561 tests) is green.
When operators ran `docker exec <c> hermes login` (or anything else that wrote under $HERMES_HOME) they defaulted to root, leaving /opt/data/auth.json root:root mode 0600. The supervised gateway (UID 10000) then couldn't read its own credentials and returned "Provider authentication failed: Hermes is not logged into Nous Portal" on every Telegram/Discord/etc. message — even though `docker exec <c> hermes chat -q ping` (also root) succeeded because root could read its own root-owned file. _load_auth_store swallowed PermissionError as a parse failure and copied the file aside as auth.json.corrupt, making the diagnostic more misleading. Fix: install a privilege-drop shim at /opt/hermes/bin/hermes, prepended ahead of the venv on PATH. When invoked as root the shim exec's the real venv binary via `s6-setuidgid hermes` — so any file the docker-exec session writes is uid-aligned with the supervised processes. Non-root callers (the supervised processes themselves, `docker exec --user hermes`, kanban subagents, anything inside the container that's not coming through docker-exec) hit a single exec to the absolute venv path with no privilege change. Recursion is impossible: the shim exec's the venv binary by absolute path (/opt/hermes/.venv/bin/hermes), so the second hop cannot re-enter the shim regardless of PATH state. No sentinel env var needed (unlike NousResearch#33583's gateway-run redirect which DOES need HERMES_S6_SUPERVISED_CHILD because there's no absolute-path equivalent for the s6 dispatch). Opt-out: `docker exec -e HERMES_DOCKER_EXEC_AS_ROOT=1 …` for diagnostic sessions where the operator deliberately wants root. Strict truthiness (1/true/yes case-insensitive); typos like `=0` do not silently opt out, mirroring HERMES_GATEWAY_NO_SUPERVISE in NousResearch#33583. If `s6-setuidgid` is missing (someone stripped s6-overlay in a downstream fork), the shim exits 126 with a remediation message pointing at `--user hermes` and the opt-out — never silently runs as root. Test plan: - tests/docker/test_docker_exec_privilege_drop.py — 11 tests - shim drops root to hermes uid (file ownership check) - shim short-circuits for non-root docker exec - HERMES_DOCKER_EXEC_AS_ROOT=1 keeps root - strict-truthiness parametrization (5 falsy values reject) - main CMD path unaffected (recursion guard) - E2E: every file written by docker-exec is readable by uid 10000 - Full tests/docker/ harness: 32/32 pass against fresh image build - shellcheck --severity=error: clean - hadolint: clean - Manual: reproduced the original symptom (root-owned auth.json) by bypassing the shim; confirmed default docker-exec produces hermes-owned files; confirmed opt-out env keeps root semantics. Known follow-up: this prevents NEW instances of the bug. Volumes that already have root:root /opt/data/auth.json from a pre-shim image need a one-time `chown hermes:hermes` before rebooting onto the new image. A stage2-hook chown sweep can self-heal that, but is deferred per scope decision.
…ontainers
Debian 13 ships only `python3` — there's no `/usr/bin/python` symlink. When
the agent emits bash commands using bare `python` (which models do frequently
from their training prior), every such call fails with:
/usr/bin/bash: python: command not found
Tool terminal returned error … exit_code 127
The agent then retries with different approaches, sessions take longer, and
agent.log fills with WARNING noise.
`python-is-python3` is the standard Debian package that drops a
`/usr/bin/python → python3` symlink. ~30 KB, zero behavior change for
anything calling `python3` directly; transparent fix for everything else.
Fixes NousResearch#33178.
) * feat(web): add collapsible sidebar for the dashboard The desktop sidebar can now be collapsed to an icon-only rail via a toggle button in the sidebar header. State is persisted in localStorage so it survives page reloads. When collapsed (lg+ only): - Sidebar shrinks from w-64 to w-14 with a smooth width transition - Nav items show only their icon with a native title tooltip - Brand text, plugin headings, system actions, theme/language switchers, auth widget, and footer are hidden - Mobile drawer behavior is unchanged (always full-width) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align sidebar tooltips to sidebar edge consistently Tooltip left position now uses the sidebar's right edge instead of the anchor element's right edge, so narrow anchors (theme/language switchers) align with full-width anchors (nav links, system actions). Co-authored-by: Cursor <cursoragent@cursor.com> * feat(web): add tooltip animations, restore theme label, rename Sessions tab - Sidebar tooltips now animate in with a subtle 120ms ease-out slide; subsequent tooltips within the same hover sequence appear instantly (no delay/animation) following Emil Kowalski's tooltip pattern - Restore theme name label when sidebar is expanded - Rename Sessions segment tab to "History" across all 16 locales Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): smooth sidebar collapse animation - Remove icon centering on collapse; icons stay left-aligned at px-5 so they don't jump during the width transition - Text labels fade out with opacity transition instead of instant display:none, clipped naturally by overflow-hidden - Slow collapse duration from 450ms to 600ms for a more relaxed feel - Gateway dot always rendered with opacity toggle so it doesn't slide in from the right on collapse - Pin gateway dot at fixed left offset (pl-[1.625rem]) to align with nav icons - Align header toggle button with justify-center when collapsed - Bottom switchers use items-start when collapsed to prevent reflow Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…Research#28577) * fix(web): allow mobile dashboard scrolling * fix(web): combine mobile root scroll rules --------- Co-authored-by: Wesley Simplicio <wesley.simplicio.ext@siemens-energy.com>
…search#33506) fal announced Krea 2 day-0 as an official API partner on 2026-05-27. Add both variants to the FAL_MODELS catalog so they appear in the 'hermes tools' model picker alongside flux-2, gpt-image, nano-banana, etc. Users who already bill through FAL or Nous Portal subscription can now use Krea without registering directly with Krea. Model IDs (as listed in fal's launch announcement): fal-ai/krea/v2/medium/text-to-image — $0.030 / image fal-ai/krea/v2/large/text-to-image — $0.060 / image Both share the same parameter schema: - aspect_ratio (1:1, 4:3, 3:2, 16:9, 2.35:1, 4:5, 2:3, 9:16) mapped from our 3 abstract ratios via size_style='aspect_ratio' - creativity (raw|low|medium|high; default medium) - seed (reproducibility) - image_style_references (up to 10 per Krea's API spec) No num_inference_steps / guidance_scale / num_images — Krea 2 does not expose those, and the supports-set filter strips them defensively if the agent ever passes them. This is the FAL-routed variant. The separate native-Krea-API plugin shipped in PR NousResearch#33236 (plugins/image_gen/krea/) remains available for users who want to bill directly through Krea's API with their own key. Both routes converge on the same underlying model. Nous Portal managed-FAL gateway: this commit makes the model IDs known to the catalog and the picker. The Portal team will need to allowlist these two endpoint slugs on the fal-queue origin server-side for them to flow through the managed billing path.
…rocesses `sqlite3.Connection.__exit__` commits/rollbacks but does NOT close the underlying FD. `with kb.connect() as conn:` in long-lived processes (gateway `run_slash`, dashboard `decompose_task_endpoint`) therefore leaks one FD to `kanban.db` per call. After enough operations the gateway dies with `[Errno 24] Too many open files` (~4 days uptime in the production report — NousResearch#33159). Fix: add a `connect_closing()` context manager in `hermes_cli/kanban_db` that wraps `connect()` with a real `try/finally: conn.close()`. Switch the 42 leak-prone call sites in `hermes_cli/kanban.py` (35), `hermes_cli/kanban_decompose.py` (4), and `hermes_cli/kanban_specify.py` (3) over to it. `kanban.py` matters because `run_slash` (called from the gateway for every `/kanban` slash command) parses argparse and dispatches to those `_cmd_*` functions in-process — each one was leaking one FD per invocation. Tests inside `tests/` are untouched: short-lived processes where OS cleanup masks the leak. Regression tests added in `test_kanban_db.py` cover both happy-path and exception-path closure, plus an explicit assertion that bare `with kb.connect()` still does NOT close (documenting the upstream sqlite3 behaviour we're working around). Closes NousResearch#33159.
`hermes dump` and the startup banner both call `git rev-parse HEAD` to
report the running commit, but `.dockerignore` line 2 excludes `.git` —
so inside the published image `hermes dump` shows
`version: ... [(unknown)]` and the banner drops its `· upstream <sha>`
suffix entirely. That makes support triage from container bug reports
impossible: we can't tell which commit the user is actually running.
Fix: thread the build-time SHA through as a Docker build-arg, write it
to `/opt/hermes/.hermes_build_sha` in the image, and have a new
`hermes_cli/build_info.get_build_sha()` read it as a fallback after the
existing live-git lookup fails. Output format is unchanged in both
callsites — same 8-char short SHA whether resolved live or baked.
Wiring:
- Dockerfile: `ARG HERMES_GIT_SHA=` + write-file step after the source
copy. Empty/missing arg → no file written → callers fall through to
live git (so local `docker build` without --build-arg is unchanged).
- docker-publish.yml: passes `HERMES_GIT_SHA=${{ github.sha }}` on all
four build-push-action steps (amd64/arm64, smoke-test + final push).
- dump.py:_get_git_commit() / banner.py:get_git_banner_state(): try
live git first, fall back to baked SHA, then to legacy `(unknown)`
/ None. Banner returns `upstream == local, ahead=0` because a built
image is by definition pinned to one commit.
Coverage:
- Unit tests cover build_info (file present/absent/empty/error,
truncation, whitespace), dump (live-git wins, both fallbacks,
identical output-format regression guard), and banner (no-repo +
baked, no-repo + no-sha, shallow-clone fallback).
- tests/docker/test_dump_build_sha.py is an integration regression
guard that runs against the real image, reads
`/opt/hermes/.hermes_build_sha`, and asserts `hermes dump` surfaces
its content (or stays at `(unknown)` if no file).
- Verified end-to-end: `docker build --build-arg HERMES_GIT_SHA=abc...`
→ `docker run ... dump` reports `[abc12345]`; without the build-arg
it reports `[(unknown)]` as before.
Snapshot review_agent._session_messages before teardown so close() can clean per-session state without dropping the user-visible self-improvement summary. Adds two regressions: - bg-review summarizer receives captured review-agent tool messages after review_agent.close() runs - context-compressor protected-head handoff rehydration populates _previous_summary and keeps the old handoff out of newly summarized turns Salvaged from PR NousResearch#26039 onto current main after agent/background_review.py extraction. Original commit 63eaf60; bg-review test updated to patch the module-level summarize_background_review_actions in agent.background_review instead of the now-forwarder AIAgent._summarize_background_review_actions.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
53 |
invalid-argument-type |
20 |
unresolved-attribute |
7 |
invalid-assignment |
7 |
unsupported-operator |
5 |
not-subscriptable |
1 |
no-matching-overload |
1 |
invalid-return-type |
1 |
First entries
tests/cron/test_cronjob_schema.py:27: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["REQUIRED"]` and `Unknown | str | dict[str, str] | ... omitted 3 union elements`
tests/cron/test_cronjob_schema.py:26: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["schedule"]` on object of type `str`
tests/plugins/test_security_guidance_plugin.py:74: [unresolved-attribute] unresolved-attribute: Attribute `exec_module` is not defined on `None` in union `Loader | None`
tests/hermes_cli/test_dashboard_auth_401_reauth.py:33: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.testclient`
tests/run_agent/test_switch_model_rollback.py:18: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_dashboard_auth_prefix.py:478: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.responses`
tests/hermes_cli/test_dashboard_auth_401_reauth.py:32: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.responses`
tests/hermes_cli/test_dashboard_auth_plugin_hook.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/run.py:13053: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.list_sessions_rich` is incorrect: Expected `str`, found `None`
tests/hermes_cli/test_dashboard_auth_gate.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.testclient`
tests/hermes_cli/test_dashboard_auth_ws_auth.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cron/test_cronjob_schema.py:26: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["properties"]` on object of type `str`
tests/plugins/dashboard_auth/test_nous_provider.py:29: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cron/test_cronjob_schema.py:16: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["action"]` on object of type `list[str]`
hermes_cli/mcp_config.py:782: [invalid-argument-type] invalid-argument-type: Argument to bound method `dict.get` is incorrect: Expected `str`, found `(Any & ~Literal["serve"] & ~Literal["picker"] & ~Literal["catalog"] & ~Literal["install"]) | None`
tests/plugins/test_security_guidance_plugin.py:70: [invalid-argument-type] invalid-argument-type: Argument to function `module_from_spec` is incorrect: Expected `ModuleSpec`, found `ModuleSpec | None`
hermes_cli/dashboard_auth/middleware.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi`
tests/hermes_cli/test_dashboard_auth_cookies.py:5: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi`
tests/hermes_cli/test_dashboard_auth_provider_base.py:10: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/plugins/test_security_guidance_plugin.py:26: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
gateway/platforms/api_server.py:1406: [invalid-argument-type] invalid-argument-type: Argument to function `APIServerAdapter._session_response` is incorrect: Expected `dict[str, Any]`, found `(Unknown & ~AlwaysFalsy) | dict[str, Any] | None`
tests/hermes_cli/test_dashboard_auth_status_endpoint.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_kanban_db.py:3579: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `Connection`, found `FailingConnWrapper`
tests/hermes_cli/test_dashboard_auth_middleware.py:17: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_memory_provider_init.py:92: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method
... and 70 more
✅ Fixed issues (33):
| Rule | Count |
|---|---|
invalid-argument-type |
13 |
unresolved-attribute |
9 |
unsupported-operator |
4 |
unresolved-import |
3 |
invalid-type-form |
1 |
call-top-callable |
1 |
invalid-return-type |
1 |
not-iterable |
1 |
First entries
cli.py:12934: [invalid-argument-type] invalid-argument-type: Argument to function `len` is incorrect: Expected `Sized`, found `(str & ~AlwaysFalsy) | (list[tuple[str, str, str]] & ~AlwaysFalsy) | (int & ~AlwaysFalsy) | (Queue[Unknown] & ~AlwaysFalsy) | list[Unknown]`
tests/tools/test_vercel_sandbox_environment.py:204: [unresolved-attribute] unresolved-attribute: Unresolved attribute `Resources` on type `ModuleType`
agent/codex_runtime.py:179: [invalid-type-form] invalid-type-form: Function `callable` is not valid in a parameter annotation: Did you mean `collections.abc.Callable`?
cli.py:7317: [unresolved-attribute] unresolved-attribute: Attribute `splitlines` is not defined on `list[tuple[str, str, str]] & ~AlwaysFalsy`, `int & ~AlwaysFalsy`, `Queue[Unknown] & ~AlwaysFalsy` in union `(str & ~AlwaysFalsy) | (list[tuple[str, str, str]] & ~AlwaysFalsy) | (int & ~AlwaysFalsy) | (Queue[Unknown] & ~AlwaysFalsy) | Literal[""]`
cli.py:7320: [invalid-argument-type] invalid-argument-type: Argument to constructor `enumerate.__new__` is incorrect: Expected `Iterable[Unknown]`, found `(str & ~AlwaysFalsy) | (list[tuple[str, str, str]] & ~AlwaysFalsy) | (int & ~AlwaysFalsy) | (Queue[Unknown] & ~AlwaysFalsy) | list[Unknown]`
tests/tools/test_vercel_sandbox_environment.py:206: [unresolved-attribute] unresolved-attribute: Unresolved attribute `SandboxStatus` on type `ModuleType`
cli.py:12610: [unsupported-operator] unsupported-operator: Operator `<=` is not supported between objects of type `Literal[0]` and `str | list[tuple[str, str, str]] | int | Queue[Unknown]`
tests/tools/test_vercel_sandbox_environment.py:17: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
cli.py:12610: [unsupported-operator] unsupported-operator: Operator `<` is not supported between objects of type `str | list[tuple[str, str, str]] | int | Queue[Unknown]` and `int`
tests/tools/test_vercel_sandbox_environment.py:209: [unresolved-attribute] unresolved-attribute: Unresolved attribute `sandbox` on type `ModuleType`
cli.py:12611: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> tuple[str, str, str], (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[tuple[str, str, str]]]` cannot be called with key of type `str` on object of type `list[tuple[str, str, str]]`
cli.py:12881: [unsupported-operator] unsupported-operator: Operator `-` is not supported between objects of type `str | list[tuple[str, str, str]] | int | Queue[Unknown]` and `Literal[1]`
cli.py:12607: [invalid-argument-type] invalid-argument-type: Argument to bound method `HermesCLI._normalize_slash_confirm_choice` is incorrect: Expected `list[tuple[str, str, str]]`, found `(str & ~AlwaysFalsy) | (list[tuple[str, str, str]] & ~AlwaysFalsy) | (int & ~AlwaysFalsy) | (Queue[Unknown] & ~AlwaysFalsy) | list[Unknown]`
cli.py:12611: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `list[tuple[str, str, str]]` on object of type `list[Unknown]`
cli.py:12611: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `list[tuple[str, str, str]]` on object of type `str`
cli.py:12611: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> tuple[str, str, str], (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[tuple[str, str, str]]]` cannot be called with key of type `list[tuple[str, str, str]]` on object of type `list[tuple[str, str, str]]`
cli.py:7337: [invalid-argument-type] invalid-argument-type: Argument to function `_append_panel_line` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | (list[tuple[str, str, str]] & ~AlwaysFalsy) | (int & ~AlwaysFalsy) | (Queue[Unknown] & ~AlwaysFalsy)`
tests/hermes_cli/test_mcp_tools_config.py:92: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Literal["exclude"]` on object of type `str`
tests/tools/test_vercel_sandbox_environment.py:135: [call-top-callable] call-top-callable: Object of type `Top[(...) -> object]` is not safe to call; its signature is not known
cli.py:7314: [invalid-argument-type] invalid-argument-type: Argument to function `_panel_box_width` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | (list[tuple[str, str, str]] & ~AlwaysFalsy) | (int & ~AlwaysFalsy) | (Queue[Unknown] & ~AlwaysFalsy)`
hermes_cli/mcp_config.py:764: [invalid-argument-type] invalid-argument-type: Argument to bound method `dict.get` is incorrect: Expected `str`, found `(Any & ~Literal["serve"]) | None`
tests/tools/test_vercel_sandbox_environment.py:203: [unresolved-attribute] unresolved-attribute: Unresolved attribute `Sandbox` on type `ModuleType`
tools/environments/vercel_sandbox.py:267: [unresolved-attribute] unresolved-attribute: Object of type `None` has no attribute `sync`
tools/environments/vercel_sandbox.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
cli.py:12611: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> LiteralString, (key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str]` cannot be called with key of type `Queue[Unknown]` on object of type `str`
... and 8 more
Unchanged: 4924 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
… bogus git error
Inside the published Docker image, `hermes update` was hitting the
".git missing → reinstall via curl" fallback:
✗ Not a git repository. Please reinstall:
curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash
That message is wrong on two counts:
1. It tells the user to run the host-side installer, which would
install a *new* Hermes on the host — not update the running
container.
2. It doesn't mention `docker pull` at all, leaving Docker users
to figure out the right action from scratch.
`hermes update --check` was worse: it bailed with "Not a git
repository — cannot check for updates." and nothing else.
Fix: detect the Docker install method (already stamped by
`docker/stage2-hook.sh` and surfaced by `detect_install_method()`)
in both update entry points and print a long-form message that
covers:
- The right command: `docker pull nousresearch/hermes-agent:latest`
- Restart guidance (`docker compose up -d --force-recreate` /
re-run `docker run`)
- How to verify the new version after restart
- Tag-pinning caveat (`:latest` doesn't move a pinned tag)
- Config persistence across upgrades (state under `HERMES_HOME` /
`/opt/data` is bind-mounted and survives)
- Fork escape hatch (build your own image with the repo's Dockerfile)
Exit code is 1 (matches `managed_error` semantic for "tried to
update but can't update this way").
Plumbing:
- hermes_cli/config.py: new `format_docker_update_message()` helper
sits next to the existing `_NIX_UPDATE_MSG` /
`format_managed_message()` family so the wording lives in one
place and both call sites (apply path + check path) consume it.
- hermes_cli/main.py:
* `cmd_update()`: bail right after the `is_managed()` gate, before
any of the apply-path branches.
* `_cmd_update_check()`: bail at the top of the function, before
the existing `method == "pip"` branch.
Neither path touches subprocess.run / git when method == "docker".
Coverage:
- 7 new tests in `tests/hermes_cli/test_cmd_update_docker.py`:
* `hermes update` in Docker → message + exit 1, no git calls
* `hermes update --check` (via cmd_update) → same
* `--yes` / `--force` don't bypass (intentional)
* `_cmd_update_check` called directly → bails too
* git/pip installs still take their normal paths (regression guards)
* `format_docker_update_message` content-lock test pinning the
five user-actionable bits the message must contain
- Existing test_cmd_update.py (21 tests) + test_managed_installs.py
(5 tests) still pass — no regression on the source-install path.
- Verified end-to-end in a real container: `docker run ... update`
and `docker run ... update --check` both render the message and
exit 1.
The regression-guard test `test_cmd_update_on_git_install_does_not_print_docker_message` mocked `is_managed` and `detect_install_method` but not `subprocess.run`, so once `cmd_update(check=True)` decided this was a git install it shelled out to a real `git fetch upstream` / `git fetch origin`. On CI runners the worktree has no `upstream` remote configured and the fetch hung past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI. Fix: stub `subprocess.run` with a successful CompletedProcess-shaped object whose stdout is `"0\n"`, so: - no real git command is ever invoked - the rev-list parsing later in the flow (`int(stdout.strip())`) succeeds rather than `ValueError`-ing through the test's SystemExit catch - the flow proceeds far enough to confirm the docker banner is absent (the actual assertion) Also broaden the except clause to `(SystemExit, Exception)`: the only assertion in this test is the negative-banner check on captured stdout; any further failure in the rest of the update flow is irrelevant to that contract. Verified locally: all 7 tests in `tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously the regression-guard test alone consumed 30s+ and got SIGTERM'd).
Two unrelated transient failures on PR NousResearch#33661's initial CI run, both pre-existing on main and recovered on rerun. Hardening: 1. tests/cron/test_scheduler.py::TestRunJobConfigLogging — added mocks for resolve_runtime_provider() and discover_mcp_tools(). The yaml-warning tests intend to exercise only the warning-log path, but _run_job_impl continues into provider resolution and MCP discovery after the warning. Both can spawn subprocesses / hit the network and pushed the test over its 30s budget under GHA load. 2. tests/tools/test_browser_supervisor.py — wrapped Chrome teardown against the stdlib subprocess._wait() race (bpo-38630). When SIGCHLD arrives during proc.wait(), _try_wait(WNOHANG) can return a foreign pid and the 'assert pid == self.pid or pid == 0' fires. Fixture now catches AssertionError/TimeoutExpired, force-kills, and always reaps so no zombie escapes. Same hardening applied to the early-skip branch.
…NousResearch#29800) * docs(voice): use `uv pip install faster-whisper` in STT install hints Three runtime messages told users to `pip install faster-whisper` (reported in NousResearch#29782 for the gateway STT failure message under Telegram-in-Docker, where the user hit `bash: pip: command not found`). The Hermes Docker image is built on `ghcr.io/astral-sh/uv` with a uv-managed venv that doesn't ship `pip` on PATH; users on modern `uv tool install` / `uv venv` installs see the same problem. The canonical install command in this repo is `uv pip install` (see `tools/lazy_deps.py:509` `feature_install_command()`), which works in Docker (uv image), in `uv tool install` venvs, and in pip-based venvs that already have uv on PATH. Changed three locations to match: - `gateway/run.py` — Telegram/Discord/Slack/WhatsApp/etc. voice reply when no STT provider is configured. Suggests `uv pip install faster-whisper` and notes that `pip install faster-whisper` also works if `pip` is on PATH. - `tools/voice_mode.py` — `/voice` status line for missing STT. - `cli.py` — Voice-mode startup error, "Option 1". No behavior change beyond the user-facing text. No production code path was touched. * docs(voice): add pip fallback to cli + voice_mode STT hints Copilot flagged that cli.py and tools/voice_mode.py recommend `uv pip install faster-whisper` without a fallback for environments where uv isn't on PATH. The gateway/run.py message already lists `pip install faster-whisper` as an alternative; this commit aligns the two remaining call sites to match. Addresses inline Copilot review on NousResearch#29800. --------- Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
… salvage Adds `squiddy@2rook.ai → MoonRay305` to AUTHOR_MAP so contributor_audit.py passes for the salvaged commits in NousResearch#33482-followup PR.
…parse errors Telegram's HTML parser chokes on common characters like '.' and '-' in session titles (e.g. 'MCP Server', 'api-keys'). Switch from ParseMode.HTML to plain text with no parse mode. Use numbered list with indented previews. Also re-applies changes from feat/telegram-resume-button-ui which was lost during a repo rebase.
- Button title truncated to ~40 chars (was 18) to use full mobile width - disable_web_page_preview=True to suppress link previews in body text
- 13 tests covering interception, button UI, callback handling, auth, and session listing - Tests verify: /resume with no args triggers button UI, /resume with args falls through, keyboard has correct buttons, labels are truncated, previews included, fallback messages, unauthorized users denied, callback routing, and get_resume_sessions behavior
77e92a1 to
0d5914b
Compare
|
Closing — this PR contains ~40 logically independent feature units (dashboard OAuth system, MCP catalog, Docker hardening, CLI flags, model catalog updates, codex fixes, API server features, security plugin, Vercel removal, and more) interleaved in a single branch. The stated scope (Telegram /resume inline keyboard) is ~5% of the diff. This appears to be a development branch pushed as a PR rather than a focused feature change. All features need to be extracted into individual PRs. The Telegram /resume UI change specifically should be re-opened against a clean branch with only the relevant files (telegram.py, gateway/run.py, display_config.py). CI is also blocking on 95 new type checker failures from unresolved imports in the dashboard auth system. |
A bare `/resume` printed the recent-sessions list but armed no selection state, so typing just `3` on the next line was sent to the agent as chat instead of resuming session #3. `/resume 3` worked, but the natural list-then-pick flow did not. Arm a one-shot pending-resume prompt when bare `/resume` shows the list, and consume the next bare numeric input as the selection (out-of-range is reported, non-numeric/other commands disarm it). Resolves against the same _list_recent_sessions(limit=10) list used everywhere else. Closes NousResearch#34584.
…NousResearch#34192) (NousResearch#34382) NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup with: /usr/bin/tini: No such file or directory The image moved from tini to s6-overlay as PID 1 (/init) earlier in 2026. Orchestration templates that still pin /usr/bin/tini as the entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no binary to exec and the container crashes immediately. Hermes has no control over the Hostinger catalog template, but we can make the image backward-compatible by symlinking /usr/bin/tini -> /init during the s6-overlay install step. External wrappers that exec /usr/bin/tini will land on the same s6-overlay reaper they would have landed on if they'd used the canonical /init entrypoint. The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim is purely for legacy external wrappers, not for the image's own runtime path. Once affected catalogs are updated, the symlink can be removed. Other issues NousResearch#34192 raises that are NOT addressed by this PR: * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148 (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger template likely needs to update its env-var propagation. * Problem #3 (incompatible session formats): RFC for pluggable SessionDB is tracked in NousResearch#23717. * Problem NousResearch#4 (Telegram polling conflict): an operations problem on Hostinger's side, not in this codebase. This PR is scoped to the one issue that can be fixed inside Dockerfile: the missing /usr/bin/tini binary. Tests (3 in test_dockerfile_tini_compat_shim.py): - test_tini_compat_symlink_present Guard: the symlink line must exist in Dockerfile. - test_tini_compat_comment_explains_why The NousResearch#34192 anchor comment must be present so future readers know why the shim is there (avoid accidental removal). - test_entrypoint_still_init_not_tini Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is only for external wrappers. Refs: NousResearch#34192 Partial fix: addresses the immediate tini-binary crash. Catalog-side fixes still needed by Hostinger for the UID and session-format problems documented in the issue. Co-authored-by: Cursor <cursoragent@cursor.com>
…bes + test-leak fix (NousResearch#40909) * fix(gateway,windows): reliability — supervisor task, JOB breakaway, status --deep Three coordinated fixes for the Windows gateway reliability story: 1. CREATE_BREAKAWAY_FROM_JOB on every detached spawn The 'hermes update' triggered from the Electron Desktop GUI ran inside Electron's job object. Without breakaway, the post-update gateway watcher spawned by update — already DETACHED_PROCESS — was still reaped when Electron's job tore down, so the gateway never came back after a GUI-initiated update. Adds CREATE_BREAKAWAY_FROM_JOB (0x01000000) to: - hermes_cli/_subprocess_compat.py::windows_detach_flags() — used by every helper that calls windows_detach_popen_kwargs(), including launch_detached_profile_gateway_restart() - The watcher subprocess's own respawn snippet in hermes_cli/gateway.py (inlined flags so the watcher's child respawn also breaks away) _spawn_detached() in gateway_windows.py already had the flag; this change brings the rest of the codebase to parity. 2. Per-minute supervisor Scheduled Task — Windows equivalent of systemd Restart=always Introduces hermes_cli/gateway_supervisor.py and registers it as a second Scheduled Task ('Hermes_Gateway_Supervisor', SC MINUTE /MO 1, LIMITED rights) alongside the existing ONLOGON task. Every minute, the supervisor uses the same gateway.status.get_running_pid() probe as 'hermes gateway status' and, if no gateway is alive, calls gateway_windows._spawn_detached() (which now includes BREAKAWAY) to bring one back. Covers every crash mode, not just 'machine rebooted': taskkill, OOM, GUI update SIGTERM, parent job teardown. Cheap — one pythonw startup per minute when down, one PID-existence check per minute when up. Wired into both the schtasks-success and Startup-folder-fallback install paths via _install_supervisor_best_effort(), and removed in uninstall(). Best-effort: a failing supervisor install logs a warning but doesn't roll back the primary install. 3. 'hermes gateway status --deep' shows per-probe PASS/FAIL Replaces the existing terse '--deep' output (which only printed paths) with an actual diagnostic table: [1] PID file present [2] Lock file held by a live process [3] get_running_pid() result [4] _pid_exists(pid) — OS-level liveness [5] gateway_state.json (state + age) [6] Last lifecycle event from gateway-exit-diag.log When the high-level summary disagrees with reality, the user can see exactly which signal is lying. Test-leak fix ------------- tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages monkey-patched is_linux/is_wsl/supports_systemd_services to simulate WSL but did NOT stub is_windows(). On a Windows host, the dispatcher in _gateway_command_inner takes the is_windows() branch BEFORE the WSL guidance branch, so the test invoked gateway_windows.install() for real. install() writes to %APPDATA%\...\Startup\Hermes_Gateway.cmd — the REAL user Startup folder, never sandboxed by tmp_path — pointing at the test's pytest-of-<user>/pytest-<N>/.../gateway-service/ wrapper. When pytest tore down the tmp_path, every subsequent Windows login flashed a cmd.exe window that failed to find the missing target. Stubs is_windows=False on all four affected tests: test_install_wsl_no_systemd test_start_wsl_no_systemd test_status_wsl_running_manual test_status_wsl_not_running Defense-in-depth: _build_startup_launcher() now prefixes the launcher with 'if not exist <target> exit /b 0', so any future stale Startup entry silently no-ops instead of flashing a console window. Status enhancements ------------------- - status() now reports supervisor task presence alongside the existing schtasks/Startup info, and nudges the user to reinstall if the supervisor isn't registered. - Deep mode dumps both the supervisor task name + script path. * fix(gateway,windows): drop the per-minute supervisor task — keep breakaway + deep probes Earlier in this branch we added a per-minute schtasks-based supervisor to respawn the gateway after crashes / GUI-update SIGTERMs. The implementation flashed a brief console window on every firing, which stole window focus. We tried several variants: - cmd.exe wrapper invoking pythonw -> flashes (cmd.exe is console-subsystem) - schtasks /TR pointing at pythonw -> flashes (uv venv launcher pythonw is actually subsystem=Console, not GUI; it respawns the real pythonw) - schtasks /TR pointing at base uv -> still flashes (Task Scheduler-side conhost preallocation; documented Windows quirk) - XML registration with <Hidden>true> -> still flashes (<Hidden> only hides the task in the Task Scheduler UI, not the spawned window) Researched what leading projects do: - Ollama: GUI-subsystem tray exe + Startup-folder shortcut. No supervisor. - Tailscale: real Windows Service via SCM. Session 0, no console possible. - Syncthing: --no-console flag inside the binary + Startup folder. - openclaw: VBS Run(..., 0, False) wrapper. Suppresses the *window* but Super User Q971162 confirms focus-steal still occurs in some cases. None of these use a per-minute polling scheduled task. The 'auto-restart on crash' responsibility belongs INSIDE the daemon (Tailscale's in-process recovery / Ollama's monitor+worker pair) OR is delegated to the Windows Service Control Manager — not Task Scheduler. So this commit drops the supervisor entirely. The CREATE_BREAKAWAY_FROM_JOB fix in _subprocess_compat.py (from commit c1e5fa4) survives — that is the *real* fix for problem #2 (GUI-update kills gateway): the post-update watcher in launch_detached_profile_gateway_restart() now breaks out of Electron's job object, so the gateway respawn watcher survives the GUI quit and successfully respawns the gateway. Surviving from c1e5fa4: * CREATE_BREAKAWAY_FROM_JOB in hermes_cli/_subprocess_compat.py (fixes #2) * Inlined breakaway flag in the watcher respawn snippet in gateway.py * hermes gateway status --deep PASS/FAIL probes (fixes #1 — visibility) * 'if not exist <target> exit /b 0' guard in _build_startup_launcher (fixes #3 — silent no-op for stale Startup entries) * tests/hermes_cli/test_gateway_wsl.py is_windows=False stubs (root cause of #3 — pytest WSL tests no longer leak Startup entries on Win hosts) Removed in this commit: * hermes_cli/gateway_supervisor.py (entire file) * Supervisor section in hermes_cli/gateway_windows.py (~180 lines): get_supervisor_task_name, get_supervisor_script_path, _build_supervisor_cmd_script, _write_supervisor_script, _install_supervisor_task, is_supervisor_task_registered, _install_supervisor_best_effort * _install_supervisor_best_effort() calls in install() (3 spots) * supervisor cleanup block in uninstall() * supervisor display lines in status() / status(deep=True) Future direction (out of scope for this PR): the right place for Windows 'Restart=always' semantics is a real Windows Service installed via pywin32's win32serviceutil.ServiceFramework — session-0 isolation, SCM auto-restart, no console window possible. That's a meaningful next-PR project, not a band-aid. Tests: 51 pass / 2 pre-existing failures in tests/hermes_cli/test_gateway_{windows,wsl}.py (the 2 failures are TestSupportsSystemdServicesWSL cases that fail on origin/main too — unrelated to this PR).
Summary
Replaces the plain-text /resume list on Telegram with an inline keyboard showing the last 12 non-cron named sessions sorted by recency.
Changes
UX