Commit Graph

14 Commits

Author SHA1 Message Date
Hermes Agent
eb760a2158 fix: allow /root workspace path; guard against split on missing [Attached files]
Removes /root from _BLOCKED_SYSTEM_ROOTS in api/workspace.py, allowing
Hermes running as root (e.g. Docker, VPS) to use /root as a workspace
without a 'system directory' rejection.

Fixes a fragile string split in api/streaming.py: base_text extraction
now guards against msg_text that contains no '[Attached files:' marker,
preventing the split from producing empty-string on those messages.

Fixes: #510, partial fix from #521 (workspace + split guard only).
Co-authored-by: ccqqlo <ccqqlo@users.noreply.github.com>
2026-04-15 07:41:36 +00:00
nesquena-hermes
37850a4dfd fix: workspace list cleaner — all 1055 tests pass (#418)
* fix: workspace list cleaner — allow own-profile paths, remove brittle string filter

Two bugs in _clean_workspace_list() caused workspace adds to silently vanish
on the next load, making the duplicate-check test and workspace rename test fail:

1. Brittle string filter: 'if test-workspace in path or webui-mvp-test in path:
   continue' — removed. The test server's workspace IS under these paths, so any
   workspace added during testing got silently dropped on the next load_workspaces()
   call. The p.is_dir() check already handles non-existent paths.

2. Cross-profile filter too broad: 'if p is under ~/.hermes/profiles/: skip' —
   this correctly blocked cross-profile leakage but also blocked the current
   profile's own paths (e.g. ~/.hermes/profiles/webui/webui-mvp-test/...).
   Fixed: only skip if the path is under profiles/ AND under a DIFFERENT profile's
   directory. Paths under the current profile's own home are kept.

* docs: v0.50.36 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-14 00:14:25 -07:00
nesquena-hermes
415270ff03 fix: cross-platform multi-workspace trust boundary (#417)
* fix: relax workspace trust boundary to user home directory

The previous restriction required workspaces to be under DEFAULT_WORKSPACE
(/home/hermes/workspace), which blocked all profile-specific workspaces
(~/CodePath, ~/General, ~/WebUI, ~/Camanji, etc.) since each profile uses
a different directory under home.

New boundary: any directory under Path.home() is trusted.
This still blocks /etc, /tmp, /var, /root, /usr and all paths outside the
user's home, while allowing any legitimate workspace under ~/

Also updates test assertions from 'trusted workspace root' to 'outside'
since the new error message says 'outside the user home directory'.

* fix: workspace trust uses home-dir + saved-list, not single ancestor

Three-layer trust model that works cross-platform and multi-workspace:

1. BLOCKLIST: /etc, /usr, /var, /bin, /sbin, /boot, /proc, /sys, /dev, /root,
   /lib, /lib64, /opt/homebrew — always rejected, even if somehow saved
2. HOME CHECK: any path under Path.home() is trusted — covers ~/CodePath,
   ~/hermes-webui-public, ~/WebUI, ~/General, ~/Camanji simultaneously;
   Path.home() is cross-platform (Linux ~/..., macOS ~/..., Windows C:\Users\...\...)
3. SAVED LIST ESCAPE HATCH: if a path is already in the saved workspace list,
   it's trusted regardless of location — covers self-hosted deployments where
   workspaces live outside home (/data/projects, /opt/workspace, etc.)

None/empty → DEFAULT_WORKSPACE (always trusted, validated at startup)

* docs: v0.50.35 release — version badge and CHANGELOG

---------

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:57:51 -07:00
nesquena-hermes
2a7a5ddfaf [security] fix(workspace): restrict session workspaces to trusted roots (#416)
* fix(workspace): restrict session workspaces to trusted roots

* fix: use boot-time DEFAULT_WORKSPACE instead of profile default for trusted workspace root

_profile_default_workspace() reads the agent's terminal.cwd which may differ
from the WebUI's configured workspace root. Use _BOOT_DEFAULT_WORKSPACE (which
respects HERMES_WEBUI_DEFAULT_WORKSPACE for test isolation) to stay consistent
with how new_session() seeds the initial workspace.

* docs: v0.50.34 release — version badge and CHANGELOG

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 23:44:03 -07:00
nesquena-hermes
dd17a0e9b7 security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)
* security: fix bandit security issues (B310, B324)

- Add usedforsecurity=False to MD5 hash in gateway_watcher.py
- Add URL scheme validation to prevent file:// access in config.py
- Add URL validation to bootstrap.py health check
- Add nosec comments where runtime validation exists

* fix: handle ConnectionResetError gracefully and add debug logging

- Add QuietHTTPServer class to suppress noisy connection reset errors
  caused by clients disconnecting abruptly (fixes log spam from
  'ConnectionResetError: [Errno 54] Connection reset by peer')

- Replace silent 'pass' statements with logger.debug() calls across
  api/auth.py, api/config.py, api/gateway_watcher.py, api/models.py,
  and api/onboarding.py for better observability during troubleshooting

- All tests pass (25 passed in test_regressions.py)

* chore: add debug logging to profiles and routes modules

- Replace silent 'pass' statements with logger.debug() calls in
  api/profiles.py for better error visibility during profile switching
  and module patching

- Add logger initialization to api/routes.py

* security: fix B110 bare except/pass issues (bandit security scan)

- Replace bare except/pass patterns with logger.debug() calls
- Fixes CWE-703 (improper check/handling of exceptional conditions)
- Files affected: routes.py, state_sync.py, streaming.py, workspace.py, server.py
- All tests pass successfully

* security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354)

