# 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 二进制,只需授权一次。