Skip to content

feat: add profiles — run multiple isolated Hermes instances#3681

Merged
teknium1 merged 2 commits intomainfrom
hermes/hermes-e6f1d362
Mar 29, 2026
Merged

feat: add profiles — run multiple isolated Hermes instances#3681
teknium1 merged 2 commits intomainfrom
hermes/hermes-e6f1d362

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

@teknium1 teknium1 commented Mar 29, 2026

Summary

Profiles let you run multiple fully independent Hermes agents from a single install. Each profile gets its own HERMES_HOME directory — separate config, API keys, memories, sessions, skills, gateway, cron, and state.db. One shared codebase, complete data isolation.

hermes profile create coder          # fresh profile + bundled skills
coder setup                          # configure API keys and model
coder chat                           # start chatting
coder gateway start                  # separate gateway with own bot token

Architecture

~/.hermes/                           "default" profile (backward compat)
  ├── config.yaml  .env  SOUL.md
  ├── active_profile                 sticky default profile name
  └── profiles/
       ├── coder/                    independent HERMES_HOME
       │    ├── config.yaml  .env  SOUL.md
       │    ├── memories/  sessions/  skills/  state.db  cron/  logs/
       │    └── gateway.pid
       └── assistant/
            └── ...
~/.local/bin/coder                   wrapper: hermes -p coder "$@"

The core mechanism: 119+ files reference get_hermes_home(). Setting HERMES_HOME before import makes every command profile-scoped automatically — zero per-command changes needed.

What's in this PR (19 files, +2788 lines)

New: hermes_cli/profiles.py (~900 lines)

  • Profile CRUD: create, delete, list, show, rename
  • Three clone levels: blank, --clone (config only), --clone-all (full duplicate including sessions, skills, memory, cron, plugins)
  • Export/import: hermes profile export coder → tar.gz, hermes profile import archive.tar.gz
  • Wrapper aliases: auto-generates ~/.local/bin/<name> with collision detection
  • Sticky default: hermes profile use coder~/.hermes/active_profile
  • Tab completion: hermes completion bash / zsh
  • Clean deletion: auto-stops gateway (disable-before-stop for systemd/launchd), removes service + alias

CLI: hermes_cli/main.py

  • _apply_profile_override(): Pre-import -p/--profile scanner + sticky default fallback. All exceptions caught — never prevents hermes from starting.
  • cmd_profile(): 9 subcommands (status, list, use, create, delete, show, alias, rename, export, import)
  • cmd_completion(): Shell completion output
  • cmd_update(): Syncs bundled skills to ALL profiles after git pull

Display: cli.py, banner.py, gateway/run.py

  • Prompt: coder ❯ — Banner: Profile: coder — Gateway log: Active profile: coder

Gateway safety (6 adapters)

  • Token locks: Discord, Slack, WhatsApp, Signal — extends existing Telegram pattern
  • Port conflict detection: API Server, Webhook — socket probe before binding

Diagnostics: hermes_cli/doctor.py

  • Profile health section: lists profiles with config/env/alias status
  • Orphan alias detection: warns when wrapper points to deleted profile

Tests: tests/hermes_cli/test_profiles.py (71 tests)

Test class Tests Coverage
TestValidateProfileName 7 Valid, invalid, boundary, special names
TestCreateProfile 7 Subdirs, clone_config, clone_all, errors
TestDeleteProfile 3 Happy path, default error, nonexistent
TestListProfiles 4 Default only, named, sorted, order
TestActiveProfile 5 Set/get, empty file, default clears
TestGetActiveProfileName 3 Default, profile, custom paths
TestResolveProfileEnv 4 Existing, default, missing, invalid
TestAliasCollision 4 Safe, reserved, subcommand, default
TestRenameProfile 5 Happy path, errors, exists
TestExportImport 4 Round-trip, errors
TestProfileIsolation 3 Config, state.db, skills paths
TestCompletion 4 Bash/zsh output validation
TestEdgeCases + helpers 18 ProfileInfo, gateway PID, boundaries

Documentation

  • website/docs/user-guide/profiles.md: Full user guide — 12 sections with code examples
  • website/docs/reference/profile-commands.md: Command reference — 12 commands with options
  • website/docs/reference/faq.md: 6 profile FAQ entries
  • website/sidebars.ts: Navigation updated

Developer guidance

  • AGENTS.md: New "Profiles: Multi-Instance Support" section with 6 rules for profile-safe code, test fixture patterns, and tool development guidance
  • AGENTS.md: New "DO NOT hardcode ~/.hermes paths" pitfall entry
  • website/docs/developer-guide/contributing.md: Profile-safe paths in Code Style section

Live E2E test results

