feat(gemini): present authentic @google/gemini-cli identity to Google backends - #50033
Closed
arminanton wants to merge 163 commits into
Closed
feat(gemini): present authentic @google/gemini-cli identity to Google backends#50033arminanton wants to merge 163 commits into
arminanton wants to merge 163 commits into
Conversation
…arch#38922, NousResearch#47056, NousResearch#43014) Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result that all stem from how the live-adapter send result is interpreted. NousResearch#38922 — duplicate message on confirmation timeout. future.result(timeout=60) raising TimeoutError bubbled to the outer except handler, which left delivered=False, so `if not delivered:` re-sent the identical message via the standalone path. future.cancel() cannot un-send a request already in flight on the wire, so a slow confirmation deterministically produced a duplicate. The send was already dispatched onto the gateway loop, so a bare timeout is now treated as delivered (assume-delivered is safer than guaranteed-duplicate) and the standalone fallback is skipped. The live-adapter media attempt is also skipped on timeout since the contended loop would re-block each 30s media budget. NousResearch#47056 — silent drop when the gateway has an active session. The old check `if send_result is None or not getattr(send_result, "success", True)` let a result object missing a `success` attribute default to True = counted as a successful delivery, so the scheduler logged "delivered via live adapter" while the gateway never processed the message. Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit, truthy `success` attribute counts; None or a `success`-less object falls through to the standalone path so the message actually arrives. A genuine send Exception (not a slow confirmation) still falls through to the standalone path, and is caught by run_job's outer handler — it is recorded as the job's last_error and never crashes the cron ticker. NousResearch#43014 — deliver=origin fails to resolve in CLI sessions. A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and auto-detect / deliver=None) was unresolvable and emitted "no delivery target resolved" on every run. An unresolvable origin with no configured home channel is now treated as local (output stays in last_output), matching the documented auto-deliver contract; a concrete unresolvable platform target still reports a real error. Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's _confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local fallback. Tests rewritten as behavior contracts (timeout => no duplicate; None / success-less result => standalone fallback; confirmed success => no fallback; CLI origin => local, explicit platform => still errors). Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com> Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
… backends New agent/google_user_agent.py builds the real GeminiCLI User-Agent + X-Goog-Api-Client headers (version from local npm pkg, cached); gemini_cloudcode_adapter + gemini_native_adapter use them (replacing static hermes-agent UA). Scoped to the Gemini-CLI UA identity so Google's cloudcode-pa/GenLang backends accept the requests.
`cronjob(action='run')` (and `hermes cron run`) only set `next_run_at = now` and returned success, relying on the scheduler ticker to actually execute the job on its next tick. When no gateway/ticker is running — a CLI-only setup, or the Windows case in NousResearch#41037 — the job never executed: `run` reported success, but `last_run_at` stayed null forever, no output, no delivery. A manual `run` should actually run. `_execute_job_now` now: - **claims the job via `claim_job_for_fire`** — the same at-most-once CAS the scheduler/external-provider fire path uses. This both advances `next_run_at` for recurring jobs and blocks a concurrently-running gateway ticker from double-firing the same job; if the claim is lost, the run is skipped (the tool reports `execution_skipped`). This closes the double-fire race that a bare `advance_next_run` left open (a tick whose `get_due_jobs` already captured the job between trigger and advance would still fire it). - **delegates firing to `run_one_job`** — the single shared execute→save→deliver→mark body the ticker and external providers use — so failure delivery, `[SILENT]` handling, and live-adapter delivery stay identical across paths and can't drift. (The original salvage re-implemented this sequence inline and had already dropped failure delivery + `[SILENT]`.) The tool response carries `executed`, `execution_success`, and either `execution_error` or `execution_skipped`. The `hermes cron run` CLI message no longer claims "It will run on the next scheduler tick" — it reports the actual "Ran now: succeeded/failed" outcome (or the skip). Salvaged from NousResearch#41130 by @kyssta-exe (authorship preserved); reworked to reuse `claim_job_for_fire` + `run_one_job` per review rather than re-implementing the fire sequence inline. Adds tests for the claim-then-fire path, claim-lost skip, failure reporting, and exception capture. Fixes NousResearch#41037 Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
…elivery-confirm fix(cron): make live-adapter delivery confirmation reliable (NousResearch#38922, NousResearch#47056, NousResearch#43014)
A recurring cron job persists `next_run_at` as an absolute timestamp with a
UTC offset (e.g. `2026-05-19T21:00:00+10:00`). Cron expressions, however,
describe *local wall-clock* intent ("run at 21:00"). When Hermes/system
timezone changes after the timestamp was persisted, the stored instant is
re-interpreted in the new zone: `21:00+10:00` is the instant `13:00+02:00`,
which is `<= now` (13:02+02:00) — so the job fires HOURS EARLY, then
`compute_next_run` advances it via croniter to `21:00+02:00` the same day,
producing a SECOND fire. (NousResearch#28934, recurrence of NousResearch#24289.)
`_get_due_jobs_locked` now detects this precise migration case before the
due check: for a `cron` job whose converted instant looks due, whose stored
UTC offset differs from the current zone's, AND whose stored *wall-clock*
time is still in the future (distinguishing a migrated offset from a
genuinely missed run), it recomputes `next_run_at` from the schedule and
skips the early fire — preserving the local wall-clock intent.
Verified against the issue's reproducer: stored `21:00+10` under runtime
`+02:00` at wall-clock `13:02` is rescheduled to `21:00+02` instead of
firing early + again.
Salvaged from NousResearch#28941 by @Tranquil-Flow (authorship preserved). Chosen over
the alternative approaches (NousResearch#28951 normalize-to-UTC, NousResearch#28985 rebase-and-match)
because UTC-normalization does not change the absolute-instant comparison and
so does not fix the early fire, and this guard is the tightest: it only acts
when all four conditions hold and reuses the existing `compute_next_run`.
Fixes NousResearch#28934
…er (NousResearch#22773) PR NousResearch#22410 added three-mode Telegram topic routing to the live message path (TelegramAdapter.send via the gateway DeliveryRouter), but the cron delivery path never got it. cron/scheduler.py::_deliver_result sent through the live adapter with a bare ``{"thread_id": ...}`` and fell back to the standalone _send_telegram, neither of which addresses Bot API Direct Messages topics correctly. After Bot API 10.0 (2026-05-08), sending to a private chat with a bare ``message_thread_id`` is rejected/mis-routed, so cron deliveries to a private DM topic landed in the General topic instead of the requested lane. Fix: the cron live-adapter branch now routes the text send through the gateway's ``DeliveryRouter._deliver_to_platform`` — the same canonical path live messages use — so it inherits all three Telegram routing modes: 1. Forum/supergroup (negative chat_id) -> message_thread_id 2. Bot API DM topics (private chat_id + numeric topic id) -> direct_messages_topic_id (the case NousResearch#22773 reported) 3. Hermes-created named private DM-topic lanes -> ensure_dm_topic + reply anchor For mode 2, a private-chat target with a numeric topic id is passed as ``direct_messages_topic_id`` metadata (verified end-to-end: TelegramAdapter._thread_kwargs_for_send turns it into ``{message_thread_id: None, direct_messages_topic_id: <int>}``), instead of a bare message_thread_id. Forum/supergroup and home-channel deliveries are unchanged. The standalone fallback (gateway down) is preserved. No new config knob and no duplicated routing logic — this reuses the existing DeliveryRouter rather than reimplementing topic routing in the cron path. Salvaged from NousResearch#42051 (stepanov1975) and NousResearch#23249 (devsart95), which both diagnosed the missing three-mode routing in the cron/standalone path; reimplemented onto the canonical DeliveryRouter that landed since those PRs were opened. Co-authored-by: Alex <9785479+stepanov1975@users.noreply.github.com> Co-authored-by: devsart95 <devsart95@gmail.com>
…elegram-dmtopic fix(cron): route Telegram DM-topic cron delivery through DeliveryRouter (NousResearch#22773)
…run-immediate fix(cron): execute job immediately on action=run
…tz-offset-repair fix(cron): repair migrated timezone offsets to prevent double-fire
…dialog NousResearch#43496 added a per-provider hide-all sentinel ('provider::') so emptying a provider in the Edit Models dialog stopped re-expanding its defaults. That fixed the single-provider case, but the dialog's toggle handler seeds its working set from effectiveVisibleKeys(), which strips ALL sentinels before returning. So persisting after any toggle silently dropped every OTHER provider's hide-all sentinel; those providers then looked 'never customized' and re-enabled all their models on the next render. Split resolution into two functions: - resolveVisibleKeys(): stored keys + curated default expansion, with hide-all sentinels PRESERVED — the canonical working set the toggle handler mutates and persists. - effectiveVisibleKeys(): resolveVisibleKeys() then strips sentinels, for display only (unchanged contract). Move the toggle set-computation into a pure, unit-tested toggleModelVisibility() that seeds from resolveVisibleKeys(), so sibling sentinels survive the persist. Add regression tests that drive the real toggle handler across multiple providers. Follow-up to NousResearch#43496; completes the fix for NousResearch#43485 (cross-provider case).
Follow-up to the salvaged NousResearch#47450 fix: - Extract expandProviderDefaults() so the curated-default expansion rule lives in one place (was duplicated between defaultVisibleKeys and resolveVisibleKeys). - Drop the redundant new Set() wrap in toggleModelVisibility (resolveVisibleKeys already returns a fresh Set; effectiveVisibleKeys already relied on this). - Document the intentional re-enable behavior (re-enabling one model of a hidden-all provider restores only that model, not the curated defaults) and tighten the toggleModelVisibility JSDoc. - Add 7 hardening tests: re-enable-restores-only-that-model, full hide/re-enable round-trip, empty-non-null stored, single toggle-off from null defaults, zero-model provider, and direct resolveVisibleKeys null/empty assertions.
…l-visibility-cross-provider-47450 fix(desktop): preserve other providers' hide-all in model visibility dialog (salvage NousResearch#47450)
A server that doesn't implement the optional 'ping' utility answers a keepalive ping with JSON-RPC method-not-found. _is_method_not_found_error latches that condition so the probe falls back to list_tools instead of reconnect-looping. The substring fallback only matched 'method not found' / '-32601' / 'not found: ping'. Servers that surface method-not-found as the common 'Unknown method: <name>' phrasing without a structural -32601 code (e.g. agentmemory's MCP server) slipped through, so the fallback never latched and the keepalive reconnect-looped every cycle. Add 'unknown method' to the substring fallback so the ping->list_tools keepalive fallback latches for these servers too. Fixes NousResearch#50028.
…ch#50028) Two regression tests for the agentmemory reconnect-loop: - _is_method_not_found_error matches the plain 'Unknown method: ping' phrasing (no structural -32601 code). - _keepalive_probe latches _ping_unsupported and falls back to list_tools when send_ping raises 'Unknown method: ping', instead of propagating (which would reconnect-loop).
Adds hermes_cli/context_switch_guard.py mirroring the model_cost_guard pattern. When a user switches models mid-session (Herm TUI picker, CLI, or /model on Telegram/Discord), the warning surfaces on the existing ModelSwitchResult.warning_message path used by the expensive-model guard if the new model's compression threshold is below the current session size. Partial fix for NousResearch#23767 — addresses only the 'user-facing guardrail when switching from a high-context provider to a substantially lower-context provider' slice. The other proposed fixes from that issue (hard preflight token guard, metadata cache invalidation on switch, compression safety invariant, oversized tool-output handling) are out of scope for this PR.
… TUI warning field Follow-up to the salvaged preflight-compression warning: - Replace silent `except Exception: pass` at all 5 guard call sites (cli.py x2, gateway/slash_commands.py x2, tui_gateway/server.py) with `logger.debug(...)` so signature drift in the guard helper isn't hidden. - tui_gateway/server.py: set the confirm dict's `warning` field to the merged message (was bare expensive-model text) so it matches `confirm_message` for any future consumer reading `warning`. - Add trailing newlines to the two new files.
…-switch-preflight-warning fix(cli): warn when in-session model switch will preflight-compress
The gateway only rewrote gateway_state.json on lifecycle transitions
(start/connect/drain/stop), never on turn start/end. Live-verified on a
hosted agent: a confirmed end-to-end turn ran while gateway_updated_at
stayed frozen at boot and active_agents was absent — so any active_agents
read from the file between transitions is stale. That makes it unusable
as a busy/idle signal for an external consumer (NAS deciding whether it's
safe to restart/migrate/auto-update an agent mid-turn).
Add _persist_active_agents(), called at every turn boundary:
- turn start: both running-agent sentinel-claim sites (normal inbound
message path + startup-resume path)
- turn end: the central _release_running_agent_state() choke point
(covers normal completion, /stop, /reset, sentinel cleanup,
stale-eviction — every path that ends a running turn)
It passes ONLY active_agents to write_runtime_status, leaving
gateway_state (and every other field) _UNSET so the read-merge-write
preserves the current lifecycle state. Passing gateway_state=None would
clobber it — hence a dedicated helper rather than reusing
_update_runtime_status. The write is the same cheap JSON write done on
lifecycle transitions today; best-effort (a failed status write never
disrupts a turn).
Behaviour-contract test: an active_agents-only write preserves both
running and draining gateway_state, and the count clamps non-negative.
Give an external consumer (NAS) a trustworthy, always-reachable busy/idle
readout it can poll before a disruptive lifecycle action (restart,
migrate, stop, auto-update). The dashboard /api/status is the only HTTP
surface guaranteed up on a hosted agent regardless of which gateway
platforms are enabled, and it already reads gateway_state.json.
Add to /api/status (additive, non-breaking):
- active_agents — in-flight gateway-turn count (now refreshed
per-turn by the companion gateway-side commit)
- gateway_busy — running AND active_agents > 0
- gateway_drainable — running and live (a valid begin-drain target)
- restart_drain_timeout — resolved seconds, so the consumer can size its
poll deadline without out-of-band knowledge
(env HERMES_RESTART_DRAIN_TIMEOUT → config
agent.restart_drain_timeout → default)
The busy/drainable contract is defined once in gateway.status
(derive_gateway_busy / derive_gateway_drainable) and consumed by both
/api/status and /health/detailed so the two surfaces can never disagree.
Liveness keys off gateway_running (a live PID/health probe), NEVER
gateway_updated_at — a healthy idle gateway never advances that timestamp.
All derived fields degrade to safe falsy values when the gateway is down
or the status file is absent/corrupt (never a spurious "busy" that would
wedge the consumer). active_sessions (the 5-min DB recency heuristic the
SPA reads) is left exactly as-is — new signal, new fields.
Tests (behaviour contracts, not snapshots): the pure derivation contract
across every running/state/count/liveness combination; /api/status
integration for busy, idle-drainable, draining, down, stale-busy-file,
corrupt-count, and timeout surfacing; and /health/detailed parity.
…nts parse Follow-up cleanups on top of the busy/idle readout (PR NousResearch#50103): - web_server.py /api/status reused the single drain-timeout resolver hermes_cli.gateway._get_restart_drain_timeout() (HERMES_RESTART_DRAIN_TIMEOUT env -> agent.restart_drain_timeout config -> default) instead of inlining a third hand-rolled copy of that precedence chain. Also fixes a subtle divergence: the inline copy used os.environ.get() so a set-but-empty env var was treated as a value rather than falling through to config; the shared resolver .strip()s and falls through correctly. - Added gateway.status.parse_active_agents() and routed BOTH HTTP surfaces (/api/status and /health/detailed) through it, so the exposed active_agents field is consistently clamped non-negative. Previously /api/status clamped while /health/detailed exposed the raw file value, diverging on a corrupt count. - Added TestParseActiveAgents covering the shared coercion contract.
…ive_agents; harden drain-timeout fallback Second cleanup pass (simplify-code review of the first follow-up): - write_runtime_status now clamps active_agents via parse_active_agents instead of an inline max(0, int(...)). Removes the duplicated clamp the helper's docstring acknowledged AND closes a write-side ValueError gap (a non-numeric active_agents previously raised; now degrades to 0). - hermes_cli/gateway.py draining-status line routes its active-agents count through parse_active_agents too — the third coercion site of the same persisted field, now consistent and non-raising with the two HTTP surfaces. - web_server.py /api/status: the drain-timeout resolver fallback now catches ImportError specifically and falls back to DEFAULT_GATEWAY_RESTART_DRAIN_TIMEOUT (a real float) instead of a blanket 'except Exception -> None'. None would have violated the surfaced field's int/float contract and stripped NAS's poll-deadline hint silently. - Dropped a redundant 'if runtime else 0' branch (parse_active_agents already handles the empty/None case) and tightened the parse_active_agents docstring to describe the actual single-contract role (write + both reads).
…way-busy-readout-50103 feat(gateway+dashboard): busy/idle readout for safe lifecycle actions (salvage NousResearch#50103)
…sResearch#23767) The tool-result persistence budget was a fixed 100K chars/result and 200K chars/turn regardless of the active model. On a small-context model (e.g. a 65K-token local model switched into mid-session) a single large tool result (reporter: a 279K-char search result) or a full 200K-char turn (~50K tokens) could by itself approach or exceed the window, forcing an oversized request that the provider rejects as "Prompt too long". - budget_config.budget_for_context_window() scales per-result/per-turn char caps to a fraction of the model window, clamped to the historical 100K/200K defaults (large models unchanged) and floored so small models stay usable. - resolve_threshold() now caps the per-tool registry value at default_result_size so tools that register a fixed 100K cap (web/terminal/x_search) don't re-inflate a scaled-down budget. No-op for the default budget (both 100K). - tool_executor wires the agent's live context_length (recomputed on model switch) into all four persist/turn-budget call sites. read_file stays inf-pinned (no persist loop). Verified E2E: a 279K-char result against a 65K model collapses to a ~1.6K preview; a 200K model is byte-identical to today.
…ch#23767) ContextCompressor.update_model() recomputed context_length/threshold/budgets but kept the cross-call calibration state (last_real_prompt_tokens, last_rough_tokens_when_real_prompt_fit, last_compression_rough_tokens, awaiting_real_usage_after_compression, _ineffective_compression_count) from the PREVIOUS model. Those fields encode 'the provider proved this prompt fit' / 'preflight can be deferred' decisions valid only for the model that produced them. Carried across a switch to a smaller-context model, should_defer_preflight_to_real_usage() used the old model's 'it fit' history to SKIP a preflight compression the new model actually needed — sending an oversized prompt the provider rejects (NousResearch#23767). update_model() now clears that state; the new model's first response repopulates it via update_from_response(). Verified E2E: after a 200K->65,536 switch, defer no longer suppresses and should_compress fires on an over-threshold estimate.
…ct provider (NousResearch#50480) * fix(agent): strip stale reasoning_content when falling back to a strict provider A reasoning primary (DeepSeek/Kimi/MiMo thinking mode) pins reasoning_content on every assistant tool-call turn (a single space " " pad). api_messages is built once under the primary; on a mid-session fallback to a strict OpenAI-compatible provider (Mistral, Cerebras, Groq, SambaNova), those stale pads were replayed verbatim and rejected with HTTP 400/422: body.messages.2.assistant.reasoning_content: Extra inputs are not permitted (input: ' ') reapply_reasoning_echo_for_provider() only ever ADDED pads, so it never reconciled history built under a reasoning primary against a strict fallback. copy_reasoning_content_for_api() also leaked empty-string and 'reasoning'-only shapes to non-pad providers. Fix both sites: when the active provider does not enforce echo-back, strip reasoning_content (empty, space-pad, or non-empty) entirely. Re-padding when switching TO a reasoning provider is preserved. Covers the Cerebras 400 from NousResearch#45655 and the DeepSeek->Mistral 422 fallback report. Refs NousResearch#45655. * test: update reasoning-replay tests for strict-provider stripping test_explicit_reasoning_content_beats_normalized_reasoning_on_replay was implicitly running on the OpenRouter fixture (non-pad); pin it to a reasoning provider so the precedence it checks is observable. Add a positive strict-provider test asserting reasoning_content is stripped on replay.
logging.basicConfig() in TrajectoryCompressor.__init__ overrides the root logger configuration every time the class is instantiated. Library code should use logging.getLogger(__name__) and let the application entry point configure the root logger. Fixes inconsistent log formatting when the compressor is used alongside other logging configuration in the gateway.
… main Same library-code anti-pattern as the compressor fix: MiniSWERunner.__init__ called logging.basicConfig(), overriding the application's root logger config every time a runner was instantiated. Moved the call into main() (the CLI entry point) where it belongs; __init__ now only does getLogger(__name__). Standalone verbose logging is preserved.
Remove the dashboard --insecure auth-bypass, add an MCP persistence guard + IOC blocklist, and raise the API-server key entropy floor. Driven by the June 2026 hermes-0day campaign (r/hermesagent, live 854.media instance): scanners find exposed Hermes dashboards/API servers, drive the root agent to plant a 'command: bash' MCP entry that appends an attacker SSH key to authorized_keys, which cron + startup then re-execute every tick. - dashboard: --insecure no longer disables the auth gate. should_require_auth returns True for every non-loopback bind; a public bind ALWAYS requires an auth provider (bundled password provider or OAuth). --insecure kept as a warned no-op for backward compat. Fail-closed error now points at the password provider, not at --insecure. - mcp_security: validate_mcp_server_entry now also rejects shell payloads that write to OS persistence surfaces (authorized_keys/.ssh/pam.d/sudoers/cron/ rc files) and hard-rejects a hermes-0day IOC blocklist (attacker SSH key + source IPs) anywhere in command/args/env. Runs at save AND spawn time. - api_server: raise network-bind API_SERVER_KEY entropy floor 8->16 chars; warn when a network-accessible API server runs an unsandboxed local backend.
The s6 dashboard entrypoint and docker integration tests relied on HERMES_DASHBOARD_INSECURE=1 to bring up a 0.0.0.0 dashboard with no auth provider. With --insecure now a no-op (auth gate mandatory on non-loopback binds), that path fails closed. - s6 dashboard/run: drop --insecure derivation; warn that the env is a no-op and point operators at HERMES_DASHBOARD_BASIC_AUTH_* / OAuth. - docker tests: supervision tests now register the bundled basic password provider (HERMES_DASHBOARD_BASIC_AUTH_USERNAME/_PASSWORD) so the gate has a provider and the dashboard binds. Rewrote the insecure-opt-out test to assert fail-closed (dashboard does NOT serve) instead of gate-bypass. - docs (en + zh-Hans): HERMES_DASHBOARD_INSECURE documented as deprecated no-op; basic-auth is the zero-infra way to authenticate a containerized public dashboard.
Surface dangerous host/deployment posture at gateway startup so operators get the 'you're exposed' signal the June 2026 MCP-config persistence campaign victims never had. Warn-only — never blocks startup, never raises. Checks (each independently fail-safe): - Running as root (POSIX uid 0) - SSH daemon with PasswordAuthentication enabled (incl. the 'yes' default) - Running in a container with no persistent volume mount over HERMES_HOME - Network-accessible API server with no API_SERVER_KEY New module hermes_cli/security_audit_startup.py; invoked once per process from start_gateway() right after setup_logging(). Cross-platform (root/SSH checks no-op on Windows). Idea: @cthulhu.
…r grace A daemon that ignores or stalls in its SIGTERM handler currently survives the process-registry reap and leaks until reboot (observed as agent-browser daemons accumulating to EMFILE on long-running gateways). _terminate_host_pid now snapshots the tree, SIGTERMs it, waits a bounded grace window (terminal.daemon_term_grace_seconds, default 2.0s, 0 disables), then SIGKILLs any survivor. The recycled-PID identity guard still gates the whole path, so escalation never reaches a stranger; Windows is unchanged (taskkill /F is already a hard kill). Config lives in config.yaml (terminal.daemon_term_grace_seconds), NOT an env var, per the .env-secrets-only policy. Implements the SIGKILL-escalation idea from @tkwong's NousResearch#15008, reworked onto the current _terminate_host_pid tree-kill path (the original predated it) and config-gated instead of env-var-gated. Co-authored-by: Benjamin Wong <tkwong@inspiresynergy.com>
…cs survivors Live testing against a real SIGTERM-ignoring process TREE (parent + children, the agent-browser daemon + renderer shape) revealed psutil.wait_procs's gone/alive partition mis-handles a parent/child tree: it reaps via Process.wait() and could mark targets gone/alive inconsistently across the tree, leaving survivors un-killed (flaky — sometimes the parent lived, sometimes a child). Replace it with: sleep out the grace window, then directly re-probe every captured target (_proc_alive, treating zombies as dead) and SIGKILL any that's still running. Add a multi-child-tree regression test. 6/6 escalation tests green across repeated runs; the real-tree E2E now kills the full tree 6/6 runs.
…ousResearch#50497) The welcome banner's 'Available Tools' merged in every toolset from the global check_tool_availability() registry walk, regardless of whether it was enabled for the current platform. On a Blank Slate CLI (file + terminal only) that surfaced discord / feishu / kanban tools the agent was never actually given — they are not in the agent's tool schema, but the banner displayed them, making it look like they were exposed. - Filter the unavailable-toolset merge to toolsets actually in enabled_toolsets (a toolset that's enabled but has unmet deps still legitimately shows as disabled/lazy). - Gate the 'Available Skills' section on the skills toolset being enabled — when it's off, the agent can't load any skill, so show 'Skills toolset disabled' instead of the on-disk catalog. When enabled_toolsets is empty (older callers), behavior is unchanged. Validation: blank-slate banner now shows only file + terminal and 'Skills toolset disabled'; a skills-enabled banner still lists the catalog. Added regression tests; full banner suite green (15/15).
…providers (NousResearch#50492) * feat(providers): remove google-gemini-cli + google-antigravity OAuth providers Google now actively bans accounts for third-party tools that piggyback on Gemini CLI / Antigravity / Code Assist OAuth, and because abuse prevention sits at a backend layer the ban can extend to the entire Google account (Gmail/Drive), with a second violation being permanent. Ref: google-gemini/gemini-cli#20632 Removes both OAuth inference providers entirely (modules, provider profiles, auth/runtime/config/models wiring, the /gquota Code Assist quota command, the antigravity-cli optional skill, desktop + docs surface in en + zh-Hans). The API-key 'gemini' provider (GOOGLE_API_KEY/GEMINI_API_KEY against generativelanguage.googleapis.com) is unaffected and stays fully supported. * fix(skills): keep the antigravity-cli skill — only the OAuth provider is removed The antigravity-cli optional skill orchestrates the external `agy` binary as a coding-agent tool via the terminal tool — it does NOT wrap Hermes inference through the banned google-antigravity OAuth provider, so it carries none of the account-ban risk that motivated removing that provider. Restore the skill, its docs page, the sidebar entry, and the optional-skills catalog row. The google-antigravity / google-gemini-cli inference providers stay fully removed.
gateway/platforms/telegram.py no longer exists (adapters moved to plugins/platforms/<name>/adapter.py) and telegram no longer uses the scoped-lock pattern. Point the token-lock canonical-pattern reference to plugins/platforms/irc/adapter.py, which acquires the lock in connect() and releases it in disconnect() — and is already cited as a canonical example in ADDING_A_PLATFORM.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns/platforms Sibling-site follow-up to the AGENTS.md token-lock fix (NousResearch#50481). Platform adapters migrated from gateway/platforms/<name>.py to plugins/platforms/<name>/adapter.py; a handful (signal, weixin, bluebubbles, qqbot, yuanbao, msgraph_webhook, webhook, api_server) still live in gateway/platforms/. - adding-platform-adapters.md: new-adapter creation path + reference-impl table - gateway-internals.md: rewrite the adapter tree to reflect the actual split - zh-Hans mirrors of both kept in parity - scripts/release.py: add TutkuEroglu to AUTHOR_MAP (CI gate)
…usResearch#50534) Plugins shelling out to bare `hermes` via the terminal tool hit `command not found` (exit 127) when the gateway was launched without the hermes install dir on PATH (systemd, service managers, cron, desktop launchers) — even though `hermes` works in the user's own interactive terminal, which sources the shell rc that exports that dir. The terminal tool's subshell PATH was the agent process PATH plus a static set of system dirs (_SANE_PATH); it never included wherever the hermes console-script actually lives (~/.local/bin, the venv bin/Scripts, pipx, nix). Resolve that dir once (which/argv0/sys.executable) and prepend-if-missing it so bare `hermes` resolves regardless of launch method.
NousResearch#50499) * feat(cli): /reasoning full to show complete thinking, not 10-line clamp The post-response Reasoning recap box hard-clamped long thinking to the first 10 lines, so there was no way to see the full reasoning trace after a turn (live streaming already shows it in full). Add display.reasoning_full (default off) plus /reasoning full|clamp to toggle it at runtime; the clamp truncation note now points at the command. Addresses repeated user requests to show all thinking tokens. * test(gateway): de-snapshot /reasoning help assertion The test froze the exact args-hint literal '/reasoning [level|show|hide]', which the new full/clamp args change to '[level|show|hide|full|clamp]'. Convert to an invariant: assert /reasoning is in help and carries its core args, not the exact hint string. * feat(tui): /reasoning full|clamp parity in tui_gateway The classic-CLI reasoning_full toggle had no TUI equivalent — typing /reasoning full in the TUI fell through to parse_reasoning_effort and errored. The TUI renders thinking as an expand/collapse section (no fixed 10-line recap), so map full -> sections.thinking=expanded (raw, uncapped via thinkingPreview mode='full') and clamp -> collapsed, persisting display.reasoning_full for cross-surface config consistency.
…h#50509) * feat(cli): /prompt — compose your next prompt in $EDITOR Adds /prompt (alias /compose): opens $VISUAL/$EDITOR on a temp markdown file so you can hand-edit a multi-line prompt, then sends the saved buffer as the next agent turn. Text after the command pre-seeds the buffer; an empty save cancels. Reuses the one-shot _pending_agent_seed the interactive loop already consumes (same mechanism as /blueprint), so no changes to the input event loop or message pipeline. CLI-only. * feat(tui): /prompt slash command opens $EDITOR (parity with CLI) The TUI already opens $EDITOR via Ctrl+G (openEditor), but had no /prompt slash command like the classic CLI. Wire openEditor into the slash handler context and register /prompt (alias /compose) to call it; inline text after the command is dropped into the composer first so it carries into the editor, matching the CLI's /prompt <text>.
…ind (NousResearch#50551) When `hermes dashboard --host 0.0.0.0` is run interactively with the auth gate engaged but no DashboardAuthProvider configured, prompt to set up the bundled username/password provider on the spot (or point at `hermes dashboard register` for OAuth) instead of only emitting the fail-closed error. - main.py: `_maybe_setup_dashboard_auth_interactively()` runs before start_server. No-ops on loopback binds, when a provider is already registered, or when stdin/stdout isn't a TTY (Docker/s6, CI, piped runs) so the fail-closed SystemExit stays the backstop for unattended deploys. On the password path it writes dashboard.basic_auth.{username,password_hash,secret} to config.yaml (scrypt hash, never plaintext), then force-rediscovers plugins so the basic provider registers before the gate check. - web_server.py: fix the fail-closed hint — it told operators to set `dashboard_auth.basic.username` but the provider reads `dashboard.basic_auth`. - docs: note the interactive setup under Fail-closed semantics. No new env vars; reuses the existing dashboard.basic_auth config surface.
# Conflicts: # agent/gemini_cloudcode_adapter.py
arminanton
added a commit
to arminanton/hermes-agent
that referenced
this pull request
Jun 22, 2026
…LE) + reproducible delta map - GITHUB-MERGEABLE-AUDIT.md: GitHub mergeable=40/41 (only NousResearch#50111 manifest conflicts). 6 PRs (NousResearch#50296/NousResearch#49644/NousResearch#50041/NousResearch#50073/NousResearch#50064/NousResearch#50033) genuinely conflicted on current origin/main (drifted past v0.17.0); each rebased (1-file complementary conflict), now MERGEABLE. - DELTA-MAP-v017.md: reproducible per-file map (PR diffs vs v0.17.0, fresh tips): 160 = 137 in-PR + 21 DISCARD + 2 upstream-NousResearch#29433 + 0 orphans, sum verified.
arminanton
added a commit
to arminanton/hermes-agent
that referenced
this pull request
Jun 22, 2026
…afety proof Addresses the Council demand for SEMANTIC correctness (tests pass after resolution), not just compile-clean. Per-PR own-test results on resolved v0.17.0 (V017-PER-PR-TEST-RESULTS.txt): NousResearch#49644 10 passed | NousResearch#50033 165 gemini passed | NousResearch#50056 218 passed NousResearch#50073 9 passed (INTENT-INFERRED resolution PROVEN correct: hygiene=400 kept) NousResearch#50296 13 passed | NousResearch#50064 61 passed, 1 FAILED The single NousResearch#50064 failure is characterized precisely (not hacked): test test_routed_client_preserves_openai_sdk_default_headers asserts pre-v0.17.0 copilot routing internals that v0.17.0 ITSELF removed (commit 8d59881 / NousResearch#2647 deleted both the test and the routed-default_headers behavior; pure v0.17.0 has 0 occurrences). Documented as a forward-compat test-removal on rebase, not a regression. NousResearch#50064's feature is intact (61/62). DISCARD ratification (NON-CONTRIBUTABLE.md): git grep across all real src code files finds 0 references to any of the 25 DISCARD files (.bak/.project-intel/transcripts) — proof they are safely droppable. v017-conflict-resolutions/README.md documents all 6 resolution strategies.
arminanton
added a commit
to arminanton/hermes-agent
that referenced
this pull request
Jun 22, 2026
…script Addresses Council items 2 & 3: Item 3 (justify every resolution against PR intent): V017-RESOLUTION-JUSTIFICATION.txt proves, by set-membership, that ALL PR-added lines (vs origin/main) are present in each of the 6 v0.17.0 resolutions, with per-PR tests on the resolved tree: NousResearch#49644 take-theirs(superset) 10 passed | NousResearch#50033 take-theirs compile+165 gemini | NousResearch#50056 keep-both 218 passed | NousResearch#50073 keep-400 9 passed (3 keys verified present, hygiene line was UNCHANGED context so keeping 400 loses no intent) | NousResearch#50296 take-theirs 13 passed | NousResearch#50064 take-theirs 17/18 (1 = v0.17.0 upstream removal NousResearch#2647, NOT lost intent = Q2). Item 2 (delivery mechanism): APPLY-RESOLUTIONS-ON-v0.17.0.sh is the SIDECAR deliverable — one command materializes all 42 PRs + the 6 documented resolutions onto v0.17.0. Verified end-to-end: 0 residual markers, 0 compile failures. Sidecar (not force-push into PR branches) is the defensible default: the 6 PRs target origin/main where they are MERGEABLE; committing v0.17.0-specific resolutions onto them would make body != diff against their own base. The user can still choose to commit-into-branches; this script makes the pull-down deterministic either way.
Contributor
Author
|
Withdrawing on safety grounds, following #50492. This PR presents the |
arminanton
added a commit
to arminanton/hermes-agent
that referenced
this pull request
Jun 22, 2026
…i-cli-UA Per @teknium1 (NousResearch#50039: agy-cli superseded by merged native antigravity NousResearch#50454) and NousResearch#50492 (removed google-gemini-cli + google-antigravity OAuth providers for account-ban safety), the agy-cli direction and the gemini-cli-UA spoof are withdrawn: - CLOSED NousResearch#50555, NousResearch#50657 (agy-cli), NousResearch#50033 (gemini-cli-UA) on GitHub. - 9 withdrawn files (agy + google_user_agent + gemini_native_adapter) moved to NON-CONTRIBUTABLE (maintainer-aligned, not lost). - 3 shared files (gemini_cloudcode_adapter.py, auth.py, runtime_provider.py) reassigned to live NousResearch#49644 (verified present in its diff). Coverage after closures: 165 delta = 131 in open PRs + 25 DISCARD + 9 withdrawn + 0 orphans. Full disposition in MAINTAINER-FEEDBACK-DISPOSITION.md.
arminanton
added a commit
to arminanton/hermes-agent
that referenced
this pull request
Jun 22, 2026
…r on v0.17.0 verification/stack-apply-v017.sh applies ALL 39 open code PRs onto ONE v0.17.0 tree (33 net-diff 3-way + 6 pre-resolved v017-patches): 39/39 CLEAN, 0 conflicts, 135 changed .py compile 0 failures, test slice 295 passed/0 failed. The set-level run CAUGHT two stale-set bugs a per-PR matrix can't: a closed PR (NousResearch#50033) wrongly in the order, and an omitted PR (NousResearch#50056); both fixed. Regenerated APPLY-ORDER.md to the current 39-open-PR set + v017-patches mechanism (was stale: closed NousResearch#50039/NousResearch#50049/ NousResearch#50033 + old forward-compat branches). The 1 pre-existing v0.17.0 kanban flake is upstream (fails on bare v0.17.0 in isolation), not PR-introduced.
arminanton
added a commit
to arminanton/hermes-agent
that referenced
this pull request
Jun 22, 2026
…03 open-PR + 2056 BucketC + 1318 maintainer-closed agy/gemini-UA + ~100 excluded/cosmetic, 0 homeless) + reproducible script; CORRECTS stale Bucket B (NousResearch#50555/NousResearch#50033 are CLOSED)
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
Makes Hermes present the authentic
@google/gemini-cliidentity on Google Code Assist (cloudcode-pa) and Generative Language API requests, so Google's backends (which reject unknown User-Agents on some routes) accept them.agent/google_user_agent.py: builds the realGeminiCLI/<ver>/<model> (<platform>; <arch>; <surface>)User-Agent and theX-Goog-Api-Client: gl-node/<node> gccl/<ver>header, with the package version resolved from the locally-installed npm package (cached on disk, same pattern ascopilot_auth).agent/gemini_cloudcode_adapter.py+agent/gemini_native_adapter.py: use those builders for the request headers (replacing the prior statichermes-agentUA strings).Scoped strictly to the Gemini-CLI User-Agent identity. Draft for review.