Skip to content

Merge upstream v2026.5.29 into main - #10

Merged
hawknewton merged 621 commits into
mainfrom
sync/upstream-v2026.5.29
Jun 2, 2026
Merged

Merge upstream v2026.5.29 into main#10
hawknewton merged 621 commits into
mainfrom
sync/upstream-v2026.5.29

Conversation

@vigo-agent

@vigo-agent vigo-agent Bot commented May 29, 2026

Copy link
Copy Markdown

Summary

Brings the AmbulnzLLC fork up to upstream NousResearch/hermes-agent tag 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

  • s6-overlay PID-1 supervision replaces the old tini + bash-entrypoint setup (docker/s6-rc.d/, docker/main-wrapper.sh, docker/cont-init.d/).
  • Multi-stage Dockerfile with pinned node:22-bookworm-slim digest — apt nodejs/npm/tini dropped.
  • New dashboard auth provider plumbing (hermes_cli/dashboard_auth/), Codex Responses transport, Bedrock-aware refactors.
  • Substantial test additions (tests/docker/, tests/cli/, dashboard auth).
  • Big i18n / docs sync.

Conflicts resolved

  • Dockerfile — apt deps unioned (kept build-essential, libportaudio2; dropped tini/nodejs/npm per upstream's s6 + multi-stage node); uv sync extras 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 by docker/stage2-hook.sh (the upstream-sanctioned extension point) instead of fighting upstream's s6 cont-init.

Fork-unique features ported into docker/stage2-hook.sh

The five fork provisioning blocks from the old bash entrypoint are now invoked by the s6 stage2 hook, in 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 "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 (script owns its chown hermes:hermes 0400); pulls PEM from AWS Secrets Manager via GITHUB_APP_PEM_SECRET_ID.
  3. Bedrock guardrail seed — invokes docker/seed_admin_config.py as hermes after upstream's seed_one "config.yaml".
  4. Default taps seed — invokes docker/seed_taps.py as hermes so tap-relative skill IDs resolve before step 5.
  5. Default skills seed — invokes docker/seed_skills.py as hermes, strictly after upstream's tools/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 --stat clean; both parents present (origin/main + v2026.5.29).
  • No unmerged paths; sh -n clean on docker/entrypoint.sh and docker/stage2-hook.sh.
  • Local fork-specific tests pass: 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_entry fails on origin/main already — docker/seed_skills.py passes force=True while the test asserts force=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:

  1. The five ported stage2-hook.sh blocks (lines around the diff) match expected behavior.
  2. Dockerfile apt set still has everything our deploys need — flag if any package was wrongly dropped from the union.
  3. CI on this branch (full deps via uv) is the source of truth for the broader test suite.

MorAlekss and others added 30 commits May 25, 2026 18:33
…, 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>
teknium1 and others added 22 commits May 28, 2026 12:26
…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.
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

🔎 Lint report: sync/upstream-v2026.5.29 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9660 on HEAD, 9157 on base (🆕 +503)

🆕 New issues (413):

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-inc-0c327c0c81

wiz-inc-0c327c0c81 Bot commented May 29, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations 1 Low
SAST Finding SAST Findings 10 Medium 3 Low 1 Info
Software Management Finding Software Management Findings -
Total 10 Medium 4 Low 1 Info

View scan details in Wiz

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).
@vigo-agent

vigo-agent Bot commented Jun 2, 2026

Copy link
Copy Markdown
Author

Updated: merged upstream v2026.5.29.2

Pulled in the upstream dot release (v2026.5.29.2) on top of the existing v2026.5.29 sync. Tiny delta — just two commits:

  • 827f7f078 fix(packaging): ship bundled plugin.yaml manifests in wheel and sdist
  • 77a1650c7 chore: bump version to v0.15.2 (2026.5.29.2)

Surface area:

  • MANIFEST.in — new
  • acp_registry/agent.json
  • hermes_cli/__init__.py
  • pyproject.toml (version 0.15.1 → 0.15.2; auto-merged cleanly)
  • tests/test_packaging_metadata.py — new

Fork survival check — none of the divergent paths were touched:

  • agent/bedrock_adapter.py (Bedrock + lazy_deps)
  • plugins/platforms/teams/adapter.py (FileConsent + Graph inbound)
  • gateway/platforms/base.py (cache_video_from_url)
  • docker/entrypoint.sh (SOUL.md immutability)

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.

@hawknewton
hawknewton merged commit 798304a into main Jun 2, 2026
20 of 24 checks passed
@hawknewton
hawknewton deleted the sync/upstream-v2026.5.29 branch June 2, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.