macos-profile-cron-runner.md 5.84 KB

macOS 多 Profile QA Cron Runner

场景

专用 QA profile 需要每十分钟扫描一次任务。默认 profile 已运行 Feishu Gateway;QA profile 使用同一个 Feishu app_id,因此第二个 Gateway 被 Hermes 主动拒绝,以避免两个本地 WebSocket 客户端竞争。

决策

  • 不停止默认 Gateway;
  • QA profile 不承担消息接收;
  • 卸载冲突的 QA Gateway service;
  • 使用官方 hermes -p qa cron tick 作为一次性调度入口;
  • 用 macOS LaunchAgent 每 60 秒调用 tick;
  • QA Job 自身保持 schedule=every 10m、repeat=forever。

关键发现

周期表达式

创建 Cron 时,简写 10m 被解析为“十分钟后执行一次”,并留下 repeat=once。只把 schedule 更新为 every 10m 仍可能保留 once。可靠修复是删除错误 Job,以 every 10m 重新创建,并重新读取确认 repeat=forever。

Gateway 冲突

日志中的有效根因应来自当前启动段,例如:

Another local Hermes gateway is already using this Feishu app_id

此时不应停止未知用途的现有 Gateway。若 QA 不需要消息平台,独立 tick runner 比第二个 Gateway 更安全。

macOS TCC

LaunchAgent 初始 WorkingDirectory 指向 Documents/.../qa,Python 在导入阶段调用 os.getcwd() 即得到:

PermissionError: [Errno 1] Operation not permitted

将调度器和 Cron workdir 改为 profile 目录后,tick 退出码恢复为 0。项目源码目录是否可访问,仍应在真实候选任务中验证;空队列运行不能覆盖该风险。

LaunchAgent 结构

核心参数:

<key>ProgramArguments</key>
<array>
  <string>/path/to/hermes/venv/bin/python</string>
  <string>-m</string>
  <string>hermes_cli.main</string>
  <string>--profile</string>
  <string>qa</string>
  <string>cron</string>
  <string>tick</string>
</array>
<key>WorkingDirectory</key>
<string>/path/to/profile/qa</string>
<key>StartInterval</key>
<integer>60</integer>
<key>RunAtLoad</key>
<true/>

stdout/stderr 应写入 profile 日志目录。不要把真实凭证放进 plist。

验证证据链

  1. plutil -lint 返回 OK;
  2. launchd service 已 bootstrap;
  3. 手动 kick 后 runs 增长;
  4. last exit code = 0;
  5. 清空旧日志后再触发,stdout/stderr 无当前错误;
  6. Hermes Cron Job 重新读取显示:
    • every 10m;
    • repeat=forever;
    • 正确 skills 和 workdir;
  7. 手动运行 Cron 后 last_status=ok;
  8. 读取持久化输出,而非只看工具返回;
  9. [SILENT] 只证明无候选路径,没有证明完整 QA 测试链;
  10. 后续应等待自然周期,并在真实候选出现时验证 Base 认领、源码访问、测试和写回。

状态命令的解释

外部 tick runner 模式下,hermes cron status 可能仍提示 profile Gateway 未运行。不要单独据此判失败或成功;交叉检查:

  • launchd runs 与最新退出码;
  • tick stdout/stderr;
  • Cron run history;
  • Job last_run/last_status;
  • 持久化输出。

一次性 tick 完成后 state = not running 是预期状态,不代表调度器未注册。

macOS TCC Documents 目录后台访问(深坑)

现象

Hermes cron 在交互式终端里能读取 ~/Documents/lemonAI/graphagent/source,但 launchd tick 运行时报:

PermissionError: [Errno 1] Operation not permitted

根因

  • LaunchAgent 是后台进程,继承用户 uid 但不继承交互式终端的 TCC 授权;
  • macOS 对 Documents / Desktop / Downloads 有 SystemPolicyDocumentsFolder 保护;
  • 进程身份不是 hermes 脚本,而是 plist 中 ProgramArguments 的第一个可执行文件:即 venv 里的 python;
  • 该 python 通常是指向 ~/.local/share/uv/python/.../python3.11 的符号链接。TCC 绑定到真实目标文件,给 symlink 加权限无效。

诊断

# 1. 确认 plist 中真正运行的二进制
ls -la ~/Library/LaunchAgents/ai.hermes.cron-qa.plist
# 看 ProgramArguments 的第一项,例如 /Users/yibo/.hermes/hermes-agent/venv/bin/python

# 2. 解析真实 Python 路径
ls -la /Users/yibo/.hermes/hermes-agent/venv/bin/python
file /Users/yibo/.hermes/hermes-agent/venv/bin/python

# 典型结果:
# python -> /Users/yibo/.local/share/uv/python/cpython-3.11.14-macos-aarch64-none/bin/python3.11

修复(推荐)

  1. 打开 System Settings → Privacy & Security → Full Disk Access;
  2. 点击 +,添加 真实 Python 二进制(不是 symlink): /Users/yibo/.local/share/uv/python/cpython-3.11.14-macos-aarch64-none/bin/python3.11 如果文件选择器找不到 .local/share/uv,按 Cmd+Shift+G 输入完整路径;
  3. 重启 cron 服务: bash launchctl kickstart -k gui/$(id - u)/ai.hermes.cron-qa launchctl kickstart -k gui/$(id - u)/ai.hermes.cron-code
  4. 验证日志不再出现 Operation not permitted: bash tail -f /Users/yibo/.hermes/profiles/qa/logs/cron-tick.error.log tail -f /Users/yibo/.hermes/profiles/code/logs/cron-ticker.error.log

替代方案

如果不想给 Python 全盘权限,把项目移出 Documents:

mv /Users/yibo/Documents/lemonAI/graphagent /Users/yibo/graphagent

然后更新对应 Cron Job 的 workdir:

hermes -p <profile> cron update <job-id> --workdir /Users/yibo/graphagent/source

注意:只改 workdir 到 profile 目录但项目仍在 Documents 里无效,Cron 内访问项目时仍会触发 TCC。

检查项

  • 不要只给 hermes shell 脚本加权限,进程身份是 Python 二进制;
  • 不要给 venv/bin/python 符号链接加权限,TCC 看的是真实目标文件;
  • 验证时必须读取真实任务触发的日志,空队列 tick 通过不等于源码访问链路已通;
  • 多 profile 场景下,每个 profile 的 LaunchAgent 使用同一个 Python 二进制,只需授权一次。