runtime: rebase fleet fixes onto upstream v0.20.1 (2026.8.13) - #11
Closed
BenSheridanEdwards wants to merge 2323 commits into
Closed
BenSheridanEdwards wants to merge 2323 commits into
BenSheridanEdwards wants to merge 2323 commits into
Conversation
…execution The unconditional 2s join before InterruptedError delayed interrupt detection when Relay managed execution was not active (CI: tests/run_agent/test_interrupt_propagation.py — detection took 2.34s against a <1.0s budget, because the mocked worker sleeps 5s and there is no Relay scope to unwind). Extract the join into _join_worker_for_relay_teardown(), which no-ops unless a Relay runtime exists AND managed execution consumers are registered — the only case where an orphaned physical scope can corrupt the LIFO stack (NousResearch#81521). Applied at all three interrupt sites (streaming, non-streaming, Bedrock streaming). The regression test now simulates a live runtime so the join path stays covered.
…sResearch#83445) `connect()` caches every path it has initialized in the process-local `_INITIALIZED_PATHS` set and then skips all first-open work for it — header validation, integrity probe, `SCHEMA_SQL`, additive migrations. That cache is keyed on a path, but the schema it stands for lives in a file, and the two can drift apart: delete or replace `kanban.db` under a live gateway/dispatcher/dashboard process and the next `connect()` takes the fast path, lets SQLite create a fresh empty database, and hands back a connection with no tables in it. Nothing notices. Every query then fails with `no such table: tasks`, `plugin_api._conn()` logs its init warning and carries on, and the board renders empty. Because the cache entry survives, the process re-creates the same schema-less ~4 KB file on every restart of the desktop app in front of it — only killing the backing process clears it. Verify the sentinel table on the fast path and self-heal when it is gone: drop the stale cache entry and fall through to the existing init path, which re-runs the probes and the schema script under the cross-process init lock. The check is one `sqlite_master` lookup on the already-resident page 1, so the steady-state path stays lock-free (NousResearch#36644) and does no schema work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…enames Three fixes for generated/displayed images in the desktop chat: - Shell fallback context menu no longer swallows right-clicks on images: the guard now yields to Electron's native image menu (Copy Image, Copy Image Address, Save Image As...) for img/picture/video/canvas targets, matching the existing editable/selection carve-outs. - Save Image As / download button: generated-image URLs (fal.media etc.) end in an extensionless content hash, so saves produced an unopenable "All Files" blob. The main-process save dialog and the renderer anchor fallback both now append a MIME-derived extension, add image type filters, and default to the user's Downloads directory instead of the process cwd (win-unpacked on packaged Windows installs). - New will-download handler routes any Chromium-initiated download through the same Downloads-dir + guaranteed-extension policy. Validation: new unit tests for the filename derivation (6 passing); npm run check:lint green (tsc x3 + eslint, 0 errors).
… touched the skill The composer's "Use skill: X" pill only checked the draft text and the workspace-name collision - it happily re-offered a skill the session had already loaded (skill_view), edited (skill_manage), or that the user had invoked via its /name command. Clicking it would re-inject the full SKILL.md into a context that already carries it. The draft provider now scans the session transcript (per-runtime $sessionStates mirror, $messages for the active session) for skill_view/skill_manage tool calls naming the skill - exact or qualified (category/name, plugin:name), from parsed args or hydrated argsText - and for user turns starting with the skill's slash command, and stands down on a hit. The scan only runs when the draft actually matched a skill, so ordinary typing pays nothing.
…ng-uv fallbacks (NousResearch#69216) Install-Uv piped the astral installer's entire output to Out-Null, so any real failure (proxy block, AV quarantine, permissions) surfaced only as the generic "uv installed but not found" message, and astral.sh was the sole install source even though corporate proxies commonly block it while the byte-identical GitHub releases installer downloads fine. Three-rung ladder, all inside Install-Uv: 1. astral.sh installer with output captured via Tee-Object. 2. GitHub releases installer mirror (same UV_INSTALL_DIR). 3. Salvage an existing uv.exe (Get-Command uv, or the astral default %USERPROFILE%\.local\bin\uv.exe) by copying it into $HermesHome\bin so the managed-first invariant holds. On total failure, print the last 15 lines of captured installer output plus the existing manual-install pointer. Reported by @BitBernd; proxy diagnosis by @gakugaku; Out-Null suppression first identified by @webtecnica in NousResearch#69366. Closes NousResearch#69216
…clones The guard's "use a separate worktree or temporary clone" advice sent agents to /tmp by default. /tmp is RAM-backed tmpfs on most distros, and parallel salvage clones each running npm ci (~1.6GB per clone) filled a 32GB tmpfs to 97% during a 15-subagent campaign, ENOSPC-ing sibling test runs. The message now recommends `git clone --shared <root> ~/.hermes/scratch/<task>` (honoring HERMES_HOME), warns that dependency installs belong on real disk, and tells the agent to delete the clone once the branch is pushed.
…has no fixed SQLite build When every patch on the current minor line (e.g. 3.11) still links a vulnerable SQLite (e.g. 3.50.4 on Windows), the provisioner now tries the next supported minor line (3.12, then 3.13) before giving up. Previously, _install_safe_python_generation only tried patches within the same minor line. On Windows, where python-build-standalone may not publish a fixed build for the installed patch, users were stuck with a repeated warning on every `hermes update` with no path forward. The requires-python constraint (>=3.11,<3.14) and the downstream import smoke test already gate compatibility, so the minor-line upgrade is safe. Adds allow_minor_upgrade parameter to _attempt_install_generation to relax the same-minor-line version guard when called from the fallback path. Fixes NousResearch#76106
…e; dedupe retried versions Follow-up to the salvaged NousResearch#76252 addressing both review gaps: - New TestMinorLineFallForward class with a direct test of the explicit-patch fallback branch: bare '3.12' resolves to a VULNERABLE build while an explicit 3.12.x patch is fixed, so recovery must go through _list_available_patches on the next minor line. Asserts the exact `uv python install` request sequence. - New all-minors-exhausted test: everything vulnerable on 3.11-3.13 returns None with per-line attempts bounded by _MAX_PATCH_RETRIES and no requests beyond 3.13 (requires-python is <3.14). - test_retry_is_bounded_by_max_retries_constant now actually uses its counting wrapper and asserts the collected install calls (the previous version collected them into a dead variable). Also dedupes the fallback loop the same way the same-minor loop does: _attempt_install_generation can now record the probed candidate version into a caller-supplied tried_versions set, so the explicit-patch pass skips the version the bare-minor request already resolved to and rejected -- previously that wasted a full download+install+probe+delete cycle per minor line re-trying a known-vulnerable build.
…ation Legacy compaction mode (compression.in_place: false) rotates the physical session_id mid-conversation. The prompt-cache scope introduced in NousResearch#79161 was derived from that physical id, so every rotation moved the same conversation into a fresh cache bucket - the prompt cache went cold at every rotation boundary (NousResearch#79017). Fix: resolve a rotation-stable logical scope - the compression-lineage ROOT of the current session (SessionDB.get_compression_lineage, fork-aware post-NousResearch#79193) - once per turn, memoized per transcript segment, and prefer it over the physical session_id at every prompt_cache_key derivation site: - agent/prompt_cache_scope.py (new): resolve_prompt_cache_scope(agent) - lineage-root walk with per-segment memo; falls back to the physical id when no DB is attached or the walk fails, degrading to pre-fix behavior. - transports/codex.py: build_kwargs accepts cache_scope_id and prefers it for the body prompt_cache_key, the xAI x-grok-conv-id header, and the Codex x-client-request-id routing header. The Codex session_id header keeps the raw physical id (transcript identity, NousResearch#57012 contract). - transports/chat_completions.py: _add_prompt_cache_key accepts cache_scope_id with the same precedence. - chat_completion_helpers.py: build_api_kwargs threads the resolved scope into all three build_kwargs call sites (codex, profile, legacy). - auxiliary_client.py: set_runtime_main carries cache_scope; the aux Responses cache-key site prefers it over the physical session_id. - turn_context.py: resolves the scope once per turn and threads it through set_runtime_main (no DB walk on the per-API-call hot path). Scope semantics preserved from NousResearch#79161: /new starts a fresh scope (new lineage), /branch children, delegate subagents, and tool children stay isolated (explicit-fork exclusion in get_compression_lineage), unrelated sessions keep distinct buckets, and cron per-fire timestamps still normalize via _cache_scope_from_session_id. Default installs compact in place (session_id never rotates), so they hit the memo and produce byte-identical keys to before. Fixes NousResearch#79017
- prompt_cache_scope: memo key now includes DB presence (a lazily attached _session_db re-resolves instead of staying pinned to the physical id); _persist_disabled agents (background-review forks that never get a DB row) memoize the fallback instead of re-querying the lineage per API call; module docstring cross-references get_conversation_root and why the two lineage resolvers must not be deduplicated. - chat_completion_helpers: hoist the triplicated _prompt_cache_scope_for_agent(agent) call to a single local above the OpenAI-wire dispatch (after the anthropic/bedrock early returns, which don't use prompt_cache_key). - codex transport docstring: x-client-request-id mirrors the derived body key, not the raw scope id. - turn_context comment: acknowledge the first-turn pre-persist fallback. - tests: +2 (persist-disabled memoization; lazy DB attach re-resolution).
/simplify-code finding: turn_context evaluated resolve_prompt_cache_scope() inside set_runtime_main's argument list under the umbrella try/except — a resolution failure would silently skip the ENTIRE runtime binding (provider/model/base_url/api_key/session_id for all aux calls that turn), not just the cache scope. - prompt_cache_scope: add resolve_prompt_cache_scope_safe() (never raises, returns None on failure/empty). - turn_context: resolve the scope into a local via the safe variant BEFORE the set_runtime_main call, so a failure can only lose the scope. - chat_completion_helpers: _prompt_cache_scope_for_agent delegates to the shared safe variant (guarded import retained). - tests: +1 (hostile-property agent -> None; normal/empty passthrough).
…a v2 + IPC + Settings UI) First slice of multi-source agent support: the desktop can now persist ANY number of named backends (local runtime, remote gateways, Hermes Cloud instances, SSH hosts) side by side instead of one global connection plus per-profile overrides. - electron/connection-registry.ts: pure v2 registry module — required case-insensitively-unique labels (device names), @name-device handle rule for duplicate profile names across sources (agentHandle), defensive normalizeRegistry for corrupt files, one-time v1→v2 migration that imports the global block + per-profile overrides (deduped by URL/host) and leaves connection.json untouched for older builds. - main.ts: connections.json storage beside connection.json (same secret posture: safeStorage-encrypted tokens, 0600, tighten-before-parse, mtime cache) + hermes:connections:* IPC (list/save/remove/set-primary/test). Test maps registry entries onto the existing testDesktopConnectionConfig probe stack — no new probe code. - Settings → Connections: manage the registry (add/edit/remove/test/make primary) with forced naming; local entry is non-removable; removing the primary retargets to local. en + zh locales. Storage-level only by design: routing/pool generalization to composite (connection, profile) keys, the multi-source roster, plugin SDK surface, and fan-out updates land as follow-up PRs.
Review fixes from NousResearch#86679 comments (trevorgordon981, helix4u, kshitijk4poor): - Edit inheritance: mergeConnectionInput preserves fields the editor does not carry (cloud org, ssh remoteHermesPath/remoteProfile) so a rename no longer wipes them. When the payload carries an ssh host string, stored user/port are NOT inherited — the composite host field is authoritative, fixing the stale user/port resurrection on edit. - Token hygiene: tokens only persist on token-auth remotes; switching an entry to oauth (or cloud) clears the stale envelope. - Plain-text opt-in: the panel now surfaces the same consent dialog as Settings -> Gateway on keyring-less machines (registry list exposes secureTokenStorage; save retries with allowPlainTextToken after consent). - Registry test isolation: hermes:connections:test builds the probe directly from the registry entry instead of coercing against v1 connection.json — no more inheriting the v1 global token for a different host, and the local entry now probes the app-managed backend (never v1 remote/ssh state, so the test button can no longer trigger a v1 file write). - 'local' id reserved at the validation boundary: a crafted IPC payload can no longer replace the local entry via upsert. - Cloud creation hidden in the editor (a dialable cloud entry comes from the Cloud sign-in/discovery flow); migrated cloud entries stay editable. - First-run migration write is guarded: a failed write keeps the migrated registry in memory instead of hard-failing every connections IPC call. - uniqueLabel(): single label-dedup helper — counts up instead of "X 2 2", clamps 253-char migrated URL-host labels under LABEL_MAX; used by normalizeRegistry and both migration paths. - UI copy: staged-rollout note replaces the "side by side" claim; test failure toast leads with the failure wording; dropped unused i18n keys. Tests: +9 pure-module cases (reserved id, token-drop rules, merge inheritance, ssh host precedence, uniqueLabel); electron+settings suites 1355 passed.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…han reaper on Windows (NousResearch#83683) On Windows _get_service_pids() is empty (no systemd/launchd query), so a Scheduled-Task-supervised gateway whose gateway.pid record is missing or stale is invisible to both the service-PID and recorded-PID exclusions the reaper already applies (NousResearch#86658) — and gets SIGTERM'd on every desktop open (NousResearch#86098 class, pidfile-less path). Add a Windows-only backstop: any reaper candidate whose parent chain reaches services.exe (the Task Scheduler launches tasks under the services tree) is spared even with no pidfile. The backstop is deliberately inert on POSIX: every process there has PID 1 (launchd/init/systemd) in its ancestry — and a genuine orphan is reparented directly to PID 1 — so supervisor-name ancestry carries zero supervision signal and would disable the reaper entirely on macOS/WSL (NousResearch#51325, NousResearch#75936). POSIX supervised gateways are already covered pidfile-independently by the _get_service_pids() exclusion. Known limitation (fail-open, documented): if the Task-launched bootstrap parent has already exited, Windows does not reparent the gateway, the chain breaks before services.exe, and the gateway is treated as an orphan. Salvaged from NousResearch#86702 by @EvanProgramming (authorship preserved); reduced to the genuinely-new Windows backstop — the PR's other two hunks were already merged on main via NousResearch#86658 (one in a strictly stronger full-parent-chain form) and its POSIX ancestry checks were dropped as unsound (verified empirically: a true double-fork orphan's psutil parent IS launchd).
The Bedrock Converse shim hardcoded 'else 4096' when the caller passed no max_tokens, so auxiliary vision descriptions stayed capped at 4096 tokens on the Bedrock wire even after NousResearch#75253 removed the vision call sites' own caps (NousResearch#10809 was only partially fixed there). Converse's inferenceConfig.maxTokens is optional; when omitted, Bedrock defaults to the model's maximum allowed output. Thread an explicit max_tokens=None through build_converse_kwargs/call_converse to omit the field, and drop an all-empty inferenceConfig from the wire request entirely. The 4096 default is unchanged for every existing caller (main transport passes params.get('max_tokens', 4096) explicitly), so only no-cap aux calls opt in. Surfaced during review of NousResearch#75253.
Self-review follow-up: cover call_converse_stream's max_tokens=None path (same builder, previously unpinned) and document why the shim reads the caller cap with truthiness rather than 'is None' (parity with the Anthropic shim's reading).
…ration) (NousResearch#86745) delegation.max_concurrent_children caps how many delegated children run in parallel per batch (and concurrent background delegation units). The old default of 3 needlessly serialized independent fan-outs (e.g. reviewing/investigating N PRs or issues at once), so large batches ran in slow chunks of 3. Raise the shipped default to 10, which sits at/below the existing high-cost advisory threshold (>10), so the default never trips the warning. Each child still consumes API tokens independently, so this is a throughput/latency win the user pays for in parallel token spend — the floor stays 1 and there is no ceiling, so anyone can tune it down or up. - config_defaults.py: default 3 -> 10; _config_version 36 -> 37. - delegate_tool.py: _DEFAULT_MAX_CONCURRENT_CHILDREN 3 -> 10 (+ docstring). - config_migrations.py: _migrate_to_37 lifts configs pinned at exactly the old default 3 to 10 (deliberate non-3 overrides preserved; unset inherits 10). - cli-config.yaml.example: documented default updated. Verified: default/fallback read 10, version 37, and the migration lifts 3->10, preserves an explicit 5, and leaves unset untouched. Co-authored-by: Teknium <teknium1@users.noreply.github.com>
The vision tools' call_kwargs hardcode max_tokens caps (2000 for vision_analyze/browser_vision, 4000 for video analysis), truncating descriptions of complex images at the cap. The centralized aux client already omits max_tokens by default (NousResearch#34845) so providers use their model max output; these three call sites were the leftovers that bypassed that policy. Remove the hardcoded caps entirely — the aux client handles the mandatory-max_tokens Anthropic wire via _resolve_anthropic_messages_max_tokens (model output ceiling) and Gemini native omits maxOutputTokens (65K ceiling), so no wire needs an explicit cap.
Covers the max-tokens-knob contract: vision call_kwargs omit max_tokens entirely (configured values, defaults, and even an explicit auxiliary.vision.max_tokens config entry must never be forwarded), so providers use their full output budget.
Sweeper follow-up: the browser-screenshot and video kwargs captures now also assert max_tokens is absent, protecting the central auxiliary no-cap policy against refactors that would restore the hardcoded caps.
…h#86796) cron.manage resolved its jobs store from the process HERMES_HOME, so a profile whose cron lives in ~/.hermes/profiles/<name>/cron/ was invisible to the default gateway (and any bot/plugin querying per-profile routines saw 'no cron jobs'). Add an optional 'profile' param that scopes the whole action via set_hermes_home_override, exactly mirroring the adjacent skills.manage handler: resolve get_profile_dir(profile), 404 (err 4064) if missing, override in a try/finally that always reset_hermes_home_override. Omitted/None keeps the launch-profile behavior, so existing callers are unaffected. cronjob() itself is unchanged (it already keys off HERMES_HOME). Enables the Hermes-Bot-Mode plugin to show a bot's real routines (NousResearch/Hermes-Bot-Mode#37). Needs a SERVE-backend gateway restart to take effect live. 2/2 in the new focused test. Co-authored-by: Teknium <teknium1@users.noreply.github.com>
…sumable) (NousResearch#86797) * feat(sessions): generic 'hidden' session flag (sidebar-hide, still resumable) Adds a source-orthogonal, archive-orthogonal 'hidden' session flag meaning 'don't show in the global Sessions sidebar, but stay fully resumable by the surface that owns it'. Mirrors the existing archived/pinned capability end to end, so it's a generic widening (any plugin that owns its own session lifecycle - kanban, Bot Mode, future plugins - can keep its sessions out of the shared recents list) rather than a per-plugin special-case. - Schema: hidden INTEGER NOT NULL DEFAULT 0 on sessions (additive; lands on existing DBs via the declarative _reconcile_columns ADD COLUMN path, same as archived/pinned - no version-gated migration). - DB: SessionDB.set_session_hidden(session_id, hidden) (clones set_session_pinned incl. the compression-lineage recursive CTE); list_sessions_rich gains include_hidden=False, appending 's.hidden = 0' by default so hidden rows drop from every listing path (and the REST sidebar endpoints inherit it with no change). - Gateway: session.set_hidden RPC (mirrors session.title); session.create accepts hidden=true, deferred via pending_hidden and applied in _ensure_session_db_row when the row is lazily created (mirrors pending_title). - REST parity: PATCH /api/sessions/{id} accepts+bool-validates 'hidden' -> set_session_hidden; _session_response exposes it. Enables Hermes-Bot-Mode to hide canonical 'Bot Chat' sessions from the sidebar (NousResearch/Hermes-Bot-Mode#46) WITHOUT retagging source (which would mis-set the agent platform). Bot Chats keep source=desktop. Gateway RPC needs a SERVE-backend restart to take effect live. 1 focused test (default-exclude / include_hidden / unhide round-trip). * fix: teach lost-and-found recovery about the 55-column sessions layout Adding the 'hidden' column makes the current sessions table 55 columns. The SQLite lost-and-found recovery classifier keys off the physical field count (SESSIONS_LAYOUT_NFIELDS) to identify a salvaged sessions row, so a recovered current-layout row (nfield=55) would otherwise be unrecognized and dropped. Add 55 to the frozenset (54/52 stay as historical prefixes) and update the column-count assertions + synthetic current-layout insert in the recovery test. --------- Co-authored-by: Teknium <teknium1@users.noreply.github.com>
…stry) Covers the named-source registry from NousResearch#86679: forced unique device names, @profile-device disambiguation, add/edit/remove/test, automatic v1 import, cloud-via-discovery, encrypted token storage, and the staged rollout note.
The green 'finished-unread' dot only cleared when a session was opened via main-thread resume (setSelectedStoredSessionId). Opening a session in a tab/tile (middle-click / Cmd-click / tile strip) never cleared it, so the dot stayed while the user was actively reading the session in a tile. With a remote hermes serve backend the effect is amplified: session.info transitions for every backend session (CLI/cron/kanban) mark unread in the desktop client, so unread dots accumulate from sessions the user never opened. Changes: - openSessionTile now calls markSessionRead(), so tile/tab open marks the session read (same as main-thread resume) - new markSessionRead / markAllSessionsRead helpers in store/session, reused by setSelectedStoredSessionId - 'Mark as read' per-row action in the session context menu (shown only while the row is unread) - 'Mark all as read' header action in the recents sidebar (shown only when unread sessions exist) - i18n: markRead (row scope), markAllRead (sidebar scope) in en/zh + types
…e-light read sessions The sidebar lights the finished-unread dot for every alias of a conversation lineage (branch children + compression root), but reading a session cleared only the exact row id — a branched/compressed conversation kept dots lit on sibling rows no matter how often they were opened. And any settled completion re-lit the dot even when the user had already viewed the session since it finished. - setSelectedStoredSessionId now clears unread for the whole family via lineageAliases, not just the selected id - handleTransition only re-arms unread when the completion settles strictly after the user's last read of that session (new last-read baseline), so an already-viewed completion never re-lights - openSession marks read at the very top, before any focus short-circuit, so re-clicking an already-visible session clears its dot (the original gap the sidebar click could not reach) 4635 desktop tests pass (incl. new family-clear, read-baseline, and openSession-short-circuit cases); tsc typecheck clean.
…n clear acks persistence, mark-all/mark-read ack watermarks, new-turn baseline reset
…ure (cross-PR prop addition)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 7aacf8b)
When oauth.refresh_owner=external, Hermes adopts scheduler-owned xAI access tokens but never POSTs refresh tokens (pool, singleton, forced, reactive, and helper routes). Fleet remains the sole rotating writer via oauth_token_write_authority=external-scheduler. Interactive hermes auth add xai-oauth now passes interactive-login write authority so Fleet recovery can persist a new OAuth row under external ownership. Unauthorized runtime writes freeze scheduler-owned tokens and usability/status fields and cannot delete OAuth rows via removed_ids; interactive-login may add/replace only authorized entry ids. Absent config keeps runtime-owned behavior; malformed ownership fails closed. Same-user accidental-writer fence only — not cryptographic. Refs: NousResearch#77553 (cherry picked from commit 2dd34e0)
(cherry picked from commit a605d23)
(cherry picked from commit 74cd7eb)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit fb06406)
test_auth_add_xai_oauth_persists_under_external_owner exercises the device-login persistence path via auth_add_command with no_browser=True, but under pytest stdin is not a tty, so the unattended xAI device-login guard (0c6fc49) raised SystemExit before the behavior under test ran. Monkeypatch sys.stdin.isatty to True so the test models a deliberate terminal-driven login; the guard's own rejection test is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ocking Salvaged from the pre-quicksilver stash (stash0-pre-quicksilver-20260720, preserved patch in backups/runtime-upgrade-20260805), re-applied onto runtime/fleet-20260805 and adapted to the v0.20.0 encrypted-cache drift. * _cache.py: file_lock() advisory cross-process lock (flock/msvcrt, 30s timeout, best-effort on lock-file creation failure) + DiskCache .lock_path()/.lock(). * bitwarden.py: _shared_cache_home() (~/.hermes) as an L2b plaintext disk cache shared by all profile homes; cold fetches take the shared lock and double-check the cache before hitting the API, so 13 concurrent profile startups coalesce into one `bws secret list`. Encrypted cache stays profile-local (keyed off the bootstrap token) and opts out of sharing. Stale network-outage fallback and clear_caches() cover the shared copy. * tests: cross-process coalescing test (two subprocess profiles, one bws call) + fixture keeps the shared cache out of the developer's ~/.hermes. Dropped from the stash: auth.py/credential_pool.py/config hunks (superseded designs) and the _apply_fleet_grants env-alias hunk — the fleet-grants subsystem was not carried onto the v0.20.0 fleet branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend the oauth.refresh_owner=external contract from xai-oauth to openai-codex. Under external ownership Hermes adopts scheduler-owned Codex pool tokens but never POSTs the refresh token: resolve gates force/expiring refresh, _refresh_codex_auth_tokens (and its Codex-CLI self-heal import) fail closed, pool proactive/reactive rotation adopts from disk instead, and load_pool stops seeding the pool from the singleton. Fleet's oauth_tokens.py scheduler remains the sole rotating writer via oauth_token_write_authority=external-scheduler. Interactive `hermes auth add openai-codex` passes interactive-login write authority (single-site principle — no other add path is granted), and an interactive re-auth under external ownership appends a device_code pool row when none exists so Fleet recovery can restore an empty pool. External ownership with no usable scheduler-owned pool row raises typed codex_external_pool_unavailable instead of silently rotating. Absent config keeps runtime-owned behavior; malformed ownership fails closed. Same-user accidental-writer fence only — not cryptographic. Re-derived from stash0-pre-quicksilver-20260720 onto the committed xAI ownership design (c0c0527); the stash's blanket interactive-login grants across nous/minimax/qwen/dashboard add paths remain rejected. Refs: NousResearch#77553
A full-suite run inside a staging worktree executed `git fetch origin main` -> `git checkout main` -> `git merge --ff-only origin/main` against that worktree, moving it off its release branch while two live gateways were served from its editable venv. The tests reported green; the reflog's +0000 stamps (the suite pins TZ=UTC) were the only trace. Root cause is a two-file interaction, neither half visible on its own: 1. test_skills_subparser.py deletes sys.modules['hermes_cli.main'] and re-imports it, never restoring it. The re-import also rebinds the `main` attribute on the `hermes_cli` package. 2. test_update_venv_health.py binds `from hermes_cli import main as cli_main` at collection time and drives `_cmd_update_impl` with every dangerous step stubbed via `patch.object(cli_main, ...)`. After (1), the alias in (2) points at a module object nothing resolves any more: production code reaches its globals through `update_cmd._m()`, which re-reads `from hermes_cli import main`. Every stub silently no-ops -- including `PROJECT_ROOT`, so the real update flow ran against the real checkout. Run alone the file is harmless, which is why it went unnoticed. Fixes: * test_skills_subparser restores BOTH bindings (sys.modules entry and the package attribute) so module identity survives the probe. * test_update_venv_health resolves `hermes_cli.main` at call time, exactly as `_m()` does, and swaps `update_cmd.subprocess` for a tripwire -- a correctly-stubbed run never shells out, so any call is now a loud assertion instead of a real `hermes update`. * tests/conftest.py grows a live-checkout git guard inside the existing `_live_system_guard`. The old guard only blocked *spawning* `hermes update`; this blocks the in-process shape -- any repo-mutating git whose resolved target is the checkout under test. Read-only git stays allowed (banner/diff/dashboard tests rely on it) and tmp_path repos are untouched: the guard keys on the resolved target repo, honouring -C, --git-dir and clone/init destinations. Escape hatch is the existing @pytest.mark.live_system_guard_bypass. * test_live_checkout_git_guard.py pins the predicate and its wiring. Verified in a throwaway clone with a fake local origin (never in an install or a runtime checkout): before, the suite flips the clone to main and rewrites the reflog; after, branch and reflog are untouched. Full tests/hermes_cli run: 146 failed / 4102 passed vs 150 / 4065 on the same tree without the fix -- zero new failures, the four leaked `test_venv_holder_guard_force_semantics` params now pass.
Telegram /new already started the next turn on the config primary. A local APIConnectionError (errno 24) then jumped Sol to Luna and wrote Luna into sessions.model, so /new felt like it did not restore primary. Classify EMFILE as local_resource, block provider fallback, and stamp the config default onto the new session row at reset time.
Fallback block only when _block_provider_fallback is True. The live checkout git guard now skips init branch options and honors -C when init has no positional dest. Reconciles Hermes PR #7 CI slices 2/3/5.
Webhook has no native audio. Auto-TTS and send_voice on webhook sessions with deliver:telegram used to fall through to the base adapter error and poison Telegram with "Couldn't deliver the audio attachment" (Jarvis PR-gate scar). - WebhookAdapter.send_voice forwards to the deliver platform adapter - GatewayRunner resolves auto-TTS delivery for webhook sources - Tests cover routing, log quiet-success, home-channel fallback Co-authored-by: Wukong <agents@local>
`migrate()` documents `discover_plugins` as "when True (default), query `plugin/list` against the live codex CLI ... Set False to skip the subprocess spawn (for tests or restricted environments)". Two tests never opt out. They assert config.toml RENDERING, not plugin discovery, but leave the parameter at its default, so each run spawns the real codex CLI, which shells out to `git ls-remote https://github.com/openai/plugins.git HEAD` -- a live network call to a third-party repository from a unit test. Every other test in the file already gets this right: the ones that pass `discover_plugins=True` monkeypatch `_query_codex_plugins` first. These two just missed the switch. Both tests PASS either way, which is why it went unnoticed -- the same silent shape as the update-flow leak fixed in eff88d7. A suite that reaches the network is non-hermetic (fails offline, depends on a third party's repo staying reachable) and slower for no benefit. Verified with a logging `git` shim that records every git invocation including those from grandchild processes: before: 1 call -- `ls-remote https://github.com/openai/plugins.git HEAD` after: 0 calls 20 passed in both cases.
…tream v0.20.1, 2 superseded by upstream SessionDB pool)
Owner
Author
|
Superseded by #20 (fleet cut to v0.20.5). This rebased 14 fleet commits onto upstream v0.20.1. #20 does the same onto v0.20.5, four releases newer, and additionally carries the patches merged to Worth noting that #20 also fixes a ReDoS that upstream introduced after v0.20.1, in a new lowercase env-assign pass in |
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.
What
Carries the 14 fleet-local fixes forward onto latest upstream main (v0.20.1, 2026.8.13), replacing the fork main that was based on v0.20.0.
Why
The shared install was hard-reset to upstream main by a banned
hermes updaterun at 08:58 on 2026-08-15 (venv also rebuilt on Python 3.11, breaking all running gateways via missing 3.12 site-packages). This PR restores the fleet's fixes on top of the version Chief ordered (v0.20.1) so the install can track fork/main again.Supersession audit
Dropped as patch-equivalent upstream: the two SessionDB read-connection-pool commits (2ae6463, 2ad59fc).
Conflict resolutions (all verified by targeted pytest, 112 passed):
gateway/status.py/telegram/adapter.py: kept upstream's needs_attention/retrying_since reconnect escalation AND the fork's sanitized platform_runtime receipt.tests/hermes_cli/test_update_venv_health.py: kept upstream's orphan-classifier pin, retargeted patches at_live_main()per the stale-alias guardrail.gateway/run.pyauto-TTS: kept upstream's callable guards + multi-path loop, delivery target nowvoice_chat_idfrom_resolve_auto_tts_delivery.🤖 Generated with Claude Code