feat: Multi-Profile Support (Issue #28) - #41
Conversation
…eb UI (Issue #28) Add full profile management to the web UI, matching the hermes-agent CLI profile system. Profiles are isolated HERMES_HOME instances with their own config, skills, memory, cron, and API keys. Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME env save/restore in streaming, module-level cache patching for skills_tool and cron/jobs. Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with CRUD UI, boot-time profile fetch, cascade refresh on switch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BUG-3 (high): /api/profile/delete missing RuntimeError catch. When
deleting the active profile while an agent was running, delete_profile_api()
called switch_profile('default') which raises RuntimeError('Cannot switch
profiles while agent is running'). This propagated to the 500 handler
giving the user 'Internal server error' with no context. Added the same
except RuntimeError -> 409 pattern that /api/profile/switch already uses.
INFO-1 (defense-in-depth): /api/profile/create had no server-side name
validation before delegating to hermes_cli.validate_profile_name. Added
server-side ^[a-z0-9][a-z0-9_-]{0,63}$ check, consistent with client-side
regex in submitProfileCreate(). Prevents path-traversal-ish names from
reaching hermes_cli even if the client-side guard is bypassed.
INFO-2 (defense-in-depth): clone_from parameter was passed directly to
hermes_cli with no validation. Applied the same name regex check to
clone_from before delegating.
BUG-11 (low): toggleProfileDropdown() and toggleWsDropdown() could both
be open simultaneously. Added cross-dropdown close calls: opening the
profile dropdown now closes the workspace dropdown, and vice versa.
Tests: 415 passed, 0 failed.
Agent Review — PR #41 Multi-Profile Support (v0.24)Verdict: APPROVED WITH FIXES — 3 issues found and fixed directly on the branch (commit 571a5a4). 415/415 tests pass post-fix. Security AuditAll mandatory checks passed:
New XSS surface check in panels.js profile rendering: all Fixes Applied (commit 571a5a4)BUG-3 [HIGH] —
INFO-1 [DEFENSE-IN-DEPTH] — No server-side profile name validation
if not re.match(r'^[a-z0-9][a-z0-9_-]{0,63}$', name):
return bad(handler, 'Invalid profile name: ...')INFO-2 [DEFENSE-IN-DEPTH] — The BUG-11 [LOW] — Profile and workspace dropdowns could both be open simultaneously Opening the profile dropdown didn't close the workspace dropdown and vice versa. Added cross-close calls: Other Findings (Not Fixed — Informational)BUG-7 [MEDIUM] — Fix (out of scope for this PR): move CLI_TOOLSETS lookup into the streaming path using BUG-6 [MEDIUM] — new = {}
# ... load into new ...
_cfg_cache.clear()
_cfg_cache.update(new)This doesn't change the external behavior but eliminates the race window. BUG-8 [LOW / NOT EXPLOITABLE] — BUG-1 [CPython-GIL-safe, theoretical] — Architecture NotesThe
Test Results
Ready to merge after your sign-off. All fixes are in commit 571a5a4 on this branch. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BUG-3 (high): /api/profile/delete missing RuntimeError catch. When
deleting the active profile while an agent was running, delete_profile_api()
called switch_profile('default') which raises RuntimeError('Cannot switch
profiles while agent is running'). This propagated to the 500 handler
giving the user 'Internal server error' with no context. Added the same
except RuntimeError -> 409 pattern that /api/profile/switch already uses.
INFO-1 (defense-in-depth): /api/profile/create had no server-side name
validation before delegating to hermes_cli.validate_profile_name. Added
server-side ^[a-z0-9][a-z0-9_-]{0,63}$ check, consistent with client-side
regex in submitProfileCreate(). Prevents path-traversal-ish names from
reaching hermes_cli even if the client-side guard is bypassed.
INFO-2 (defense-in-depth): clone_from parameter was passed directly to
hermes_cli with no validation. Applied the same name regex check to
clone_from before delegating.
BUG-11 (low): toggleProfileDropdown() and toggleWsDropdown() could both
be open simultaneously. Added cross-dropdown close calls: opening the
profile dropdown now closes the workspace dropdown, and vice versa.
Tests: 415 passed, 0 failed.
feat: Multi-Profile Support (Issue nesquena#28)
BUG-3 (high): /api/profile/delete missing RuntimeError catch. When
deleting the active profile while an agent was running, delete_profile_api()
called switch_profile('default') which raises RuntimeError('Cannot switch
profiles while agent is running'). This propagated to the 500 handler
giving the user 'Internal server error' with no context. Added the same
except RuntimeError -> 409 pattern that /api/profile/switch already uses.
INFO-1 (defense-in-depth): /api/profile/create had no server-side name
validation before delegating to hermes_cli.validate_profile_name. Added
server-side ^[a-z0-9][a-z0-9_-]{0,63}$ check, consistent with client-side
regex in submitProfileCreate(). Prevents path-traversal-ish names from
reaching hermes_cli even if the client-side guard is bypassed.
INFO-2 (defense-in-depth): clone_from parameter was passed directly to
hermes_cli with no validation. Applied the same name regex check to
clone_from before delegating.
BUG-11 (low): toggleProfileDropdown() and toggleWsDropdown() could both
be open simultaneously. Added cross-dropdown close calls: opening the
profile dropdown now closes the workspace dropdown, and vice versa.
Tests: 415 passed, 0 failed.
feat: Multi-Profile Support (Issue nesquena#28)
BUG-3 (high): /api/profile/delete missing RuntimeError catch. When
deleting the active profile while an agent was running, delete_profile_api()
called switch_profile('default') which raises RuntimeError('Cannot switch
profiles while agent is running'). This propagated to the 500 handler
giving the user 'Internal server error' with no context. Added the same
except RuntimeError -> 409 pattern that /api/profile/switch already uses.
INFO-1 (defense-in-depth): /api/profile/create had no server-side name
validation before delegating to hermes_cli.validate_profile_name. Added
server-side ^[a-z0-9][a-z0-9_-]{0,63}$ check, consistent with client-side
regex in submitProfileCreate(). Prevents path-traversal-ish names from
reaching hermes_cli even if the client-side guard is bypassed.
INFO-2 (defense-in-depth): clone_from parameter was passed directly to
hermes_cli with no validation. Applied the same name regex check to
clone_from before delegating.
BUG-11 (low): toggleProfileDropdown() and toggleWsDropdown() could both
be open simultaneously. Added cross-dropdown close calls: opening the
profile dropdown now closes the workspace dropdown, and vice versa.
Tests: 415 passed, 0 failed.
feat: Multi-Profile Support (Issue nesquena#28)
Summary
hermes_cli.profilesvia import, monkey-patches cached paths at runtimeBackend (1 new file, 4 modified)
api/profiles.py(NEW): Profile state management, thread-safe switching, module-level cache patchingapi/config.py: Dynamic config reloading (get_config()/reload_config()), profile-aware path resolution for auth.json and .envapi/routes.py: 5 new endpoints (GET /api/profiles,GET /api/profile/active,POST /api/profile/switch|create|delete), fixed hardcoded memory pathsapi/streaming.py: HERMES_HOME added to env save/restore block around agent runsapi/models.py:profilefield on Session (backward-compatible, defaults to null)Frontend (5 modified)
index.html: Profile chip in topbar, Profiles nav tab, management panel with create formstyle.css: Profile chip (purple accent), dropdown, card styles, gateway status badgespanels.js: Profile dropdown rendering, management panel, switchToProfile() with cascade refreshui.js:activeProfilestate, topbar syncboot.js: Fetches active profile on startupDocs
CHANGELOG.md: v0.24 release notesSPRINTS.md: Sprint 22 completed, parity tables updatedCloses #28
Test plan
pytest tests/— 392 pass / 23 fail (identical to baseline, zero regressions)GET /api/profilesreturns default profile🤖 Generated with Claude Code