Skip to content

runtime: rebase fleet fixes onto upstream v0.20.1 (2026.8.13) - #11

Closed
BenSheridanEdwards wants to merge 2323 commits into
mainfrom
runtime/fleet-20260815
Closed

BenSheridanEdwards wants to merge 2323 commits into
mainfrom
runtime/fleet-20260815

Conversation

@BenSheridanEdwards

Copy link
Copy Markdown
Owner

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 update run 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.py auto-TTS: kept upstream's callable guards + multi-path loop, delivery target now voice_chat_id from _resolve_auto_tts_delivery.

🤖 Generated with Claude Code

teknium1 and others added 30 commits August 14, 2026 22:11
…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.
gitong and others added 26 commits August 15, 2026 01:21
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
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
(cherry picked from commit b058c25)

Co-authored-by: Matthieu Talbot <1246794+MartyLake@users.noreply.github.com>
(cherry picked from commit 711da12)
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)
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)
@BenSheridanEdwards

Copy link
Copy Markdown
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 main after this branch was cut (#12 through #16, and #18).

Worth noting that #20 also fixes a ReDoS that upstream introduced after v0.20.1, in a new lowercase env-assign pass in redact_sensitive_text. That regression is present on any base newer than this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.