Commit graph

1851 commits

Author SHA1 Message Date
teknium1
acfefa4fda feat(sessions): full prune-filter set + --redact on sessions export
- export now shares _add_session_filter_args / build_prune_filters with
  prune/archive: AGE grammar (5h/2d/1w/ISO) on --older-than plus the full
  filter set (--model, --provider, --min-messages, --min-cost, --branch,
  --chat-id, ...) for both JSONL and md/qmd bulk exports; --dry-run works
  on JSONL too; removes the one-off list_export_candidates helper.
- new --redact flag runs exported message content and tool output through
  force-mode secret redaction (agent.redact) for jsonl, md, and qmd.
- docs EN + zh-Hans updated; new tests for AGE grammar, extended filters,
  filtered JSONL, and redaction.
2026-07-07 12:36:41 -07:00
teknium1
f3c27e30eb refactor(sessions): fold Markdown/QMD export into 'sessions export --format'
Replaces the parallel export-md subcommand from the salvaged commit with
a --format jsonl|md|qmd flag on the existing export subcommand, so all
session export formats share one surface. Adds AUTHOR_MAP entry.

Salvage follow-up for PR #59542 by @web3blind.
2026-07-07 12:36:41 -07:00
web3blind
91885a32b3 feat(sessions): export sessions to markdown 2026-07-07 12:36:41 -07:00
Teknium
491689784e feat: add uninstall dry-run mode
Port from qwibitai/nanoclaw#2719: let operators preview the uninstall plan without stopping services or deleting files.
2026-07-07 05:12:24 -07:00
teknium1
ba865e4038 refactor(setup): route dependency installs through the canonical uv→pip→ensurepip ladder
Replace the hand-rolled ensurepip bootstrap (and five other one-off
pip-install code paths) with hermes_cli.tools_config._pip_install, which
prefers the bundled uv (fast, needs no pip in the venv), falls back to
python -m pip, and bootstraps pip via ensurepip only when missing.

Sites unified:
- hermes_cli/setup.py: _install_neutts_deps, _install_kittentts_deps,
  modal SDK install, daytona SDK install
- hermes_cli/memory_setup.py: memory-plugin pip deps (previously dead-ended
  when uv AND pip binaries were both absent)
- hermes_cli/dingtalk_auth.py: qrcode auto-install (previously invoked
  'python -m uv' which is not how uv ships)
- agent/lsp/install.py: --target LSP server installs
- plugins/google_meet/cli.py, plugins/platforms/matrix/adapter.py,
  plugins/platforms/google_chat/oauth.py, plugins/memory/honcho/cli.py

