Hermes Agent Profile 隔离详解
基于 v0.12.0 源码分析 | 2026-05-02
一、问题背景:为什么需要 Profile 隔离?
在 v0.12.0 之前,Hermes 使用 ~/.hermes 作为固定数据目录。当用户需要同时使用多个独立配置时(比如工作用一个账号、个人用一个账号),无法真正隔离——所有配置、API Key、Session、Memory 都混在一起。
核心痛点:
- API Key 混用:工作用的 OpenAI key 和个人用的 key 会互相覆盖
- Session 串扰:一个项目的对话历史污染另一个项目
- Memory 混乱:个人记忆和企业知识库混在一起
- Gateway 冲突:多个实例可能竞争同一个端口
二、Profile 隔离的核心原理
目录结构
~/.hermes/ ← HERMES_ROOT(根目录,由 get_default_hermes_root() 返回)
├── active_profile ← 当前激活的 profile 名称(Sticky 模式)
├── config.yaml ← default profile 配置
├── .env ← default profile 环境变量(API Key 等)
├── memories/ ← default profile 记忆
├── sessions/ ← default profile 会话
├── skills/ ← default profile 技能
├── gateway.pid ← default profile gateway 进程
│
└── profiles/ ← 命名 profile 存放目录
├── coder/ ← 名为 "coder" 的 profile
│ ├── config.yaml
│ ├── .env
│ ├── memories/
│ ├── sessions/
│ ├── skills/
│ ├── home/ ← 子进程 HOME 目录(git/ssh/gh 配置隔离)
│ └── gateway.pid
│
└── work/ ← 名为 "work" 的 profile
├── config.yaml
├── ...
Docker 兼容:当
HERMES_HOME环境变量指向~/.hermes以外的路径(如/opt/data)时,profiles 目录会自动创建在HERMES_HOME/profiles/下,数据落在持久卷中。
三种激活方式
方式 1:CLI 参数(临时)
hermes -p coder chat
# 等价于:HERMES_HOME=~/.hermes/profiles/coder hermes chat
方式 2:Wrapper 脚本(一键别名)
hermes profile create coder
# 自动生成 ~/.local/bin/coder 脚本(内容:#!/bin/sh exec hermes -p coder "$@")
coder chat # 等价于 hermes -p coder chat
方式 3:Sticky 激活(永久切换)
hermes profile use coder
# 写入 ~/.hermes/active_profile 文件,内容为 "coder"
# 下次直接运行 hermes chat,自动读取该文件激活对应 profile
三、关键技术实现
1. 模块级 HERMES_HOME 注入(main.py L95-167)
这是最关键的设计。普通程序在代码里写死 ~/.hermes,但 Hermes 在 main.py 最开头、任何模块 import 之前,拦截 --profile/-p 参数并设置 HERMES_HOME 环境变量:
# main.py 第 85-167 行
def _apply_profile_override() -> None:
"""在所有模块 import 前执行,设置 HERMES_HOME"""
argv = sys.argv[1:]
profile_name = None
# 1. 解析 --profile/-p 参数
for i, arg in enumerate(argv):
if arg in ("--profile", "-p") and i + 1 < len(argv):
profile_name = argv[i + 1]
break
elif arg.startswith("--profile="):
profile_name = arg.split("=", 1)[1]
break
# 1.5 如果环境变量已设置,直接继承(子进程安全)
if profile_name is None and os.environ.get("HERMES_HOME"):
return # 继承父进程的 profile 选择,不重复解析
# 2. 如果没有显式参数,读 active_profile 文件(Sticky 模式)
if profile_name is None:
active_path = get_default_hermes_root() / "active_profile"
if active_path.exists():
profile_name = active_path.read_text().strip()
# 3. 解析并设置 HERMES_HOME
if profile_name:
hermes_home = resolve_profile_env(profile_name)
os.environ["HERMES_HOME"] = hermes_home
_apply_profile_override() # 在所有 import 前执行!
核心逻辑:
- 子进程通过
env参数显式传递HERMES_HOME,父进程已设置过,所以子进程走if ... return分支直接继承,无需再次解析 profile 标志 --profile参数从sys.argv中剥离,避免后续 argparse 报错
2. 全局路径单一来源(hermes_constants.py)
之前 30+ 模块各自调用 Path.home() / ".hermes",现在全部改为调用 get_hermes_home():
# hermes_constants.py
_profile_fallback_warned = False
def get_hermes_home() -> Path:
val = os.environ.get("HERMES_HOME", "").strip()
if val:
return Path(val)
return Path.home() / ".hermes" # fallback 到 default
导入方(部分):
| 模块 | 用途 |
|---|---|
mcp_serve.py |
MCP server 数据目录 |
acp_adapter/session.py |
Session DB 路径 |
tui_gateway/server.py |
Gateway 配置目录 |
plugins/memory/* |
各 memory plugin 数据目录 |
hermes_cli/backup.py |
备份目标路径 |
hermes_cli/kanban_db.py |
Kanban DB 和 logs 目录 |
hermes_cli/config.py |
config.yaml 路径 |
3. 跨 Profile 数据串扰防护(hermes_constants.py L25-55)
如果 HERMES_HOME 未设置但 active_profile 显示是某个非 default profile,向 stderr 打印一次性警告:
[HERMES_HOME fallback] HERMES_HOME is unset but active profile is 'coder'.
Falling back to ~/.hermes, which is the DEFAULT profile — not 'coder'.
Any data this process writes will land in the wrong profile.
The subprocess spawner should pass HERMES_HOME explicitly (see issue #18594).
原因:30+ 模块在 import 时就调用 get_hermes_home(),日志系统尚未初始化(依赖 config.yaml),只能用 sys.stderr.write() 直接输出。使用 global 标记确保只警告一次。
4. 子进程 HOME 隔离(hermes_constants.py L145-160)
每个 profile 有一个 home/ 子目录,用于隔离系统工具配置:
def get_subprocess_home() -> str | None:
"""返回 per-profile HOME,用于 git/ssh/gh 配置隔离"""
hermes_home = os.getenv("HERMES_HOME")
if not hermes_home:
return None
profile_home = os.path.join(hermes_home, "home")
if os.path.isdir(profile_home):
return profile_home
return None
在 kanban_db.py 的任务分发器中,子进程接收:
env["HOME"] = get_subprocess_home() # 注入 profile 专属 HOME
# git config、SSH key、npm 配置都写入 profile 的 home/ 目录
# Docker 场景下保证工具配置落在持久卷
5. Kanban 任务的跨 Profile 分发(kanban_db.py L2050-2130)
每个任务指定 assignee(profile 名),分发时:
cmd = [
"hermes",
"-p", task.assignee, # 激活目标 profile
"--skills", "kanban-worker", # 强制加载 kanban-worker 技能
"chat",
"-q", prompt, # 任务描述
]
proc = subprocess.Popen(
cmd,
env=env, # HERMES_HOME 在 env 中传递
cwd=workspace,
)
四、Profile 的应用场景
场景 1:工作 / 个人完全隔离
# 工作 profile
hermes profile create work --clone # 克隆当前配置(含 API key)
hermes profile use work # 激活 work 作为默认 profile
# 个人 profile
hermes profile create personal
personal chat # 通过 wrapper 直接进入 personal profile
隔离内容:config.yaml(模型偏好)、.env(API Key)、SOUL.md(人格设定)、memories/(记忆)、sessions/(历史会话)
场景 2:多角色 Agent 协作
在 Kanban 系统中,可以为不同角色创建独立 profile:
hermes profile create researcher # 研究员 profile
hermes profile create coder # 程序员 profile
hermes profile create reviewer # 评审员 profile
# kanban task 指定 assignee="researcher"
# 系统自动用 researcher profile 执行
每个 profile 有自己的 skills/ 目录,可以预装不同技能组合。
场景 3:客户项目隔离(SaaS / 外包场景)
hermes profile create client-a-project
hermes profile create client-b-project
# 每次对话自动带上正确的数据上下文
client-a chat "查看客户A的项目进度"
场景 4:多模型对比测试
hermes profile create gpt4-test
hermes profile create claude-test
hermes profile create gemini-test
# 每个 profile 用不同的 API key 和模型配置
场景 5:共享技能库 + 独立配置
hermes profile create shared-skills --clone-config
# 复制 config.yaml, .env, SOUL.md, memories
# 但 skills/ 目录为空,从 default profile 继承共享技能
五、Clone 模式对比
| 模式 | config.yaml | .env | SOUL.md | memories | skills | home/ |
|---|---|---|---|---|---|---|
--clone |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
--clone-all |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
--clone:适合创建空技能的新 profile,保留配置和记忆--clone-all:适合完整迁移,包含 git/ssh/gh 配置(home/目录)
注意:
gateway.pid、processes.json、auth.json、errors.log等运行时文件在--clone-all中会被剥离,不会复制进程状态。
六、Wrapper 脚本机制
# ~/.local/bin/coder 自动生成,内容:
#!/bin/sh
exec hermes -p coder "$@"
创建时检查:
~/.local/bin必须在 PATH 中- 不能与 PATH 中已有的二进制重名
- 不能与 hermes 子命令重名(如
chat、gateway) - 已有的同名 wrapper 如果内容也是
hermes -p xxx则可覆盖
删除 profile 时自动清理对应 wrapper。
七、Profile 与 Gateway Service
每个 profile 可以独立运行 Gateway(接受外部消息):
hermes profile create myprofile
myprofile gateway start # 在 myprofile 的 HERMES_HOME 下启动 gateway
systemctl --user status hermes-gateway-myprofile.service
service name 包含 profile 名(如 hermes-gateway-coder.service),避免多 profile 运行时 service 名称冲突。
八、Profile 相关命令一览
hermes profile create <name> # 创建空白 profile
hermes profile create <name> --clone # 从当前 profile 克隆配置+记忆
hermes profile create <name> --clone-all # 全量克隆(含 skills、home)
hermes profile list # 列出所有 profile
hermes profile use <name> # 设为默认(sticky 激活)
hermes profile delete <name> # 删除 profile 及 wrapper、service
coder chat # 通过 wrapper 使用 coder profile
hermes -p coder chat # 等价于 wrapper 方式
九、关键技术点总结
v0.12.0 Profile 隔离通过三层机制实现完整隔离:
| 层次 | 机制 | 关键代码 |
|---|---|---|
| 环境变量层 | HERMES_HOME 在 CLI 入口早期注入 |
main.py L95-167,_apply_profile_override() |
| 目录隔离层 | 每个 profile 拥有独立 config.yaml、.env、memories/、sessions/、skills/、home/ |
profiles.py L30-42,_PROFILE_DIRS |
| 子进程传播层 | subprocess 通过显式 env 参数传递 HERMES_HOME |
kanban_db.py L2050-2130 |
| 路径统一层 | 30+ 模块全部通过 get_hermes_home() 读取路径 |
hermes_constants.py |
修复的历史问题
- issue #18594:
HERMES_HOME未传递导致子进程错误读写 default profile - issue #4426:git/ssh/gh 配置跨 profile 串扰
- Docker 兼容:profile 目录可落在持久卷(
HERMES_HOME/profiles/<name>)