- api/gateway_watcher.py: MD5 usedforsecurity=False (B324)
- api/config.py, bootstrap.py: URL scheme validation before urlopen (B310)
- 12 files: replace bare except/pass with logger.debug() (B110)
- server.py: QuietHTTPServer suppresses client disconnect log noise
- server.py: fix sys.exc_info() (was traceback.sys.exc_info(), impl detail)
- tests/test_sprint43.py: 19 new tests covering all security fixes
- CHANGELOG.md: v0.50.14 entry; 841 tests total (up from 822)

---------

Co-authored-by: lawrencel1ng <lawrence.ling@global.ntt>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-13 11:11:56 -07:00
Nguyễn Công Thuận Huy
4d333acbbc chore: add missing type hints across 10 files 2026-04-05 13:30:20 +07:00
Nathan Esquenazi
e184eb5ff5 fix: correct modified/untracked counting in git status parser
Agent review: l[0:2].strip() produced incorrect matches for git status
--porcelain XY format. Now checks both X (index) and Y (worktree)
columns for M/A/R status codes independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:25:07 -07:00
Nathan Esquenazi
d8e6079a2c feat: workspace git detection with branch/status badge
When the workspace root is a git repo, a badge in the panel header
shows the current branch name, dirty file count, and ahead/behind
status. Updates on every root directory load.

Backend:
- git_info_for_workspace() in api/workspace.py runs lightweight git
  commands (rev-parse, status --porcelain, rev-list) with 3s timeout
- New GET /api/git-info endpoint returns branch, dirty count, modified,
  untracked, ahead, behind

Frontend:
- _refreshGitBadge() in workspace.js fetches git info on root load
- Git badge element in panel header shows branch + status
- Badge turns gold when workspace has uncommitted changes

Inspired by PR #75 (@MartinNielsenDev).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:08:25 -07:00
Nathan Esquenazi
ad755e49e5 fix: workspace isolation, session filtering, and clean migration path
Three interrelated fixes:

