Commit graph

758 commits

Author SHA1 Message Date
Brooklyn Nicholson
b5f0e451c1 feat(cli): restore cwd on resume (--no-restore-cwd)
Resuming a session cd's back into its recorded working directory, so it resumes
in the repo it belonged to. `--no-restore-cwd` opts out; skipped under
--worktree (that path owns its dir); best-effort — a missing dir warns and stays
put rather than failing the resume.

Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
2026-07-12 05:08:13 -04:00
Brooklyn Nicholson
0c4aed2499 feat(cli): sessions list --workspace filter + Workspace column
`hermes sessions list --workspace <needle>` filters to one workspace (git repo
root or project dir, matched by path substring or basename) and adds a
Workspace column. The column only appears once at least one listed session
carries a workspace, so all-unbound listings render exactly as before.

Co-authored-by: Cary Palmer <palmer@dugoutfantasy.com>
2026-07-12 05:08:03 -04:00
Teknium
7550c594ce
feat(reasoning): add max and ultra effort levels (#62650) 2026-07-12 00:26:49 -07:00
teknium1
d2e64fcb89 fix(cli): widen --yolo env guarantee to the _prepare_agent_startup chokepoint + AUTHOR_MAP
The salvaged fix sets HERMES_YOLO_MODE in main()'s dispatch path before
_prepare_agent_startup(); this follow-up also sets it inside
_prepare_agent_startup() itself so every launcher that triggers plugin/tool
discovery (incl. the Termux fast-CLI path) gets the same ordering guarantee
before tools.approval freezes _YOLO_MODE_FROZEN (#60328).
2026-07-09 16:58:37 -07:00
chenkun
501616e8e6 fix(cli): set HERMES_YOLO_MODE before plugin discovery at startup 2026-07-09 16:58:37 -07:00
Brooklyn Nicholson
b06e2f846c fix(kanban): no-TTY gate in _wants_tui_early — the actual worker-crash fix
The earlier fix gated _resolve_use_tui, but the EARLY launcher
(_wants_tui_early) decides TUI from display.interface before cmd_chat
runs — so a `display.interface: tui` default still booted the Ink UI for
headless spawns (kanban workers), whose no-TTY bail-out exits 0 →
"protocol violation". Gate the early resolver on a real TTY: headless
stdio never boots the TUI regardless of config; explicit --tui still does.
2026-07-09 16:13:59 -05:00
Brooklyn Nicholson
e87c495dc2 fix(kanban): spawn workers headless — TUI can never eat a worker run
An inherited HERMES_TUI=1 or a `display.interface: tui` config default sent
kanban workers into the Ink TUI, whose no-TTY bail-out exits 0 without doing
the task — every attempt ended in "protocol violation". Two layers:

- _default_spawn pins `--cli` (highest-precedence interface flag) and strips
  HERMES_TUI from the child env (covers older builds on PATH).
- _resolve_use_tui gates ambient TUI prefs (env/config) behind a real TTY;
  an explicit --tui still wins so the informative bail-out stays reachable.
2026-07-09 16:13:59 -05:00
kshitijk4poor
f5bc18f901 fix: write expanded HERMES_WEB_DIST back for web_server's raw read
Phase-2 review finding: the validation branch expanduser()s the path but
web_server.py reads os.environ['HERMES_WEB_DIST'] raw at import — a
'~/dist' value would validate here and still 404 there. Write the
expanded path back before the web_server import. Adds a regression test
asserting the env var holds the expanded path after cmd_dashboard.
2026-07-09 14:48:50 +05:30
kshitijk4poor
d928017742 fix(dashboard): validate HERMES_WEB_DIST before startup
A custom HERMES_WEB_DIST without --skip-build skipped BOTH the web UI
build and any validation: cmd_dashboard fell through the build gate and
started the server against a dist that may not exist, serving 404s with
no obvious cause. This is the same failure mode issue #23817 fixed for
the --skip-build branch — the env-var branch was left unvalidated.

Add the missing else-branch: fail fast with actionable guidance when
HERMES_WEB_DIST has no index.html, proceed (still without building) when
it does.

Credit: @Caelier (#17845) originally proposed dist validation for the
dashboard startup path; the --skip-build half of that PR's scope has
since landed via the #23817 fix, this covers the remaining env-var path
on the rewritten cmd_dashboard surface.
2026-07-09 14:34:01 +05:30
ethernet
4d7f8ade3e
feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)
* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)

pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.

- hermes_cli/config.py: shared is_unsupported_install_method() /
  format_unsupported_install_warning() helpers so the wording and docs
  link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
  warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
  the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
  'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
  install_warning; applyRuntimeInfo + the live session.info event fire
  a snoozable warning toast via a new reportInstallMethodWarning(),
  mirroring the existing backend-contract-skew toast pattern. i18n
  strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
  Homebrew banner test, and two tui_gateway session_info tests
  (install_warning present for pip, absent for git).

* fix(nix): make `hermes` in developement environment actually work

install modules as editable overlay with uv

* feat: print install method when running --version

* fix: correct detect install method when running from a subtree
2026-07-07 21:13:19 -07:00
Teknium
0e04d14209
feat(sessions): trace export + HF upload via 'sessions export --format trace' (#60507)
* feat(trace): upload sessions to HF Agent Trace Viewer

Salvage trace upload as a smaller CLI-first feature: deterministic Claude Code JSONL export, fail-closed redaction, lazy Hugging Face dependency, and no gateway slash-command wiring.

* chore(trace): drop external porting references from docstrings

Describe the trace-upload design in Hermes' own terms.

* feat(sessions): fold trace upload into 'sessions export --format trace'

Integrates the HF Agent Trace Viewer exporter (PR #36145) onto the
unified export surface instead of a separate 'hermes trace' subcommand:

- --format trace: Claude Code JSONL to stdout/file, or one
  <id>.trace.jsonl per session for filtered bulk export; defaults to
  the most recent session when no --session-id/filters given.
- --upload pushes to the user's private HF traces dataset (--public to
  opt out of private); reads HF_TOKEN with guided setup when missing.
- traces are secret-redacted by default (force mode); --no-redact opts
  out after review; redaction failure blocks export (fail closed).
- hermes_cli/trace.py + subcommands/trace.py removed; agent/trace_upload.py
  is the single engine. Docs EN + zh-Hans; 4 new CLI tests.
2026-07-07 15:12:49 -07:00
teknium1
f76899facf feat(sessions): wire html + prompt-only formats into 'sessions export'
Salvage follow-up integrating PR #30481 (@simplast) and PR #57683
(@catbearlove1-lang) into the unified export surface:

- --format html: standalone self-contained HTML transcript (single
  session or multi-session with sidebar), works with all shared filters
  and --redact; requires a file output path.
- --only user-prompts: prompt-only export (jsonl records or md sections)
  via the shared session_export renderer; the separate export-prompts
  subcommand from the original PR is subsumed by this flag.
- AUTHOR_MAP entries for both contributors; docs EN + zh-Hans.
2026-07-07 13:29:58 -07:00
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
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
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
5431bf2921 fix(desktop): default HERMES_DESKTOP_CWD to cwd when --cwd omitted
Salvaged from #40363; re-verified on main, tightened, tested.

Co-authored-by: alex-heritier <alex-heritier@users.noreply.github.com>
2026-07-06 11:15:29 -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
Taylor H. Perkins
5c4c0e9d9b feat(secrets): add 1Password (op://) secret source
Resolve provider credentials from 1Password op://vault/item/field references
at startup via the official `op` CLI, alongside the existing Bitwarden source.
Users map env-var names to references in secrets.onepassword.env; after .env
loads, each is resolved with `op read` and injected into os.environ. Auth is
whatever `op` already uses (service-account token or desktop/interactive
session) — Hermes never authenticates or installs `op` itself.

Startup-safe and fail-open: a missing binary, expired auth, a bad reference,
or an empty value each warn and fall back to existing credentials, never
blocking startup. Successful, complete pulls are cached in-process and on disk
(<hermes_home>/cache/op_cache.json, 0600) via the shared DiskCache; only
secret values are stored, never the token (auth is fingerprinted into the
key). Adds `hermes secrets onepassword {setup,status,set,remove,sync,disable}`
(aliases op/1password), config defaults, the cli-config example, docs, and
hermetic tests.

Hardening applied across both backends in env_loader: each source runs in its
own guard, config sections are coerced to dict, and cache_ttl_seconds is
coerced defensively — so a malformed secrets: section can't abort startup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 04:58:07 -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
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
Sami Rusani
d8b51269ca fix(update): skip cua-driver refresh when Applications is unwritable 2026-07-05 03:15:06 -07:00
teknium1
c6dc7c03c3
Revert "Merge pull request #30179 from NousResearch/feat/iron-proxy"
This reverts commit 8790adc4c6, reversing
changes made to fe5054bccf.
2026-07-04 13:38:59 -07:00
teknium1
14cbbd541e
Merge remote-tracking branch 'origin/main' into iron-proxy-followups
# Conflicts:
#	hermes_cli/config.py
#	hermes_cli/main.py
#	website/docs/reference/cli-commands.md
2026-07-04 03:09:43 -07:00
teknium1
86fcb2fe5f
feat(egress): first-class x-api-key providers + hot reload via management API
Both wired against features the iron-proxy author (@mslipper) confirmed on
PR #30179 — and both verified present in the pinned v0.39.0 source.

Header-auth providers (match_headers):
- New _HEADER_AUTH_PROVIDERS: Anthropic native (x-api-key), Azure OpenAI
  (api-key on *.openai.azure.com / *.cognitiveservices / *.services.ai),
  Gemini (x-goog-api-key + ?key= query param via match_query).
- TokenMapping grows match_headers + alias_env_names; per-provider header
  sets flow into the secrets rules; mappings.json roundtrips them
  (legacy files load with the Authorization default).
- GEMINI_API_KEY / GOOGLE_API_KEY collapse into ONE mapping (two
  require-rules on the same host would reject each other); the sandbox
  gets the token under both names, and the proxy child env mirrors the
  alias into the canonical name when only the alias is set.
- Docker backend injects alias env names alongside canonical ones.
- The fail-closed tier is now empty, so fail_on_uncovered_providers and
  discover_blocked_providers are deleted (dead toggle otherwise);
  _NON_BEARER_PROVIDERS shrinks to genuinely-unswappable signature auth
  (AWS SigV4, GCP service-account OAuth) — warn-only, as before.

Management API (hot reload):
- Generated proxy.yaml enables the v0.39 management listener: loopback
  only at tunnel_port+2, bearer key from HERMES_IRON_PROXY_MGMT_KEY.
- Key minted at setup (management.token, 0600); start_proxy injects it
  (v0.39 refuses to start when api_key_env is empty).
- hermes egress reload -> POST /v1/reload: re-reads proxy.yaml and
  atomically swaps the pipeline; 422 leaves the running ruleset
  untouched; actionable errors for not-running / pre-management config /
  key mismatch. Secrets changes still require restart (daemon env is
  read at spawn) — the CLI says so.

Validation: 218/218 unit+CLI+docker tests; 3/3 gated live E2E against the
real v0.39.0 binary (Authorization swap, x-api-key swap, live reload with
token rotation on the same pid). Docs updated.
2026-07-04 02:49:31 -07:00
Shannon Sands
dcbce869ae Add safe Hermes console REPL 2026-07-03 20:18:00 +05:30
Teknium
87ae4ae94b
fix(update): harden #57659 follow-ups — task restore on failure, --force-venv split, trampoline detection, managed-install health (#57680)
Five follow-ups to #57659 from post-merge review:

1. install.ps1: gateway scheduled-task re-enable now runs in a finally
   (a thrown Remove-Item/uv venv failure previously stranded the user's
   gateway autostart disabled), and tasks that were already disabled
   before the install are no longer blindly re-enabled.
2. The venv-python holder guard is no longer bypassed by plain --force
   (which the desktop bootstrap passes on every update while its lock
   probe only checks hermes.exe/app.asar). New explicit --force-venv is
   the escape hatch; --force keeps bypassing only the hermes.exe shim
   guard.
3. _detect_venv_python_processes now also catches uv/base-interpreter
   trampolines whose exe is outside the venv, via cmdline (venv path or
   '-m hermes_cli.main' tied to this install root) and cwd.
4. Missing venv python is now UNHEALTHY on managed installs
   (.hermes-bootstrap-complete / .update-incomplete markers) so the
   repair lane runs instead of 'Already up to date!'; the repair branch
   recreates the venv first when it's gone entirely. Dev checkouts keep
   reporting healthy.
5. install.ps1 comment no longer claims a Startup-folder disarm the
   code doesn't perform (logon-only, not a mid-install respawner).
2026-07-03 04:08:37 -07:00
Teknium
b14d75f8af
fix(update): prevent and self-heal half-updated venvs on Windows (#57659)
Root-causes the July 2026 Windows incident chain (locked _brotlicffi.pyd /
_sodium.pyd during install, then 'No module named annotated_doc' with
'hermes update' insisting 'Already up to date!'):

- hermes update: probe venv core imports even when the checkout is current;
  a half-updated venv (dep sync killed mid-flight by a locked .pyd) is now
  detected and repaired instead of being reported as up to date
- hermes update (Windows): after pausing gateways, refuse to mutate the venv
  while other processes run from the venv interpreter (the Desktop backend
  runs as python.exe so the hermes.exe shim guard never saw it); --force
  keeps the old behavior
- install.ps1 venv stage: disarm gateway autostart Scheduled Tasks before
  the kill sweep (they respawn the gateway inside the kill->delete window),
  make the sweep a bounded loop requiring 3 clean passes, and rename-then-
  delete the old venv (a rename succeeds even with mapped DLLs) with stale-
  dir cleanup on the next run
- desktop updater: 'venv shim still locked after 15s' now ABORTS the update
  hand-off (restarting our backend, surfacing the holder to the user)
  instead of 'proceeding anyway (force)' into guaranteed venv corruption;
  the unlock wait also re-kills respawned backends each poll tick
2026-07-03 03:24:08 -07:00
Steve Lawton
c73e74386b feat(vertex): add Google Vertex AI provider for Gemini (OAuth2)
Adds Vertex AI as a first-class provider for Gemini models via Vertex's
OpenAI-compatible endpoint. Vertex authenticates with short-lived OAuth2
access tokens (service-account JSON or ADC), not a static API key — the
missing piece behind the recurring requests (#13484, #12639, #56259).

- agent/vertex_adapter.py: OAuth2 token minting + refresh-on-expiry
  (5-min margin), ADC->service-account fallback, global vs regional
  endpoint URLs. Config precedence: env var > config.yaml > default.
- plugins/model-providers/vertex/: provider profile (auth_type=vertex),
  reuses Gemini's extra_body.google.thinking_config translation.
- runtime_provider: vertex short-circuit BEFORE the credential pool so a
  credentials-file path is never mistaken for a static API key; mints a
  fresh token + computes base_url per resolve.
- run_agent + conversation_loop: _try_refresh_vertex_client_credentials()
  re-mints the token and rebuilds the client on a mid-session 401, so a
  long-lived gateway agent survives token expiry (~1h).
- auxiliary_client: vertex auth_type branch for side-LLM tasks.
- config.yaml: vertex.project_id / vertex.region (non-secret, bridged to
  env); credential path stays in .env (VERTEX_CREDENTIALS_PATH).
- setup wizard + model picker: dedicated _model_flow_vertex; curated
  google/gemini-* model list; --provider choices.
- pricing/metadata: Vertex prices off the gemini docs snapshot; endpoint
  host auto-maps to the vertex provider (no probe spam).
- lazy_deps + pyproject [vertex] extra: google-auth, opt-in only.
- docs: guides/google-vertex.md + providers page; tests for adapter +
  runtime resolution.

Salvages and modernizes #8427 by @slawt onto current main: rewired from
the legacy PROVIDER_REGISTRY path to the provider-profile architecture,
moved non-secret config out of .env into config.yaml, and added the
per-turn 401 token-refresh the original lacked.
2026-07-01 05:25:33 -07:00
Brooklyn Nicholson
f0f8c84d1b feat(cli): make hermes serve a real headless backend
`serve` (added in #54568) reused cmd_dashboard wholesale, so it still
behaved like a dashboard: it ran a full vite build every launch, mounted
and served the SPA whenever a stray web_dist/ existed, printed
"Hermes Web UI →", and announced HERMES_DASHBOARD_READY. It's the headless
JSON-RPC/WS backend the desktop app and remote clients run — pure socket
clients that never load the browser SPA.

Mark serve with headless_backend=True (resolved once in cmd_dashboard) and:

- skip _build_web_ui entirely on the serve path
- export HERMES_SERVE_HEADLESS=1 so mount_spa() disables the SPA even when a
  dist is present — only the JSON-RPC/WS/API surface is reachable
- announce the bind ("Hermes backend listening on host:port") instead of a
  browser/auth-gated URL
- print a neutral HERMES_BACKEND_READY sentinel; dashboard keeps the legacy
  one and the desktop port-discovery regex matches either
- preserve serve across the named-profile re-exec so it can't rebuild as
  dashboard

`hermes dashboard` is unchanged (builds + serves the browser UI). Backward
compatible: old apps only ever spawn dashboard (legacy token + UI intact)
and never invoke serve; the ready-file side channel is name-agnostic. The
one behavior change is that a remote `hermes serve` no longer serves the
browser dashboard as a side effect — that's `hermes dashboard`'s job.

Tests: serve headless_backend contract, SPA-disabled-with-dist, the
HERMES_BACKEND_READY desktop parse (17/17 node), and the existing
serve/dashboard/web_server suites. AGENTS.md documents the behavior.
2026-06-30 17:58:52 -05:00
Brooklyn Nicholson
e971dc1e9d feat(journey): CLI + TUI learning timeline (/journey)
Terminal rendition of the desktop Star Map / Memory Graph: learned skills
and memories on a timeline, shared by `hermes journey` and the TUI
`/journey` overlay via one size-aware Python renderer
(agent/learning_graph_render.py).

- TUI overlay mirrors /agents: static chart overview + selectable slice
  list → slice detail → single skill/memory body, with the shared
  inverse-row selection treatment and a pinned footer.
- Reuse primitives: extract OverlayScrollbar into its own module (now
  shared with agentsOverlay), scroll the item body via ScrollBox, and
  unify both lists through one table-driven ListRow.
- No animation/playback in the TUI — pure data; the renderer's reveal
  scrubber stays available in the CLI (`--play`, `--reveal`).
2026-06-30 04:44:58 -05:00
Brooklyn Nicholson
dff491a2b9 feat(cli): add headless hermes serve backend; desktop no longer launches dashboard
The desktop app spawned `hermes dashboard --no-open` as its backend, which
made the dashboard look like a desktop prerequisite. Add a dedicated headless
`hermes serve` command that boots the same gateway (shared cmd_dashboard /
start_server) but never opens a browser, and point the desktop backend spawn
exclusively at it. dashboard and serve are now independent surfaces — neither
launches the other.

- subcommands/dashboard.py: factor shared server args; add `serve` parser
  (always headless; accepts legacy --no-open as a no-op)
- main.py: register serve in _BUILTIN_SUBCOMMANDS + coalesce set + gui-log
  detection; extend stale-backend reaper patterns to match `serve`
- desktop electron: spawn `serve`, rename dashboardArgs -> backendArgs,
  update comments + windows-child-process test assertions
- docs: desktop README, desktop.md (incl. remote-backend), AGENTS.md, and
  cli-commands.md now describe `hermes serve` as the desktop/headless backend
2026-06-28 22:04:22 -05:00
Teknium
11183e8332 fix(profiles): validate custom alias names to prevent path traversal
`hermes profile alias <profile> --name <custom>` accepted arbitrary
strings and used them verbatim as a filename under ~/.local/bin. Because
normalize_profile_name only lowercases/strips (no regex gate), a value
like `../../.bashrc` escaped the wrapper directory and clobbered
arbitrary user-writable files. remove_wrapper_script had the same sink.

Add validate_alias_name (reusing the profile-id regex, which forbids
`/`, `.`, and `..`) and wire it into check_alias_collision,
create_wrapper_script, remove_wrapper_script, and the CLI alias action so
the rejection surfaces a clear "Invalid alias name" error instead of
silently writing or unlinking outside the wrapper dir.

Co-authored-by: Gutslabs <gutslabsxyz@gmail.com>
Co-authored-by: Xowiek <xowiekk@gmail.com>
2026-06-28 18:53:33 -07:00
aaronagent
5c1ac6c70d fix(config): strip export prefix in .env parsers across three modules
All three .env parsers use `line.partition("=")` without stripping the
bash-compatible `export ` prefix first.  A line like `export API_KEY=sk-...`
produces key `"export API_KEY"` instead of `"API_KEY"`, silently ignoring
the variable and causing auth failures for users who copy-paste from
bash profiles or follow tutorials that include `export`.

- tools/skills_tool.py: `load_env()` for skill environment
- hermes_cli/config.py: `load_env()` for core config
- hermes_cli/main.py: `_has_any_provider_configured()` inline parser

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-28 18:53:00 -07:00
Teknium
980622d0ec
perf(startup): parse config + plugin manifests with libyaml CSafeLoader (#54486)
The startup config/manifest reads used PyYAML's pure-Python SafeLoader,
which is ~8x slower than the libyaml-backed CSafeLoader C extension.
config.yaml is parsed several times during launch (cli config, raw
config, early interface/redaction bridge, logging config) and every
plugin manifest is parsed once — all on the slow path.

Add utils.fast_safe_load (CSafeLoader-preferring, pure-Python fallback,
true drop-in for safe_load) and route the hot startup parse sites
through it: hermes_cli/config.py (config + manifest reads),
hermes_cli/plugins.py (manifest parse), env_loader, cli.load_cli_config,
hermes_logging, and the two pre-config early YAML bridges in main.py.

Behavior is identical (same restricted safe tag set); only speed changes.
safe_load calls on the startup path drop from ~79 to ~0, cutting the
YAML parse cost from ~0.9s to ~0.15s under profiling.

Adds tests/test_fast_safe_load.py asserting equivalence with safe_load
across input shapes, empty-doc falsiness, C-loader preference, and that
python/object tags are still rejected (safe, not full loader).
2026-06-28 15:38:39 -07:00
Gille
df8e2523fa fix(windows): verify launchers after primary install 2026-06-28 17:02:05 -05:00
HexLab98
95994bbc56 fix(windows): repair missing hermes.exe after pip install (#52931)
On Windows, uv pip install -e . can register hermes.exe in package metadata
while the launcher never lands on disk. Detect missing [project.scripts]
shims and reinstall entry points under the existing quarantine path in
hermes update and install.ps1.
2026-06-28 17:01:31 -05:00
Teknium
cb982ad997 fix(windows): hide console-window flash on backend git/gh/wmic/bash subprocess spawns
The Windows desktop GUI runs its backend headless via pythonw.exe. Several
auxiliary subprocess sites that run inside that windowless backend spawned
console-subsystem children (git, gh, wmic, powershell, bash, rg, taskkill)
WITHOUT CREATE_NO_WINDOW, so Windows allocated a fresh conhost per call and
flashed a black window on screen — sometimes continuously (the dashboard
Projects-tree git probe alone fired ~118 spawns in 60s on startup).

The terminal tool, cron, browser, code_execution, and gateway-spawn paths
already carry windows_hide_flags(); these auxiliary probe/scan/launcher legs
were missed. Wire the existing helper into them:

- tui_gateway/git_probe.py: run_git (+ encoding=utf-8/errors=replace, fixes the
  cp950 UnicodeDecodeError on CJK paths from the same site)
- agent/coding_context.py: _git (per-turn git status/log/diff)
- agent/context_references.py: _run_git + _rg_files (@file/@ref resolution)
- hermes_cli/copilot_auth.py: gh auth token probe (auxiliary provider:auto)
- hermes_cli/gateway.py: wmic + PowerShell Get-CimInstance PID scan
- hermes_cli/main.py: wmic stale-dashboard PID scan
- gateway/status.py: taskkill /T /F force-kill

windows_hide_flags() returns 0 on POSIX, so every changed call is a no-op on
Linux/macOS (verified: real git/rg probes still work; Windows-simulated calls
all pass creationflags=CREATE_NO_WINDOW).

Scoped to the windowless-backend paths that cause the reported flashing. The
Electron updater-handoff leg (main.cjs windowsHide:false) and the
interactive-CLI banner probes (cli.py) are intentionally NOT touched here —
the former needs a Windows-tested change of its own, the latter runs in a
visible console anyway.

Tracking: #54220
Refs: #53178 #53631 #53781 #53957 #49602 #52982 #53424 #53053 #53016
2026-06-28 05:28:45 -07:00
teknium1
7c9cdad9fd test(cli): cover Windows self-lock recovery guard + cmd-quote its hint
Add two tests for the self-lock guard in _recover_from_interrupted_install:
one asserting it clears the marker and skips install when hermes.exe is a
process ancestor (breaking the #52378/#45542 loop), one asserting it falls
through to a normal recovery install when the shim is NOT an ancestor.

The guard's manual-recovery hint runs only inside the Windows branch, so
quote it for cmd.exe (cd /d, double-quoted paths) — the cross-platform
fallback hint at the end of the function is left POSIX-correct.

Map Icather in scripts/release.py AUTHOR_MAP for the salvage.
2026-06-28 02:40:37 -07:00
灵越羽毛
b6f592dbdc fix(cli): detect self-lock in update recovery to break infinite retry loop on Windows 2026-06-28 02:40:37 -07:00
teknium1
c918d42d88 feat(desktop): config-driven Electron launch flags + GPU policy
Adds a desktop: section to config.yaml so headless/VM users can make
`hermes desktop` launch correctly without a wrapper command:

- desktop.electron_flags: extra Electron CLI flags (e.g. --ozone-platform=x11)
  appended to every launch. Accepts a list or a shell-split string.
- desktop.disable_gpu: auto|true|false, bridged to the HERMES_DESKTOP_DISABLE_GPU
  env var the Electron app already reads. An explicit env var still wins.

cmd_gui() reads these via _desktop_launch_options() and applies them. This is
the config.yaml form of the capability proposed as a raw env var in #38934
(@1RB) — behavioral settings belong in config.yaml, not a new HERMES_* env var.

Co-authored-by: ray <86501179+1RB@users.noreply.github.com>
2026-06-27 22:26:43 -07:00
Rafael Millan
54ea059919 fix: fall back to no-sandbox for desktop launch on restricted Linux hosts 2026-06-27 22:16:20 -07:00
Teknium
d3d621f7c3
revert(windows): roll back terminal-popup PRs #53791 #53810 #53829 (#53853)
* Revert "fix(windows): capture is not a no-window boundary; route flashing spawns through chokepoint (#53829)"

This reverts commit 2ecca1e7d3.

* Revert "fix(windows): stop terminal-window popups from background spawns (#53810)"

This reverts commit 5db1430af9.

* Revert "fix(windows): stop subprocess console-window popups + add CI guard (#53791)"

This reverts commit ef17cd204d.
2026-06-27 15:59:00 -07:00
Teknium
2ecca1e7d3
fix(windows): capture is not a no-window boundary; route flashing spawns through chokepoint (#53829)
Follow-up to #53791 addressing review feedback: the footgun checker treated
capture_output=/stdout=/stderr=/check_output as proof a subprocess can't pop a
Windows console. That invariant is false — stream redirection controls where a
child's output goes, not whether a console is allocated. From a console-less
parent (Desktop/Electron, pythonw.exe, detached gateway/cron) a console-subsystem
child still flashes a window even when fully captured.

- check-windows-footguns.py: capture/redirect/check_output is no longer a blanket
  safe-pass. Added _WINDOWS_FLASHING_PROGRAMS (git/gh/npm/node/python/uv/ffmpeg/
  docker/powershell/…); calls to those are flagged even when captured. Non-flashing
  programs keep the capture exemption (no 271-site noise). _subprocess_compat.run/
  popen calls are inherently safe (wrapper injects CREATE_NO_WINDOW).
- Routed the 35 genuine flashing git/gh/npm/uv/ffmpeg/docker spawns through the
  _subprocess_compat.run/popen chokepoint (Brooklyn's wrapper from #53810) — the
  durable fix, not per-site annotations. cmd.exe /c start stays # ok (intentional).
- Updated tests + CONTRIBUTING.md rule #17 to the corrected invariant.
2026-06-27 14:49:41 -07:00
brooklyn!
5db1430af9
fix(windows): stop terminal-window popups from background spawns (#53810)
* fix(windows): stop terminal-window popups from background spawns

Native-Windows desktop/gateway users saw cmd/conhost windows flash on
gateway restart, image paste, the dashboard Projects tree, voice notes,
and ~5 min after closing the app (detached cron). Two root causes:

- Console-subsystem exes (taskkill, schtasks, wmic, netstat, tasklist,
  agent-browser, git, ffmpeg, powershell, git-bash) spawned via raw
  subprocess allocate a fresh console when the launching process has
  none (pythonw desktop backend / detached gateway) - even with output
  captured.
- uv venv pythonw shims re-exec console python.exe, so Python children
  get a console regardless of how they're launched.

Fixes:
- Single hidden-spawn primitive (_subprocess_compat.run/.popen) that ORs
  CREATE_NO_WINDOW on Windows, no-op on POSIX. Route every Hermes-owned
  console-exe spawn through it.
- FreeConsole() catch-all in hermes_bootstrap: any Python child that
  exclusively owns an auto-allocated console detaches it at startup
  (GetConsoleProcessList()==1 gate leaves shared interactive consoles
  untouched).
- Replace PowerShell/wmic gateway PID scans with in-process psutil.
- Skip schtasks queries on non-interactive desktop restarts.
- Prefer native agent-browser .exe over .cmd shims.
- Guard test bans raw subprocess spawns of the Windows-only console
  tools repo-wide so the popup class can't regress.

* fix(windows): scope FreeConsole to background entry points; fix merge fallout

Console detach review (per #53810 feedback): GetConsoleProcessList()==1 can't
tell a uv pythonw->python phantom console apart from a user opening the
interactive CLI/TUI in its own fresh console (double-click, shortcut, ConPTY) —
both report a single attached process with a tty. Running FreeConsole() in the
import-time bootstrap therefore risked detaching a legitimately-interactive
terminal.

- Extract FreeConsole into explicit hermes_bootstrap.detach_orphan_console();
  remove it from apply_windows_utf8_bootstrap() (import side effect).
- Call it only from known background mains: gateway run, dashboard backend
  (start_server, what the desktop spawns), cron standalone, tui_gateway entry,
  slash worker. Interactive CLI/TUI never calls it.
- Behavior-contract tests: frees only when solo owner, leaves shared console,
  no-op without console / on POSIX, and asserts it's not an import side effect.

Merge fallout from origin/main (#53791):
- local.py: 3-way merge left a dangling **_popen_kwargs (NameError crashing
  every terminal init). _subprocess_compat.popen already hides the window, so
  drop it.
- discord adapter: merge stacked an undefined windows_hide_flags() onto the
  primitive call; drop the redundant arg.
- test_gateway: scan now goes psutil-first (zero spawn); rewrite the
  case-variant test to drive that production path.

* test(claw): mock _subprocess_compat.run seam for Windows process scan

claw.py's Windows tasklist/powershell scan routes through the hidden-spawn
primitive; the tests still patched claw_mod.subprocess, so on win32 the mock
was never hit and real spawns returned nothing. Patch the actual seam.
2026-06-27 14:02:24 -07:00
Teknium
ef17cd204d
fix(windows): stop subprocess console-window popups + add CI guard (#53791)
* fix(windows): stop subprocess console-window popups + add CI guard

The single biggest source of Windows 'terminal popup' bug reports was bare
subprocess.run/Popen calls spawning a console window. The compat helpers
(windows_hide_flags / windows_detach_popen_kwargs) already existed but the
footgun checker had no rule to stop new bare calls from reintroducing the flash.

- scripts/check-windows-footguns.py: new AST-based rule flagging subprocess
  calls that can create a new console — output-redirection-aware (capture/
  redirect/check_output exempt) and POSIX-only-program-aware (launchctl/
  systemctl/brew/etc. exempt). Comprehensive on real popups, no annotation
  burden on calls that can't flash.
- Swept all genuine window-spawning sites through windows_hide_flags()/
  windows_detach_popen_kwargs(); marked intentionally-visible launches
  (editor/terminal/foreground re-exec) with '# windows-footgun: ok'.
- tests/scripts/test_windows_footgun_subprocess_rule.py: behavior-contract
  tests + full-repo cleanliness invariant.
- CONTRIBUTING.md: documents the rule + the helper pattern.

* test: accept creationflags kwarg in psutil_android fake_subprocess_run

The Windows no-window sweep added creationflags=windows_hide_flags() to
install_psutil_android.py's subprocess.run call; the test's fake stub had a
fixed (cmd) signature and raised TypeError on the new kwarg.
2026-06-27 13:03:51 -07:00