22 live tests passed during development:

  • Profile create/delete/list/show/rename/export/import ✓
  • Session isolation (separate state.db verified) ✓
  • Config isolation (settings don't leak between profiles) ✓
  • -p flag in all formats (before/after subcommand, --profile=) ✓
  • Interactive PTY: banner + prompt show profile name ✓
  • Port conflict detection (API server blocked for second gateway) ✓
  • Doctor shows correct profile paths ✓
  • Default profile completely unaffected throughout all testing ✓

CI results

All 6 checks pass:

  • test ✓ — docs-site-checks ✓ — build-and-push ✓ — nix (ubuntu) ✓ — nix (macOS) ✓ — supply chain scan ✓

Backward compatibility

Zero impact for non-profile users. _apply_profile_override() returns immediately when no -p flag and no active_profile file exist — zero imports, zero overhead, zero visual changes. No config migration needed. The feature is fully opt-in.

Pre-work PRs (merged)

Command reference

hermes profile                     Show current profile status
hermes profile list                List all profiles
hermes profile use <name>          Set sticky default
hermes profile create <name>       Create + seed + alias
       --clone / --clone-all / --clone-from / --no-alias
hermes profile delete <name> [-y]  Delete + cleanup
hermes profile show <name>         Profile details
hermes profile alias <name>        Manage aliases
hermes profile rename <old> <new>  Rename profile
hermes profile export <name>       Export to tar.gz
hermes profile import <archive>    Import from tar.gz
hermes -p <name> <command>         Target a specific profile
hermes completion bash|zsh         Shell completion
@teknium1 teknium1 force-pushed the hermes/hermes-e6f1d362 branch 2 times, most recently from 81bc7ac to d774104 Compare March 29, 2026 14:58
Each profile is a fully independent HERMES_HOME with its own config,
API keys, memory, sessions, skills, gateway, cron, and state.db.

Core module: hermes_cli/profiles.py (~900 lines)
  - Profile CRUD: create, delete, list, show, rename
  - Three clone levels: blank, --clone (config), --clone-all (everything)
  - Export/import: tar.gz archive for backup and migration
  - Wrapper alias scripts (~/.local/bin/<name>)
  - Collision detection for alias names
  - Sticky default via ~/.hermes/active_profile
  - Skill seeding via subprocess (handles module-level caching)
  - Auto-stop gateway on delete with disable-before-stop for services
  - Tab completion generation for bash and zsh

CLI integration (hermes_cli/main.py):
  - _apply_profile_override(): pre-import -p/--profile flag + sticky default
  - Full 'hermes profile' subcommand: list, use, create, delete, show,
    alias, rename, export, import
  - 'hermes completion bash/zsh' command
  - Multi-profile skill sync in hermes update

Display (cli.py, banner.py, gateway/run.py):
  - CLI prompt: 'coder ❯' when using a non-default profile
  - Banner shows profile name
  - Gateway startup log includes profile name

Gateway safety:
  - Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern)
  - Port conflict detection: API server, webhook adapter

Diagnostics (hermes_cli/doctor.py):
  - Profile health section: lists profiles, checks config, .env, aliases
  - Orphan alias detection: warns when wrapper points to deleted profile

Tests (tests/hermes_cli/test_profiles.py):
  - 71 automated tests covering: validation, CRUD, clone levels, rename,
    export/import, active profile, isolation, alias collision, completion
  - Full suite: 6760 passed, 0 new failures

Documentation:
  - website/docs/user-guide/profiles.md: full user guide (12 sections)
  - website/docs/reference/profile-commands.md: command reference (12 commands)
  - website/docs/reference/faq.md: 6 profile FAQ entries
  - website/sidebars.ts: navigation updated
@teknium1 teknium1 force-pushed the hermes/hermes-e6f1d362 branch from d774104 to aa1848d Compare March 29, 2026 15:32
@devorun
Copy link
Copy Markdown
Contributor

devorun commented Mar 29, 2026

sir , I'm not entirely sure, but I wanted to point out a possible bug here
In connect() the scoped lock is acquired using the app_token value that exists at that moment. However in disconnect() the token is read again via os.getenv("SLACK_APP_TOKEN") before releasing the lock.

If the environment were to change between those two calls (for example with the new profile system), the value used to release the lock might differ from the one used to acquire it, which could leave the original lock unreleased.

Other adapters seem to store the identity at connect time and reuse it during disconnect. Maybe Slack should follow the same pattern, but I might be misunderstanding the flow.

@teknium1 teknium1 force-pushed the hermes/hermes-e6f1d362 branch from d748928 to 08f3bca Compare March 29, 2026 16:42
eloklam added a commit to eloklam/hermes-agent that referenced this pull request Mar 29, 2026
Add OhMyOpenCode-style multi-agent orchestration enabling one profile
to delegate tasks to other profiles via HTTP with full conversation
history passing and SQLite persistence.

Features:
- Profile-to-profile delegation via HTTP (sync and async modes)
- Full conversation history passing between multi-turn delegations
- Async mode with SQLite persistence (survives process restarts)
- Cross-task memory loading from previous orchestration tasks
- ThreadPoolExecutor for reliability (simplified from aiohttp)
- Dual lookup pattern: memory + SQLite for check_profile_task

