This repository was archived by the owner on Jun 4, 2026. It is now read-only.
chore: sync inkbox with upstream main (2026-05-27) - #41
Merged
on-call-hermes-agent[bot] merged 109 commits intoMay 27, 2026
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>
…url override Operators behind reverse proxies that don't reliably forward X-Forwarded-Host / X-Forwarded-Proto / X-Forwarded-Prefix (manual nginx setups, on-prem ingresses, custom-domain Fly deploys with incomplete proxy chains) had no way to force the absolute base URL the OAuth callback redirects from. The dashboard would reconstruct the redirect_uri from request headers, the IDP would echo it back, and the user would land on the wrong host or wrong path — 404. Add `dashboard.public_url` to config.yaml with env override HERMES_DASHBOARD_PUBLIC_URL. When set, it is the complete authority — scheme + host + optional path prefix (e.g. https://example.com/hermes) — and becomes the base for the OAuth `redirect_uri`. X-Forwarded-Prefix is IGNORED on this code path because the operator has explicitly declared the public URL; we no longer need to guess from proxy headers, and stacking the prefix on top would double-prefix the common case where the prefix is already baked into public_url. When unset, the existing proxy_headers + X-Forwarded-Prefix reconstruction runs untouched. Existing Fly.io deploys continue to work without configuration — this is purely additive. Precedence mirrors dashboard.oauth.client_id: env (non-empty) > config.yaml > reconstructed from request Implementation: - hermes_cli/config.py: add dashboard.public_url to DEFAULT_CONFIG with a multi-paragraph doc comment explaining the use case, the X-Forwarded-Prefix interaction, and the validation rules. - hermes_cli/dashboard_auth/prefix.py: factored out the existing _REJECT_CHARS frozenset, added _normalise_public_url() validator (requires http/https scheme + non-empty host + no header-injection chars), _load_dashboard_section() loader (robust to load_config raising, non-dict shapes), and resolve_public_url() entry point with the env-overrides-config precedence. A malformed value silently falls through to ""; the caller treats "" as "reconstruct from request" so a typo never breaks the login flow. - hermes_cli/dashboard_auth/routes.py: rewrite _redirect_uri() docstring to spell out the three resolution tiers; add the public_url short-circuit before the existing X-Forwarded-Prefix splicing. Source-level comment notes that X-Forwarded-Prefix is intentionally ignored when public_url is set so a future reader doesn't try to "fix" the missing prefix layering. - cli-config.yaml.example: extend the existing dashboard section with a public_url block. - website/docs/user-guide/features/web-dashboard.md: new "Public URL override" section between the provider configuration and the OAuth flow walkthrough. Documents the env-vs-config table, the validation rules, and the `http://` `public_url` ↔ Secure cookie footgun. Test coverage — new TestPublicUrlOverride class (8 tests): - env var overrides request reconstruction (the primary motivating case) - config.yaml used when env unset - env wins over config (precedence pin) - public_url with a path prefix already baked in (the Q1-a case the user explicitly chose) - public_url suppresses X-Forwarded-Prefix layering (defends against the double-prefix bug) - trailing slash stripped from public_url (no //auth/callback) - malformed public_url falls through to reconstruction (six hostile inputs: javascript:, ftp:, missing scheme, missing host, quote chars, CRLF injection) - empty env string doesn't shadow config.yaml entry (CI / Fly provisioned-but-empty secret case) Mutation-tested: flipping the precedence in resolve_public_url() trips exactly test_env_overrides_config_public_url; weakening the validator (accept any scheme) trips exactly test_malformed_public_url_falls_through_to_reconstruction. Both other tests in each pair stay green, confirming the suite discriminates the specific regression each test pins.
…sResearch#33142) Background processes whose command contains `gh pr view --json statusCheckRollup` or `gh pr checks | jq` now get a runtime hint in the result pointing at the canonical green-ci-policy snippets. The homebrew shape has caused at least seven silent CI-watcher failures in the past two weeks (NousResearch#31329, NousResearch#31448, NousResearch#31695, NousResearch#31709, NousResearch#31745, NousResearch#32264, NousResearch#33131) — each one a different jq/awk/grep variation of the same fundamental problem (stdout buffering, jq null-key edge cases, conclusion-vs-status confusion, TTY-only banner grepping). The skill that documents this anti-pattern is excellent, but a skill only fires if the agent loads it. The tool surface fires on every misuse. This is the embed-footguns-in-tool-surface pattern from PR NousResearch#31289 applied to a recurring failure mode that's outgrown skill-only enforcement. Detector is deliberately narrow — flags two specific shapes: 1. Any command containing `statusCheckRollup` (the JSON-API path — conclusion vs status field semantics keep burning us). 2. `gh pr view` / `gh pr checks` combined with `jq` (gh pr checks doesn't emit JSON, so any `| jq` here is confused intent; the canonical column-2 poller uses awk-on-tabs, not jq). Does NOT flag the blessed column-2 awk-on-tabs poller (which uses `awk -F"\t" "\==\"pending\""`) or the exit-code-driven `gh pr checks $PR >/dev/null` snippet. Hint composes with the existing background-without-notify_on_complete hint — both can fire on the same call. Each is independently actionable. Tests: - 4 new cases in tests/tools/test_notify_on_complete.py - test_homebrew_ci_poller_via_statusCheckRollup_emits_hint (positive) - test_homebrew_ci_poller_via_gh_pr_checks_piped_to_jq_emits_hint (positive) - test_canonical_column2_awk_poller_does_not_emit_homebrew_hint (negative) - test_canonical_gh_pr_checks_exit_code_loop_does_not_emit_hint (negative) - test_non_ci_background_command_does_not_emit_homebrew_hint (negative) - 30/30 passing (was 26)
reasoning.encrypted_content is sealed to the Responses endpoint that minted it. When a session switches model providers mid-conversation — say the user runs /model gpt-5.5 after several turns on grok-4.3, or vice versa — the persisted codex_reasoning_items carry blobs the new endpoint cannot decrypt, and every subsequent turn fails with HTTP 400 invalid_encrypted_content. This is the cross-issuer prevention layer. Pairs with: * PR NousResearch#33035 — runtime recovery when the HTTP 400 fires anyway * PR NousResearch#33146 — prevention for transient rs_tmp_* items Stamps each reasoning item with the issuer kind that minted it (codex_backend / xai_responses / github_responses / other:<url>) at normalize time, then drops items at replay time when the active endpoint differs from the stamp. Unstamped (legacy) items pass through for backwards compatibility. Cherry-picked from @chaconne67's PR NousResearch#31629. Conflict against current main (NousResearch#33035's replay_encrypted_reasoning parameter) resolved as 'keep both' — the two guards compose: replay_encrypted_reasoning=False is the session-wide kill switch, current_issuer_kind is the per-item filter that runs only when replay is still enabled.
…tting The two new display-resolution sites added by NousResearch#31034 (busy_ack_detail and long_running_notifications) wrapped resolve_display_setting() in try/except Exception. The existing 4 call sites in this file don't — the function is safe by contract. Match the established pattern and drop the redundant guards. -16 LOC, no behaviour change.
Codex re-auth via `hermes setup` / `hermes model` wrote fresh OAuth tokens to providers.openai-codex.tokens but left the credential_pool device_code entry holding the consumed refresh token and stale error markers. Since the runtime selects from the pool, the next request spent a dead token and got a 401 token_invalidated. Update the singleton-seeded pool entries in lockstep and clear their error state. Fixes NousResearch#33000
…dentials When the Codex OAuth token endpoint returns 429 (usage-limit / quota exhaustion), refresh_codex_oauth_pure raised a generic auth error that the gateway surfaced as 'Primary provider auth failed: No Codex credentials stored. Run hermes auth', prompting re-auth that cannot lift a quota cap. Classify 429 distinctly (codex_rate_limited, relogin_required=False) with a non-alarming quota message that honors Retry-After, log it as 'Primary provider rate-limited (429)', and stop format_auth_error from appending the re-authenticate remediation. Also log the fallback provider's literal config key instead of the resolved runtime category. Refs NousResearch#32790
Two tests in test_verbose_command.py asserted Telegram's tool_progress default was "new" and expected /verbose to cycle that to "all". The default has since been overridden to "off" in gateway/display_config.py (_PLATFORM_DEFAULTS for telegram — tier-1 inbox preset that keeps mobile chats final-answer-first), making the first /verbose invocation cycle off → new, not all → verbose. The behavioral change was intentional; the tests were stale and missing from the same commit. Surfaced as a pre-existing failure on origin/main during CI for the unrelated NousResearch#33164 / NousResearch#33168 Codex auth salvages.
…394-docker-anthropic-package fix(docker): include anthropic, bedrock, azure-identity extras in image Fixes NousResearch#30394. Air-gapped/restricted-network Docker containers can't reach PyPI for lazy-install, so `--extra anthropic --extra bedrock --extra azure-identity` are now added to the Dockerfile's `uv sync` so these provider packages are baked into the published image. The [all] extra deliberately excludes these (per the 2026-05-12 lazy-install policy on [all]) to keep `uv sync --locked` from breaking when one of their pinned versions gets PyPI-quarantined. The Dockerfile adds them back via additive --extra flags, mirroring the existing --extra messaging pattern (issue NousResearch#24698 / test_dockerfile_pid1_reaping.py). Follow-up: separate PR will bump pyproject.toml's [anthropic] extra from 0.86.0 to 0.87.0 to converge with tools/lazy_deps.py's CVE-patched pin (CVE-2026-34450, CVE-2026-34452).
The image's Dockerfile runs npx playwright install chromium, which
populates $PLAYWRIGHT_BROWSERS_PATH (=/opt/hermes/.playwright) with a
`chromium_headless_shell-<build>/chrome-headless-shell-linux64/` tree.
agent-browser (the runtime CLI Hermes spawns for the browser tool)
doesn't recognise this layout in its own cache scan and fails with
`Auto-launch failed: Chrome not found` — even though the binary is
right there.
Reproduction on current main:
$ docker run --rm <image> sh -c 'npx -y agent-browser snapshot --url about:blank'
✗ Auto-launch failed: Chrome not found. Checked:
- agent-browser cache: /tmp/.../.agent-browser/browsers
- System Chrome installations
- Puppeteer browser cache
- Playwright browser cache
Run `agent-browser install` to download Chrome, or use --executable-path.
Fix: at boot, locate the binary under $PLAYWRIGHT_BROWSERS_PATH and
export AGENT_BROWSER_EXECUTABLE_PATH via /run/s6/container_environment
so the with-contenv shebang on main-wrapper.sh propagates it into the
supervised `hermes` process and thence to agent-browser subprocesses.
Filename-matched (chrome / chromium / chrome-headless-shell /
chromium-browser), not path-matched: the chromium dir contains many
shared libraries (libGLESv2.so, libEGL.so, ...) which inherit the
executable bit from Playwright's tarball but are NOT browser binaries.
Compare PR NousResearch#18635's earlier `find | grep -Ei 'chrome|chromium'` which
would match the path .../chrome-headless-shell-linux64/libGLESv2.so
and pick a .so as the browser binary.
User overrides (e.g. `-e AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/...`)
are respected — the discovery block is skipped when the env var is
already set. Quietly skipped when $PLAYWRIGHT_BROWSERS_PATH doesn't
exist (e.g. custom builds that strip Playwright).
This salvages PR NousResearch#18635 by @jackey8616, who identified the bug and
proposed the same env-var approach but in the now-deprecated
docker/entrypoint.sh shim and with a path-match find command that
selected .so files instead of the chrome binary. The fix retargets
docker/stage2-hook.sh (the s6-overlay cont-init script where boot-time
env setup belongs) with a corrected filename-match query.
Fixes NousResearch#15697
Closes NousResearch#18635
Co-authored-by: Clooooode <12930377+jackey8616@users.noreply.github.com>
…on is empty Closes NousResearch#32992. The chat path resolves Codex credentials via `resolve_codex_runtime_credentials` which only reads `providers.openai-codex.tokens` (the singleton). The auxiliary path uses `_read_codex_access_token` which checks the credential_pool first. For users whose tokens live only in the pool — manual seed, partial re-auth, restore from backup, or any state where the singleton is empty but the pool is healthy — the chat path raised AuthError or (worse, since OpenAI(api_key='') silently attaches no header) the wire saw HTTP 401 "Missing Authentication header" while the auxiliary path worked fine. This adds a pool fallback to `resolve_codex_runtime_credentials`: when the singleton has no usable access_token, scan `credential_pool.openai-codex` for the first entry that has a non-empty access_token and isn't in an exhaustion cooldown window (`last_error_reset_at` in the future). If found, return that token with `source="credential_pool"`. If no usable entry exists, the original AuthError propagates as before. Regression tests cover: - Empty singleton + healthy pool entry → pool token returned - Pool fallback skips entries currently in cooldown - Empty singleton + empty/wedged pool → AuthError propagates (existing contract preserved)
…eartbeat in place (NousResearch#33187) NousResearch#33151 flipped THREE Telegram display defaults to false: - tool_progress: new -> off (kept: per-tool stream is too chatty) - interim_assistant_messages: T -> F (REVERTED here) - long_running_notifications: T -> F (REVERTED here) - busy_ack_detail: T -> F (kept: verbose iteration counter) The two reverts were wrong. interim_assistant_messages = the model's REAL words mid-turn ("I'll inspect the repo first.", "Let me check both files in parallel"). That is signal, not noise. Suppressing it left Telegram users staring at "typing..." for the entire turn duration with no feedback. long_running_notifications = the periodic heartbeat. Silent agent for 30 minutes is worse than one bubble updating every 3 minutes. Changes: - gateway/display_config.py: Telegram tier-1 inbox keeps both defaults on (only tool_progress and busy_ack_detail stay off). - gateway/run.py _notify_long_running(): edit a single heartbeat message in place (where the adapter supports it) instead of posting a new "Still working..." bubble each interval. Telegram, Discord, Slack, Matrix all qualify. Falls back to send-new when edit fails. - gateway/run.py: tighten heartbeat text. "⏳ Still working... (12 min elapsed — iteration 21/60, running: terminal)" -> "⏳ Working — 12 min, terminal". Verbose iteration detail moves behind busy_ack_detail (one knob now controls both busy acks AND heartbeat verbosity). - tests/, cli-config.yaml.example, website/docs/user-guide/messaging: updated to reflect the corrected story.
…r slash enums xAI's /v1/responses endpoint rejects service_tier with HTTP 400 "Argument not supported: service_tier" when users activate /fast mode. Also add a safety-net strip_slash_enum call in _preflight_codex_api_kwargs to catch any tool schemas that might slip through the caller-level sanitization. xAI's Responses API grammar compiler rejects enum values containing forward slashes (e.g. HuggingFace model IDs like "Qwen/Qwen3.5-0.8B") with the opaque "Invalid arguments passed to the model" error. Fixes the root cause of "Invalid arguments passed to the model" errors reported by xAI OAuth (SuperGrok) users.
…tests (NousResearch#28490) Three additions on top of @Nami4D's salvage: 1. Gate the preflight slash-enum strip on the model name pattern (grok-* / x-ai/grok-*). The original PR stripped slash-containing enum values from every codex_responses request, but native Codex (OpenAI) and GitHub Models DO accept slash enums — stripping them there would silently degrade tool-schema constraints. xAI is the only Responses-API surface that rejects the shape. 2. Resolve the merge conflict in agent/transports/codex.py by preserving both the timeout-forwarding block that landed on main between the PR's branch point and now AND the new service_tier strip. Behavioural intent of both is preserved. 3. Six new tests in tests/agent/transports/test_codex_transport.py covering: - TestCodexTransportXaiServiceTierStrip (3 tests): xAI strips service_tier from request_overrides; non-xAI codex_responses and GitHub Models both KEEP service_tier (regression guards so the strip stays xAI-only). - TestPreflightSlashEnumStrip (3 tests): Grok and aggregator- prefixed Grok model names both trigger the safety-net strip; non-Grok models preserve slash enums as a regression guard against the strip becoming too broad. 51/51 in tests/agent/transports/test_codex_transport.py. Co-authored-by: Nami4D <hello@nami4d.tech>
Remove the ancestor-check gate and the separate move-latest job. On main pushes, the merge job now tags both :main and :latest in a single imagetools create call. Releases still get :<tag> only. Removed: - move-latest job (ancestor check + retag dance) - Decide whether to move :main step (ancestor check in merge) - Compute tag step - push_main gate on manifest push - merge job outputs (nothing downstream needs them anymore)
…ousResearch#33228) Closes NousResearch#33175. switch_model() in agent/agent_runtime_helpers.py mutated agent.model and agent.provider before rebuilding the client, with no try/except to restore them on failure. If the rebuild raised (bad API key, network error, build_anthropic_client failure, etc.) the agent was left with the new model+provider name paired with the OLD client — producing HTTP 400s like "claude-sonnet-4-6 is not supported on openai-codex" on the next turn. Callers in cli.py, gateway/run.py, and tui_gateway/server.py already catch the exception and warn the user, but the warning was misleading because the swap had partially succeeded; the agent's state was torn. Snapshot every mutated field before the swap, wrap the swap+rebuild block in try/except, and restore the snapshot on failure before re-raising so the caller's warning surfaces. Reported by @amirariff91. Tests cover both branches (chat_completions and anthropic_messages) and the cross-branch case (anthropic -> openai).
Merge 106 upstream commits from NousResearch/hermes-agent main into the
inkbox branch.
Conflicts resolved:
- gateway/run.py — upstream introduced an editable heartbeat (edit the
existing "Still working" message rather than spamming new ones). The
fork's contribution to this code path was a metadata tag
`notice_type: notify_interval` so Inkbox email/SMS adapters can filter
out heartbeats from real channels. Combined: kept upstream's
edit/fallback flow, threaded the inkbox tag through the metadata that
reaches send().
- README.md — upstream rewrote the top-of-file intro into a Nous
marketing pitch. Kept the fork's existing intro ("A fork of Nous
Research's Hermes Agent wired up to Inkbox..."); the marketing pitch
belongs in upstream, not here.
- uv.lock — regenerated from the merged pyproject.toml rather than
hand-resolved. Locks inkbox v0.4.5 (was 0.4.0) + the rest of the
upstream dep updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
46 |
invalid-argument-type |
16 |
unresolved-attribute |
5 |
invalid-assignment |
4 |
unsupported-operator |
4 |
invalid-return-type |
1 |
First entries
hermes_cli/mcp_catalog.py:493: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `list[str] | None`, found `list[object]`
plugins/security-guidance/__init__.py:40: [unresolved-import] unresolved-import: Cannot resolve imported module `.`
cli.py:7204: [invalid-assignment] invalid-assignment: Object of type `int | float` is not assignable to attribute `_slash_confirm_deadline` of type `int`
agent/codex_runtime.py:279: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `str`
tests/gateway/test_mcp_reload_refreshes_cached_agents.py:20: [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`
cli.py:7185: [unresolved-attribute] unresolved-attribute: Attribute `loop` is not defined on `None` in union `None | Unknown`
tests/gateway/test_session_api.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp.test_utils`
tests/cron/test_cronjob_schema.py:28: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["action=create"]` and `Unknown | str | dict[str, str] | ... omitted 3 union elements`
tests/gateway/test_session_api.py:7: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp`
hermes_cli/dashboard_auth/cookies.py:54: [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_ws_auth.py:27: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.testclient`
hermes_cli/dashboard_auth/middleware.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi`
tests/hermes_cli/test_mcp_catalog.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_dashboard_auth_gate.py:6: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
agent/codex_runtime.py:482: [invalid-argument-type] invalid-argument-type: Argument to function `_consume_codex_event_stream` is incorrect: Expected `str`, found `Unknown | None`
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`
gateway/platforms/api_server.py:1473: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `None` in union `dict[str, Any] | None`
tests/hermes_cli/test_dashboard_auth_cookies.py:4: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_dashboard_auth_status_endpoint.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cron/test_cronjob_schema.py:41: [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["required"]` on object of type `str`
tests/plugins/test_security_guidance_plugin.py:74: [unresolved-attribute] unresolved-attribute: Attribute `loader` is not defined on `None` in union `ModuleSpec | None`
tests/hermes_cli/test_dashboard_auth_prefix.py:477: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi`
hermes_cli/dashboard_auth/cookies.py:55: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.responses`
... and 51 more
✅ Fixed issues (33):
| Rule | Count |
|---|---|
invalid-argument-type |
13 |
unresolved-attribute |
9 |
unsupported-operator |
4 |
unresolved-import |
3 |
call-top-callable |
1 |
not-iterable |
1 |
invalid-type-form |
1 |
invalid-return-type |
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:206: [unresolved-attribute] unresolved-attribute: Unresolved attribute `SandboxStatus` on type `ModuleType`
tests/tools/test_vercel_sandbox_environment.py:204: [unresolved-attribute] unresolved-attribute: Unresolved attribute `Resources` on type `ModuleType`
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]`
tools/environments/vercel_sandbox.py:267: [unresolved-attribute] unresolved-attribute: Object of type `None` has no attribute `sync`
cli.py:7231: [unresolved-attribute] unresolved-attribute: Attribute `put` is not defined on `str`, `list[tuple[str, str, str]]`, `int` in union `str | list[tuple[str, str, str]] | int | Queue[Unknown]`
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]]`
tools/environments/vercel_sandbox.py:23: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tools/environments/vercel_sandbox.py:300: [unresolved-import] unresolved-import: Cannot resolve imported module `vercel.sandbox`
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`
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`
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
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`
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:12888: [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:12610: [unsupported-operator] unsupported-operator: Operator `<` is not supported between objects of type `str | list[tuple[str, str, str]] | int | Queue[Unknown]` and `int`
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`
tests/tools/test_vercel_sandbox_environment.py:203: [unresolved-attribute] unresolved-attribute: Unresolved attribute `Sandbox` on type `ModuleType`
plugins/image_gen/openai-codex/__init__.py:154: [unresolved-attribute] unresolved-attribute: Module `openai` has no member `OpenAI`
tests/tools/test_vercel_sandbox_environment.py:209: [unresolved-attribute] unresolved-attribute: Unresolved attribute `sandbox` 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]`
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)`
cli.py:7320: [not-iterable] not-iterable: Object of type `_T@enumerate` is not iterable
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]`
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`?
... and 8 more
Unchanged: 4964 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Upstream commit 7c622b6 ("fix(kanban): migrate task session index after columns") removed three CREATE INDEX statements from SCHEMA_SQL and re-added the corresponding indexes inside _migrate_add_optional_columns, after the columns themselves are added. The legacy-board migration was failing because CREATE TABLE IF NOT EXISTS does not add new columns to existing tables, so the SCHEMA_SQL-level CREATE INDEX ran against a table that didn't yet have the indexed column. Two prior github-actions[bot] sync PRs (#29, #33) left the inkbox branch with the migration half of the fix applied (the new indexes are created after columns) but missed deleting one line from SCHEMA_SQL — specifically `CREATE INDEX IF NOT EXISTS idx_tasks_session_id ON tasks(session_id);`. That kept the bug live: the test `test_connect_migrates_legacy_db_before_optional_column_indexes` has been failing on the inkbox branch since at least PR #39, and the running gateway throws `sqlite3.OperationalError: no such column: session_id` from the kanban dispatcher every minute. Surgical: delete the orphan line. After this commit, kanban_db.py is byte-identical to upstream/main. Test passes locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…R_MAP The local git config in ~/hermes-agent uses inkbox-on-call-agent@inkboxmail.com as the committer email (the on-call agent identity). The check-attribution workflow scans every new email in a PR and fails if any are missing from AUTHOR_MAP. Map this email to dimavrem22 — the on-call agent identity belongs to Dima. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Brings the
inkboxbranch 0 commits behindNousResearch/hermes-agentmainby merging the 106 upstream commits that have landed since the previous sync (#39, 2026-05-26).Conflicts resolved
gateway/run.py— upstream made the "Still working..." heartbeat editable (edit the prior message instead of sending a new one each interval). The fork's contribution to this path was anotice_type: notify_intervalmetadata tag so the Inkbox email/SMS adapter filters out heartbeats from real channels. Kept both: upstream's edit/fallback flow + the inkbox metadata tag plumbed through tosend().README.md— upstream rewrote the top intro into a Nous marketing pitch with a feature table. Kept the fork's "A fork of Nous Research's Hermes Agent wired up to Inkbox..." intro; the marketing pitch isn't appropriate for the fork README.uv.lock— regenerated from the mergedpyproject.tomlinstead of hand-resolved. Locksinkbox v0.4.5(up from 0.4.0) plus the upstream dep updates.How to Test
inkboxbase now)grep -rE '^(<<<<<<< |>>>>>>> )' .returns nothing (no stray markers like the broken sync that produced commit `5d42a44c`)