1. api/workspace.py — clean workspace isolation with auto-migration
   _clean_workspace_list(): sanitizes any workspace list by:
   - Removing test artifacts (webui-mvp-test, test-workspace paths)
   - Removing paths that no longer exist on disk
   - Removing cross-profile leaks (paths under ~/.hermes/profiles/*)
   - Renaming 'default' workspace label to 'Home' (avoids confusion
     with the 'default' profile name)

   _migrate_global_workspaces(): one-time migration for upgrading users.
   Reads the legacy global workspaces.json, runs _clean_workspace_list,
   rewrites it cleaned. This runs automatically on first load after upgrade
   for the default profile only.

   load_workspaces(): now cleans every read and persists cleaned version
   if anything changed. Named profiles always start fresh (no global leak).
   Empty results fall back to 'Home' entry pointing at profile's workspace.
   Default label for auto-generated single-entry lists is 'Home', not 'default'.

2. api/models.py — legacy session profile backfill (already committed,
   this commit adds the sessions.js filter tightening counterpart)

3. static/sessions.js — strict profile filter
   Removed the '!s.profile' escape hatch from the profile filter.
   Server now backfills profile='default' on legacy sessions, so every
   session has an explicit tag. Filter is now exact:
     s.profile === S.activeProfile
   Named profiles see zero legacy clutter. Default profile sees its own
   sessions. 'All profiles' toggle still shows everything.

Migration story for users pulling this update:
- Existing sessions (profile=null) -> attributed to 'default' at read time
- Global workspaces.json -> cleaned of test artifacts and cross-profile paths
  on first server start after upgrade
- Named profile workspace files -> cleaned on first read, persisted clean
- No manual intervention needed

Tests: 426 passed, 0 failed.
2026-04-03 20:01:12 +00:00
Nathan Esquenazi
3d8cf85ef2 fix: profile default workspace reads terminal.cwd; dropdown opens upward
1. _profile_default_workspace() now checks terminal.cwd
   Profile config.yaml files don't have a 'workspace' or 'default_workspace' key
   — they store the working directory as terminal.cwd (the hermes-agent CLI
   setting). Added it as the third fallback after 'workspace' and
   'default_workspace', so switching to camanji correctly resolves
   ~/Camanji, webui resolves ~/webui-mvp, etc.

2. Workspace dropdown opens upward (bottom: calc(100% + 4px))
   The dropdown is now anchored at the bottom of the sidebar. Opening it
   downward (top: 100%) caused it to clip off screen. Flipped to open upward
   with an upward shadow so it expands into the session list area instead.

Tests: 426 passed, 0 failed.
2026-04-03 19:47:38 +00:00
Nathan Esquenazi
7ef203cd41 fix(review): 5 issues found in agent review of PR #43
BUG-1 (critical): api/profiles.py _DEFAULT_HERMES_HOME used Path.home()/.hermes
hardcoded, ignoring the HERMES_HOME env var. conftest.py sets HERMES_HOME to a
test-isolated state dir -- but profiles.py bypassed it and read/wrote real ~/.hermes
during every test run (active_profile file, .env loading). Fixed by reading
os.getenv('HERMES_HOME', ...) at module load time.

BUG-7 (medium): api/workspace.py load_workspaces() fell back to the global
workspaces.json for ALL profiles when their profile-local file didn't exist yet.
New named profiles silently inherited the default profile's workspace list instead
of starting clean. Fixed: the global file fallback now only applies to the default
profile (migration path); named profiles start with a fresh default entry.

BUG-4 (high): test_sessions_list_includes_profile had a vacuous 'if matching:'
guard -- if the session wasn't found the assert was silently skipped and the test
passed. Fixed with hard assert. Also changed to use /api/session?session_id=
directly instead of scanning /api/sessions (which filters out empty Untitled
sessions with 0 messages, causing the test to always see an empty match list).

BUG-5 / test ordering regression: test_profile_switch_returns_default_model_and_workspace
failed with 409 because test_chat_stream_opens_successfully (runs earlier in the
suite) starts a real LLM stream that stays alive in STREAMS. Added a wait loop
(up to 30s) polling /health active_streams before attempting the profile switch.

BUG-8 (low): Removed dead import _profile_default_workspace in switch_profile()
-- was imported but never used (get_last_workspace() already delegates to it).

Also: test_profile_active_endpoint hardcoded assert data['name'] == 'default'
which fails if a prior run left a non-default active_profile on disk. Changed
to assert name is a non-empty string (the endpoint contract), not a specific value.

Tests: 423 passed, 0 failed.
2026-04-03 19:03:16 +00:00
Nathan Esquenazi
3520fa5643 feat: Sprint 23 -- profile/workspace/model coherence
Fix five coherence bugs in profile switching:
1. Model picker ignored profile default (localStorage stale key)
2. Workspace list was global (not profile-scoped)
3. DEFAULT_WORKSPACE was a boot-time singleton
4. Session list showed all profiles (no filtering)
5. switchToProfile() didn't refresh workspaces or sessions

Backend: workspace storage is now profile-local for named profiles,
switch_profile() returns default_model and default_workspace.
Frontend: switchToProfile() clears stale model pref, refreshes
workspace list and session list, sessions.js filters by active profile
with 'Show N from other profiles' toggle.

8 new tests. 400 pass / 23 fail (identical to baseline).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:46:15 -07:00
Hermes
7019c25021 Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish
Sprint 11 (v0.13): multi-provider model support, streaming smoothness
- Dynamic model dropdown populated from configured API keys (OpenAI, Anthropic,
  Google, DeepSeek, GLM, Kimi, MiniMax, OpenRouter, Nous Portal)
- Scroll pinning during streaming (no forced scroll when user has scrolled up)
- All route handlers extracted to api/routes.py (server.py now ~76 lines)

Sprint 12 (v0.14): settings panel, SSE reconnect, session QoL
- Settings panel (gear icon) -- persist default model and workspace server-side
- SSE auto-reconnect on network blips
- Pin/star sessions to top of sidebar
- Import session from JSON export

Sprint 13 (v0.15): cron alerts, background errors, session duplicate, tab title
- Cron completion alerts: toast per completion + unread badge on Tasks tab
- Background agent error banner when a non-active session errors mid-stream
- Session duplicate button
- Browser tab title reflects active session name

Sprint 14 (v0.16): Mermaid diagrams, file ops, session archive/tags, timestamps
- Mermaid diagram rendering inline (dark theme, lazy CDN load)
- File rename (double-click in file tree) and create folder
- Session archive (hide without deleting, toggle to show)
- Session tags -- #hashtag in title becomes colored chip + click-to-filter
- Message timestamps (HH:MM on hover, full date as tooltip)

Test suite: 224 tests across 14 sprint files + regression gate, 0 failures.
2026-03-31 07:02:47 +00:00
Nathan Esquenazi
a4e2174c29 Hermes WebUI v0.1.0 — initial public release 2026-03-30 20:40:19 -07:00