Tests updated to assert the ladder behavior (uv-first, pip fallback,
ensurepip bootstrap) instead of the removed bespoke branches.
2026-07-07 04:09:35 -07:00
teknium1
87b65e24a7 refactor(compression): scope Codex-native compaction to the app-server runtime
Drop the Responses-API native compaction path and its opt-in umbrella
flag from the salvaged feature. On the Codex OAuth chat route Hermes
owns the message list and the summary compressor works (and stays
provider-portable — encrypted compaction items would lock the session
history to chatgpt.com and break /model switches and provider
fallback). On the app-server runtime (codex CLI/agent) the codex agent
owns the real thread context, so thread/compact/start is the only
mechanism that can actually shrink it (#36801) — that path is now the
default behavior for codex_app_server sessions, controlled by
compression.codex_app_server_auto (native|hermes|off), no umbrella
flag.

Removed: responses.compact() call path, codex_compaction_items replay/
persistence plumbing, codex_native_compaction + codex_responses_threshold
config keys, desktop settings fields, and their tests. Kept: everything
app-server (compact_thread(), compaction notifications, bookkeeping,
docs, tests) plus cache-busting keys for the surviving knobs.
2026-07-07 02:39:54 -07:00
hmirin
d1c8c03416 feat(agent): add Codex-native compaction paths 2026-07-07 02:39:54 -07:00
Teknium
299d5c6603 fix(cli): safe mode also skips shell-hook registration
--safe-mode promised to disable ALL customizations, but shell hooks
declared in config.yaml's hooks: block registered anyway —
register_from_config() runs independently of plugin discovery and
load_config() does not honor HERMES_IGNORE_USER_CONFIG. Gate it on
HERMES_SAFE_MODE at the single chokepoint so troubleshooting runs fire
zero user-configured code (plugins, MCP, and hooks).

Docs (en + zh) updated; positive + negative tests added.
2026-07-07 02:32:32 -07:00
Teknium
fc02b1c276 refactor(cli): simplify safe-mode startup wiring
Since safe mode already landed on main via #45488, reduce this branch to cleanup: centralize env setup, remove duplicated comments, and tighten tests.
2026-07-07 02:32:32 -07:00
Teknium
76979a0869
fix(auth): per-profile Anthropic OAuth file + complete port-binding platform set (#57563 salvage) (#59339)
* fix(auth): resolve Anthropic OAuth file per-profile + close port-binding platform gaps

Two focused pieces salvaged from PR #57563:

1. _HERMES_OAUTH_FILE was computed at module import time — frozen before
   HERMES_HOME/profile overrides, so multiplexed profile turns read and
   wrote the DEFAULT profile's .anthropic_oauth.json (OAuth path hijack).
   Replaced with a lazy _get_hermes_oauth_file(); all web_server.py call
   sites updated.

2. _PORT_BINDING_PLATFORM_VALUES was missing whatsapp_cloud and line —
   both bind aiohttp TCP listeners, so a secondary multiplex profile
   enabling them would collide with the primary's listener instead of
   failing fast at startup.

Original work by @austinlaw076. The rest of #57563 was redundant on
main (adapter routing sweep superseded by #56854's salvage; cron secret
scope landed in fdab380a1; nested-config fallback in from_dict).

* chore(release): map austinlaw076 author email for PR #57563 salvage

* test(hermes_cli): patch _get_hermes_oauth_file instead of removed _HERMES_OAUTH_FILE constant

---------

Co-authored-by: Austin <austin@openvm067.space>
Co-authored-by: Ben <ben@nousresearch.com>
2026-07-07 08:33:06 +00:00
Ben
5eac665252 feat(status): expose nous_session_valid on /api/status for hosted-agent self-heal
A hosted agent whose Nous bootstrap session dies terminally (invalid_grant /
quarantine) looks HEALTHY to every liveness/connectivity probe — the machine,
relay ws, and dashboard all stay up — yet every inference turn hard-fails with
a provider-auth error until a human re-logs-in. Nothing currently surfaces that
condition to NAS.

Add get_nous_session_validity() (valid|terminal|unknown), classified from local
auth-store state (no working token required), and report it on the public
/api/status payload. NAS's 2-min health sweep reads it and re-mints the
bootstrap session in place on 'terminal'.

Anti-flap: only a terminal failure (relogin_required / persisted quarantine
marker with tokens cleared) maps to 'terminal'; transient/mid-rotation blips and
merely-expiring tokens report 'unknown' so a healthy box never triggers a
spurious re-mint.

Part of the hosted-agent bootstrap-session self-heal (NAS side reads this field).
2026-07-07 00:53:56 -07:00
Ben
182256206a test: drop worktree-path sanity guard that fails in CI
The test_module_resolves_to_this_worktree guard asserted auth.__file__ contained
'worktrees/bootstrap-h2-logging' — a local dev crutch to defeat the editable-
install trap (venv points at the main checkout). In CI the code lives at
/home/runner/work/... so the assertion always fails. It never belonged in the
committed suite; the 5 behavioural tests are what matter.
2026-07-07 00:53:48 -07:00
Ben
444dc0da89 feat(auth): log forensic detail at Nous quarantine so terminal auth death is visible
A NAS-hosted Fly agent's Nous bootstrap session can take a terminal
invalid_grant and get quarantined in _quarantine_nous_oauth_state, which
clears the dead tokens from auth.json. Until now this quarantine was
completely silent: the only signal was a downstream "No access token found"
WARNING once the credential pool was already empty, which is too late to
root-cause. Because the Fly log drain is WARNING-only, nothing about the
terminal death reached centralized logging, and a real incident could not be
diagnosed because the evidence was never recorded.

Emit a WARNING+ forensic record AT the quarantine point, before the token
material is cleared. Fields: refresh_token hash prefix (12-char SHA-256 hex,
correlates to NAS's refreshTokenHash), client_id, agent_key_id, error code,
reason, auth.json path/size/mtime/exists, and whether the token was already
past its own expiry. WARNING level is deliberate — INFO never reaches the Fly
drain.

Redaction safety (load-bearing): the log dict is built only from computed
values (hash prefix, sizes, booleans). No raw refresh_token, access_token, or
agent_key bytes are ever passed into the log call, avoiding Hermes's known
credential-literal corruption bug class. A test asserts the raw refresh token
substring is absent from all emitted log output.

Note: no session_id field exists on Nous auth state; provenance is captured
via client_id + agent_key_id, which are non-secret routing identifiers.
2026-07-07 00:53:48 -07:00
helix4u
4b4f058860 fix(tui): probe active custom model provider 2026-07-06 13:08:50 -07:00
teknium1
586acf5307 feat(curator): add hermes curator usage — all-skills usage view
Surfaces the usage_report()/provenance() data layer added in #36701 as a
user-facing CLI command. Unlike `hermes curator status` (scoped to
curator-managed agent-created candidates), `usage` lists every skill on disk
— bundled built-ins and hub-installed included — with per-skill use/view/patch
counts and an agent/bundled/hub provenance tag.

Flags: --sort {activity,recent,name}, --provenance {agent,bundled,hub} filter,
--json for machine-readable output.
2026-07-06 12:26:21 -07:00
teknium1
94cdd56b82 feat(plugins): surface entry-point plugins in hermes plugins list
Salvaged from #40346; re-verified on main, tightened, tested.

Co-authored-by: tjboudreaux <tjboudreaux@users.noreply.github.com>
2026-07-06 11:20:47 -07:00
brooklyn!
91c68bf834
Merge pull request #55923 from NousResearch/bb/serve-headless-no-web-build
feat(cli): make hermes serve a real headless backend (no web UI build/mount, neutral ready sentinel)
2026-07-06 13:18:00 -05:00
teknium1
51e6ef5fca feat(banner): size skills display to terminal width instead of fixed 8/47
Salvaged from #40273; re-verified on main, tightened, tested.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
2026-07-06 11:17:41 -07:00
Teknium
7dfd5077ce
feat(oneshot): add --usage-file JSON usage report to hermes -z (#59615)
* feat(oneshot): add --usage-file JSON usage report to hermes -z

Pipelines driving hermes -z (batch reviewers, cron scripts, eval
harnesses) had no way to account for per-invocation spend: the agent
computes estimated_cost_usd and full token counts internally, but
oneshot mode discards everything except the final response text.

- hermes -z PROMPT --usage-file PATH writes a JSON report after the
  run: estimated_cost_usd, cost_status/source, input/output/cache/
  reasoning/total tokens, api_calls, model, provider, session_id,
  completed, failed.
- Written even when the run fails (with a failure field) so callers
  can always account for spend; the write itself is best-effort and
  never masks the run's own outcome.
- Flag registered in both the full parser and the Termux fast path;
  added to both value-flag scan sets so profile detection stays
  correct.

Validation: 6 unit tests + live E2E (real -z run produced a report
with real OpenRouter cost + token counts).

* test: include usage_file kwarg in oneshot dispatch assertions

The two dispatch tests assert the exact kwargs dict passed to
run_oneshot; the new usage_file kwarg must appear there.
2026-07-06 11:10:51 -07:00
Brooklyn Nicholson
409560a7d9 Merge remote-tracking branch 'origin/main' into bb/serve-headless-no-web-build 2026-07-06 12:51:15 -05:00
Teknium
27f74b26c5
fix(web): correct 'disabled plugin' diagnosis for web backends (#59573)
When a bundled web provider (firecrawl, tavily, exa, ...) is listed in
plugins.disabled, its provider never registers and the web_search/
web_extract dispatchers emitted the misleading "No web extract provider
configured. Set web.extract_backend to ..." — even though the backend was
configured correctly. The real fix is to re-enable the plugin.

- web_tools.py + web_search_registry.py: when the configured backend names
  a disabled bundled web plugin, both dispatchers now point the user at the
  actual cause (re-enable the plugin) instead of a wrong config hint.
- plugins_cmd.py cmd_enable: enabling by canonical key now also clears the
  manifest-name alias (web-firecrawl) from plugins.disabled, so the
  suggested command actually re-enables the plugin ('explicit disable wins'
  matches on the name too).
- plugins_cmd.py cmd_toggle / _run_composite_ui / _run_composite_fallback:
  the interactive 'hermes plugins' menu now persists the canonical key
  (web/firecrawl), never the bare manifest name — the drift that put the
  offending entry in plugins.disabled in the first place.

Follow-up to #59518 (which fixed web credential resolution, a different
cause). Fixes the disabled-plugin symptom reported after that PR.
2026-07-06 04:38:17 -07:00
giggling-ginger
e53e8a782c fix(mcp): sanitize server names for auth env keys
Server names with non-env-safe characters (dots, slashes, spaces)
produced invalid env-var keys like MCP_MY.SERVER_API_KEY or
MCP_GITHUB/MCP_API_KEY, breaking .env writes and ${VAR} header
substitution. _env_key_for_server now replaces any character outside
[A-Za-z0-9_] with an underscore.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
2026-07-06 03:18:07 -07:00
izumi0uu
ef79ad014d fix(dashboard): accept HA ingress prefix paths
Allow mainstream reverse-proxy path mounts to keep their X-Forwarded-Prefix when Home Assistant Supervisor ingress already consumes nearly the old 64-character budget. Keep validation bounded and keep rejected non-empty prefixes diagnosable with a deduplicated warning.

Constraint: HA Supervisor ingress prefixes are 63 chars before add-on subpaths, so the old 64-char cap dropped valid dashboard deployments.

Rejected: remove the length cap entirely | a bounded header budget is still a conservative validation guard.

Confidence: high

Scope-risk: narrow

Directive: Keep prefix validation centralized in hermes_cli.dashboard_auth.prefix so auth routes, cookies, and SPA asset rewriting agree.

Tested: python probe for the 73-char HA ingress prefix; scripts/run_tests.sh tests/hermes_cli/test_dashboard_auth_prefix.py -q; .venv/bin/python -m pytest tests/hermes_cli/test_web_server.py -k 'spa_assets_are_read_as_utf8' -q; python -m ruff check hermes_cli/dashboard_auth/prefix.py tests/hermes_cli/test_dashboard_auth_prefix.py; git diff --check

Not-tested: full test suite
2026-07-06 03:18:02 -07:00
Tranquil-Flow
9d848cc60a fix(cli): pass custom_providers to resolve_display_context_length (#59314)
The CLI model-switch display (both picker and direct-switch paths)
omitted the custom_providers keyword when calling
resolve_display_context_length(). The function already supports it
(and the gateway correctly passes it), but the CLI call sites relied
on the fallthrough to probe-down default (256K) even when a
custom_providers entry specified a per-model context_length.

Fix: pass agent._custom_providers at both resolve_display_context_length
call sites in HermesCLI._apply_model_switch_result(), matching the
pattern already used for config_context_length.
2026-07-06 02:46:24 -07:00
teknium1
3ba5ba89c2 test(cron): cover cron_list/status/tick/create CLI helpers
Salvaged from #40430; re-verified on main, tightened, tested.

Co-authored-by: xuezhaolan <xuezhaolan@users.noreply.github.com>
2026-07-06 02:21:32 -07:00
Teknium
845a2d8152
feat(sessions): any prune filter matches all ages; preview shows age span (#59415)
Bare 'hermes sessions prune' keeps the historical 90-day default, but any
filter — now including --source — suppresses the implicit cutoff, so
'prune --source cron' targets ALL cron sessions instead of silently only
those older than 90 days (the surprise a user hit live: 'No sessions
match ... source cron' despite plenty of recent cron runs).

- CLI preview + confirmation now show the match count plus the oldest
  and newest matching session start times before deleting.
- Dashboard /api/sessions/prune mirrors the semantics: attribute filters
  without an explicit older_than_days match all ages (model_fields_set
  distinguishes an explicit 90 from the Pydantic default); dry_run
  responses gain oldest_started_at/newest_started_at.
- Docs + argparse help updated; tests for both surfaces.
2026-07-05 23:01:10 -07:00
Teknium
040a5e30dd
feat(sessions): full filter surface for prune + bulk archive subcommand (#59327)
* feat(sessions): full filter surface for prune + new bulk archive subcommand

hermes sessions prune previously only supported --older-than N (integer
days) and --source — no way to target a window like 'the last 5 hours'
(e.g. a batch of CI smoke-test sessions), and no non-destructive option.

- SessionDB.prune_sessions gains keyword filters that AND together:
  started_before/started_after epoch bounds, title_like, end_reason,
  cwd_prefix, min/max_messages, archived tri-state. Default call is
  byte-for-byte compatible (90-day cutoff, ended-only, source).
- New SessionDB.list_prune_candidates (backs --dry-run + confirmation
  previews) and SessionDB.archive_sessions (bulk soft-hide via the
  existing set_session_archived lineage-aware path; nothing deleted).
- CLI: prune gains --newer-than/--before/--after (durations like 5h/2d/1w,
  bare days, or ISO timestamps), --title, --end-reason, --cwd,
  --min/--max-messages, --include-archived, --dry-run. New
  'hermes sessions archive' takes the same filters, requires at least one,
  and is idempotent. Both show a preview before confirming.
- Dashboard /api/sessions/prune accepts the same filters + dry_run.
- Docs: sessions.md + cli-commands.md updated.

Filter parsing lives in hermes_cli/session_filters.py with unit tests;
DB filters covered in tests/test_hermes_state.py.

* feat(sessions): prune/archive filters for model, provider, user, chat, branch, tokens, cost, tool calls

Extends the prune/archive filter surface to everything identifiable in
the sessions table:

- --model (substring on model slug), --provider (exact on
  billing_provider, case-insensitive), --user, --chat-id, --chat-type
  (exact), --branch (substring on git_branch), --min/--max-tokens
  (input+output), --min/--max-cost (USD, actual_cost_usd falling back to
  estimated_cost_usd), --min/--max-tool-calls.
- SessionDB prune/archive/list_prune_candidates now share the filter
  kwargs via **filters into _prune_filter_where (unknown names raise
  TypeError); candidates listing + CLI preview now include the model.
- Any attribute filter (except legacy --source) suppresses the implicit
  90-day default so 'prune --model X' matches all ages.
- Dashboard /api/sessions/prune passes the new fields through.
- Docs + tests updated (7 new DB tests, 3 new parser tests).
2026-07-05 22:04:52 -07:00
konsisumer
18e840469f fix(install): guard Windows desktop installs against broken web_server 2026-07-05 19:35:30 -07:00
dodo-reach
21a012b6ac test(prompt-size): cover resolved-toolset parity and blank-slate minimal count
Regression tests from PR #51586: the inspection agent must receive the
platform-resolved enabled_toolsets and agent.disabled_toolsets, and a
Blank Slate profile's prompt-size must count exactly the 6 file/terminal
tool schemas.
2026-07-05 19:13:20 -07:00
Sam Beran
d52d2973a1 feat(cli): add --connect-timeout flag to hermes mcp add
Persists as the server's connect_timeout in config, which the probe
now honors. CLI-flag portion of PR #54494; the probe-wrapper portion
was superseded by resolving connect_timeout inside _probe_single_server.
2026-07-05 19:10:00 -07:00
Michael Musser
a348368019 fix: honor configured connect_timeout on MCP OAuth login path
_reauth_oauth_server (hermes mcp login / reauth) called
_probe_single_server without a timeout, so it always used the 30s
probe default — far too short for a human browser OAuth round-trip
(open → sign in → consent → loopback redirect). The server-level
connect_timeout in config.yaml was silently ignored, so login timed
out at ~40s no matter what the user configured.

Pass the server's configured connect_timeout through, with a 180s
floor for the interactive login path. Update the two TestMcpLogin
probe mocks for the new kwarg and assert the login path propagates a
>=180s timeout.
2026-07-05 19:10:00 -07:00
Stephen Schoettler
087aa74e6e fix(cli): honor MCP probe connect timeout
(cherry picked from commit b106dbe1c6a892789dcc4a0fdd460e1d100b8a66)
(cherry picked from commit 2142c95ccfd9fc882f4252be561c844752c76a37)
2026-07-05 19:10:00 -07:00
HexLab98
a05b64d677 test(setup): blank-slate disabled list must not overlap kept tools
Overlap-invariant regression test from PR #58686 — no toolset in the
blank-slate disabled_toolsets may share a tool with a kept toolset,
since the subtraction happens at tool granularity (#57315, #58281).
2026-07-05 14:51:49 -07:00
liuhao1024
b57fe5ca01 fix(setup): exclude posture toolsets from blank-slate disabled_toolsets
Blank Slate's _blank_slate_minimal_toolsets() adds every TOOLSETS entry
to agent.disabled_toolsets except file and terminal.  The coding
posture toolset (session-level, selected by agent/coding_context.py)
slips through because the loop only skips hermes-* composites and
includes-only groups.

At runtime, model_tools.get_tool_definitions() resolves coding and
subtracts its tools — terminal, read_file, write_file, patch,
search_files, process — erasing the entire Blank Slate minimal surface.
The agent ends up with only cronjob.

Skip posture toolsets in the disabled-list computation.  Posture
toolsets are not user-facing capabilities to disable; they are
per-session selections that should never appear in agent.disabled_toolsets.

Fixes #57315
2026-07-05 14:51:49 -07:00
HexLab98
c9adbaff5e test(mcp): cover probe capability + config gating for prompts/resources
Assert the "Test server" probe skips prompts/list when tools.prompts is false,
skips both families when the server advertises neither capability (the Unreal
MCP server case), probes both when advertised and enabled, and falls back to
the legacy always-try behaviour when no capability info was captured.
2026-07-05 14:51:36 -07:00
falkoro
e3203e4d80 fix(config): invalidate load_config cache when referenced ${VAR} env values change
The load_config() cache is keyed on config file mtime/size only, so a
load_config() that runs before load_hermes_dotenv() populates the process
environment caches the unexpanded ${VAR} literal and serves it for the
life of the process — auxiliary.<task>.api_key/base_url env refs reach the
provider client verbatim (auth failure / silent fallback), while
providers.* appear to work because provider credential resolution re-reads
the environment at call time.

Record a snapshot of every ${VAR} name referenced in the raw config
(user + managed) with its os.environ value at expansion time, and treat
the cache as stale when any of those values change. Covers both the late
.env load and in-process key rotation; an unchanged environment still
takes the cache-hit path.

Fixes #58514
2026-07-05 14:41:40 -07:00
kshitijk4poor
74cc9ee3f0 Revert "Merge pull request #58698 from kshitijk4poor/feat/pre-tool-call-approve-escalation"
This reverts commit 368e5f197e, reversing
changes made to abf9638f4e.
2026-07-06 02:36:52 +05:30
Teknium
55e3ee1ab8
fix: remove dead f-string prefixes via ruff F541 (216 sites) (#52336)
ruff check --fix --select F541 . on current main. Pure prefix removals;
adjacent-string concatenations keep the f only on interpolating fragments.
No string content or live placeholder altered.
2026-07-05 13:42:46 -07:00
kshitijk4poor
beaa1a08e6 fix(config): guard xai migration writer + drop gratuitous annotation
Phase-2 review follow-ups on the unreadable-config chokepoint work:

- hermes_cli/xai_retirement.py apply_migration() is a full-file config.yaml
  rewriter (ruamel round-trip + plain open("w")) that lives outside the
  atomic_yaml_write path, so the chokepoint didn't cover it. It reads the
  file first (which already fails closed on an unreadable file), but add
  require_readable_config_before_write() right before the write as a
  backstop for the read-then-write window, and a regression test asserting
  the original bytes survive an unreadable config.
- Drop the unnecessary "Path" string quotes on atomic_config_write's
  annotation — Path is imported eagerly at module top, no forward ref needed.

auth.py _update_config_for_provider / _reset_config_provider intentionally
keep their standalone require_readable_config_before_write guard + bare
atomic_yaml_write: the guard must fire BEFORE the read (fail-fast) at those
read-then-write sites, and a test pins the atomic_yaml_write call. Both are
already fully guarded against the bug; routing them through the wrapper
would move the check to write time for no benefit.
2026-07-05 23:00:34 +05:30
kshitijk4poor
123c6f3a23 fix(config): close unreadable-overwrite bug class at a single chokepoint
The unreadable-config-overwrite bug (an existing config.yaml that reads as
{} on a permission/IO error gets replaced with only defaults or the edited
section) is not limited to save_config / config set / auth. The same
read-then-atomic_yaml_write pattern lives at ~7 other independent write
sites that don't route through those functions:

  - gateway/slash_commands.py: _save_config_key, memory/skills write_approval
    toggles, tool_progress toggle, runtime_footer toggle, personality set
  - hermes_cli/doctor.py --fix (stale root-key migration)
  - gateway/platforms/yuanbao.py auto-sethome
  - plugins/platforms/telegram/adapter.py topic thread_id persistence
  - tui_gateway/server.py _save_cfg
  - agent/onboarding.py mark_seen

Rather than sprinkle require_readable_config_before_write() at each site,
add a single fail-closed chokepoint, atomic_config_write(), that runs the
guard then delegates to atomic_yaml_write, and route every config.yaml
write through it. Root cause remains that read_raw_config() can't tell an
absent file from an unreadable one (returns {} for both) — read-only
callers correctly stay fail-open, but any full-file replacement now fails
closed in one enforced place instead of relying on each caller to remember
the guard.

save_config / set_config_value / auth keep the contributor's original
guard calls (their commit); this commit widens the fix to the sibling
call paths and adds a regression test on the chokepoint (fails closed on
unreadable existing file + still creates a genuinely absent file).
2026-07-05 23:00:34 +05:30
luyifan
b109adede6 fix(config): refuse unreadable config overwrites 2026-07-05 23:00:34 +05:30
kshitij
368e5f197e
Merge pull request #58698 from kshitijk4poor/feat/pre-tool-call-approve-escalation
feat(plugins): pre_tool_call approve action escalates to human gate (closes #51221)
2026-07-05 22:04:23 +05:30
Teknium
24a7546918
fix(cli): drop shell=True from cua-driver installer — download to mkstemp, exec as argv (#58796)
Replaces the POSIX `/bin/bash -c "$(curl …)"` invocation with a
download-then-exec flow: curl the upstream install.sh into a mkstemp
temp file (unpredictable name, 0600) and run it as a plain argv list.
No shell=True, no command substitution. The temp script is removed in
a finally block; download failures return cleanly without exec.

Salvages the intent of #34974 by @ErnestHysa. His original patch
targeted a fixed /tmp/cua-driver-install.sh path (symlink/TOCTOU-prone
on multi-user hosts) and predates Windows/Linux installer support;
this version uses mkstemp and keeps the powershell path untouched.

Co-authored-by: ErnestHysa <takis312@hotmail.com>
2026-07-05 05:35:44 -07:00
Teknium
2c0820c9ff
feat(cli): autocomplete + ghost text for stacked slash-skill invocations (#58763)
Follow-up to #57987: after /skill-a the completer previously went silent
for a second /skill token. Now, while the leading tokens form an unbroken
skill chain (each token a distinct installed skill, under the 5-cap) and
the word under the cursor starts with '/', the completer keeps offering
the remaining skill commands, and SlashCommandAutoSuggest ghost-suggests
the rest of the next skill name. Instruction text, path-like tokens, and
broken chains get no suggestions. The TUI's complete.slash RPC reuses
SlashCommandCompleter, so it inherits the behavior with no changes.
2026-07-05 04:34:14 -07:00
Teknium
7fde19afcc
fix(cli): unwedge cua-driver installer timeouts — group-kill, stale-lock pre-clear, 660s ceiling (#58767)
* fix(cli): unwedge cua-driver installer timeouts — group-kill on timeout, stale-lock pre-clear, 660s ceiling

The cua-driver refresh in hermes update could wedge permanently:
subprocess timeout (300s) killed only the outer shell, orphaning the
curl|bash grandchildren and the upstream installer's concurrent-install
lock (~/.cua-driver/packages/.install.lock.d). The installer only
reclaims a stale lock after 600s of waiting — longer than our old
ceiling — so every subsequent run was killed before recovery could
fire: 'always times out'.

- Run the installer in its own process group (start_new_session) and
  SIGKILL the whole group on timeout, so no lock-holding orphans survive.
- Pre-clear a provably-stale lock (dead holder pid, or pid-less and
  older than the upstream 600s window) before invoking the installer.
- Raise the ceiling to 660s (> upstream LOCK_STALE_AFTER_SECONDS=600).
- Timeout message now names the lock path and the manual re-run command.

Fixes #58762

* chore: suppress windows-footgun lint on platform-gated kill calls

Both sites are POSIX-only: _clear_stale_cua_install_lock early-returns
on win32, and os.killpg sits in the 'not is_windows' branch.
2026-07-05 03:16:06 -07:00
Sami Rusani
d8b51269ca fix(update): skip cua-driver refresh when Applications is unwritable 2026-07-05 03:15:06 -07:00
yoma
791583704b fix(auth): prune stale custom model credentials 2026-07-05 00:55:51 -07:00
Lord_dubious
fc18d15f40 fix: preserve static custom provider models 2026-07-05 00:55:51 -07:00
Ahmett101
8d9684c9da fix(profiles): allowlist default-export paths + preserve symlinks (#58394)
`hermes profile export default` crashed with `shutil.Error` when
HERMES_HOME pointed outside ~/.hermes (common in Docker deployments)
and the workspace contained broken symlinks. Two root causes:

1. `copytree` defaults to `symlinks=False` and follows link targets;
   broken ones crash. #58397 (liuhao1024) drafted a minimal
   `symlinks=True` flag fix; this PR adopts that change.
2. `copytree` was invoked against the entire HERMES_HOME root (which
   doubles as cwd in Docker layouts). The post-hoc blacklist at
   `_DEFAULT_EXPORT_EXCLUDE_ROOT` is a fixed-length enumerate-and-pray
   list that can't anticipate every unrelated sibling directory
   (`x11-dev/`, etc.). Replaced with a positive allow-list at
   `_DEFAULT_EXPORT_INCLUDE_ROOT` enumerating the known Hermes profile
   artifacts (config, persona, skills, cron, scripts, sessions,
   plugins, memories, knowledge, preferences). Sensitive runtime
   surfaces (`state.db`, `logs/`, auth files, other profiles) are
   intentionally not in the allow-list so the export stays a
   portable, credential-free snapshot of the user-facing surface —
   which means the existing `test_export_default_excludes_infrastructure`
   regressions remain green.

Adds two regression tests:
  * test_export_default_uses_allowlist_for_unrelated_dirs — >x11-dev<
    sibling directories must not leak into the archive.
  * test_export_default_handles_broken_symlinks — symlinks inside
    allowed artifacts survive instead of crashing the export.

closing that PR as superseded once this lands.

Closes #58394
2026-07-05 00:48:50 -07:00
liuhao1024
b6b9bcd2a1 fix(profiles): preserve symlinks during profile export
shutil.copytree() defaults to symlinks=False which follows symlinks and
crashes on broken ones.  In Docker/custom HERMES_HOME deployments,
unrelated directories may contain stale symlinks that break export.

Add symlinks=True to both copytree() calls in export_profile() so
broken symlinks are preserved as symlink entries in the archive.

Fixes #58394
2026-07-05 00:48:50 -07:00