x

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.pidprocesses.jsonauth.jsonerrors.log 等运行时文件在 --clone-all 中会被剥离,不会复制进程状态。


六、Wrapper 脚本机制

# ~/.local/bin/coder 自动生成,内容:
#!/bin/sh
exec hermes -p coder "$@"

创建时检查:

  • ~/.local/bin 必须在 PATH 中
  • 不能与 PATH 中已有的二进制重名
  • 不能与 hermes 子命令重名(如 chatgateway
  • 已有的同名 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.envmemories/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 #18594HERMES_HOME 未传递导致子进程错误读写 default profile
  • issue #4426:git/ssh/gh 配置跨 profile 串扰
  • Docker 兼容:profile 目录可落在持久卷(HERMES_HOME/profiles/<name>
Left-click: follow link, Right-click: select node, Scroll: zoom
x