fix: rebase kanban review controls on current upstream#4
Merged
Conversation
…nt (salvage NousResearch#10144) (NousResearch#33035) * fix(codex-responses): gracefully recover from invalid_encrypted_content (salvage NousResearch#10144) When an OpenAI-compatible Responses API surface accepts an initial request but later rejects the replayed `codex_reasoning_items` encrypted blob with HTTP 400 `invalid_encrypted_content`, the session previously got stuck retrying the same poisoned payload. Recovery: classify the error as a dedicated FailoverReason, and on the first hit disable encrypted reasoning replay for the rest of the session, strip cached items from message history, and retry once. Changes: * error_classifier: add FailoverReason.invalid_encrypted_content branch in _classify_400 (before context_overflow so the messages that mention 'encrypted content … could not be verified' don't trip context heuristics), in _classify_by_error_code, and extend _extract_error_code to peek inside wrapped JSON in error.message and ignore the bare '400' as a code. * agent_init: initialize `_codex_reasoning_replay_enabled = True` on every agent. * run_agent: add AIAgent._disable_codex_reasoning_replay() helper that flips the flag and pops cached items. * codex_responses_adapter: thread a `replay_encrypted_reasoning` kwarg through _chat_messages_to_responses_input so that when the flag is False we don't replay codex_reasoning_items. * transports/codex.py: read `replay_encrypted_reasoning` from params, thread it into the adapter, and gate the `include=['reasoning.encrypted_content']` request hint on it. * chat_completion_helpers: pass the agent's replay flag through to the transport. * conversation_loop: in the retry loop, add an invalid_encrypted_content recovery branch that fires once per session, only when api_mode == codex_responses, only when replay is still enabled, and only when at least one assistant message in history actually carries cached reasoning items (otherwise the 400 has nothing to do with our cache and the normal retry path handles it). Tests: * test_error_classifier: new wrapped-JSON _extract_error_code case; new TestClassifyApiError cases proving the 400 is retryable with no fallback, that the broad message match doesn't catch a generic 'parsed' message, and that the error code match is case-insensitive. * test_run_agent_codex_responses: end-to-end test of the recovery branch firing once and disabling replay, plus a sibling test that proves the branch does *not* fire (and the flag stays True) when history has no cached reasoning items. Salvages PR NousResearch#10144 onto the post-refactor module layout (error_classifier / codex_responses_adapter / transports/codex / conversation_loop / agent_init) since the original diff was written against the pre-refactor monolithic run_agent.py. * chore(release): map victorGPT in AUTHOR_MAP for NousResearch#10144 salvage --------- Co-authored-by: victorGPT <[email protected]>
…_HOME (NousResearch#19795) Replaces the recursive chown of $HERMES_HOME in stage2-hook.sh with a targeted approach: chown the top-level dir (so hermes can create new subdirs) plus the specific hermes-owned subdirectories (cron/, sessions/, logs/, hooks/, memories/, skills/, skins/, plans/, workspace/, home/, profiles/) — the same canonical list seeded by the s6-setuidgid mkdir -p block below. Avoids clobbering host-side file ownership when $HERMES_HOME is a bind mount that contains user-owned files not managed by hermes (issue NousResearch#19788). Original fix targeted docker/entrypoint.sh which is now a deprecated shim; retargeted to docker/stage2-hook.sh where the recursive chown moved during the s6-overlay rework. Co-authored-by: Ptichalouf <[email protected]>
…d works (NousResearch#28851) When HERMES_UID remaps the hermes user from 10000 to another UID (e.g. matching the host user's UID for bind-mount ergonomics), the TUI launcher's esbuild step fails: ✘ [ERROR] Failed to write to output file: open /opt/hermes/ui-tui/dist/entry.js: permission denied TUI build failed. This is because the Dockerfile's build-time `chown -R hermes:hermes` on `/opt/hermes/{.venv,ui-tui,node_modules}` (line 154) wrote UID 10000, and stage2-hook.sh only re-chowned `.venv` on UID remap — leaving the TUI build trees still owned by the old UID. Extend the stage2 re-chown to include the same set as the build-time chown: `.venv`, `ui-tui`, `node_modules`. These are the runtime-writable trees under $INSTALL_DIR; everything else under /opt/hermes is read-only at runtime so keeping it root-owned is fine. Original fix targeted docker/entrypoint.sh which is now a deprecated shim; retargeted to docker/stage2-hook.sh where the .venv chown moved during the s6-overlay rework. Co-authored-by: Andreas Steffan <[email protected]>
…kworm-slim (NousResearch#4977) Debian trixie's bundled `nodejs` package is pinned to 20.19.2, which reached LTS EOL in April 2026. Trixie won't upgrade in place; Debian 14 (forky) — where the apt nodejs is 24.x — isn't released until ~mid-2027. To stay on a supported LTS without waiting for Debian 14, copy node + npm + corepack from the upstream `node:22-bookworm-slim` image as a multi-stage source, matching the existing `uv_source` and `gosu_source` patterns in the Dockerfile. Bookworm-based slim image is used so the produced binary links against glibc 2.36, which runs cleanly on Debian 13 (trixie, glibc 2.41). Changes: - Add `FROM node:22-bookworm-slim@sha256:... AS node_source` stage - Remove `nodejs npm` from `apt-get install` (now sourced from node_source) - Add `ca-certificates` explicitly to apt install (was a transitive of the apt nodejs package; removing nodejs broke the chain and curl inside the build failed with "error setting certificate file") - COPY node binary + npm + corepack from node_source; recreate the symlinks at /usr/local/bin/{npm,npx,corepack} - Update the npm_config_install_links=false comment block — npm 10's default is already `install-links=false`, but we keep the env as defense-in-depth against future Node-source-version regressions Future bumps to Node 24/26 are a one-line ARG change. Validation: - Built --no-cache against current origin/main; build succeeds in 1m42s - Image size: 3.27 GB (pre-salvage-1 baseline) → 3.14 GB (this PR); net 130 MiB savings (60 MiB from this change alone vs current main — removing apt nodejs+transitive deps that duplicated what node bundles) - Node 22.22.3 / npm 10.9.8 / esbuild 0.27.7 all run cleanly under trixie's glibc 2.41 - Standard image smoke (6/6), Node-version E2E (8/8), chown E2E from NousResearch#19788 (6/6), TUI UID-remap E2E from NousResearch#28851 (4/4) — 24 checks total Co-authored-by: Prithvi Monangi <[email protected]>
shellcheck doesn't recognize the s6-overlay `#!/command/with-contenv sh`
shebang and aborts with SC1008 ("This shebang was unrecognized. ShellCheck
only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to
specify."). The error fires at --severity=error too, so it fails the
"Docker / shell lint" CI job on every PR that touches docker/.
Add the canonical `# shellcheck shell=sh` directive — same fix already
applied to the sibling cont-init.d scripts (`02-reconcile-profiles` and
`015-supervise-perms`) when they adopted the with-contenv shebang.
The shebang was changed from `#!/bin/sh` → `#!/command/with-contenv sh`
in PR NousResearch#32412 (commit 29c71e9) to fix env-propagation through s6's PID 1.
The shellcheck-directive line was missed in that PR; this patches it.
Reproduces locally:
docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:stable \
--severity=error --format=gcc docker/main-wrapper.sh
Before: docker/main-wrapper.sh:1:1: error: [SC1008] (rc=1)
After: (no output) (rc=0)
Script behavior is unchanged — the directive is a comment, and `sh -n`
/ `bash -n` parse the file cleanly either way.
…lege drop (NousResearch#18488) When HERMES_HOME points at a custom path whose parent directories only root can create (e.g. HERMES_HOME=/home/hermes/.hermes in a Compose file, or any path under a fresh / not pre-populated by the image), stage2-hook.sh fails on first boot: [stage2] Warning: chown failed (rootless container?) - continuing mkdir: cannot create directory '/custom': Permission denied mkdir: cannot create directory '/custom': Permission denied ... (one per s6-setuidgid hermes mkdir invocation) cont-init: info: /etc/cont-init.d/01-hermes-setup exited 1 The mkdirs fail because s6-setuidgid drops to hermes (UID 10000) before invoking mkdir -p, and the runtime user has no permission to create root-owned ancestor directories. 02-reconcile-profiles then crashes with FileNotFoundError, .install_method never lands, and the container limps on in a half-initialized state. Bootstrap HERMES_HOME with mkdir -p while still root, before the ownership normalization. Idempotent on the default /opt/data path (directory already exists from the Dockerfile RUN mkdir -p) and on any subsequent restart. (NousResearch#18482) Retargeted from the original PR's docker/entrypoint.sh (now a deprecated shim) to docker/stage2-hook.sh where the related chown logic moved during the s6-overlay rework. Co-authored-by: wpengpeng168 <[email protected]>
…irectly (NousResearch#33042) * refactor(codex): drop SDK responses.stream() helper; consume events directly The OpenAI Python SDK's high-level `client.responses.stream(...)` helper does post-hoc typed reconstruction from the terminal `response.completed.response.output` field. The chatgpt.com Codex backend has been observed (today, gpt-5.5) to ship `response.output = null` on terminal frames, which crashes the SDK with `TypeError: 'NoneType' object is not iterable` mid-iteration. Carlton's NousResearch#32963 patched the symptom by wrapping the helper in try/except and recovering from the same per-event accumulator the SDK was supposed to populate. This PR removes the helper from the call path entirely: we now use `client.responses.create(stream=True)` (raw AsyncIterable of SSE events) and assemble the final response object ourselves from `response.output_item.done` events as they arrive. The terminal event's `output` field is never read for content. Same strategy OpenClaw uses for the same backend. This makes Hermes structurally immune to the bug class, not patched. The next time OpenAI ships a shape change to chatgpt.com's terminal frame, our consumer keeps working because it doesn't read that frame for content — only for usage/status/id. Changes - `agent/codex_runtime.py`: new `_consume_codex_event_stream()` shared consumer; `run_codex_stream()` uses `responses.create(stream=True)`; `run_codex_create_stream_fallback()` collapses into a thin alias since the primary path now does what the fallback used to do. - `agent/auxiliary_client.py`: `_CodexCompletionsAdapter` uses the same consumer; old null-output recovery helpers deleted as unreferenced. - Tests migrated: fixtures that mocked `responses.stream` now mock `responses.create` returning a raw iterable. New regression test asserts the auxiliary path returns streamed items even when the terminal event's `output` is literally `null`. Validation - Live: tested against fresh OAuth on `chatgpt.com/backend-api/codex` with `gpt-5.5` — response built correctly with `response.output=null` on the terminal frame, all events consumed, usage/reasoning tokens propagated. - `tests/run_agent/test_run_agent_codex_responses.py` + `tests/agent/test_auxiliary_client.py`: 242 passed. * test+fix(codex): migrate streaming tests, raise on truncated streams CI surfaced 10 test failures across tests/run_agent/test_streaming.py and tests/run_agent/test_codex_xai_oauth_recovery.py — both files had their own `responses.stream(...)` mocks I missed in the first sweep. agent/codex_runtime.py: _consume_codex_event_stream() now raises "Codex Responses stream did not emit a terminal response" when the stream ends without any terminal frame AND no usable content. This preserves the signal callers used to get from the SDK's high-level helper, which they distinguished from "completed with empty body" in error handling. Tests migrated: - test_streaming.py: text-delta callback, activity-touch, and remote-protocol-error tests all switch from mocking responses.stream to responses.create returning an iterable of events. - test_codex_xai_oauth_recovery.py: prelude-error tests are recast as wire-error-event tests (the new path raises _StreamErrorEvent directly when the wire emits type=error, which is strictly better than the old two-phase "SDK RuntimeError → retry → fallback"). The retry-on-transport-error test moves from responses.stream side-effect to responses.create side-effect. Verified live against chatgpt.com Codex with gpt-5.5 — AIAgent.chat() through the full codex_responses path returns correctly, 319/319 targeted tests passing.
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend Both Vercel-hosted integrations are removed end-to-end. Users on the AI Gateway should switch to OpenRouter or one of the other aggregators (Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should switch to Docker, Modal, Daytona, or SSH. What's removed: - `plugins/model-providers/ai-gateway/` provider plugin - `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper - `tools/environments/vercel_sandbox.py` terminal backend - `ai-gateway` provider wiring across auth, doctor, setup, models, config, status, providers, main, web_server, model_normalize, dump - `vercel_sandbox` backend wiring across terminal_tool, file_tools, code_execution_tool, file_operations, approval, skills_tool, environments/local, credential_files, lazy_deps, prompt_builder, cli, gateway/run - `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client header set, run_agent base-URL header/reasoning special-cases - `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock - env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`, `TERMINAL_VERCEL_RUNTIME` - Tests: deletes test_ai_gateway_models.py and test_vercel_sandbox_environment.py; scrubs references across 23 surviving test files (no entire tests deleted unless they were dedicated to AI Gateway / Sandbox) - Docs: provider tables, env-var reference, setup guides, security notes, tool config, terminal-backend tables — English plus zh-Hans i18n parity - `hermes-agent` skill: provider table entry and remote-backend list What stays (intentional): - `popular-web-designs/templates/vercel.md` — CSS design reference, unrelated to Vercel-the-AI-product - `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN response header, useful diag signal on any Vercel-hosted endpoint - `vercel-labs/agent-browser` URL in browser config — lightpanda browser project, different OSS effort - `userStories.json` historical contributor entry mentioning Vercel Sandbox — archive, not active docs Validation: - 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`) - Full repo `py_compile` clean - Live import of every touched module + invariant check (no `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`) * test: convert profile-count check from change-detector to invariant The hardcoded "== 34" assertion broke when ai-gateway was removed. Per AGENTS.md change-detector-test guidance, assert the relationship (registry count >= number of plugin dirs) instead of a literal count. Counts shift when providers are added/removed; that's expected.
…3016) Lets external clients enumerate the agent's skills and resolved toolsets deterministically over the OpenAI-compatible API server, without standing up the dashboard web server or sending a chat message and asking the model to list them. - GET /v1/skills — list installed skills (name, description, category) - GET /v1/toolsets — list toolsets resolved for the api_server platform, with enabled/configured state and the concrete tool names each expands to - Both gated by API_SERVER_KEY (same Bearer scheme as every other /v1/* endpoint) - /v1/capabilities advertises both new endpoints Closes the gap a community user just hit asking how to list skills over REST when only the OpenAI-compatible server is running. Test plan - python -m pytest tests/gateway/test_api_server.py -k "Skills or Toolsets or Capabilities" -o 'addopts=' -q → 9/9 pass - python -m pytest tests/gateway/test_api_server.py -o 'addopts=' -q → 156/156 pass, no regressions - E2E: started a real adapter on an isolated HERMES_HOME with a fake skill installed; curl-equivalent calls to /v1/capabilities, /v1/skills, /v1/toolsets returned the expected JSON; unauthenticated calls returned 401 with the configured API_SERVER_KEY.
) * fix(plugins/discord): correct install_hint extra to [messaging] The Discord platform registered install_hint pointing at 'hermes-agent[discord]', but pyproject.toml has no [discord] extra — the deps live in [messaging] alongside Telegram and Slack. Users hitting "Platform 'Discord' requirements not met" were directed at a pip command that installs nothing. * feat(nix): add #messaging and #full package variants Make Discord/Telegram/Slack work out of the box for `nix profile install` users. Messaging deps were dropped from [all] on 2026-05-12 in favor of lazy-install, but lazy-install can't write to the read-only /nix/store — users hit "No adapter available for discord" with no actionable guidance. - #messaging: pre-built with discord.py/telegram/slack (+33 MB venv) - #full: all 18 platform-portable extras + matrix on Linux only (python-olm lacks Darwin PyPI wheels) (+738 MB venv) Also adds a `messaging-variant` flake check that verifies `import discord` succeeds in the sealed venv — regression guard for the lazy-install migration. Docs updated: Quick Start callout, extraDependencyGroups rewrite with messaging as primary example + full extras table, troubleshooting row, cheatsheet row. Closure size deltas (measured x86_64-linux): default 1792 MB pkg / 512 MB venv messaging 1826 MB pkg / 546 MB venv (+33 MB) full 2530 MB pkg / 1250 MB venv (+738 MB) * chore(nix): trim variant comments + alphabetize full extras Drop the date-stamped changelog from messaging-variant's comment and the "+33 MB / +704 MB" numbers from the variant defs — those drift and belong in the PR description, not source. Alphabetize the 18-extra list in #full so future additions produce clean one-line diffs. No semantic change. messaging-variant check still passes.
…s shipped NousResearch#33016 added GET /v1/skills + /v1/toolsets on the API server; the capability flag introduced in this branch was placeholder-False. Flip to True so capability probers see the truth.
… lists Alibaba's latest flagship Qwen model is released but not yet present in the DashScope (alibaba) or Alibaba Coding Plan curated catalogs. Add it so it shows up in the /model picker and setup wizard for those providers. OpenCode Go routing for qwen3.7-max already landed via NousResearch#32780 (commit 2fc77c5). OpenRouter + Nous catalog entries already landed via NousResearch#32809 (commit ccd3d04). This salvage picks up the remaining alibaba / alibaba-coding-plan entries from NousResearch#32806 — the AI Gateway entry is dropped because Vercel AI Gateway was removed in NousResearch#33067.
…s code writes (NousResearch#33131) New opt-in plugin that scans the content passed to write_file / patch / skill_manage for 25 known-dangerous code patterns — pickle.load, yaml.load, eval(, os.system, subprocess(shell=True), child_process.exec, dangerouslySetInnerHTML, innerHTML/outerHTML/document.write/ insertAdjacentHTML, crypto.createCipher (no IV), AES ECB, TLS verification disabled, XXE-prone xml.etree/minidom parsers, <script src=//...> without SRI, torch.load without weights_only=True, GitHub Actions ${{ github.event.* }} injection — and appends a "Security guidance" warning block to the tool result via the transform_tool_result hook. Default behaviour is non-blocking: the file is written and the warning rides back to the model in the next turn so it can self-correct or document why the construct is safe. SECURITY_GUIDANCE_BLOCK=1 upgrades to refusing the write entirely; SECURITY_GUIDANCE_DISABLE=1 is the kill switch. Pattern data (patterns.py) is a verbatim Apache-2.0 fork of Anthropic's claude-plugins-official/plugins/security-guidance/hooks/ patterns.py at commit 0bde168 (2026-05-26). LICENSE and NOTICE preserve attribution. The Hermes-side plugin glue (__init__.py, plugin.yaml, README.md, tests) is original work. Plugin is opt-in like all bundled plugins: hermes plugins enable security-guidance Inspired by https://x.com/ClaudeDevs/status/1927108527247... — Anthropic shipped this as their security-guidance plugin for Claude Code on 2026-05-26 with a measured 30-40% reduction in security-related PR comments on internal rollout. What's NOT ported (deferred): * Layer 2 (LLM diff review on turn end) — would route through main model by default on Hermes, real money on reasoning models. A follow-up can wire it to a cheap aux model with explicit opt-in. * Layer 3 (agentic commit-time review) — agent can run this on demand via delegate_task today. * .hermes/security-guidance.md project-rules file — only used by layers 2/3 upstream.
…ness Phase 0, Task 0.1 of the dashboard-oauth plan. Establishes a baseline for the loopback dashboard's auth surface so future phases can prove they didn't regress the existing _SESSION_TOKEN flow when adding the OAuth gate.
Phase 0, Task 0.2. Single source of truth for 'is the auth gate active?'. Reuses the existing _LOOPBACK_HOST_VALUES frozenset so this stays in sync with the DNS-rebinding host-header check. RFC1918/CGNAT/link-local are treated as public — exact threat model the gate exists for.
Phase 0, Task 0.3. start_server now computes should_require_auth(host, allow_public) and records it on app.state.auth_required BEFORE the existing legacy SystemExit guard fires. This gives middleware, the SPA token-injection path, and WS endpoints a consistent read source for 'is the gate active'. The flag is set but no one reads it yet — Phase 3 registers the gate middleware. Note: 4 pre-existing test failures in tests/hermes_cli/test_web_server.py (PtyWebSocket) + test_update_hangup_protection.py reproduce on pristine HEAD and are unrelated to this change (starlette TestClient WS regression).
…class
Phase 1, Task 1.1. New package hermes_cli/dashboard_auth/ contains:
base.py - DashboardAuthProvider ABC with 5 abstract methods
(start_login, complete_login, verify_session,
refresh_session, revoke_session), Session + LoginStart
frozen dataclasses, three exception types
(ProviderError / InvalidCodeError / RefreshExpiredError),
and assert_protocol_compliance() for plugins to call
in their own tests.
registry.py - Module-level register/get/list/clear with a lock.
Nothing reads the registry yet — Phase 2 adds the StubAuthProvider and
Phase 3 wires the gate middleware. The plugin hook lands in Task 1.3.
Phase 1, Task 1.2. Verifies registration order is preserved, duplicate names are rejected with ValueError, and non-compliant providers fail at register time (not later when the middleware tries to dispatch).
…text Phase 1, Task 1.3. Mirrors the existing register_image_gen_provider pattern (plugins.py:531) — wrong-type or duplicate-name registrations log at WARNING and silently return rather than raising, so a misbehaving auth plugin cannot crash the host. Deviation from plan: the plan's draft raised TypeError on non-provider input; switched to silent-warn to match the established image_gen convention. Test updated to match.
…oard-auth.log Phase 1, Task 1.4. Records every auth event (login start/success/failure, logout, refresh success/failure, revoke, session verify failure, WS ticket mint) as one JSON object per line. Token-like kwargs (access_token, refresh_token, code, code_verifier, state, ticket, cookie, Authorization) are dropped before serialisation so the log never contains live secrets. Write failures log at WARNING but never raise — auth flows must not fail because the audit logger broke.
Phase 2, Task 2.1. Self-contained fake IDP — start_login redirects
straight back to {redirect_uri}?code=stub_code&state=<s> so tests can
walk the OAuth round trip in-process. Tokens are HMAC-signed JSON blobs
(not real JWTs) — enough structure for verify_session to detect tamper
and expiry without pulling in pyjwt.
Lives in tests/ only — never registered as a real plugin. Phase 3's
end-to-end tests import StubAuthProvider directly.
Convention: exp <= now counts as expired (TTL=0 means born-expired)
— matches what Phase 6's silent-refresh test will need.
Phase 3, Task 3.1. Three cookies: - hermes_session_at: OAuth access token (HttpOnly, TTL = token TTL) - hermes_session_rt: OAuth refresh token (HttpOnly, 30d max-age) - hermes_session_pkce: PKCE state + verifier + provider hint (10min) All SameSite=Lax + Path=/. Secure flag is set ONLY when the request scheme is https — uvicorn proxy_headers=True (enabled in gated mode at Phase 3.5) rewrites scheme from X-Forwarded-Proto so Fly's TLS terminator works.
…HTML Phase 3, Tasks 3.2 + 3.3 + 3.4. These three pieces are mutually dependent so they land together. middleware.py - gated_auth_middleware engages when app.state.auth_required is True. Allowlists /login, /auth/*, /api/auth/providers, and static asset paths; everything else demands a valid session_at cookie. Verifies by trying every registered provider's verify_session in turn (multi- provider stack); attaches verified Session to request.state.session. Returns 401 JSON for /api/* and 302 -> /login for HTML. ProviderError during verify -> 503. routes.py - APIRouter with: GET /login server-rendered HTML GET /auth/login?provider=N 302 to IDP + PKCE cookie GET /auth/callback?code,state completes login, sets session cookies POST /auth/logout clears cookies + best-effort revoke GET /api/auth/providers public bootstrap endpoint (503 if zero) GET /api/auth/me verified session as JSON (auth-required) login_page.py - Inline-CSS HTML template, no React, no JavaScript. web_server.py - Mounted gated_auth_middleware between host_header and auth_middleware (FastAPI runs middlewares in registration order: host check -> cookie auth -> token auth). auth_middleware short-circuits when auth_required so cookie auth is authoritative in gated mode. Router is included before mount_spa so the catch-all doesn't swallow /login or /auth/*. 17 new behavioural tests; loopback regression harness still green.
… gated; suppress _SESSION_TOKEN injection
Phase 3, Task 3.5. Three changes to web_server.py:
1. start_server replaces the legacy SystemExit-refusing-to-bind guard
with: if app.state.auth_required and no providers registered, exit
with a clear message; otherwise log the gate-on banner. --insecure
keeps its existing behaviour.
2. uvicorn proxy_headers flag is computed from app.state.auth_required.
Loopback / --insecure keep it False (so _ws_client_is_allowed sees
the real peer for the loopback gate); gated mode flips it True so
X-Forwarded-Proto from Fly's TLS terminator is honoured for cookie
Secure-flag decisions in detect_https().
3. _serve_index no longer injects window.__HERMES_SESSION_TOKEN__ when
the gate is on — the SPA reads identity from /api/auth/me using
cookie auth instead. window.__HERMES_AUTH_REQUIRED__ flag lets the
SPA pick between ticket-auth (gated) and token-auth (loopback) for
/api/pty + /api/ws (Phase 5 will wire this in the React layer).
4 new behavioural tests; loopback regression harness still green.
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-assignment |
7 |
invalid-argument-type |
3 |
unresolved-import |
2 |
First entries
tests/cli/test_cli_yolo_toggle.py:30: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/cli/test_cli_yolo_toggle.py:241: [invalid-argument-type] invalid-argument-type: Argument to function `HermesCLI._transfer_session_yolo` is incorrect: Expected `HermesCLI`, found `SimpleNamespace`
hermes_cli/dump.py:236: [invalid-assignment] invalid-assignment: Object of type `Literal["(unknown)"]` is not assignable to `Literal["0.15.0"]`
hermes_cli/dump.py:237: [invalid-assignment] invalid-assignment: Object of type `Literal[""]` is not assignable to `Literal["2026.5.28"]`
tests/hermes_cli/test_signal_handler_kanban_worker.py:31: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tools/xai_http.py:71: [invalid-assignment] invalid-assignment: Object of type `Literal["unknown"]` is not assignable to `Literal["0.15.0"]`
tests/cli/test_cli_yolo_toggle.py:191: [invalid-argument-type] invalid-argument-type: Argument to function `HermesCLI._is_session_yolo_active` is incorrect: Expected `HermesCLI`, found `SimpleNamespace`
gateway/platforms/yuanbao.py:103: [invalid-assignment] invalid-assignment: Object of type `Literal["0.0.0"]` is not assignable to `Literal["0.15.0"]`
acp_adapter/server.py:82: [invalid-assignment] invalid-assignment: Object of type `Literal["0.0.0"]` is not assignable to `Literal["0.15.0"]`
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`
gateway/platforms/email.py:80: [invalid-assignment] invalid-assignment: Object of type `Literal["0"]` is not assignable to `Literal["0.15.0"]`
cli.py:11860: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def reset_current_session_key(token: Token[str]) -> None`
✅ Fixed issues (6):
| Rule | Count |
|---|---|
invalid-assignment |
6 |
First entries
hermes_cli/dump.py:237: [invalid-assignment] invalid-assignment: Object of type `Literal[""]` is not assignable to `Literal["2026.5.16"]`
gateway/platforms/yuanbao.py:103: [invalid-assignment] invalid-assignment: Object of type `Literal["0.0.0"]` is not assignable to `Literal["0.14.0"]`
acp_adapter/server.py:82: [invalid-assignment] invalid-assignment: Object of type `Literal["0.0.0"]` is not assignable to `Literal["0.14.0"]`
gateway/platforms/email.py:80: [invalid-assignment] invalid-assignment: Object of type `Literal["0"]` is not assignable to `Literal["0.14.0"]`
tools/xai_http.py:71: [invalid-assignment] invalid-assignment: Object of type `Literal["unknown"]` is not assignable to `Literal["0.14.0"]`
hermes_cli/dump.py:236: [invalid-assignment] invalid-assignment: Object of type `Literal["(unknown)"]` is not assignable to `Literal["0.14.0"]`
Unchanged: 5045 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…ch#28181) The single-query signal handler in cli.py raises KeyboardInterrupt on SIGTERM/SIGHUP. For interactive 'hermes chat -q' that unwinds the main thread cleanly. For kanban workers spawned by the dispatcher, the worker process is likely to have a non-daemon thread alive (terminal _wait_for_process, custom plugins, etc.). With KeyboardInterrupt only the main thread unwinds; the non-daemon thread keeps the process alive, the gateway has already restarted, and the dispatcher's _pid_alive check returns True forever — task stuck in 'running' indefinitely. When HERMES_KANBAN_TASK is set (dispatcher-spawned worker), flush logging + stdout/stderr, then os._exit(0) instead of raising KeyboardInterrupt. The kernel reclaims the PID immediately, and the existing zombie-state detection in _pid_alive flips the task to crashed on the next dispatcher tick. detect_crashed_workers then re-spawns it on the following tick — no manual recovery needed. A SIGALRM(2s) deadman is armed before the flush so a pathological blocking-I/O flush can't wedge the worker forever. In practice the reporter measured flush in <1ms; the alarm is a failsafe, never the common path. Interactive (non-kanban) chat -q is unchanged — the env-gated branch only fires for dispatcher-spawned workers. Live verification on this machine: - Without HERMES_KANBAN_TASK + non-daemon thread alive: process hangs alive 4+ seconds after SIGTERM. Dispatcher's _pid_alive returns True → task stuck. - With HERMES_KANBAN_TASK + same non-daemon thread: process exits in 0.10s via os._exit(0). Dispatcher reclaims on next tick. Tests: - tests/hermes_cli/test_signal_handler_kanban_worker.py (3 cases): end-to-end subprocess test with a non-daemon thread, HERMES_KANBAN_TASK env, SIGTERM, dispatcher-style _pid_alive check. Plus a source-level invariant test catching future refactors that drop the env-gated exit. - 452/452 kanban tests pass. Co-authored-by: andrewhosf <[email protected]>
The CLI's in-chat `/yolo` toggle mutated `os.environ["HERMES_YOLO_MODE"]`
but had no effect because `tools/approval.py:_YOLO_MODE_FROZEN` captures
that env var once at module-import time (a deliberate security floor that
keeps prompt-injected skills from flipping the bypass mid-run). By the
time the user reaches `/yolo` in a running CLI session, `tools.approval`
has already been imported, so the env flip after that is a silent no-op.
Result: `/yolo` advertised "⚠ YOLO" in the status bar while every
dangerous command still hit the approval prompt or got denied. Only
`hermes --yolo` (set before tool imports), `HERMES_YOLO_MODE=1 hermes ...`,
and `hermes config set approvals.mode off` actually bypassed.
This patches the CLI to match what the gateway and TUI `/yolo` handlers
already do, plus mirrors the TUI's session-rename YOLO transfer:
* `_toggle_yolo()` now calls `enable_session_yolo(self.session_id)` /
`disable_session_yolo(self.session_id)` instead of touching the env
var. Matches `gateway/run.py:_handle_yolo_command` and the
`tui_gateway/server.py` key=="yolo" branch.
* Around each `run_conversation()` call, `run_agent()` now binds
`set_current_session_key(self.session_id)` so
`tools.approval.is_current_session_yolo_enabled()` resolves against
the same key the toggle writes under, and resets it in `finally` so
reused threads don't see stale identity. Matches the
`tui_gateway/server.py` and `gateway/platforms/api_server.py` binding
pattern.
* New `_transfer_session_yolo()` helper carries YOLO bypass state
across `self.session_id` reassignments — `/branch` forking into a
new session id and the auto-compression sync that rotates into a
fresh continuation session id. Without this, the same UX failure
mode the rest of this fix addresses (silent `/yolo` no-op) would
reappear after a single `/branch` or auto-compression event.
Mirrors `tui_gateway/server.py` ~line 1297-1305.
* New `_is_session_yolo_active()` helper replaces the two
`bool(os.getenv("HERMES_YOLO_MODE"))` reads in the status-bar
builders, so the badge reflects the actual bypass state. Uses
`getattr(self, "session_id", None)` so status-bar test fixtures
that bypass `__init__` via `HermesCLI.__new__(HermesCLI)` don't
trip `AttributeError` (the builders swallow exceptions silently
and lose every field after the failure). Still honors
`_YOLO_MODE_FROZEN` so `hermes --yolo` keeps lighting it up.
The `_YOLO_MODE_FROZEN` security freeze is preserved — env-var-based
opt-in still only works when set before process start, which is the
documented contract for `--yolo` / `HERMES_YOLO_MODE`.
Closes NousResearch#33925
…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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8252fb0. Configure here.
…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.
the24thLetter
pushed a commit
that referenced
this pull request
Jun 18, 2026
…NousResearch#34192) (NousResearch#34382) NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup with: /usr/bin/tini: No such file or directory The image moved from tini to s6-overlay as PID 1 (/init) earlier in 2026. Orchestration templates that still pin /usr/bin/tini as the entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no binary to exec and the container crashes immediately. Hermes has no control over the Hostinger catalog template, but we can make the image backward-compatible by symlinking /usr/bin/tini -> /init during the s6-overlay install step. External wrappers that exec /usr/bin/tini will land on the same s6-overlay reaper they would have landed on if they'd used the canonical /init entrypoint. The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim is purely for legacy external wrappers, not for the image's own runtime path. Once affected catalogs are updated, the symlink can be removed. Other issues NousResearch#34192 raises that are NOT addressed by this PR: * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148 (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger template likely needs to update its env-var propagation. * Problem #3 (incompatible session formats): RFC for pluggable SessionDB is tracked in NousResearch#23717. * Problem #4 (Telegram polling conflict): an operations problem on Hostinger's side, not in this codebase. This PR is scoped to the one issue that can be fixed inside Dockerfile: the missing /usr/bin/tini binary. Tests (3 in test_dockerfile_tini_compat_shim.py): - test_tini_compat_symlink_present Guard: the symlink line must exist in Dockerfile. - test_tini_compat_comment_explains_why The NousResearch#34192 anchor comment must be present so future readers know why the shim is there (avoid accidental removal). - test_entrypoint_still_init_not_tini Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is only for external wrappers. Refs: NousResearch#34192 Partial fix: addresses the immediate tini-binary crash. Catalog-side fixes still needed by Hostinger for the UID and session-format problems documented in the issue. Co-authored-by: Cursor <[email protected]>
the24thLetter
pushed a commit
that referenced
this pull request
Jun 18, 2026
…eSessionPage (NousResearch#43487) When auto-compression rotates the session tip (old #4 → new #5), the incoming page carries the new tip but the previous list still holds the old one. The old tip's id differs from the new tip's id, so the existing id-only dedup in mergeSessionPage() preserves both as separate sidebar rows. Add lineage-level dedup: build a set of incoming lineage keys (`_lineage_root_id ?? id`) and filter survivors whose lineage key matches any incoming row. This mirrors the existing sessionPinId() logic used for pin stability. Fixes NousResearch#43483
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
mainforward from723e4140b0b23e7c32f5def55cfada37e0598ddfto current upstreamorigin/mainea5a6c216b99319353bddc99b2a1a0c1b2241b6d, plus the OpenClaw local-fork Kanban/review-gate/update-wrapper changes.originremains upstream fetch-only with push disabled;forkremains the push/PR remote; gateway/Kanban continuity is handled byscripts/update_local_fork_and_restart.shafter merge/deploy.:latestrelease/backport fixes, and current-head Cursor/Bugbot redaction fixes.Validation
88dcbea5ede796f276959849af476cb0304aa2e4.mainat723e4140b0b23e7c32f5def55cfada37e0598ddf.mainand upstreamorigin/mainare ancestors of the PR head, so merging as-is updates the fork to current upstream plus local fork changes.MERGEABLE/clean.88dcbea5ede796f276959849af476cb0304aa2e4: passing; expected build/merge/save-duration jobs are skipped.88dcbea5ede796f276959849af476cb0304aa2e4: clean via check run78383040450, no issues found.HERMES_HOME=/Users/openclaw/ai-os-migration/configs/hermes ./scripts/update_local_fork_and_restart.sh --dry-runcompleted and showed backup, narrow Git helper, editable reinstall, config migrate/check, Kanban continuity reassertion, LaunchDaemon gateway restart, dashboard restart, dispatcher/dashboard/board verification, and no standalone Kanban daemon.Codex/Cursor sweep summary
Bugs found and fixed
3a491657-9598-4868-95c7-5205a8257093:force=Trueredaction now masks sensitive full-URL query params and HTTP access-log request-target query params at user-facing safety boundaries, while normal log redaction still preserves navigable magic links/OAuth/pre-signed URLs.:latestfindings remain fixed/documented in this rebased/integration branch: main pushes do not move:latest; release/backport logic compares against the registry latest revision label before moving:latest.Implemented changes in the PR
scripts/update_local_fork_and_restart.shwraps the narrow Git helper with Hermes state backup, editable reinstall, config migrate/check, scalar workflow setting reassertion, canonical LaunchDaemon gateway restart, dashboard restart, dispatcher tick verification, board stats, dashboard HTTP verification, and SQLite/WAL error checks.:latestrelease-only and avoids backport regressions.Validation
88dcbea5ede796f276959849af476cb0304aa2e4is mergeable against forkmainand has both forkmainand upstreamorigin/mainas ancestors.88dcbea5ede796f276959849af476cb0304aa2e4: all pass or expected skip.88dcbea5ede796f276959849af476cb0304aa2e4: clean via check run78383040450, no issues found.scripts/update_local_fork_and_restart.sh: completed and covered gateway, dashboard, Kanban config continuity, board visibility/stat checks, dispatcher activity check, and no-standalone-daemon verification.Activation note
After merge, run the continuity wrapper from this checkout to activate/deploy the fork state and restart the canonical gateway/dashboard:
cd /Users/openclaw/ai-os-migration/configs/hermes/hermes-agent HERMES_HOME=/Users/openclaw/ai-os-migration/configs/hermes ./scripts/update_local_fork_and_restart.shGateway restart must use the canonical system LaunchDaemon
system/ai.hermes.gateway.openclaw; do not start the GUI LaunchAgent or a standalone Kanban daemon.Note
Medium Risk
Wide release surface plus security-sensitive changes (media delivery defaults, redaction boundaries, approval bypass semantics); behavior is mostly opt-in or aligned with existing gateway patterns but needs regression on CLI YOLO, gateway file send, and context compression.
Overview
Bumps the product to v0.15.0 (package, ACP manifest, release notes) and extends Claude Opus 4.8 across adapters, defaults, and pricing.
Agent & reliability: On context overflow, the loop stops stepping down guessed probe tiers; it only shrinks
context_lengthwhen the provider reports a lower limit, otherwise it keeps the configured window and compresses. Log redaction leaves normal web URL query strings intact (magic links/OAuth/pre-signed flows) while still masking credentials;force=Trueadditionally strips sensitive query params and HTTP request-target queries at safety boundaries, and URL userinfo passwords stay redacted.CLI & approvals:
/yolotoggles per-session approval bypass (aligned with gateway/TUI), binds the session key per turn, and transfers YOLO when the session id changes (/branch, compression continuation)./model --refreshandhermes model --refreshclear a new disk-cached provider model list for faster pickers.Gateway: Outbound media delivery is permissive by default (denylist only);
gateway.strict/HERMES_MEDIA_DELIVERY_STRICTrestores allowlist + recency rules for public bots. Deploy-site can trigger Vercel onworkflow_dispatchas well as release publish.Other: OpenRouter
session_idinextra_body, Hindsight defaultrecall_types→observation, skills index crawls skills.sh via sitemap, Kanban workersos._exiton SIGTERM to avoid stuck running tasks, and contributor/release script housekeeping.Reviewed by Cursor Bugbot for commit 88dcbea. Bugbot is set up for automated code reviews on this repo. Configure here.