Merge upstream v2026.5.29 into main - #10
Conversation
…, ClawHub, browse.sh, OpenAI, …) (NousResearch#32336) The Skills Hub page was stuck on a stale Feb 25 snapshot, showing only Built-in + Optional + Anthropic + LobeHub. The unified index already has 2078 skills from skills.sh / ClawHub / LobeHub / GitHub taps / Claude Marketplace, and BrowseShSource adds another ~330 — none of it was reaching the page. Changes: - website/scripts/extract-skills.py: read website/static/api/skills-index.json (the unified multi-source catalog, rebuilt twice daily) as the canonical external source. Keep the legacy skills/index-cache/ fallback for offline builds. Add friendly per-source labels (skills.sh, ClawHub, browse.sh, OpenAI, HuggingFace, Anthropic, LobeHub, etc.) and per-entry installCmd. - website/src/pages/skills/index.tsx: add source pills + ordering for the 11 new sources; render installCmd from the index entry. - website/scripts/prebuild.mjs: when no local skills-index.json exists, fetch the live one from hermes-agent.nousresearch.com so local 'npm run build' matches production without burning GitHub API quota. - scripts/build_skills_index.py: crawl BrowseShSource so browse.sh entries land in the unified index. Adjust source_order. - tools/skills_hub.py: GitHubSource.DEFAULT_TAPS — openai/skills moved its skills into skills/.curated/ and skills/.system/, so add both as explicit taps (the listing code skips dotted dirs by design). Drop VoltAgent/awesome-agent-skills (README-only, no SKILL.md files) and MiniMax-AI/cli (singular skill, not a tap directory). Net effect: github source jumps from 83 → 143 skills, with OpenAI properly included. - .github/workflows/deploy-site.yml: build the unified index BEFORE running extract-skills.py — previous order meant extract-skills always fell back to the legacy cache. Drop the 'skip if file exists' guard; the file is gitignored and must be rebuilt every deploy. - .github/workflows/skills-index.yml: drop the broken 'deploy-with-index' job (it cp'd 'landingpage/\*' which no longer exists, failing every cron run since the landingpage move). Replace it with a workflow_dispatch trigger of deploy-site.yml so the index refresh still reaches production on schedule. - website/docs/user-guide/features/skills.md: drop VoltAgent from the default-taps doc list to match the code. Before: 695 skills (Built-in 90, Optional 84, Anthropic 16, LobeHub 505). After: 2168 skills across 9 source pills, including the 1212 skills.sh entries the user expected to see.
… 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).
…NousResearch#32345) Layered safety so the Skills Hub at /docs/skills stays in sync without silent rot. Three pieces: 1. build_skills_index.py — refuses to ship a degenerate index. EXPECTED_FLOORS per source (skills.sh ≥100, lobehub ≥100, clawhub ≥50, official ≥50, github ≥30, browse-sh ≥50) and MIN_TOTAL=1500. Any source collapsing to zero (the silent OpenAI breakage that hid for weeks) now fails the workflow loud — broken index never reaches the live site. 2. extract-skills.py + the React page — visible freshness signal. Sidecar website/src/data/skills-meta.json carries the index's generated_at timestamp, plus per-source counts. Skills Hub renders a 'Catalog refreshed N hours ago · auto-rebuilt twice daily' line under the hero copy. If the cron stalls, users see the staleness immediately. 3. .github/workflows/skills-index-freshness.yml — watchdog cron. Every 4 hours, fetches the live /docs/api/skills-index.json, validates shape, checks age (>26h is stale), checks the same per-source floors, and opens (or appends to) a GitHub issue when anything is off. The issue is title-prefixed [skills-index-watchdog] so subsequent failures append a comment instead of spamming new issues. Net effect: - A silent regression like 'OpenAI tap moved its skills' now fails the build instead of shipping a quietly broken catalog. - A stuck cron (like the landingpage breakage that ran red for weeks) now files an issue within 4 hours. - Users see how fresh the catalog is on the page itself. Test plan: - Local: built skills-meta.json from the live index → 'Catalog refreshed N minutes ago' rendered correctly in the static HTML. - Probe logic dry-run against the live index: total=2456, all 6 sources above floor, age 0.1h — issues=NONE. - Triggered skills-index.yml manually; both jobs green, deploy-site.yml dispatch fired.
…st bullet
The GFM → Telegram-row-group rewriter previously joined every line in
every row with a blank line ("\n\n".join(rendered_rows)), which made
multi-column tables explode into one-bullet-per-paragraph walls on
mobile. It also emitted the row heading twice when the table had no
row-label column: once as the standalone bold heading and once again
as the first labeled bullet (heading == headers[0] == data_cells[0]).
This commit:
* Uses single newlines between the heading and its bullets within a
row-group, and a blank line only BETWEEN row-groups.
* Skips any bullet whose value duplicates the heading text when the
table has no row-label column (the heading already carries that
information). Tables WITH a row-label column are unaffected since
the heading comes from the label cell and never duplicates a header.
Updated existing test assertions accordingly and added two regression
tests: one that reproduces the screenshot bug (wide five-column "Plays"
comparison table) and one that pins the row-label-column behavior so
the dedup logic doesn't accidentally swallow real data.
tests/gateway/test_telegram_format.py: 101 passed
SubdirectoryHintTracker was scanning directories outside the active working directory, allowing files like ~/.codex/AGENTS.md or ~/.claude/CLAUDE.md to be loaded and injected into the agent context. This causes cross-agent context contamination and instruction mixup. Add _is_ancestor_or_same() helper and a path boundary check in _is_valid_subdir(): only directories within the working directory tree (i.e. path.is_relative_to(working_dir)) are allowed. Also add exist_ok=True to mkdir() calls in new tests to prevent pytest-xdist race conditions when workers share the same tmp_path parent. Tests added: - test_outside_working_dir_rejected: verifies sibling dirs are blocked - test_outside_working_dir_absolute_path_rejected: verifies ~/.codex paths blocked - test_inside_workspace_subdir_allowed: verifies normal subdir access unaffected - test_sibling_repo_not_loaded_via_ancestor_walk: ancestor walk stays within workspace
…sedxml Two small defensive-hardening changes: - web/src/components/Markdown.tsx: render links only for http(s)/mailto schemes; other schemes (javascript:, data:, vbscript:) are dropped to plain text so a crafted link in rendered content can't execute on click. - gateway/platforms/wecom_callback.py: parse the untrusted, pre-auth WeCom callback request body with defusedxml instead of xml.etree, blocking entity-expansion / billion-laughs (and XXE) on the parse path. defusedxml is already a dependency (uv.lock); response-building XML in wecom_crypto.py is unchanged (it is not parsed from untrusted input). Verified: dashboard typechecks and builds; defusedxml blocks an entity-expansion payload while valid WeCom envelopes still parse.
Follow-up on top of @TheOnlyMika's NousResearch#32155 cherry-pick. The defusedxml hardening import was unconditional, which would break the gateway for anyone running a WeComCallback adapter without the (transitive-only) defusedxml present. - Wrap the import in the same try/except pattern as aiohttp/httpx in the same file. Sets DEFUSEDXML_AVAILABLE flag. - Extend check_wecom_callback_requirements() to gate on the flag, so the gateway logs the actual missing dep and skips the adapter instead of crashing. - Add [wecom] extra to pyproject.toml with defusedxml==0.7.1. - Register platform.wecom_callback in tools/lazy_deps.py so users get prompted to install it on first WeComCallback configuration, same pattern as discord/slack/matrix. defusedxml is still the right call for pre-auth XML parsing — this commit just makes the dep declarative and recoverable instead of a hard import-time crash.
…astes (NousResearch#32447) Follow-up to NousResearch#32087 after community report from @ethernet that 8000-char single-line pastes get dumped raw into the input box. A) Fallback regression revert paste_collapse_threshold_fallback default: 0 -> 5 NousResearch#32087 disabled the fallback handler by default. The fallback path has been always-on with line_count >= 5 since NousResearch#3065 (March 2026); the previous shape was the salvaged contributor's design and didn't match pre-existing behavior for terminals without bracketed paste support (Windows terminals, some SSH setups). Restoring the original on-by-default. B) Long single-line paste guard New config key: paste_collapse_char_threshold (default 2000) Bracketed-paste handler and fallback handler now BOTH collapse when line count >= line threshold OR total char length >= char threshold. Catches the case ethernet hit: ~8000 chars of minified JSON / log output on a single line dumped raw into the buffer. TUI mirrors the same config via uiStore.pasteCollapseChars. Set 0 to disable. Defaults verified: paste_collapse_threshold: 5 paste_collapse_threshold_fallback: 5 paste_collapse_char_threshold: 2000 Tests: tests/hermes_cli/test_config.py: 87/87 pass ui-tui useConfigSync.test.ts: 34/34 pass ui-tui useComposerState.test.ts: 9/9 pass tsc: 0 new errors in touched files
…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>
…vior The old test asserted that a non-MiniMax provider returning a generic overflow (no provider-reported max) would step down to the 128K probe tier. The salvaged fix from NousResearch#33673 deliberately removes that step-down because guessed tiers cause configured 1M sessions to silently shrink. Update the test to assert the new contract: keep the configured 200K window and rely on compression instead.
Auto-recall used to surface every fact type Hindsight had on the
session — `world`, `experience`, and `observation`. That triple-ships
the same underlying signal in three different framings: observations
are the concrete events the user said/did/asked, while world and
experience facts are aggregate summaries Hindsight derives from those
exact observations. Including all three burns most of
`recall_max_tokens` on rephrasings, crowds out events the model
actually needs to see, and produces effective duplicates in the
prompt — observations themselves are deduplicated by construction
so observation-only recall is denser per token and closer to
conversational ground truth.
Change
------
- Default `_recall_types = ["observation"]` (was `None`, which
delegated to server-side "return everything").
- `initialize()` now treats a missing `recall_types` config the same
way; also accepts comma-separated strings for parity with `recall_tags`.
- An explicit `recall_types=[]` config falls back to the default rather
than disabling the filter (would silently widen recall vs. the new
default).
- Added to `get_config_schema()` so it's discoverable via `hermes config`.
Per-call `hindsight_recall` tool invocations are unaffected — they
already only forward `types` when the caller passes the argument.
Docs / migration
----------------
plugins/memory/hindsight/README.md grows a "Behavior change" callout
explaining the why (no-duplicates, information-efficient) and how to
restore the legacy broad recall:
"recall_types": "observation,world,experience" # or a JSON list
in `~/.hermes/hindsight/config.json`.
Tests
-----
- `test_default_values` updated for the new default.
- New cases: explicit list override, CSV string accepted, empty list
falls back to default (not "wider than default").
The original change's description and README claimed the per-call hindsight_recall tool was unaffected by the new observation-only default. That is inaccurate: hindsight_recall reads the same self._recall_types instance attribute as the auto-recall prefetch path, and RECALL_SCHEMA exposes no per-call types argument, so the model cannot override it. Narrowing the default narrows BOTH paths. Corrects the README behavior-change note, the config-table row, and the get_config_schema description to reflect that recall_types applies to both auto-recall and the hindsight_recall tool.
…usResearch#34081) Today's three skills-index PRs (NousResearch#33748, NousResearch#33809, NousResearch#34025) merged to main but the live Vercel-hosted docs site didn't pick them up — Vercel is fired by the deploy-vercel job, which was gated on release events only. Out-of-band main commits between releases couldn't reach Vercel without cutting a tag. Widen the gate to also include workflow_dispatch so 'gh workflow run deploy-site.yml' can ship pending main changes to Vercel on demand. Release-tag behavior is unchanged.
Picks up the deferred GPU-tier detection fix (design-language) that stops the synchronous WebGL probe from blocking first paint, which was causing a boot-time flash in the dashboard backdrop. nix/web.nix npmDepsHash is a placeholder here and is corrected in the follow-up commit using the hash reported by the Nix CI job. Co-authored-by: Cursor <cursoragent@cursor.com>
The web/package-lock.json changed when bumping @nous-research/ui to 0.18.2, so the fetchNpmDeps fixed-output hash in nix/web.nix was stale. Update it to the hash prefetch-npm-deps computes for the new lockfile. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds an optional `messages` keyword to the `MemoryProvider.sync_turn` contract so external/community memory plugins can receive the OpenAI-style conversation message list for the completed turn — including assistant tool calls and tool result content — not just the final assistant text. Dispatch uses signature inspection (`_provider_sync_accepts_messages`): only providers that declare a `messages` parameter (or `**kwargs`) receive it; all existing in-tree providers keep their legacy text-only signature and are called unchanged. No structured-trace envelope is added to core — providers reconstruct whatever they need from the standard message list. Also documents Memori as a standalone community memory provider. Salvaged from NousResearch#28065 — rebased onto current main. Co-authored-by: Dave Heritage <david@memorilabs.ai>
Maps both commit emails (david@memorilabs.ai, dave@devwdave.com) used on NousResearch#28065 to the devwdave GitHub account so the contributor audit in scripts/release.py passes.
…ri-trace-messages feat: expose completed-turn message context to memory providers (salvage NousResearch#28065)
In loopback mode the dashboard's identity probe (/api/auth/me) returns 401 by design — AuthWidget swallows it and renders nothing. But the probe routed through fetchJSON, whose loopback 401 handler treats a 401 as a rotated session token and full-page-reloads to pick up a fresh one. That reload is guarded by a one-shot sessionStorage flag which every *successful* request clears, so with auth/me reliably 401ing and the other dashboard calls (status/config/sessions) reliably succeeding, the guard never sticks and the page reload-loops indefinitely (the "boot flash"). Add an allowUnauthorized option to fetchJSON that skips only the loopback stale-token reload (the 401 still throws so AuthWidget can catch it, and the gated-mode login_url envelope redirect is unaffected), and use it for getAuthMe. Co-authored-by: Cursor <cursoragent@cursor.com>
…E from NousResearch#33583 (NousResearch#33751) * docs(reference): document --no-supervise / HERMES_GATEWAY_NO_SUPERVISE (en) * docs(reference): document --no-supervise / HERMES_GATEWAY_NO_SUPERVISE (en) * docs(reference): document --no-supervise / HERMES_GATEWAY_NO_SUPERVISE (zh) * docs(reference): document --no-supervise / HERMES_GATEWAY_NO_SUPERVISE (zh)
… bind host
The s6 dashboard run script flipped `--insecure` on whenever
`HERMES_DASHBOARD_HOST` was anything other than 127.0.0.1 / localhost.
That comment ("the dashboard refuses otherwise") predates the OAuth
auth gate: back when it was written, `start_server` would SystemExit
on any non-loopback bind, so the run script's `--insecure` was the
only way to make in-container deployments work at all.
The gate has since been replaced by `should_require_auth(host,
allow_public)`, which engages the OAuth flow when a
`DashboardAuthProvider` is registered (the bundled `dashboard_auth/nous`
provider auto-registers on `HERMES_DASHBOARD_OAUTH_CLIENT_ID`) and
fails closed with a specific operator-facing error when none is. The
host-derived `--insecure` ran upstream of all that and silently
disabled the gate on every container-deployed dashboard.
Most visible under the portal's wildcard-subdomain rollout: every Fly
machine binds 0.0.0.0 so the edge can reach Flycast, every machine
boots with the correct `HERMES_DASHBOARD_OAUTH_CLIENT_ID`, the nous
provider registers — and `/api/status` still returns
`{"auth_required": false, "auth_providers": ["nous"]}` because the
run script disabled the gate before `start_server` ever saw the
request. The dashboard SPA was served to anyone, no `/login` redirect,
no OAuth challenge.
Fix: derive `--insecure` from an explicit opt-in env var,
`HERMES_DASHBOARD_INSECURE` (truthy values matching the rest of the
s6 boolean envs: 1, true, TRUE, True, yes, YES, Yes). Operators on
trusted LANs behind a reverse proxy without the OAuth contract
(the existing `docker-compose.windows.yml` use case) opt in
explicitly; portal-managed agent deployments leave it unset and let
the gate engage.
`docker-compose.windows.yml` already passes `--insecure` on the
`command:` array directly (line 38), so it doesn't depend on the s6
auto-injection. No compose-file change required.
Tests:
* `tests/test_docker_home_override_scripts.py` — extends the existing
static-text guard with a regression assertion that the legacy
host-derived case-statement is gone and the new env-var opt-in is
present (locks against accidental revert).
* `tests/docker/test_dashboard.py` — adds two Docker-in-Docker tests
exercising the actual `/api/status` round-trip:
- 0.0.0.0 bind + `HERMES_DASHBOARD_OAUTH_CLIENT_ID` → gate engaged
- 0.0.0.0 bind + `HERMES_DASHBOARD_INSECURE=1` → gate disabled
Docs:
* `website/docs/user-guide/docker.md` + zh-Hans i18n — adds the new
env var to the table, replaces the stale prose ("the entrypoint
no longer auto-enables insecure mode" — which until this PR was
flat-out wrong) with an accurate description of the gate's
trigger conditions and the explicit opt-out.
shellcheck clean. Python static-text test passes locally. Behavioural
test will run against any future image build (CI's Docker harness).
When the Hermes Docker image runs an stdio MCP server configured with an
explicit env.PATH that omits /usr/local/bin (a common pattern when users
hand-author PATH for sandboxing), the MCP env-filter passes that narrow
PATH straight through to the subprocess. _resolve_stdio_command's
fallback for bare 'npx' / 'npm' / 'node' commands only checked
$HERMES_HOME/node/bin/ and ~/.local/bin/, so execvp() failed with
'[Errno 2] No such file or directory: npx' on every Node-based stdio
MCP server (Railway, Anthropic, GitHub Copilot, etc.).
The naive workaround — symlink /usr/local/bin/npx into the user's PATH —
fails one layer deeper because npx's shebang re-execs /usr/bin/env node
and node also lives at /usr/local/bin/node.
Fix: add /usr/local/bin/<cmd> as a third candidate in the fallback list.
This is the canonical install location for Node on:
- Linux from-source builds
- the upstream node:bookworm-slim image, which the Hermes Docker
image copies node + npm + corepack from since NousResearch#4977 (the Node 22 LTS
refactor that exposed this)
- macOS Homebrew on Intel
Because the resolver already calls _prepend_path(resolved_env, command_dir)
after locating the command, /usr/local/bin gets prepended to the env's
PATH automatically, which also fixes the second-layer shebang failure
(npx-cli.js can now find node).
Scope is intentionally narrow: the fix activates only when the bare
command isn't otherwise locatable through the user's PATH. Users who
explicitly narrowed PATH for a non-Node MCP server see no change in
behavior.
Tested:
- tests/tools/test_mcp_tool_issue_948.py: new test
test_resolve_stdio_command_falls_back_to_usr_local_bin (mirrors the
existing hermes-node-bin fallback test)
- Full MCP test suite: 254/254 pass across 7 test files
- E2E against a freshly-built Docker image: reproduced the original
failure mode (env.PATH=/opt/data/bin:/usr/bin:/bin), confirmed the
resolver returns /usr/local/bin/npx and prepends /usr/local/bin to
PATH; subprocess.run of the resolved command prints '10.9.8' and
exits 0 with empty stderr
- Negative E2E on the host (where Node is already on PATH via mise):
resolver still hits the mise install dir, /usr/local/bin candidate
is not consulted, PATH is unchanged
…only (NousResearch#34194) Regression from PR NousResearch#33809 (lazy-fetch refactor). The `sources` and `categoryEntries` useMemo blocks were derived from `allSkillsLocal` but had empty/incomplete deps arrays — so they computed once at mount when the catalog was still `[]`, then never recomputed when the fetch resolved. Symptom: live site shows only the "All 87,639" source button and "All Skills 87,639" category — no per-source pills (ClawHub, skills.sh, LobeHub, etc.) and no category breakdown. Filtering by source/category is unusable. Fix: add `allSkillsLocal` to both deps arrays so they recompute when data arrives. Local build green on en + zh-Hans.
…ds-primitives refactor(web): consume DS primitives, remove local component copies
The Docker integration test job started failing on main after fb51253 ("docker: opt in to dashboard --insecure via env var"). Two distinct failures, both fallout from that change being more behaviour-changing than the existing test harness anticipated. Failure 1 — test_dashboard_port_override (silent regression in an already-existing test) The test starts the container with just HERMES_DASHBOARD=1, defaults to host=0.0.0.0, no HERMES_DASHBOARD_OAUTH_CLIENT_ID, no HERMES_DASHBOARD_INSECURE. Pre-fix that combination got --insecure auto-injected by the s6 run script (anything non-loopback was implicitly insecure), so the OAuth gate stayed off and start_server bound the port. Post-fix the gate engages, no provider is registered, and start_server raises SystemExit before binding — under s6 the dashboard goes into a restart loop and the test's /proc/net/tcp poll finds nothing. Same silent regression was masking three sibling tests (test_dashboard_slot_reports_up_when_enabled, test_dashboard_opt_in_starts, test_dashboard_restarts_after_crash) — they all only sample pgrep or s6-svstat and so caught the supervised process mid-restart loop, appearing to pass while the dashboard was actually never reaching a healthy state. Fix: pin HERMES_DASHBOARD_INSECURE=1 on every test that enables the dashboard but doesn't itself exercise the auth gate. Each pinned site carries an inline comment pointing back to test_dashboard_slot_reports_up_when_enabled for the full rationale. Failure 2 — test_dashboard_oauth_gate_engages_on_non_loopback_bind (bug in the test I added in fb51253) The probe used urllib.request.urlopen() against /api/status. Under the now-engaged OAuth gate /api/status no longer answers unauthenticated callers (the gate middleware runs upstream of the legacy _SESSION_TOKEN allowlist and 401s anything without a valid session cookie). urlopen() raises HTTPError on the 401, the wrapper treated that as "not ready yet", and the poll loop hit timeout. Fix: split the probe into a generic _http_probe() helper that returns (status_code, body) for any HTTP response — including 401, which IS the gate-engaged success signal. The helper feeds a multi-line Python program over stdin via a POSIX heredoc so the try/except branch reads naturally; far less fragile than the earlier semicolon-laden -c one-liner. The OAuth-gate test now verifies two independent observable consequences of the gate being on: 1. GET /api/auth/providers (publicly reachable through the gate so the login page can bootstrap) returns 200 with `nous` in the provider list — proves the bundled provider registered. 2. GET /api/status returns 401 — proves the OAuth gate runs upstream of the legacy public-paths allowlist and is actively intercepting unauthenticated callers. The insecure-opt-out test still hits /api/status, but now asserts status_code == 200 first (proves the gate is bypassed) before parsing the JSON for auth_required: false (proves the gate-state flag is also correctly off). Verified locally end-to-end against a fresh image build on a real Docker daemon: all 41 tests under tests/docker/ pass in 2m38s, including the two formerly-failing dashboard tests and the three sibling tests that were passing by accident.
NousResearch#34210) Kanban workers now scan the task body for local image paths and http(s) image URLs and attach them to the worker's first user turn — matching the CLI/gateway behaviour for inbound images. Before, a user pasting `/home/me/screenshot.png` or `https://example.com/img.png` into a kanban task description had it sent to the model as plain text and the pixels were never seen. How it works: * agent/image_routing.py gains extract_image_refs(text) → (paths, urls) that mirrors gateway/platforms/base.py:extract_local_files (absolute / ~-relative paths, image extensions only, ignores fenced/inline code). * build_native_content_parts() accepts an optional image_urls= kwarg and emits passthrough image_url parts for remote URLs alongside the base64 data: URLs used for local paths. * cli.py (single-query/quiet branch — the path every dispatcher-spawned worker takes) detects HERMES_KANBAN_TASK, reads the task body via kanban_db.get_task, runs extract_image_refs, and threads the results into the existing image-routing decision (native vs text). Best-effort: enrichment failures never block worker startup. Tested: * tests/agent/test_image_routing.py — 22 new tests for extract_image_refs and URL pass-through in build_native_content_parts. * tests/hermes_cli/test_kanban_worker_image_extraction.py — 10 new tests driving real kanban_db round-trip (create task → read body → extract refs → build parts). * E2E: created a fake kanban task with a body referencing both a local PNG and an https URL; verified the worker pipeline produces a multimodal user turn with 1 text part + 2 image_url parts (data URL for the local file, passthrough URL for the remote).
The v0.15.0 PyPI wheel shipped every plugin's Python code but none of its plugin.yaml manifests, so plugin discovery (hermes_cli/plugins.py) found zero plugins and ALL gateway platforms failed with "No adapter available for <platform>" (discord, slack, mattermost, ...). Same gap also dropped the web-search provider manifests (NousResearch#28149). Declare manifest coverage in both packaging channels: - wheel: [tool.setuptools.package-data] plugins += **/plugin.yaml, **/plugin.yml - sdist: MANIFEST.in recursive-include plugins plugin.yaml plugin.yml (Homebrew and other downstream packagers build from the sdist) Verified by building the wheel before/after: plugin.yaml count went 0 -> 69, discord's manifest now ships. Adds a regression test asserting both channels cover manifests. Fixes NousResearch#34034 Co-authored-by: outsourc-e <201563152+outsourc-e@users.noreply.github.com> Co-authored-by: Dhruvil Parikh <41384593+dparikh79@users.noreply.github.com> Co-authored-by: ousiaresearch <261687298+ousiaresearch@users.noreply.github.com> Co-authored-by: libre-7 <6366424+libre-7@users.noreply.github.com>
Brings the AmbulnzLLC fork up to upstream `v2026.5.29` (b9a9551), 617 commits ahead of the prior sync point (#8, merged 2026-05-22). All 18 fork-unique commits since the last sync are preserved. ## What landed from upstream - s6-overlay PID-1 supervision (`docker/s6-rc.d/`, `docker/main-wrapper.sh`, `docker/cont-init.d/`) replacing tini + the bash entrypoint. - Multi-stage Dockerfile with pinned `node:22-bookworm-slim` digest (drops apt `nodejs`/`npm`/`tini`). - New dashboard auth provider plumbing (`hermes_cli/dashboard_auth/`), Codex Responses transport, Bedrock-aware refactors, and substantial test additions (esp. `tests/docker/`, `tests/cli/`, dashboard auth tests). - Big i18n / docs sync (`website/i18n/zh-Hans/...`, locale yamls). ## Conflicts resolved - **`Dockerfile`** (3 hunks): - apt deps: union of fork's set with upstream's, dropped `tini` (replaced by s6-overlay /init), dropped apt `nodejs`/`npm` (replaced by the pinned multi-stage `node:22-bookworm-slim` copy upstream introduced). Kept `build-essential` and `libportaudio2` (needed for `--extra voice`). - `uv sync` extras: union all fork-required extras (`all messaging anthropic bedrock azure-identity web pty voice`), deduped `bedrock`. - chown comment block: took upstream's s6-overlay phrasing; the actual chown command is identical. - **`docker/entrypoint.sh`**: took upstream's 6-line shim that forwards to `docker/stage2-hook.sh`. The fork's full provisioning logic is now ported into `stage2-hook.sh` (see below) so it lives alongside upstream's s6 cont-init steps instead of fighting them. ## Fork-unique features ported into `docker/stage2-hook.sh` The five fork provisioning blocks that lived in the old bash entrypoint are now invoked by the s6 stage2 hook, in the correct order: 1. **SOUL.md immutability** — re-copies `docker/SOUL.md` to `$HERMES_HOME/SOUL.md` on every boot as `root:root 0444`. Upstream's `seed_one` only seeds on first boot, which doesn't satisfy the fork's "agent identity is immutable" contract — a compromised hermes-uid process could otherwise rewrite its own soul. 2. **GitHub App PEM install** — invokes `docker/install_github_app_pem.py` as root (it manages its own `os.chown` to `hermes:hermes 0400`). Reads `GITHUB_APP_PEM_SECRET_ID` and pulls the PEM from AWS Secrets Manager. Silent no-op when env unset. 3. **Bedrock guardrail seed** — invokes `docker/seed_admin_config.py` as `hermes` to merge `BEDROCK_GUARDRAIL_*` env into `config.yaml`. Runs after upstream's `seed_one "config.yaml" ...`. 4. **Default taps seed** — invokes `docker/seed_taps.py` as `hermes` so tap-relative skill identifiers resolve before the next step. 5. **Default skills seed** — invokes `docker/seed_skills.py` as `hermes`, strictly *after* upstream's `tools/skills_sync.py` (so the bundled tree exists) and after taps + PEM are in place (private-repo network auth). Each of the four scripts is environment-driven and idempotent — silent no-op when its trigger env var is unset, so non-Ambulnz deploys are unaffected. ## Known issues (not introduced by this merge) - `tests/scripts/test_seed_skills.py::test_main_installs_each_entry` fails on `origin/main` already — `docker/seed_skills.py` passes `force=True` while the test asserts `force=False`. Needs a separate decision (does the admin path want to bypass scans? if yes, fix the test; if no, fix the code) and a follow-up PR.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
142 |
unresolved-attribute |
96 |
invalid-argument-type |
84 |
invalid-assignment |
28 |
unsupported-operator |
22 |
invalid-method-override |
18 |
not-subscriptable |
6 |
unresolved-reference |
5 |
unresolved-global |
5 |
no-matching-overload |
3 |
invalid-parameter-default |
1 |
unused-type-ignore-comment |
1 |
invalid-return-type |
1 |
not-iterable |
1 |
First entries
tests/hermes_cli/test_kanban_promote.py:114: [unresolved-attribute] unresolved-attribute: Attribute `assignee` is not defined on `None` in union `Task | None`
tests/run_agent/test_partial_stream_finish_reason.py:200: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_cached_system_prompt` on type `AIAgent`
tests/cron/test_cronjob_schema.py:18: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["schedule"]` and `Unknown | str | dict[str, str] | ... omitted 3 union elements`
tests/agent/test_context_engine_host_contract.py:31: [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/agent/test_transcription_registry.py:20: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_dashboard_auth_prefix.py:33: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_tirith_security.py:775: [invalid-argument-type] invalid-argument-type: Argument to function `islink` is incorrect: Expected `int | str | bytes | PathLike[str] | PathLike[bytes]`, found `str | None`
plugins/platforms/mattermost/adapter.py:867: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `list[str]`, found `(list[str] & ~AlwaysFalsy) | None`
tests/hermes_cli/test_cmd_update_docker.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_discord_component_auth.py:24: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `SlashConfirmView`
plugins/platforms/discord/adapter.py:2722: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
hermes_cli/dashboard_auth/cookies.py:55: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.responses`
tests/hermes_cli/test_dashboard_auth_middleware.py:25: [unresolved-import] unresolved-import: Cannot resolve imported module `fastapi.testclient`
tests/agent/test_tts_registry.py:282: [invalid-argument-type] invalid-argument-type: Argument to function `resolve_output_format` is incorrect: Expected `str | None`, found `Literal[123]`
tests/hermes_cli/test_project_plugin_rce_bypass.py:39: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cli/test_cli_yolo_toggle.py:166: [invalid-argument-type] invalid-argument-type: Argument to function `HermesCLI._toggle_yolo` is incorrect: Expected `HermesCLI`, found `SimpleNamespace`
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/gateway/test_subagent_protection_30170.py:35: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/plugins/dashboard_auth/test_nous_provider.py:29: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_active_session_text_merge.py:283: [invalid-assignment] invalid-assignment: Object of type `def _fake_start(event, session_key, *, interrupt_event=None) -> Unknown` is not assignable to attribute `_start_session_processing` of type `def _start_session_processing(self, event: MessageEvent, session_key: str, *, interrupt_event: Event | None = None) -> bool`
tests/run_agent/test_fallback_credential_isolation.py:20: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_nous_inference_url_validation.py:97: [invalid-argument-type] invalid-argument-type: Argument to function `_validate_nous_inference_url_from_network` is incorrect: Expected `str | None`, found `Literal[12345]`
tests/agent/test_codex_ttfb_watchdog.py:29: [no-matching-overload] no-matching-overload: No overload of bound method `MutableMapping.setdefault` matches arguments
tests/run_agent/test_codex_no_tools_nonetype.py:176: [unresolved-import] unresolved-import: Cannot resolve imported module `openai.resources.responses.responses`
... and 388 more
✅ Fixed issues (169):
| Rule | Count |
|---|---|
unresolved-attribute |
58 |
invalid-argument-type |
45 |
invalid-assignment |
19 |
unresolved-import |
18 |
invalid-method-override |
7 |
unresolved-reference |
5 |
unsupported-operator |
5 |
unresolved-global |
5 |
invalid-type-form |
1 |
invalid-return-type |
1 |
not-iterable |
1 |
unused-type-ignore-comment |
1 |
call-non-callable |
1 |
call-top-callable |
1 |
invalid-parameter-default |
1 |
First entries
tests/tools/test_vercel_sandbox_environment.py:203: [unresolved-attribute] unresolved-attribute: Unresolved attribute `Sandbox` on type `ModuleType`
tools/xai_http.py:71: [invalid-assignment] invalid-assignment: Object of type `Literal["unknown"]` is not assignable to `Literal["0.14.0"]`
gateway/platforms/discord.py:5506: [unresolved-attribute] unresolved-attribute: Attribute `Embed` is not defined on `None` in union `Unknown | None`
cli.py:9843: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent` has no attribute `tools`
cli.py:9529: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent & ~AlwaysFalsy` has no attribute `model`
cli.py:9306: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent & ~AlwaysFalsy` has no attribute `compression_enabled`
agent/codex_runtime.py:178: [invalid-type-form] invalid-type-form: Function `callable` is not valid in a parameter annotation: Did you mean `collections.abc.Callable`?
gateway/platforms/discord.py:4213: [unresolved-reference] unresolved-reference: Name `UpdatePromptView` used when not defined
cli.py:9481: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent & ~AlwaysFalsy` has no attribute `context_compressor`
gateway/platforms/discord.py:2731: [unresolved-attribute] unresolved-attribute: Attribute `http` is not defined on `None` in union `Unknown | None`
tests/gateway/test_discord_model_picker.py:14: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `ModelPickerView`
gateway/platforms/discord.py:340: [unresolved-import] unresolved-import: Cannot resolve imported module `nacl.secret`
tools/environments/vercel_sandbox.py:300: [unresolved-import] unresolved-import: Cannot resolve imported module `vercel.sandbox`
tests/tools/test_vercel_sandbox_environment.py:206: [unresolved-attribute] unresolved-attribute: Unresolved attribute `SandboxStatus` on type `ModuleType`
gateway/platforms/discord.py:5561: [unresolved-attribute] unresolved-attribute: Attribute `ButtonStyle` is not defined on `None` in union `Unknown | None`
tests/run_agent/test_plugin_context_engine_init.py:62: [invalid-assignment] invalid-assignment: Object of type `MagicMock` is not assignable to attribute `update_model` of type `def update_model(self, model: str, context_length: int, base_url: str = "", api_key: str = "", provider: str = "") -> None`
cli.py:7885: [invalid-argument-type] invalid-argument-type: Argument to function `get_tool_definitions` is incorrect: Expected `list[str]`, found `list[str] | set[str]`
cli.py:9369: [invalid-argument-type] invalid-argument-type: Argument to bound method `AIAgent._flush_messages_to_session_db` is incorrect: Expected `list[dict[Unknown, Unknown]]`, found `None`
cli.py:12104: [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]`
tests/run_agent/test_plugin_context_engine_init.py:85: [unresolved-attribute] unresolved-attribute: Object of type `bound method _StubEngine.update_model(model: str, context_length: int, base_url: str = "", api_key: str = "", provider: str = "") -> None` has no attribute `assert_called_once`
cli.py:4586: [invalid-argument-type] invalid-argument-type: Argument to `AIAgent.__init__` is incorrect: Expected `list[str] | None`, found `Unknown | None | str | list[str | Unknown]`
cli.py:6885: [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)`
cli.py:11293: [unresolved-attribute] unresolved-attribute: Object of type `AIAgent | None` has no attribute `_active_children`
gateway/platforms/discord.py:4543: [unresolved-attribute] unresolved-attribute: Attribute `user` is not defined on `None` in union `Unknown | None`
tests/tools/test_vercel_sandbox_environment.py:121: [invalid-return-type] invalid-return-type: Return type does not match returned value: expected `str`, found `object`
... and 144 more
Unchanged: 4674 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
… fork Dot release on top of the v2026.5.29 sync. Two upstream commits: 827f7f0 fix(packaging): ship bundled plugin.yaml manifests in wheel and sdist 77a1650 chore: bump version to v0.15.2 (2026.5.29.2) Surface area: MANIFEST.in, acp_registry/agent.json, hermes_cli/__init__.py, pyproject.toml, tests/test_packaging_metadata.py. No fork-divergent paths touched (Bedrock adapter, Teams plugin, gateway/platforms/base.py, docker/entrypoint.sh untouched).
Updated: merged upstream v2026.5.29.2Pulled in the upstream dot release (v2026.5.29.2) on top of the existing v2026.5.29 sync. Tiny delta — just two commits:
Surface area:
Fork survival check — none of the divergent paths were touched:
A live fork-survival smoke test against build #10 (pre-dot-release) passed all 5 checks earlier today; this update is additive and shouldn't regress anything. |
Summary
Brings the AmbulnzLLC fork up to upstream
NousResearch/hermes-agenttag v2026.5.29 (b9a9551b). Last sync was #8 (merged 2026-05-22, 617 commits behind). All 18 fork-unique commits since are preserved.Notable upstream changes pulled in
tini+ bash-entrypoint setup (docker/s6-rc.d/,docker/main-wrapper.sh,docker/cont-init.d/).node:22-bookworm-slimdigest — aptnodejs/npm/tinidropped.hermes_cli/dashboard_auth/), Codex Responses transport, Bedrock-aware refactors.tests/docker/,tests/cli/, dashboard auth).Conflicts resolved
Dockerfile— apt deps unioned (keptbuild-essential,libportaudio2; droppedtini/nodejs/npmper upstream's s6 + multi-stage node);uv syncextras unioned (all messaging anthropic bedrock azure-identity web pty voice); chown comment took upstream phrasing.docker/entrypoint.sh— accepted upstream's 6-line shim; the fork's bootstrap logic is now hosted bydocker/stage2-hook.sh(the upstream-sanctioned extension point) instead of fighting upstream's s6 cont-init.Fork-unique features ported into
docker/stage2-hook.shThe five fork provisioning blocks from the old bash entrypoint are now invoked by the s6 stage2 hook, in correct order:
docker/SOUL.mdto$HERMES_HOME/SOUL.mdon every boot asroot:root 0444. Upstream'sseed_oneonly seeds on first boot, which doesn't satisfy the "agent identity is immutable" contract; a compromised hermes-uid process could otherwise rewrite its own soul.docker/install_github_app_pem.pyas root (script owns itschown hermes:hermes 0400); pulls PEM from AWS Secrets Manager viaGITHUB_APP_PEM_SECRET_ID.docker/seed_admin_config.pyashermesafter upstream'sseed_one "config.yaml".docker/seed_taps.pyashermesso tap-relative skill IDs resolve before step 5.docker/seed_skills.pyashermes, strictly after upstream'stools/skills_sync.py(bundled tree exists) and after taps + PEM (private-repo network auth).Each script is env-driven and idempotent — silent no-op when its trigger env is unset, so non-Ambulnz deploys are unaffected.
Verification
git diff origin/main...HEAD --statclean; both parents present (origin/main+v2026.5.29).sh -nclean ondocker/entrypoint.shanddocker/stage2-hook.sh.tests/scripts/test_install_github_app_pem.py,test_seed_admin_config.py,test_seed_taps.py(65/66).Known issue (not introduced by this merge)
tests/scripts/test_seed_skills.py::test_main_installs_each_entryfails onorigin/mainalready —docker/seed_skills.pypassesforce=Truewhile the test assertsforce=False. Needs a separate decision (does admin path bypass scans?) and a follow-up. Predates this PR.Reviewer notes
Per vigo-agent[bot] policy, this PR is automated. Please check:
stage2-hook.shblocks (lines around the diff) match expected behavior.Dockerfileapt set still has everything our deploys need — flag if any package was wrongly dropped from the union.