Bug Fixes:
- UnboundLocalError: 'json' (removed local import shadowing)
- TypeError: parent_agent unexpected (added to function signatures)
- asyncio not defined (simplified to ThreadPoolExecutor)
- Task IDs expiring (SQLite persistence)
- Results returning None (fixed _sync_request + callback)
- check_profile_task intermittent (dual lookup)
- cancel_profile_task unknown (SQLite status update)

Stress Test Results:
- 72 consecutive calls, 100% success rate
- 15 parallel async + 15 parallel sync + 15 rapid sequential
- Edge cases: unicode, special chars, multiline, heavy computation

Files changed:
- NEW: tools/profile_orchestrator.py (~1000 lines)
- NEW: hermes_cli/profiles.py (~45 lines)
- MODIFIED: tools/delegate_tool.py (+400 lines, bug fixes)
- MODIFIED: gateway/platforms/api_server.py (session creation)
- MODIFIED: hermes_state.py (orchestration_tasks table)
- MODIFIED: toolsets.py (delegation toolset extended)

Requires PR NousResearch#3681 (profile infrastructure)
100% offline capable using local SQLite
@teknium1 teknium1 merged commit f6db1b2 into main Mar 29, 2026
6 checks passed
jecruz pushed a commit to jecruz/hermes-agent that referenced this pull request Mar 29, 2026
…arch#3681)

Each profile is a fully independent HERMES_HOME with its own config,
API keys, memory, sessions, skills, gateway, cron, and state.db.

Core module: hermes_cli/profiles.py (~900 lines)
  - Profile CRUD: create, delete, list, show, rename
  - Three clone levels: blank, --clone (config), --clone-all (everything)
  - Export/import: tar.gz archive for backup and migration
  - Wrapper alias scripts (~/.local/bin/<name>)
  - Collision detection for alias names
  - Sticky default via ~/.hermes/active_profile
  - Skill seeding via subprocess (handles module-level caching)
  - Auto-stop gateway on delete with disable-before-stop for services
  - Tab completion generation for bash and zsh

CLI integration (hermes_cli/main.py):
  - _apply_profile_override(): pre-import -p/--profile flag + sticky default
  - Full 'hermes profile' subcommand: list, use, create, delete, show,
    alias, rename, export, import
  - 'hermes completion bash/zsh' command
  - Multi-profile skill sync in hermes update

Display (cli.py, banner.py, gateway/run.py):
  - CLI prompt: 'coder ❯' when using a non-default profile
  - Banner shows profile name
  - Gateway startup log includes profile name

Gateway safety:
  - Token locks: Discord, Slack, WhatsApp, Signal (extends Telegram pattern)
  - Port conflict detection: API server, webhook adapter

Diagnostics (hermes_cli/doctor.py):
  - Profile health section: lists profiles, checks config, .env, aliases
  - Orphan alias detection: warns when wrapper points to deleted profile

Tests (tests/hermes_cli/test_profiles.py):
  - 71 automated tests covering: validation, CRUD, clone levels, rename,
    export/import, active profile, isolation, alias collision, completion
  - Full suite: 6760 passed, 0 new failures

Documentation:
  - website/docs/user-guide/profiles.md: full user guide (12 sections)
  - website/docs/reference/profile-commands.md: command reference (12 commands)
  - website/docs/reference/faq.md: 6 profile FAQ entries
  - website/sidebars.ts: navigation updated
erosika added a commit to erosika/hermes-agent that referenced this pull request Mar 30, 2026
Derives the Honcho host key from the active Hermes profile so that each
profile gets its own Honcho host block, workspace, and AI peer identity.

Profile "coder" resolves to host "hermes.coder", reads from
hosts["hermes.coder"] in honcho.json, and defaults workspace + aiPeer
to the derived host name.

Resolution order: HERMES_HONCHO_HOST env var > active profile name >
"hermes" (default).

Complements NousResearch#3681 (profiles) with the Honcho identity layer that was
part of NousResearch#2845 (named instances), adapted to the merged profiles system.
erosika added a commit to erosika/hermes-agent that referenced this pull request Mar 30, 2026
Derives the Honcho host key from the active Hermes profile so that each
profile gets its own Honcho host block, workspace, and AI peer identity.

Profile "coder" resolves to host "hermes.coder", reads from
hosts["hermes.coder"] in honcho.json, and defaults workspace + aiPeer
to the derived host name.

Resolution order: HERMES_HONCHO_HOST env var > active profile name >
"hermes" (default).

Complements NousResearch#3681 (profiles) with the Honcho identity layer that was
part of NousResearch#2845 (named instances), adapted to the merged profiles system.
erosika added a commit to erosika/hermes-agent that referenced this pull request Mar 31, 2026
Derives the Honcho host key from the active Hermes profile so that each
profile gets its own Honcho host block, workspace, and AI peer identity.

Profile "coder" resolves to host "hermes.coder", reads from
hosts["hermes.coder"] in honcho.json, and defaults workspace + aiPeer
to the derived host name.

Resolution order: HERMES_HONCHO_HOST env var > active profile name >
"hermes" (default).

Complements NousResearch#3681 (profiles) with the Honcho identity layer that was
part of NousResearch#2845 (named instances), adapted to the merged profiles system.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants