# 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 冲突
日志中的有效根因应来自当前启动段,例如:
```text
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()` 即得到:
```text
PermissionError: [Errno 1] Operation not permitted
```
将调度器和 Cron workdir 改为 profile 目录后,tick 退出码恢复为 0。项目源码目录是否可访问,仍应在真实候选任务中验证;空队列运行不能覆盖该风险。
## LaunchAgent 结构
核心参数:
```xml
ProgramArguments
/path/to/hermes/venv/bin/python
-m
hermes_cli.main
--profile
qa
cron
tick
WorkingDirectory
/path/to/profile/qa
StartInterval
60
RunAtLoad
```
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 运行时报:
```text
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 加权限无效。
### 诊断
```bash
# 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:
```bash
mv /Users/yibo/Documents/lemonAI/graphagent /Users/yibo/graphagent
```
然后更新对应 Cron Job 的 `workdir`:
```bash
hermes -p cron update --workdir /Users/yibo/graphagent/source
```
注意:只改 `workdir` 到 profile 目录但项目仍在 Documents 里无效,Cron 内访问项目时仍会触发 TCC。
### 检查项
- 不要只给 `hermes` shell 脚本加权限,进程身份是 Python 二进制;
- 不要给 `venv/bin/python` 符号链接加权限,TCC 看的是真实目标文件;
- 验证时必须读取真实任务触发的日志,空队列 tick 通过不等于源码访问链路已通;
- 多 profile 场景下,每个 profile 的 LaunchAgent 使用同一个 Python 二进制,只需授权一次。